packages feed

tala-compiler (empty) → 0.1.0

raw patch · 70 files changed

+29341/−0 lines, 70 filesdep +basedep +bytestringdep +containers

Dependencies added: base, bytestring, containers, directory, filepath, tala-compiler, tala-core, tala-frontend, tala-syntax, text

Files

+ LICENSE view
@@ -0,0 +1,373 @@+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
+ LICENSE-EXCEPTION view
@@ -0,0 +1,81 @@+Progmasoft Linking Exception, Version 1.1
+
+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.
+
+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.
+
+"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.
+
+"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.
+
+2. Additional Permission for Combined Works
+
+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.
+
+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.
+
+3. Boundary of Covered Software
+
+Linking, loading, calling, communicating with, packaging, installing, or distributing an Independent Component with Covered Software does not, by itself:
+
+(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.
+
+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.
+
+4. Outputs
+
+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.
+
+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.
+
+5. Preservation of MPL-2.0 Obligations
+
+This Exception does not remove, replace, waive, or narrow any MPL-2.0 obligation applicable to Covered Software. In particular:
+
+(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.
+
+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.
+
+6. Packaging and Deployment
+
+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.
+
+7. Direct Grant to Recipients
+
+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.
+
+8. Termination and Reinstatement
+
+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.
+
+A recipient may elect to use Covered Software under MPL-2.0 without relying on this Exception.
+
+9. Other Rights
+
+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.
+
+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.
+
+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.
+
+11. Applicability and Version
+
+This Exception applies only to a file that expressly identifies it with this exact SPDX license expression:
+
+MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+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. 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.
+
+ src/Progmasoft/Tala/Compiler.hs view
@@ -0,0 +1,382 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Composition of the target-independent Tala frontend stages.
+
+A physical source file is a compilation unit, not a module. Namespace is the
+semantic grouping key and does not need to match the directory layout. The
+project compiler therefore parses every file independently, merges units by
+namespace, runs each namespace through the complete Haskell pipeline, and
+selects the configured namespace/class entry only after type checking.
+-}
+module Progmasoft.Tala.Compiler
+    ( CompilerInput (..)
+    , FrontendArtifacts (..)
+    , NamespaceArtifacts (..)
+    , ProjectFrontendArtifacts (..)
+    , compileToCorePrep
+    , compileEntryToCorePrep
+    , compileProjectToCorePrep
+    , compileTemplateSpecializations
+    , projectEntryCore
+    , validateEntryPoint
+    ) where
+
+import Data.List (groupBy, sortOn)
+import Data.Maybe (mapMaybe)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Monomorphization
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Desugarer
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Discovery
+import Progmasoft.Tala.Template.Specialization
+import Progmasoft.Tala.Template.Specialization.Verifier
+
+{- | Artifacts for one semantic namespace after all of its physical source
+units have been merged. Keeping every stage visible makes stage ownership and
+regression tests explicit; CorePrep remains internal at the process boundary.
+-}
+data FrontendArtifacts = FrontendArtifacts
+    { artifactParsedAST :: ParsedAST
+    , artifactRenamedAST :: RenamedAST
+    , artifactResolvedAST :: ResolvedAST
+    , artifactTypedAST :: TypedAST
+    , artifactTemplateDemandDiscovery :: TemplateDemandDiscovery
+    , artifactTemplateSpecializations :: TemplateSpecializationPlan
+    , artifactCore :: CoreModule
+    , artifactMonomorphizationPlan :: MonomorphizationPlan
+    , artifactOptimizedCore :: CoreModule
+    , artifactCorePrep :: CorePrepModule
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Frontend stages and source provenance for one merged semantic namespace.
+data NamespaceArtifacts = NamespaceArtifacts
+    { artifactNamespace :: Maybe QualifiedName
+    , artifactSourceFiles :: [FilePath]
+    , artifactFrontend :: FrontendArtifacts
+    }
+    deriving (Eq, Ord, Read, Show)
+
+{- | A project may contain several namespaces, but the current private Core wire
+carries one module at a time. All namespaces are still parsed, renamed,
+resolved, typed, lowered, optimized, and verified. The selected entry
+namespace is the module handed to the native CorePrep/Xpp/Xmm boundary.
+-}
+data ProjectFrontendArtifacts = ProjectFrontendArtifacts
+    { projectEntryName :: QualifiedName
+    , projectNamespaces :: [NamespaceArtifacts]
+    , projectEntryNamespace :: NamespaceArtifacts
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Run the source pipeline through verified, optimized CorePrep for one file.
+compileToCorePrep :: CompilerInput -> Either [Diagnostic] FrontendArtifacts
+compileToCorePrep input = analyzeSemantics input >>= compileSemanticToCorePrep
+
+-- | Compile one source file and validate its namespace-qualified entry class.
+compileEntryToCorePrep :: QualifiedName -> CompilerInput -> Either [Diagnostic] FrontendArtifacts
+compileEntryToCorePrep entry input = do
+    artifacts <- compileToCorePrep input
+    validateEntryPoint entry (artifactTypedAST artifacts)
+    pure artifacts
+
+{- | Materialize an explicit batch of resolved template demands and lower the
+resulting closed declaration view to verified Core. Demand discovery and
+constraint ordering stay outside this function: callers must pass demands
+produced by semantic resolution rather than asking the driver to infer syntax.
+-}
+compileTemplateSpecializations ::
+    TemplateSpecializationLimits ->
+    TypedAST ->
+    [TemplateSpecializationDemand] ->
+    Either [Diagnostic] (TemplateSpecializationPlan, CoreModule)
+compileTemplateSpecializations limits typed demands = do
+    plan <-
+        mapLeft templateSpecializationDiagnostics (planTemplateSpecializations limits typed demands)
+            >>= mapLeft templatePlanDiagnostics . verifyTemplateSpecializationPlan
+    core <- runDesugarer defaultDesugarer (specializationTypedAST plan) >>= verifyCore
+    pure (plan, core)
+
+{- | Compile a complete discovered source set. Diagnostics from independent
+files and namespaces are accumulated in deterministic input order instead of
+stopping at the first bad file, which makes project output useful without
+weakening any stage boundary.
+-}
+compileProjectToCorePrep :: QualifiedName -> [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
+compileProjectToCorePrep entry inputs = 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)
+
+-- | Retrieve the selected namespace's optimized Core module for native lowering.
+projectEntryCore :: ProjectFrontendArtifacts -> CoreModule
+projectEntryCore = artifactOptimizedCore . artifactFrontend . projectEntryNamespace
+
+compileParsedToCorePrep :: ParsedAST -> Either [Diagnostic] FrontendArtifacts
+compileParsedToCorePrep parsed = analyzeParsedSemantics parsed >>= compileSemanticToCorePrep
+
+compileSemanticToCorePrep :: SemanticArtifacts -> Either [Diagnostic] FrontendArtifacts
+compileSemanticToCorePrep semantic = do
+    let parsed = semanticParsedAST semantic
+        renamed = semanticRenamedAST semantic
+        resolved = semanticResolvedAST semantic
+        typed = semanticTypedAST semantic
+        templateDiscovery = discoverTemplateDemands typed
+    templatePlan <-
+        mapLeft
+            templateSpecializationDiagnostics
+            (planTemplateSpecializations defaultTemplateSpecializationLimits typed (discoveredTemplateDemands templateDiscovery))
+            >>= mapLeft templatePlanDiagnostics . verifyTemplateSpecializationPlan
+    -- The two trees are one program: which calls have an effect is
+    -- decided from both.
+    let specializations = specializationTypedAST templatePlan
+        desugarer = desugarerWithin [typed, specializations]
+    ordinaryCore <- runDesugarer desugarer typed >>= verifyCore
+    specializationCore <- runDesugarer desugarer specializations >>= verifyCore
+    let core =
+            CoreModuleWithSources
+                (coreModuleName ordinaryCore)
+                (coreModuleFunctions ordinaryCore ++ coreModuleFunctions specializationCore)
+                (unique (coreModuleSourceFiles ordinaryCore ++ coreModuleSourceFiles specializationCore))
+                (coreModuleFunctionSources ordinaryCore ++ coreModuleFunctionSources specializationCore)
+    verifiedCore <- verifyCore core
+    -- Demand discovery runs before optimization so dead-code elimination
+    -- cannot silently erase a type required by the checked source contract.
+    -- The declaration planner consumes this discovery immediately. The later
+    -- Core graph remains separate because it describes backend work rather
+    -- than source declaration selection.
+    specializationPlan <- mapLeft monomorphizationDiagnostic (planCoreMonomorphization verifiedCore)
+    optimized <- runCoreOptimizer defaultCoreOptimizer verifiedCore >>= verifyCore
+    prepared <- prepareCore optimized >>= verifyCorePrep
+    pure
+        ( FrontendArtifacts
+            parsed
+            renamed
+            resolved
+            typed
+            templateDiscovery
+            templatePlan
+            verifiedCore
+            specializationPlan
+            optimized
+            prepared
+        )
+
+mapLeft :: (failure -> mapped) -> Either failure value -> Either mapped value
+mapLeft transform result = case result of
+    Left failure -> Left (transform failure)
+    Right value -> Right value
+
+monomorphizationDiagnostic :: MonomorphizationError -> [Diagnostic]
+monomorphizationDiagnostic failure =
+    [ Diagnostic
+        CoreStage
+        Error
+        "TAM1001"
+        Nothing
+        (renderMonomorphizationError failure)
+    ]
+
+templateSpecializationDiagnostics :: [TemplateSpecializationError] -> [Diagnostic]
+templateSpecializationDiagnostics = zipWith diagnostic [1 :: Int ..]
+    where
+        diagnostic index failure =
+            Diagnostic
+                TypeCheckerStage
+                Error
+                ("TAT2" ++ pad index)
+                Nothing
+                (renderTemplateSpecializationError failure)
+        pad value
+            | value < 10 = "00" ++ show value
+            | value < 100 = "0" ++ show value
+            | otherwise = show value
+
+templatePlanDiagnostics :: [TemplatePlanIssue] -> [Diagnostic]
+templatePlanDiagnostics = zipWith diagnostic [1 :: Int ..]
+    where
+        diagnostic index failure =
+            Diagnostic
+                TypeCheckerStage
+                Error
+                ("TAT3" ++ pad index)
+                Nothing
+                (renderTemplatePlanIssue failure)
+        pad value
+            | value < 10 = "00" ++ show value
+            | value < 100 = "0" ++ show value
+            | otherwise = show value
+
+data ParsedNamespace = ParsedNamespace
+    { parsedNamespaceName :: Maybe QualifiedName
+    , parsedNamespaceFiles :: [FilePath]
+    , parsedNamespaceDeclarations :: [Declaration Identifier ()]
+    }
+
+groupParsedUnits :: [(FilePath, ParsedAST)] -> [ParsedNamespace]
+groupParsedUnits units = map mergeGroup grouped
+    where
+        keyed = sortOn namespaceSortKey [(syntaxNamespace tree, file, syntaxDeclarations tree) | (file, ParsedAST tree) <- units]
+        grouped = groupBy sameNamespace keyed
+        sameNamespace (left, _, _) (right, _, _) = left == right
+        mergeGroup [] = ParsedNamespace Nothing [] []
+        mergeGroup values@((namespaceName, _, _) : _) =
+            ParsedNamespace
+                namespaceName
+                [file | (_, file, _) <- values]
+                (concat [declarations | (_, _, declarations) <- values])
+
+namespaceSortKey :: (Maybe QualifiedName, FilePath, declarations) -> (Maybe QualifiedName, FilePath)
+namespaceSortKey (namespaceName, file, _) = (namespaceName, file)
+
+compileNamespace :: ParsedNamespace -> Either [Diagnostic] NamespaceArtifacts
+compileNamespace parsed = do
+    let syntaxTree = SyntaxTree (parsedNamespaceName parsed) (parsedNamespaceDeclarations parsed)
+    artifacts <- compileParsedToCorePrep (ParsedAST syntaxTree)
+    let frontend = artifacts
+        sourceFiles = unique (map portablePath (parsedNamespaceFiles parsed))
+        attachSources core = core {coreModuleSourceFiles = unique (sourceFiles ++ coreModuleSourceFiles core)}
+        prepared =
+            (artifactCorePrep frontend)
+                { corePrepModuleSourceFiles = unique (sourceFiles ++ corePrepModuleSourceFiles (artifactCorePrep frontend))
+                }
+        updatedFrontend =
+            frontend
+                { artifactCore = attachSources (artifactCore frontend)
+                , artifactOptimizedCore = attachSources (artifactOptimizedCore frontend)
+                , artifactCorePrep = prepared
+                }
+    _ <- verifyCore (artifactOptimizedCore updatedFrontend)
+    _ <- verifyCorePrep prepared
+    pure
+        ( NamespaceArtifacts
+            (parsedNamespaceName parsed)
+            sourceFiles
+            updatedFrontend
+        )
+
+portablePath :: FilePath -> FilePath
+portablePath = map (\character -> if character == '\\' then '/' else character)
+
+selectNamespace :: QualifiedName -> [NamespaceArtifacts] -> Either [Diagnostic] NamespaceArtifacts
+selectNamespace namespaceName artifacts =
+    case [candidate | candidate <- artifacts, artifactNamespace candidate == Just namespaceName] of
+        [selected] -> Right selected
+        [] -> Left [compilerProblem "TAE0011" Nothing ("entry namespace was not found: " ++ renderQualifiedName namespaceName)]
+        _ ->
+            Left
+                [ compilerProblem "TAE0012" Nothing ("entry namespace was produced more than once: " ++ renderQualifiedName namespaceName)
+                ]
+
+-- | Check that an entry path names a public static parameterless void Main method.
+validateEntryPoint :: QualifiedName -> TypedAST -> Either [Diagnostic] ()
+validateEntryPoint entry (TypedAST (SyntaxTree namespace declarations)) = do
+    expectedNamespace <- entryNamespaceName entry
+    className <- entryClassName entry
+    if namespace /= Just expectedNamespace
+        then Left [entryProblem "TAE0002" Nothing "entry namespace does not match the selected source namespace"]
+        else case matchingTypes className declarations of
+            [] -> Left [entryProblem "TAE0003" Nothing ("entry class was not found: " ++ identifierText className)]
+            [declaration] -> validateEntryMethod declaration
+            _ -> Left [entryProblem "TAE0013" Nothing ("entry class is ambiguous: " ++ identifierText className)]
+
+matchingTypes :: Identifier -> [Declaration ResolvedName Type] -> [Declaration ResolvedName Type]
+matchingTypes className declarations =
+    [ declaration
+    | declaration@TypeDeclaration {} <- declarations
+    , resolvedSpelling (declarationName declaration) == className
+    ]
+
+validateEntryMethod :: Declaration ResolvedName Type -> Either [Diagnostic] ()
+validateEntryMethod TypeDeclaration {typeMembers = members} =
+    case matchingMethods of
+        [] -> Left [entryProblem "TAE0004" Nothing "entry class must declare Main"]
+        [ method@FunctionDeclaration
+                { declarationAccess = access
+                , declarationIsStatic = isStatic
+                , declarationParameters = parameters
+                , declarationReturnSyntax = returnSyntax
+                }
+            ]
+                | access /= PublicAccess -> failure method "TAE0005" "entry Main must be public"
+                | not isStatic -> failure method "TAE0006" "entry Main must be static"
+                | not (null parameters) -> failure method "TAE0007" "entry Main must not declare parameters"
+                | not (syntaxSpellsVoid returnSyntax) ->
+                    failure method "TAE0008" "entry Main must return void"
+                | otherwise -> Right ()
+        _ -> Left [entryProblem "TAE0014" (declarationSpan <$> firstMethod) "entry class must declare exactly one Main method"]
+    where
+        matchingMethods =
+            [ member
+            | member@FunctionDeclaration {} <- members
+            , identifierText (resolvedSpelling (declarationName member)) == "Main"
+            ]
+        firstMethod = case matchingMethods of
+            [] -> Nothing
+            method : _ -> Just method
+        failure method code message = Left [entryProblem code (Just (declarationSpan method)) message]
+validateEntryMethod _ = Left [entryProblem "TAE0009" Nothing "entry target is not a class"]
+
+syntaxSpellsVoid :: TypeSyntax -> Bool
+syntaxSpellsVoid syntax = case syntax of
+    ExplicitType (Identifier "void") -> True
+    QualifiedTypeSyntax (QualifiedName [Identifier "void"]) [] -> True
+    _ -> False
+
+entryNamespaceName :: QualifiedName -> Either [Diagnostic] QualifiedName
+entryNamespaceName (QualifiedName parts)
+    | length parts < 2 = Left [entryProblem "TAE0001" Nothing "entry must name a namespace-qualified class"]
+    | otherwise = Right (QualifiedName (init parts))
+
+entryClassName :: QualifiedName -> Either [Diagnostic] Identifier
+entryClassName (QualifiedName parts) =
+    case reverse parts of
+        className : _ : _ -> Right className
+        _ -> Left [entryProblem "TAE0001" Nothing "entry must name a namespace-qualified class"]
+
+renderQualifiedName :: QualifiedName -> String
+renderQualifiedName (QualifiedName parts) = joinWithDot (map identifierText parts)
+
+joinWithDot :: [String] -> String
+joinWithDot [] = ""
+joinWithDot [value] = value
+joinWithDot (value : remaining) = value ++ "." ++ joinWithDot remaining
+
+unique :: (Eq value) => [value] -> [value]
+unique = foldl add []
+    where
+        add values value
+            | value `elem` values = values
+            | otherwise = values ++ [value]
+
+collectResults :: [Either [Diagnostic] value] -> Either [Diagnostic] [value]
+collectResults values =
+    case concat [problems | Left problems <- values] of
+        [] -> Right (mapMaybe rightValue values)
+        problems -> Left problems
+    where
+        rightValue (Right value) = Just value
+        rightValue (Left _) = Nothing
+
+compilerProblem :: String -> Maybe SourceSpan -> String -> Diagnostic
+compilerProblem code spanValue message = Diagnostic TypeCheckerStage Error code spanValue message
+
+entryProblem :: String -> Maybe SourceSpan -> String -> Diagnostic
+entryProblem = compilerProblem
+ src/Progmasoft/Tala/Diagnostic/SideChannel.hs view
@@ -0,0 +1,44 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Write validated frontend diagnostics to the private native-driver channel.
+
+The module never prints or chooses user-facing presentation; the C++ CLI owns
+that policy and supplies a fresh destination for each compilation request.
+-}
+module Progmasoft.Tala.Diagnostic.SideChannel
+    ( SideChannelError (..)
+    , writeDiagnosticFile
+    ) where
+
+import Control.Exception (IOException, try)
+import Data.ByteString qualified as ByteString
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Diagnostic.Protocol
+
+-- | Failure while validating, encoding, or writing a diagnostic document.
+data SideChannelError
+    = SideChannelModelError DiagnosticProtocolError
+    | SideChannelEncodingError DiagnosticProtocolError
+    | SideChannelWriteError FilePath String
+    deriving (Eq, Ord, Read, Show)
+
+-- The caller selects a fresh path for each compiler process. This function
+-- converts the frontend's one-based source model, encodes the complete TADG
+-- document in memory, and performs only one write after all validation passes.
+-- It returns failures instead of printing because the native driver owns the
+-- public presentation policy.
+
+-- | Encode a complete diagnostic document before performing one file write.
+writeDiagnosticFile :: FilePath -> [Diagnostic] -> IO (Either SideChannelError ())
+writeDiagnosticFile path diagnostics
+    | null path = pure (Left (SideChannelWriteError path "diagnostic path is empty"))
+    | otherwise = case diagnosticDocument diagnostics of
+        Left issue -> pure (Left (SideChannelModelError issue))
+        Right document -> case encodeDiagnosticDocument defaultDiagnosticProtocolLimits document of
+            Left issue -> pure (Left (SideChannelEncodingError issue))
+            Right bytes -> do
+                result <- try (ByteString.writeFile path bytes) :: IO (Either IOException ())
+                pure $ case result of
+                    Left issue -> Left (SideChannelWriteError path (show issue))
+                    Right () -> Right ()
+ src/Progmasoft/Tala/Driver/Command.hs view
@@ -0,0 +1,240 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Private command model shared by the Haskell shared-library boundary and
+the small command-line adapter. It returns owned bytes instead of opening an
+output artifact: the native driver consumes Core in memory and is the only
+component that writes user-requested outputs.
+-}
+module Progmasoft.Tala.Driver.Command
+    ( FrontendOutcome (..)
+    , FrontendOutputKind (..)
+    , runFrontendArguments
+    ) where
+
+import Data.ByteString qualified as ByteString
+import Data.Char (isAlpha, isAlphaNum)
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Text
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core (CoreModule)
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.SourceSet
+import Progmasoft.Tala.SourceWarnings
+
+{- | Output tags are part of the private in-process ABI, not public artifact
+names. Each output is borrowed only for the synchronous native callback.
+-}
+data FrontendOutputKind
+    = CoreWireOutput
+    | ProjectSourceListOutput
+    | DiagnosticWireOutput
+    | ErrorTextOutput
+    deriving (Bounded, Enum, Eq, Ord, Read, Show)
+
+{- | Owned result of one private frontend request, ready for native consumption.
+
+A success carries the warnings of the sources it compiled. A rejection lists
+its errors first and the warnings after them, because a warning often
+explains the error that follows it in the same statement.
+-}
+data FrontendOutcome
+    = FrontendSuccess FrontendOutputKind ByteString.ByteString [Diagnostic]
+    | FrontendDiagnostics [Diagnostic]
+    | FrontendFailure String
+
+data FrontendCommand
+    = CompileFile FilePath
+    | CompileFiles QualifiedName [FilePath]
+    | CompileProject FilePath QualifiedName [FilePath] [FilePath]
+    | ListProjectSources FilePath [FilePath] [FilePath]
+
+data CommandOptions = CommandOptions
+    { optionSourceFiles :: [FilePath]
+    , optionProjectRoot :: Maybe FilePath
+    , optionEntry :: Maybe String
+    , optionSourceRoots :: [FilePath]
+    , optionExcludes :: [FilePath]
+    , optionListSources :: Bool
+    }
+
+emptyOptions :: CommandOptions
+emptyOptions = CommandOptions [] Nothing Nothing [] [] False
+
+{- | Parse the deliberately narrow private argument vocabulary. Values remain
+separate from option names so file names and glob patterns cannot be
+reinterpreted by a shell or by option-prefix heuristics.
+-}
+runFrontendArguments :: [String] -> IO FrontendOutcome
+runFrontendArguments arguments = case parseCommand arguments of
+    Left problem -> pure (FrontendFailure problem)
+    Right command -> executeCommand command
+
+parseCommand :: [String] -> Either String FrontendCommand
+parseCommand arguments = parseOptions emptyOptions arguments >>= finishCommand
+
+parseOptions :: CommandOptions -> [String] -> Either String CommandOptions
+parseOptions options [] = Right options
+parseOptions options (name : remaining) = case name of
+    "--source-file" -> repeatedValue name addSourceFile options remaining
+    "--project-root" -> uniqueValue name optionProjectRoot setProjectRoot options remaining
+    "--entry" -> uniqueValue name optionEntry setEntry options remaining
+    "--source-root" -> repeatedValue name addSourceRoot options remaining
+    "--exclude" -> repeatedValue name addExclude options remaining
+    "--list-sources"
+        | optionListSources options -> Left "--list-sources may be specified only once"
+        | otherwise -> parseOptions options {optionListSources = True} remaining
+    _ -> Left ("unknown private frontend option: " ++ name)
+
+uniqueValue ::
+    String ->
+    (CommandOptions -> Maybe String) ->
+    (String -> CommandOptions -> CommandOptions) ->
+    CommandOptions ->
+    [String] ->
+    Either String CommandOptions
+uniqueValue name getter setter options remaining = case remaining of
+    [] -> Left (name ++ " requires one value")
+    value : rest
+        | null value -> Left (name ++ " value is empty")
+        | getter options /= Nothing -> Left (name ++ " may be specified only once")
+        | otherwise -> parseOptions (setter value options) rest
+
+repeatedValue ::
+    String ->
+    (String -> CommandOptions -> CommandOptions) ->
+    CommandOptions ->
+    [String] ->
+    Either String CommandOptions
+repeatedValue name setter options remaining = case remaining of
+    [] -> Left (name ++ " requires one value")
+    value : rest
+        | null value -> Left (name ++ " value is empty")
+        | otherwise -> parseOptions (setter value options) rest
+
+setProjectRoot, setEntry :: String -> CommandOptions -> CommandOptions
+setProjectRoot value options = options {optionProjectRoot = Just value}
+setEntry value options = options {optionEntry = Just value}
+
+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"
+
+{- | 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.
+-}
+loadSourceFiles :: [FilePath] -> IO (Either [Diagnostic] [LoadedSource])
+loadSourceFiles paths = do
+    loaded <- mapM loadSourceFile paths
+    let problems = concat [issues | Left issues <- loaded]
+        sources = [source | Right source <- loaded]
+    pure $ if null problems then Right (uniqueSources sources) else Left problems
+    where
+        uniqueSources = foldr keepFirst []
+        keepFirst source rest =
+            source : filter ((/= loadedSourcePath source) . loadedSourcePath) rest
+
+parseQualifiedName :: String -> Either String QualifiedName
+parseQualifiedName text =
+    let parts = splitDot text
+     in if length parts < 2
+            then Left "--entry must name a namespace-qualified class"
+            else
+                if all validIdentifier parts
+                    then Right (QualifiedName (map Identifier parts))
+                    else Left ("--entry contains an invalid identifier: " ++ text)
+
+validIdentifier :: String -> Bool
+validIdentifier [] = False
+validIdentifier (first : remaining) =
+    (isAlpha first || first == '_') && all (\value -> isAlphaNum value || value == '_') remaining
+
+splitDot :: String -> [String]
+splitDot [] = [""]
+splitDot ('.' : remaining) = "" : splitDot remaining
+splitDot (value : remaining) = case splitDot remaining of
+    [] -> [[value]]
+    first : rest -> (value : first) : rest
+
+executeCommand :: FrontendCommand -> IO FrontendOutcome
+executeCommand command = case command of
+    CompileFile 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
+                        Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
+                        Right artifacts -> encodeCoreOutcome warnings (artifactOptimizedCore artifacts)
+    CompileFiles 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)
+                 in case compiled of
+                        Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
+                        Right core -> encodeCoreOutcome warnings core
+    CompileProject 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
+                        Left diagnostics -> pure (FrontendDiagnostics (diagnostics ++ warnings))
+                        Right artifacts -> encodeCoreOutcome warnings (projectEntryCore artifacts)
+    ListProjectSources root roots excludes -> do
+        discovered <- discoverSourceSet (SourceSetRequest root roots excludes)
+        pure $ case discovered of
+            Left diagnostics -> FrontendDiagnostics diagnostics
+            Right paths ->
+                FrontendSuccess
+                    ProjectSourceListOutput
+                    (Text.encodeUtf8 (Text.pack (concatMap (++ "\0") paths)))
+                    []
+
+toProjectCompilerInput :: LoadedSource -> CompilerInput
+toProjectCompilerInput source = CompilerInput (loadedSourceRelativePath source) (loadedSourceText source)
+
+loadedSourceWarnings :: LoadedSource -> [Diagnostic]
+loadedSourceWarnings source = sourceWarnings (loadedSourceRelativePath source) (loadedSourceText source)
+
+encodeCoreOutcome :: [Diagnostic] -> CoreModule -> IO FrontendOutcome
+encodeCoreOutcome warnings core = pure $ case encodeCore defaultCoreWireLimits core of
+    Left issue -> FrontendFailure ("could not encode verified Core: " ++ show issue)
+    Right bytes -> FrontendSuccess CoreWireOutput (ByteString.pack bytes) warnings
+ src/Progmasoft/Tala/Pipeline/Stage.hs view
@@ -0,0 +1,31 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Stage names shared by the compiler driver's internal pipeline reporting.
+Only Core, Xpp, and Xmm currently have explicit emission extensions.
+-}
+module Progmasoft.Tala.Pipeline.Stage
+    ( Stage (..)
+    , artifactExtension
+    ) where
+
+-- | Stable stage names in the renewed Tala compiler pipeline.
+data Stage
+    = ParsedAst
+    | ResolvedAst
+    | TypedAst
+    | Core
+    | CorePrep
+    | Xpp
+    | Xmm
+    | LlvmBitcode
+    deriving (Bounded, Enum, Eq, Ord, Read, Show)
+
+{- | Public file extension for stages that may be explicitly emitted.
+Other stages remain in-memory unless a later backend contract defines an artifact.
+-}
+artifactExtension :: Stage -> Maybe String
+artifactExtension Core = Just ".core"
+artifactExtension Xpp = Just ".xpp"
+artifactExtension Xmm = Just ".xmm"
+artifactExtension _ = Nothing
+ tala-compiler.cabal view
@@ -0,0 +1,176 @@+cabal-version: 3.12
+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+name: tala-compiler
+version: 0.1.0
+synopsis: Target-independent Tala compiler driver
+description:
+    The target-independent driver of the Tala compiler: it runs the
+    frontend and Core over a program and produces verified CorePrep.
+    .
+    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
+    (LICENSE-EXCEPTION), an additional permission to MPL-2.0 for works that
+    combine this package with independent code.
+license: MPL-2.0
+license-files:
+    LICENSE
+    LICENSE-EXCEPTION
+author: Progmasoft
+maintainer: support@progmasoft.com
+homepage: https://tala.run/
+bug-reports: https://github.com/Progmasoft/tala/issues
+category: Language, Compiler
+tested-with: GHC ==9.10.3
+build-type: Simple
+
+library
+    exposed-modules:
+        Progmasoft.Tala.Compiler
+        Progmasoft.Tala.Driver.Command
+        Progmasoft.Tala.Diagnostic.SideChannel
+        Progmasoft.Tala.Pipeline.Stage
+    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.FloatingLiteral,
+        tala-syntax:Progmasoft.Tala.Lexer,
+        tala-syntax:Progmasoft.Tala.NestingLimits,
+        tala-syntax:Progmasoft.Tala.NumericLiteral,
+        tala-syntax:Progmasoft.Tala.Parser,
+        tala-syntax:Progmasoft.Tala.RuntimeCall,
+        tala-syntax:Progmasoft.Tala.SourceText,
+        tala-syntax:Progmasoft.Tala.SourceWarnings,
+        tala-frontend:Progmasoft.Tala.Frontend,
+        tala-frontend:Progmasoft.Tala.NumericSemantics,
+        tala-frontend:Progmasoft.Tala.BuiltinTypes,
+        tala-frontend:Progmasoft.Tala.ConstantEvaluation,
+        tala-frontend:Progmasoft.Tala.Closure.Analysis,
+        tala-frontend:Progmasoft.Tala.Resolver.NameResolution,
+        tala-frontend:Progmasoft.Tala.Resolver.Renamer,
+        tala-frontend:Progmasoft.Tala.SourceSet,
+        tala-frontend:Progmasoft.Tala.TemplateValue,
+        tala-frontend:Progmasoft.Tala.Template.Application,
+        tala-frontend:Progmasoft.Tala.Template.Discovery,
+        tala-frontend:Progmasoft.Tala.Template.Freshen,
+        tala-frontend:Progmasoft.Tala.Template.Instantiation,
+        tala-frontend:Progmasoft.Tala.Template.Mangling,
+        tala-frontend:Progmasoft.Tala.Template.MemberReachability,
+        tala-frontend:Progmasoft.Tala.Template.Specialization,
+        tala-frontend:Progmasoft.Tala.Template.Specialization.Verifier,
+        tala-frontend:Progmasoft.Tala.Template.Verifier,
+        tala-frontend:Progmasoft.Tala.TypeClassification,
+        tala-frontend:Progmasoft.Tala.TypeChecker,
+        tala-core:Progmasoft.Tala.Core,
+        tala-core:Progmasoft.Tala.Core.Artifact,
+        tala-core:Progmasoft.Tala.Core.CorePrep,
+        tala-core:Progmasoft.Tala.Core.CorePrep.Verifier,
+        tala-core:Progmasoft.Tala.Core.CorePrep.Wire,
+        tala-core:Progmasoft.Tala.Core.CorePrep.Wire.Decode,
+        tala-core:Progmasoft.Tala.Core.CorePrep.Wire.Encode,
+        tala-core:Progmasoft.Tala.Core.CorePrep.Wire.Format,
+        tala-core:Progmasoft.Tala.Core.Monomorphization,
+        tala-core:Progmasoft.Tala.Core.Optimizer,
+        tala-core:Progmasoft.Tala.Core.Optimizer.IntegerFacts,
+        tala-core:Progmasoft.Tala.Core.Scalar,
+        tala-core:Progmasoft.Tala.Core.Specialization,
+        tala-core:Progmasoft.Tala.Core.Symbols,
+        tala-core:Progmasoft.Tala.Core.Template,
+        tala-core:Progmasoft.Tala.Core.Verifier,
+        tala-core:Progmasoft.Tala.Core.Wire,
+        tala-core:Progmasoft.Tala.Desugarer
+    hs-source-dirs: src
+    build-depends:
+        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
+    default-language: GHC2024
+
+test-suite tala-compiler-tests
+    type: exitcode-stdio-1.0
+    main-is: Main.hs
+    other-modules:
+        ClosureTests
+        CoreInliningTests
+        CoreLinearInliningTests
+        CoreOptimizerTests
+        CoreOptimizerSourceTests
+        CoreVerifierTests
+        CompileTimeParityTests
+        EnumTests
+        FloatingOptimizerTests
+        IntegerEvaluationTests
+        InferredReturnTests
+        IntegerFlowTests
+        AssignmentExpressionTests
+        ConditionalExpressionTests
+        CoreInterpreter
+        IterationTests
+        BranchingDiagnosticTests
+        BranchingEvaluationCases
+        BranchingOracleTests
+        BranchingTests
+        LazyEvaluationTests
+        ConsoleTests
+        EffectTests
+        QualifiedNameTests
+        FormatSweepTests
+        FallThroughTests
+        RuntimeCallTests
+        RuntimeText
+        MemoizeTests
+        LoopExpressionTests
+        LoopFlowTests
+        LoopFlowOracleTests
+        DiagnosticProtocolTests
+        DiagnosticSideChannelTests
+        NumericTests
+        PatternTests
+        MonomorphizationTests
+        NestingLimitTests
+        ParserContractTests
+        ScalarWireTests
+        ShortCircuitTests
+        SpecializationTests
+        SourceSetTests
+        SourceTextTests
+        SourceWarningTests
+        StaticMemberCoreTests
+        StaticMemberOverloadTests
+        StaticMemberParserTests
+        StaticMemberProjectTests
+        StaticMemberSemanticTests
+        TemplateTests
+        TemplateApplicationTests
+        TemplateDeclarationTests
+        TemplateDiscoveryTests
+        TemplateInstantiationTests
+        TemplateManglingTests
+        TemplateMemberReachabilityTests
+        TemplatePlanVerifierTests
+        TemplateSpecializationPlannerTests
+        TemplateVerifierTests
+        TypeClassificationTests
+        VoidTests
+    hs-source-dirs: test
+    build-depends:
+        base >=4.20 && <4.23,
+        bytestring >=0.12 && <0.14,
+        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
+    default-language: GHC2024
+
+source-repository head
+    type: git
+    location: https://github.com/Progmasoft/tala
+    subdir: Talac/Haskell/Driver
+ test/AssignmentExpressionTests.hs view
@@ -0,0 +1,951 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Vertical tests for assignments and increments used as values:
+@target = value@, @target op= value@, @++target@, @target++@ and
+@target--@ in operand position.
+
+Core expressions never store into a local, so these source forms lower to
+statements plus a store-free read. The shape tests pin that lowering. The
+evaluation tests are the semantic oracle: each one states the value a source
+program must compute under left-to-right evaluation and runs the reference
+evaluator of "CoreInterpreter" on the Core the Desugarer produced and again
+on the Core the optimizer left, so a lowering that reorders a read and a
+store, or an optimization that does, fails with a wrong number.
+-}
+module AssignmentExpressionTests (assignmentExpressionTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Specialization
+
+assignmentExpressionTests :: [(String, Bool)]
+assignmentExpressionTests =
+    parserTests
+        ++ typeTests
+        ++ loweringTests
+        ++ evaluationTests
+        ++ pipelineTests
+
+-- ---------------------------------------------------------------- sources
+
+-- | One method with immutable parameters; tests declare the locals they store into.
+body :: String -> String
+body statements =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ bool flag, _ bool other, _ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "    public static int Next() { return 7; }"
+        , "    public static int Twice(_ int value) { return value + value; }"
+        , "    public static int Minus(_ int first, _ int second) { return first - second; }"
+        , "    public static int Pick(_ int first, _ int second, _ int third) { return first * 100 + second * 10 + third; }"
+        , "    public static void Touch() { }"
+        , "}"
+        ]
+
+returning :: String -> String
+returning expression = body ("int a = left; int b = right; return " ++ expression ++ ";")
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource text = do
+    tokens <- runLexer defaultLexer (LexerInput "assignment.tala" text)
+    runParser defaultParser (ParserInput "assignment.tala" tokens)
+
+parseRejected :: String -> Bool
+parseRejected text = either (const True) (const False) (parseSource text)
+
+parseRejectedWith :: String -> String -> Bool
+parseRejectedWith code text = case parseSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- | Statements of the first method of the first class in a parsed unit.
+firstStatements :: String -> Maybe [Statement Identifier ()]
+firstStatements text = case parseSource text of
+    Right (ParsedAST (SyntaxTree _ (TypeDeclaration {typeMembers = member : _} : _))) -> case member of
+        FunctionDeclaration {declarationBody = Block statements} -> Just statements
+        _ -> Nothing
+    _ -> Nothing
+
+-- | The expression returned by 'returning'.
+returned :: String -> Maybe (Expression Identifier ())
+returned expression = case firstStatements (returning expression) of
+    Just [_, _, ReturnStatement _ value] -> value
+    _ -> Nothing
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "assignment.tala" text)
+
+accepted :: String -> Bool
+accepted text = either (const False) (const True) (compileSource text)
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+isName :: String -> Expression Identifier () -> Bool
+isName expected expression = case expression of
+    NameExpression _ (Identifier actual) () -> actual == expected
+    _ -> False
+
+isInteger :: Integer -> Expression Identifier () -> Bool
+isInteger expected expression = case expression of
+    LiteralExpression _ (IntegerLiteral actual) () -> actual == expected
+    _ -> False
+
+-- ---------------------------------------------------------------- parser
+
+parserTests :: [(String, Bool)]
+parserTests =
+    [ ("an assignment in operand position is an assignment expression", assignmentParses)
+    , ("assignment expressions group to the right", assignmentIsRightAssociative)
+    , ("a statement assignment keeps its statement node", statementKeepsItsNode)
+    , ("a chained statement assignment nests an assignment expression", chainedStatementNests)
+    , ("a binding initializer may be an assignment expression", bindingInitializerNests)
+    , ("every compound operator parses as an assignment expression", all compoundParses compoundOperators)
+    , ("a simple assignment groups a compound assignment on its right", simpleGroupsCompound)
+    , ("assignment binds more weakly than a conditional", assignmentIsWeakerThanConditional)
+    , ("assignment binds more weakly than logical OR", assignmentIsWeakerThanLogicalOr)
+    , ("an assignment is a call argument", assignmentIsCallArgument)
+    , ("a parenthesized assignment is an operand of a binary operator", parenthesizedAssignmentIsOperand)
+    , ("an unparenthesized assignment cannot be a conditional result", parseRejected (returning "flag ? a = 1 : 2"))
+    , ("an unparenthesized assignment cannot follow a binary operator", parseRejected (returning "1 + a = 2"))
+    , ("a call is not an assignment target in an expression", parseRejected (returning "Next() = 1"))
+    , ("prefix increment parses as the prefix form", incrementParses "++a" True)
+    , ("postfix increment parses as the postfix form", incrementParses "a++" False)
+    , ("a double dash after a name starts a comment, not a decrement", doubleDashIsComment)
+    , ("the old postfix decrement spelling leaves only the name before the comment", oldPostfixSpellingIsName)
+    , ("the old prefix decrement spelling is entirely a comment", oldPrefixSpellingIsComment)
+    , ("a postfix increment is the left operand of a following addition", postfixThenAddition)
+    , ("a prefix increment is an operand of unary minus", prefixUnderNegation)
+    , ("a statement increment keeps its statement node", incrementStatementKeepsItsNode)
+    , ("a statement compound assignment keeps its statement node", compoundStatementKeepsItsNode)
+    , ("prefix increment of a literal has no storage", parseRejectedWith "TAP0028" (returning "++10"))
+    , ("prefix increment of a parenthesized value has no storage", parseRejectedWith "TAP0028" (returning "++(a)"))
+    , ("postfix increment of a sum has no storage", parseRejectedWith "TAP0028" (returning "(a + b)++"))
+    , ("postfix increment of a call has no storage", parseRejectedWith "TAP0028" (returning "Next()++"))
+    , ("postfix increment of a literal has no storage", parseRejectedWith "TAP0028" (returning "10++"))
+    , ("an increment result is not a storage location", parseRejected (returning "a++++"))
+    , ("discard is not a value in a binding", parseRejectedWith "TAP0032" (body "int value = (_ = Next()); return value;"))
+    , ("discard is not a value in a return", parseRejectedWith "TAP0032" (returning "_ = 1"))
+    , ("discard is not a value in a call argument", parseRejectedWith "TAP0032" (returning "Twice(_ = 1)"))
+    , ("the discard statement itself still parses", discardStatementStillParses)
+    ,
+        ( "null-coalescing assignment is still reported as unimplemented"
+        , parseRejectedWith "TAP0031" (body "int a = 0; a ??= 1; return a;")
+        )
+    ]
+
+compoundOperators :: [(String, BinaryOperator)]
+compoundOperators =
+    [ ("+=", Add)
+    , ("-=", Subtract)
+    , ("*=", Multiply)
+    , ("/=", Divide)
+    , ("//=", FloorDivide)
+    , ("%=", Remainder)
+    , ("**=", Power)
+    , ("<<=", ShiftLeft)
+    , (">>=", ShiftRight)
+    , ("&=", BitwiseAnd)
+    , ("^=", BitwiseXor)
+    , ("|=", BitwiseOr)
+    ]
+
+assignmentParses :: Bool
+assignmentParses = case returned "a = 5" of
+    Just (AssignmentExpression _ Nothing (Identifier "a") value ()) -> isInteger 5 value
+    _ -> False
+
+assignmentIsRightAssociative :: Bool
+assignmentIsRightAssociative = case returned "a = b = 5" of
+    Just (AssignmentExpression _ Nothing (Identifier "a") (AssignmentExpression _ Nothing (Identifier "b") value ()) ()) ->
+        isInteger 5 value
+    _ -> False
+
+statementKeepsItsNode :: Bool
+statementKeepsItsNode = case firstStatements (body "int a = 0; a = 5; return a;") of
+    Just [_, AssignmentStatement _ (Identifier "a") () value, _] -> isInteger 5 value
+    _ -> False
+
+chainedStatementNests :: Bool
+chainedStatementNests = case firstStatements (body "int a = 0; int b = 0; a = b = 10; return a;") of
+    Just [_, _, AssignmentStatement _ (Identifier "a") () (AssignmentExpression _ Nothing (Identifier "b") value ()), _] ->
+        isInteger 10 value
+    _ -> False
+
+bindingInitializerNests :: Bool
+bindingInitializerNests = case firstStatements (body "int a = 0; int b = a = 3; return b;") of
+    Just [_, BindingStatement _ _ _ (Identifier "b") () (AssignmentExpression _ Nothing (Identifier "a") value ()), _] ->
+        isInteger 3 value
+    _ -> False
+
+compoundParses :: (String, BinaryOperator) -> Bool
+compoundParses (operatorText, operator) = case returned ("a " ++ operatorText ++ " b") of
+    Just (AssignmentExpression _ (Just actual) (Identifier "a") value ()) -> actual == operator && isName "b" value
+    _ -> False
+
+simpleGroupsCompound :: Bool
+simpleGroupsCompound = case returned "a = b += 2" of
+    Just (AssignmentExpression _ Nothing (Identifier "a") (AssignmentExpression _ (Just Add) (Identifier "b") value ()) ()) ->
+        isInteger 2 value
+    _ -> False
+
+assignmentIsWeakerThanConditional :: Bool
+assignmentIsWeakerThanConditional = case returned "a = flag ? 1 : 2" of
+    Just (AssignmentExpression _ Nothing (Identifier "a") (ConditionalExpression _ condition first second ()) ()) ->
+        isName "flag" condition && isInteger 1 first && isInteger 2 second
+    _ -> False
+
+assignmentIsWeakerThanLogicalOr :: Bool
+assignmentIsWeakerThanLogicalOr = case firstStatements (body "bool seen = false; return (seen = flag || other) ? 1 : 0;") of
+    Just
+        [ _
+            , ReturnStatement _ (Just (ConditionalExpression _ (AssignmentExpression _ Nothing (Identifier "seen") value ()) _ _ ()))
+            ] ->
+            case value of
+                BinaryExpression _ LogicalOr left right () -> isName "flag" left && isName "other" right
+                _ -> False
+    _ -> False
+
+assignmentIsCallArgument :: Bool
+assignmentIsCallArgument = case returned "Twice(a = 3)" of
+    Just (CallExpression _ callee [AssignmentExpression _ Nothing (Identifier "a") value ()] ()) ->
+        isName "Twice" callee && isInteger 3 value
+    _ -> False
+
+parenthesizedAssignmentIsOperand :: Bool
+parenthesizedAssignmentIsOperand = case returned "a + (a = 5)" of
+    Just (BinaryExpression _ Add left (AssignmentExpression _ Nothing (Identifier "a") value ()) ()) ->
+        isName "a" left && isInteger 5 value
+    _ -> False
+
+incrementParses :: String -> Bool -> Bool
+incrementParses expression isPrefix = case returned expression of
+    Just (IncrementExpression _ actualPrefix (Identifier "a") ()) -> actualPrefix == isPrefix
+    _ -> False
+
+-- `a--; return a;` is the name `a` followed by a comment. The name is then
+-- the last thing in the block, which reads it as the block's final
+-- expression: nothing is subtracted and no diagnostic is produced.
+oldPostfixSpellingIsName :: Bool
+oldPostfixSpellingIsName = case firstStatements (body "int a = 1; a--; return a;") of
+    Just [BindingStatement {}, ExpressionStatement _ value False] -> isName "a" value
+    _ -> False
+
+oldPrefixSpellingIsComment :: Bool
+oldPrefixSpellingIsComment = case firstStatements (body "int a = 1; --a; return a;") of
+    Just [BindingStatement {}] -> True
+    _ -> False
+
+-- The language has no decrement operator, so everything from `--` to the end
+-- of the line is a comment and the statement before it is all that remains.
+doubleDashIsComment :: Bool
+doubleDashIsComment = case firstStatements (body "return left;-- - right--;") of
+    Just [ReturnStatement _ (Just value)] -> isName "left" value
+    _ -> False
+
+postfixThenAddition :: Bool
+postfixThenAddition = case returned "a+++b" of
+    Just (BinaryExpression _ Add (IncrementExpression _ False (Identifier "a") ()) right ()) -> isName "b" right
+    _ -> False
+
+prefixUnderNegation :: Bool
+prefixUnderNegation = case returned "-++a" of
+    Just (UnaryExpression _ UnaryNegate (IncrementExpression _ True (Identifier "a") ()) ()) -> True
+    _ -> False
+
+incrementStatementKeepsItsNode :: Bool
+incrementStatementKeepsItsNode = case firstStatements (body "int a = 0; a++; ++a; return a;") of
+    Just [_, IncrementStatement _ (Identifier "a") (), IncrementStatement _ (Identifier "a") (), _] -> True
+    _ -> False
+
+compoundStatementKeepsItsNode :: Bool
+compoundStatementKeepsItsNode = case firstStatements (body "int a = 0; a += 2; return a;") of
+    Just [_, CompoundAssignmentStatement _ Add (Identifier "a") () value, _] -> isInteger 2 value
+    _ -> False
+
+discardStatementStillParses :: Bool
+discardStatementStillParses = case firstStatements (body "_ = Next(); return 0;") of
+    Just [DiscardStatement _ _, _] -> True
+    _ -> False
+
+-- ---------------------------------------------------------------- type checker
+
+typeTests :: [(String, Bool)]
+typeTests =
+    [ ("an assignment expression is accepted where its target type fits", accepted (returning "(a = 5) + b"))
+    , ("a chained assignment is accepted", accepted (body "int a = 0; int b = 0; a = b = left; return a + b;"))
+    , ("a compound assignment expression is accepted", accepted (returning "(a += 5) * 2"))
+    , ("a Boolean assignment expression is accepted", accepted (body "bool seen = false; return (seen = flag) ? 1 : 0;"))
+    , ("an increment expression is accepted", accepted (returning "a++ + ++b"))
+    , ("an assignment expression statement is not a pure value statement", accepted (body "int a = 0; (a = 5); return a;"))
+    ,
+        ( "an assignment in a while condition is accepted"
+        , accepted (body "int a = left; int v = 0; while ((v = (a -= 1)) > 0) { } return v;")
+        )
+    , ("an assignment yields the target type, not the value type", yieldsTargetType)
+    ,
+        ( "a statement assignment types its literal from the target"
+        , accepted (body "long wide = 1; wide = 5; return wide > 4 ? 1 : 0;")
+        )
+    ,
+        ( "a literal that does not fit the target is still rejected"
+        , not (accepted (body "ubyte small = 1; small = 300; return 0;"))
+        )
+    , ("assigning to a parameter is rejected", rejectedWith "TAT0003" (body "return (left = 1);"))
+    , ("assigning to a final binding is rejected", rejectedWith "TAT0003" (body "final int a = 1; return (a = 2);"))
+    ,
+        ( "a compound assignment to a final binding is rejected"
+        , rejectedWith "TAT0003" (body "final int a = 1; return (a += 2);")
+        )
+    , ("a value of the wrong type is rejected", rejectedWith "TAT0004" (body "int a = 0; return (a = flag) ? 1 : 0;"))
+    , ("an assignment result of the wrong type is rejected by its receiver", resultTypeIsChecked)
+    ,
+        ( "a compound operator that does not apply is rejected"
+        , rejectedWith "TAT0012" (body "bool seen = false; return (seen += flag) ? 1 : 0;")
+        )
+    ,
+        ( "a compound result that is not the target type is rejected"
+        , rejectedWith "TAT0035" (body "double ratio = 7.5; return (ratio //= 2.0) > 1.0 ? 1 : 0;")
+        )
+    , ("incrementing a final binding is rejected", rejectedWith "TAT0022" (body "final int a = 1; return a++;"))
+    , ("incrementing a parameter is rejected", rejectedWith "TAT0022" (body "return ++left;"))
+    , ("incrementing a Boolean is rejected", rejectedWith "TAT0024" (body "bool seen = false; return seen++ ? 1 : 0;"))
+    , ("an assignment to an undeclared name is rejected", not (accepted (body "return (missing = 1);")))
+    , ("an increment of an undeclared name is rejected", not (accepted (body "return missing++;")))
+    , ("the value of an assignment is checked like any other", not (accepted (body "int a = 0; return (a = absent);")))
+    ]
+
+-- The stored value is read back from the target, so the expression has the
+-- target type even when the right operand is a literal that could have been
+-- typed otherwise.
+yieldsTargetType :: Bool
+yieldsTargetType = case compileSource (body "long wide = 1; long copy = (wide = 5); return copy > 4 ? 1 : 0;") of
+    Right _ -> True
+    Left _ -> False
+
+resultTypeIsChecked :: Bool
+resultTypeIsChecked =
+    not (accepted (body "long wide = 1; int narrow = 0; narrow = (wide += 1); return narrow;"))
+
+-- ---------------------------------------------------------------- lowering
+
+loweringTests :: [(String, Bool)]
+loweringTests =
+    [ ("an assignment expression lowers to a store and a read of the target", assignmentLowers)
+    , ("a prefix increment lowers to a store and a read of the target", prefixLowers)
+    , ("a postfix increment keeps the previous value in a temporary", postfixLowers)
+    , ("a later store binds an earlier read of the same local", earlierReadIsBound)
+    , ("a later store binds an earlier call", earlierCallIsBound)
+    , ("a later store does not bind a literal", literalIsNotBound)
+    , ("a later store does not bind a read of another local", otherLocalIsNotBound)
+    , ("operands after the store are not bound", laterOperandIsNotBound)
+    , ("a compound assignment keeps its earlier target value when its operand stores", compoundKeepsTarget)
+    , ("a compound assignment without a storing operand has no temporary", compoundHasNoTemporary)
+    , ("a store in a conditional result becomes a statement inside that branch", conditionalArmIsLazy)
+    , ("a conditional whose test stores stays a Core conditional", conditionalTestStaysExpression)
+    , ("a store on the right of && runs under the left operand", logicalAndIsLazy)
+    , ("a store on the right of || runs only when the left operand is false", logicalOrIsLazy)
+    , ("a store in a coalescing fallback runs under the false branch", coalesceFallbackIsLazy)
+    , ("a while condition that stores is tested at the top of the body", whileConditionMoves)
+    , ("a while condition without a store stays in the loop header", whileConditionStays)
+    , ("a for condition that stores keeps the update clause", forConditionMoves)
+    , ("a do/while condition that stores is skipped on the first pass only", doWhileConditionMoves)
+    , ("a do/while condition without a store stays a Core do/while", doWhileConditionStays)
+    , ("an assignment expression statement leaves no evaluation statement", expressionStatementHasNoEvaluate)
+    , ("a discarded assignment leaves no evaluation statement", discardedAssignmentHasNoEvaluate)
+    , ("a discarded call is still an evaluation statement", discardedCallIsEvaluated)
+    , ("generated temporaries are immutable except result slots", temporariesAreImmutable)
+    ]
+
+-- | Unoptimized Core body of the method named @Evaluate@.
+loweredBody :: String -> Maybe [CoreStatement]
+loweredBody text = case compileSource text of
+    Right artifacts -> functionBody "Evaluate" (artifactCore artifacts)
+    Left _ -> Nothing
+
+functionBody :: String -> CoreModule -> Maybe [CoreStatement]
+functionBody name moduleValue =
+    case [coreFunctionBody function | function <- coreModuleFunctions moduleValue, spelling (coreFunctionName function) == name] of
+        statements : _ -> Just statements
+        [] -> Nothing
+
+spelling :: ResolvedName -> String
+spelling = identifierText . resolvedSpelling
+
+-- | The statements after the two bindings that 'returning' declares.
+loweredReturning :: String -> Maybe [CoreStatement]
+loweredReturning expression = drop 2 <$> loweredBody (returning expression)
+
+isRead :: String -> CoreExpression -> Bool
+isRead expected expression = case expression of
+    CoreVariable name _ -> spelling name == expected
+    _ -> False
+
+isGenerated :: String -> ResolvedName -> Bool
+isGenerated prefix name = prefix `isPrefixOf` spelling name
+
+isLiteral :: Integer -> CoreExpression -> Bool
+isLiteral expected expression = case expression of
+    CoreLiteral (CoreInteger actual) _ -> actual == expected
+    _ -> False
+
+assignmentLowers :: Bool
+assignmentLowers = case loweredReturning "a = 5" of
+    Just [CoreAssign target value, CoreReturn result] -> spelling target == "a" && isLiteral 5 value && isRead "a" result
+    _ -> False
+
+prefixLowers :: Bool
+prefixLowers = case loweredReturning "++a" of
+    Just [CoreAssign target (CorePrimitive CoreAdd [from, one] _), CoreReturn result] ->
+        spelling target == "a" && isRead "a" from && isLiteral 1 one && isRead "a" result
+    _ -> False
+
+postfixLowers :: Bool
+postfixLowers = case loweredReturning "a++" of
+    Just
+        [ CoreBind (CoreBinding previous _ False initial)
+            , CoreAssign target (CorePrimitive CoreAdd [CoreVariable from _, one] _)
+            , CoreReturn (CoreVariable result _)
+            ] ->
+            isGenerated "$previous" previous
+                && isRead "a" initial
+                && spelling target == "a"
+                && from == previous
+                && isLiteral 1 one
+                && result == previous
+    _ -> False
+
+earlierReadIsBound :: Bool
+earlierReadIsBound = case loweredReturning "a + (a = 5)" of
+    Just
+        [ CoreBind (CoreBinding held _ False initial)
+            , CoreAssign target _
+            , CoreReturn (CorePrimitive CoreAdd [CoreVariable left _, right] _)
+            ] ->
+            isGenerated "$operand" held && isRead "a" initial && spelling target == "a" && left == held && isRead "a" right
+    _ -> False
+
+earlierCallIsBound :: Bool
+earlierCallIsBound = case loweredReturning "Next() + (a = 5)" of
+    Just
+        [ CoreBind (CoreBinding held _ False CoreApply {})
+            , CoreAssign _ _
+            , CoreReturn (CorePrimitive CoreAdd [CoreVariable left _, _] _)
+            ] ->
+            isGenerated "$operand" held && left == held
+    _ -> False
+
+literalIsNotBound :: Bool
+literalIsNotBound = case loweredReturning "1 + (a = 5)" of
+    Just [CoreAssign _ _, CoreReturn (CorePrimitive CoreAdd [left, right] _)] -> isLiteral 1 left && isRead "a" right
+    _ -> False
+
+otherLocalIsNotBound :: Bool
+otherLocalIsNotBound = case loweredReturning "b + (a = 5)" of
+    Just [CoreAssign _ _, CoreReturn (CorePrimitive CoreAdd [left, right] _)] -> isRead "b" left && isRead "a" right
+    _ -> False
+
+laterOperandIsNotBound :: Bool
+laterOperandIsNotBound = case loweredReturning "(a = 5) + a" of
+    Just [CoreAssign _ _, CoreReturn (CorePrimitive CoreAdd [left, right] _)] -> isRead "a" left && isRead "a" right
+    _ -> False
+
+compoundKeepsTarget :: Bool
+compoundKeepsTarget = case loweredBody (body "int a = left; a += (a = right); return a;") of
+    Just
+        [ _
+            , CoreBind (CoreBinding previous _ False initial)
+            , CoreAssign inner _
+            , CoreAssign outer (CorePrimitive CoreAdd [CoreVariable from _, operand] _)
+            , _
+            ] ->
+            isGenerated "$target" previous
+                && isRead "a" initial
+                && spelling inner == "a"
+                && spelling outer == "a"
+                && from == previous
+                && isRead "a" operand
+    _ -> False
+
+compoundHasNoTemporary :: Bool
+compoundHasNoTemporary = case loweredBody (body "int a = left; int b = 0; a += (b = right); return a;") of
+    Just [_, _, CoreAssign inner _, CoreAssign outer (CorePrimitive CoreAdd [from, operand] _), _] ->
+        spelling inner == "b" && spelling outer == "a" && isRead "a" from && isRead "b" operand
+    _ -> False
+
+conditionalArmIsLazy :: Bool
+conditionalArmIsLazy = case loweredReturning "flag ? (a = 1) : b" of
+    Just
+        [ CoreBind (CoreBinding slot _ True _)
+            , CoreIf condition [CoreAssign stored _, CoreAssign first _] [CoreAssign second value]
+            , CoreReturn (CoreVariable result _)
+            ] ->
+            isGenerated "$selected" slot
+                && isRead "flag" condition
+                && spelling stored == "a"
+                && first == slot
+                && second == slot
+                && isRead "b" value
+                && result == slot
+    _ -> False
+
+conditionalTestStaysExpression :: Bool
+conditionalTestStaysExpression = case loweredReturning "(a = 0) ? 1 : 2" of
+    Just [CoreAssign target _, CoreReturn (CoreConditional condition _ _ _)] -> spelling target == "a" && isRead "a" condition
+    _ -> False
+
+logicalAndIsLazy :: Bool
+logicalAndIsLazy = case loweredBody (body "int a = 0; bool both = flag && (a = 5) > 0; return a;") of
+    Just
+        [ _
+            , CoreBind (CoreBinding slot _ True initial)
+            , CoreIf left [CoreAssign stored _, CoreIf _ [CoreAssign set _] []] []
+            , CoreBind _
+            , _
+            ] ->
+            isGenerated "$logical" slot
+                && initial == CoreLiteral (CoreBoolean False) boolType
+                && isRead "flag" left
+                && spelling stored == "a"
+                && set == slot
+    _ -> False
+
+logicalOrIsLazy :: Bool
+logicalOrIsLazy = case loweredBody (body "int a = 0; bool either = flag || (a = 5) > 0; return a;") of
+    Just
+        [ _
+            , CoreBind (CoreBinding slot _ True _)
+            , CoreIf left [CoreAssign set _] [CoreAssign stored _, CoreIf _ [CoreAssign later _] []]
+            , CoreBind _
+            , _
+            ] ->
+            isGenerated "$logical" slot && isRead "flag" left && set == slot && spelling stored == "a" && later == slot
+    _ -> False
+
+coalesceFallbackIsLazy :: Bool
+coalesceFallbackIsLazy = case loweredReturning "b ?: (a = 9)" of
+    Just
+        [ CoreBind (CoreBinding subject _ False initial)
+            , CoreBind (CoreBinding slot _ True _)
+            , CoreIf (CoreVariable test _) [CoreAssign kept (CoreVariable keptValue _)] [CoreAssign stored _, CoreAssign replaced _]
+            , CoreReturn (CoreVariable result _)
+            ] ->
+            isGenerated "$coalesce" subject
+                && isRead "b" initial
+                && isGenerated "$selected" slot
+                && test == subject
+                && kept == slot
+                && keptValue == subject
+                && spelling stored == "a"
+                && replaced == slot
+                && result == slot
+    _ -> False
+
+alwaysTrue :: CoreExpression
+alwaysTrue = CoreLiteral (CoreBoolean True) boolType
+
+whileConditionMoves :: Bool
+whileConditionMoves = case loweredBody (body "int a = left; int v = 0; while ((v = a) > 0) { a -= 1; } return v;") of
+    Just [_, _, CoreWhile header (CoreAssign stored _ : CoreIf _ [] [CoreBreak] : loopBody), _] ->
+        header == alwaysTrue && spelling stored == "v" && length loopBody == 1
+    _ -> False
+
+whileConditionStays :: Bool
+whileConditionStays = case loweredBody (body "int a = left; while (a > 0) { a -= 1; } return a;") of
+    Just [_, CoreWhile (CorePrimitive CoreGreaterThan _ _) [_], _] -> True
+    _ -> False
+
+forConditionMoves :: Bool
+forConditionMoves = case loweredBody (body "int v = 0; for (int i = 0; (v = i) < left; i++) { } return v;") of
+    Just [_, _, CoreFor header [CoreAssign stored _, CoreIf _ [] [CoreBreak]] [CoreAssign updated _], _] ->
+        header == alwaysTrue && spelling stored == "v" && spelling updated == "i"
+    _ -> False
+
+doWhileConditionMoves :: Bool
+doWhileConditionMoves = case loweredBody (body "int a = left; int n = 0; do { n += 1; } while ((a -= 1) > 0); return n;") of
+    Just
+        [ _
+            , _
+            , CoreBind (CoreBinding first _ True initial)
+            , CoreWhile header [CoreIf (CoreVariable test _) [CoreAssign cleared _] [CoreAssign stored _, CoreIf _ [] [CoreBreak]], _]
+            , _
+            ] ->
+            isGenerated "$first" first
+                && initial == alwaysTrue
+                && header == alwaysTrue
+                && test == first
+                && cleared == first
+                && spelling stored == "a"
+    _ -> False
+
+doWhileConditionStays :: Bool
+doWhileConditionStays = case loweredBody (body "int a = left; do { a -= 1; } while (a > 0); return a;") of
+    Just [_, CoreDoWhile [_] (CorePrimitive CoreGreaterThan _ _), _] -> True
+    _ -> False
+
+evaluations :: [CoreStatement] -> Int
+evaluations statements = length [() | CoreEvaluate _ <- statements]
+
+expressionStatementHasNoEvaluate :: Bool
+expressionStatementHasNoEvaluate = case loweredBody (body "int a = 0; (a = 5); a++; return a;") of
+    Just statements -> evaluations statements == 0 && length statements == 4
+    Nothing -> False
+
+discardedAssignmentHasNoEvaluate :: Bool
+discardedAssignmentHasNoEvaluate = case loweredBody (body "int a = 0; _ = (a = 5); _ = a++; return a;") of
+    Just statements -> evaluations statements == 0
+    Nothing -> False
+
+discardedCallIsEvaluated :: Bool
+discardedCallIsEvaluated = case loweredBody (body "int a = 0; _ = Twice(a = 5); return a;") of
+    Just [_, CoreAssign _ _, CoreEvaluate CoreApply {}, _] -> True
+    _ -> False
+
+temporariesAreImmutable :: Bool
+temporariesAreImmutable = all immutableTemporaries (map fst evaluationCases)
+    where
+        immutableTemporaries statements = case compileSource (body statements) of
+            Right artifacts -> all acceptable (moduleBindings (artifactCore artifacts))
+            Left _ -> False
+        acceptable binding =
+            let name = coreBindingName binding
+                slot = any (`isGenerated` name) ["$selected", "$logical", "$first"]
+                held = any (`isGenerated` name) ["$operand", "$previous", "$target", "$coalesce", "$pattern"]
+             in if held then not (coreBindingMutable binding) else not slot || coreBindingMutable binding
+
+moduleStatements :: CoreModule -> [CoreStatement]
+moduleStatements = concatMap (concatMap nested . coreFunctionBody) . coreModuleFunctions
+    where
+        nested statement =
+            statement : case statement of
+                CoreIf _ whenTrue whenFalse -> concatMap nested (whenTrue ++ whenFalse)
+                CoreWhile _ loopBody -> concatMap nested loopBody
+                CoreDoWhile loopBody _ -> concatMap nested loopBody
+                CoreFor _ loopBody update -> concatMap nested (loopBody ++ update)
+                _ -> []
+
+moduleBindings :: CoreModule -> [CoreBinding]
+moduleBindings moduleValue = [binding | CoreBind binding <- moduleStatements moduleValue]
+
+-- ---------------------------------------------------------------- evaluation
+
+{- | Source bodies with the arguments to run them on and the value each run
+must return. The expected values follow from left-to-right evaluation of
+operands and from the Spec rules that an assignment yields the stored value,
+a prefix form the new value and a postfix form the previous one.
+-}
+evaluationCases :: [(String, [((Bool, Bool, Integer, Integer), Integer)])]
+evaluationCases =
+    [ ("int a = left; int b = (a = right); return a * 10 + b;", [(plain 3 4, 44), (plain 0 9, 99)])
+    , ("int a = 0; int b = 0; a = b = left; return a * 10 + b;", [(plain 3 0, 33)])
+    , ("int a = 1; int b = 2; int c = 3; a = b = c = left; return a + b + c;", [(plain 5 0, 15)])
+    , ("int a = left; return a = right;", [(plain 3 4, 4)])
+    , ("int a = left; return a + (a = right);", [(plain 3 4, 7), (plain 10 1, 11)])
+    , ("int a = left; return (a = right) + a;", [(plain 3 4, 8)])
+    , ("int a = left; return (a + 1) * (a = right);", [(plain 3 4, 16)])
+    , ("int a = left; return a + (a = right) + a;", [(plain 3 4, 11)])
+    , ("int a = left; return a - (a = right) - a;", [(plain 10 3, 4)])
+    , ("int a = left; return (a = 2) * (a = 3) + a;", [(plain 0 0, 9)])
+    , ("int a = left; return -(a = right) + a;", [(plain 3 4, 0)])
+    , ("int a = left; int b = right; return (a = b) + (b = 1) + a + b;", [(plain 3 4, 10)])
+    , ("int a = left; return a++ + a;", [(plain 3 0, 7)])
+    , ("int a = left; return ++a + a;", [(plain 3 0, 8)])
+    , ("int a = left; return a++ + a++;", [(plain 3 0, 7)])
+    , ("int a = left; return ++a * ++a;", [(plain 3 0, 20)])
+    , ("int a = left; int b = a++; return a * 10 + b;", [(plain 3 0, 43)])
+    , ("int a = left; int b = ++a; return a * 10 + b;", [(plain 3 0, 44)])
+    , ("int a = left; return a + a++ + a;", [(plain 3 0, 10)])
+    , ("int a = left; int total = (a += right); return a + total;", [(plain 3 4, 14)])
+    , ("int a = left; a += (a = right); return a;", [(plain 3 4, 7)])
+    , ("int a = left; a -= (a = right); return a;", [(plain 10 4, 6)])
+    , ("int a = left; return (a += 1) * (a += 2);", [(plain 3 0, 24)])
+    , ("int a = left; return a + (a += right) + a;", [(plain 3 4, 17)])
+    , ("int a = left; a *= a++; return a;", [(plain 3 0, 9)])
+    , ("int a = left; int b = right; a = b += 2; return a * 10 + b;", [(plain 0 4, 66)])
+    , ("int a = left; int b = right; a += b -= 1; return a * 10 + b;", [(plain 3 4, 63)])
+    , ("int a = left; return (a <<= 2) + a;", [(plain 3 0, 24)])
+    , ("int a = left; return (a %= right) + a;", [(plain 17 5, 4)])
+    , ("int a = left; return (a &= right) | (a ^= 1);", [(plain 6 3, 3)])
+    , ("int a = left; return Twice(a = right) + a;", [(plain 3 4, 12)])
+    , ("int a = left; return Twice(a) + (a = right);", [(plain 3 4, 10)])
+    , ("int a = left; return Minus(a, a = right);", [(plain 10 3, 7)])
+    , ("int a = left; return Minus(a = right, a);", [(plain 10 3, 0)])
+    , ("int a = left; return Minus(a++, a++);", [(plain 5 0, -1)])
+    , ("int a = left; return Pick(a, a = right, a);", [(plain 1 2, 122)])
+    , ("int a = left; return Pick(a++, a++, a);", [(plain 1 0, 123)])
+    , ("int a = left; return Pick(Twice(a), a += 1, Twice(a));", [(plain 1 0, 224)])
+    ,
+        ( "int a = left; int r = flag ? (a = 1) : (a = 2); return a * 10 + r;"
+        , [(flags True False 7 0, 11), (flags False False 7 0, 22)]
+        )
+    ,
+        ( "int a = left; int r = flag ? (a = 100) : right; return a + r;"
+        , [(flags True False 3 4, 200), (flags False False 3 4, 7)]
+        )
+    ,
+        ( "int a = left; int r = flag ? right : (a = 100); return a + r;"
+        , [(flags True False 3 4, 7), (flags False False 3 4, 200)]
+        )
+    , ("int a = left; int r = (a = right) ? a + 1 : -1; return r;", [(plain 3 4, 5), (plain 3 0, -1)])
+    ,
+        ( "int a = left; int r = flag ? (other ? (a = 1) : (a = 2)) : (a = 3); return a * 10 + r;"
+        , [(flags True True 9 0, 11), (flags True False 9 0, 22), (flags False True 9 0, 33)]
+        )
+    , ("int a = left; return a + (flag ? (a = right) : 0) + a;", [(flags True False 3 4, 11), (flags False False 3 4, 6)])
+    ,
+        ( "int a = 0; bool r = flag && (a = 5) > 0; return a + (r ? 100 : 0);"
+        , [(flags True False 0 0, 105), (flags False False 0 0, 0)]
+        )
+    ,
+        ( "int a = 9; bool r = flag && (a = 0) > 0; return a + (r ? 100 : 0);"
+        , [(flags True False 0 0, 0), (flags False False 0 0, 9)]
+        )
+    ,
+        ( "int a = 0; bool r = flag || (a = 5) > 3; return a + (r ? 100 : 0);"
+        , [(flags True False 0 0, 100), (flags False False 0 0, 105)]
+        )
+    , ("int a = 0; bool r = flag || (a = 5) > 7; return a + (r ? 100 : 0);", [(flags False False 0 0, 5)])
+    ,
+        ( "int a = 0; bool r = (a = left) > 0 && (a = right) > 0; return a + (r ? 100 : 0);"
+        , [(plain 3 4, 104), (plain 0 4, 0), (plain 3 0, 0)]
+        )
+    ,
+        ( "int a = 0; int b = 0; bool r = flag && (a = 1) > 0 && (b = 2) > 0; return a * 10 + b + (r ? 100 : 0);"
+        , [(flags True False 0 0, 112), (flags False False 0 0, 0)]
+        )
+    ,
+        ( "int a = 0; bool r = flag && other || (a = 5) > 0; return a + (r ? 100 : 0);"
+        , [(flags True True 0 0, 100), (flags True False 0 0, 105)]
+        )
+    , ("int a = 0; int r = left ?: (a = 9); return a * 100 + r;", [(plain 4 0, 4), (plain 0 0, 909)])
+    , ("int a = left; int r = (a = right) ?: 7; return a * 10 + r;", [(plain 3 4, 44), (plain 3 0, 7)])
+    , ("int a = 0; if ((a = left) > 2) { return a; } return -a;", [(plain 5 0, 5), (plain 1 0, -1)])
+    ,
+        ( "int n = 0; int sum = 0; int v = 0; while ((v = n++) < left) { sum += v; } return sum * 10 + n;"
+        , [(plain 4 0, 65), (plain 0 0, 1)]
+        )
+    ,
+        ( "int n = 0; int sum = 0; int v = 0; while ((v = n++) < left) { if (v == 2) { continue; } sum += v; } return sum;"
+        , [(plain 4 0, 4)]
+        )
+    ,
+        ( "int n = 0; int sum = 0; int v = 0; while ((v = n++) < left) { if (v == 2) { break; } sum += v; } return sum * 10 + n;"
+        , [(plain 4 0, 13)]
+        )
+    , ("int n = 0; int count = 0; while (n++ < left) { count++; } return count * 10 + n;", [(plain 3 0, 34)])
+    , ("int n = 0; int count = 0; while (++n < left) { count++; } return count * 10 + n;", [(plain 3 0, 23)])
+    ,
+        ( "int n = left; int count = 0; do { count++; } while ((n = n - 1) > 0); return count;"
+        , [(plain 3 0, 3), (plain 0 0, 1)]
+        )
+    ,
+        ( "int n = left; int count = 0; do { count++; if (count == 12) { continue; } count += 10; } while ((n -= 1) > 0); return count;"
+        , [(plain 3 0, 23)]
+        )
+    ,
+        ( "int n = left; int count = 0; do { count++; if (count == 2) { break; } } while ((n -= 1) > 0); return count * 10 + n;"
+        , [(plain 5 0, 24)]
+        )
+    , ("int n = 0; int count = 0; do { count++; } while (n++ < left); return count * 10 + n;", [(plain 3 0, 44)])
+    ,
+        ( "int sum = 0; int v = 0; for (int i = 0; (v = i * 2) < left; i++) { if (v == 2) { continue; } sum += v; } return sum;"
+        , [(plain 7 0, 10)]
+        )
+    ,
+        ( "int sum = 0; int v = 0; for (int i = 0; (v = i * 2) < left; i++) { if (v == 4) { break; } sum += v; } return sum * 10 + v;"
+        , [(plain 7 0, 24)]
+        )
+    , ("int sum = 0; for (int i = 0; i++ < left; i += 0) { sum += i; } return sum;", [(plain 3 0, 6)])
+    ,
+        ( "int total = 0; for (int i = 0; i < left; i += 1) { int j = 0; while ((j += 1) < 3) { total += i * j; } } return total;"
+        , [(plain 3 0, 9)]
+        )
+    , ("int a = left; (a = right); return a;", [(plain 3 4, 4)])
+    , ("int a = left; _ = (a = right); return a;", [(plain 3 4, 4)])
+    , ("int a = left; _ = Twice(a = right); return a;", [(plain 3 4, 4)])
+    , ("int a = left; _ = a++; _ = ++a; return a;", [(plain 3 0, 5)])
+    , ("int a = left; int b = 0; b = a++ + a++ + a++; return b * 10 + a;", [(plain 1 0, 64)])
+    , ("int a = left; bool big = (a += right) is > 5; return big ? a : -a;", [(plain 3 4, 7), (plain 1 2, -3)])
+    ]
+
+plain :: Integer -> Integer -> (Bool, Bool, Integer, Integer)
+plain = flags False False
+
+flags :: Bool -> Bool -> Integer -> Integer -> (Bool, Bool, Integer, Integer)
+flags flag other left right = (flag, other, left, right)
+
+evaluationTests :: [(String, Bool)]
+evaluationTests =
+    concat
+        [ [ ("unoptimized Core computes " ++ label, runs artifactCore statements arguments expected)
+          , ("optimized Core computes " ++ label, runs artifactOptimizedCore statements arguments expected)
+          ]
+        | (statements, runsOfCase) <- evaluationCases
+        , (arguments, expected) <- runsOfCase
+        , let label = show expected ++ " for " ++ show arguments ++ ": " ++ statements
+        ]
+        ++ [ ("the reference evaluator rejects a run that never ends", endlessLoopIsBounded)
+           , ("the reference evaluator computes a plain loop", plainLoopEvaluates)
+           ]
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> (Bool, Bool, Integer, Integer) -> Integer -> Bool
+runs select statements (flag, other, left, right) expected = case compileSource (body statements) of
+    Right artifacts ->
+        runFunction
+            (select artifacts)
+            "Evaluate"
+            [BooleanValue flag, BooleanValue other, IntegerValue left, IntegerValue right]
+            == Just (IntegerValue expected)
+    Left _ -> False
+
+-- The evaluator must fail closed: a wrong lowering that loops forever has to
+-- fail its test, not hang the suite.
+endlessLoopIsBounded :: Bool
+endlessLoopIsBounded = case compileSource (body "int a = 0; while (true) { a += 1; } return a;") of
+    Right artifacts ->
+        runFunctionWithBudget
+            5000
+            (artifactCore artifacts)
+            "Evaluate"
+            [BooleanValue False, BooleanValue False, IntegerValue 0, IntegerValue 0]
+            == Nothing
+    Left _ -> False
+
+plainLoopEvaluates :: Bool
+plainLoopEvaluates = runs artifactCore "int sum = 0; for (int i = 1; i <= left; i += 1) { sum += i; } return sum;" (plain 10 0) 55
+
+-- ---------------------------------------------------------------- pipeline
+
+pipelineTests :: [(String, Bool)]
+pipelineTests =
+    [ ("every lowered case verifies as Core", all coreVerifies (map fst evaluationCases))
+    , ("every optimized case verifies as Core", all optimizedVerifies (map fst evaluationCases))
+    , ("every case verifies as CorePrep", all corePrepVerifies (map fst evaluationCases))
+    , ("Core wire round-trips every lowered case", all wireRoundTrips (map fst evaluationCases))
+    , ("a closure body may store into its own local", accepted closureSource)
+    , ("an explicit capture initializer may store into an enclosing local", accepted captureSource)
+    , ("a template member keeps its assignment and increment expressions when instantiated", templateInstantiates)
+    , ("a call in an assigned value is reachable from the template member", templateReachesAssignedCalls)
+    , ("a member that no assignment reaches is not instantiated", "Unused" `notElem` specializedNames)
+    ]
+
+coreVerifies :: String -> Bool
+coreVerifies statements = case compileSource (body statements) of
+    Right artifacts -> verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+    Left _ -> False
+
+optimizedVerifies :: String -> Bool
+optimizedVerifies statements = case compileSource (body statements) of
+    Right artifacts -> verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+    Left _ -> False
+
+corePrepVerifies :: String -> Bool
+corePrepVerifies statements = case compileSource (body statements) of
+    Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+
+wireRoundTrips :: String -> Bool
+wireRoundTrips statements = case compileSource (body statements) of
+    Right artifacts ->
+        let core = artifactCore artifacts
+         in (encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits) == Right core
+    Left _ -> False
+
+templateSource :: String
+templateSource =
+    unwords
+        [ "template<typename T> class Box {"
+        , "int Entry(_ int seed) {"
+        , "int value = seed;"
+        , "int copy = 0;"
+        , "copy = value = First(seed);"
+        , "int total = (value += Second(copy)) + value++ + ++copy;"
+        , "while ((copy = Third(copy)) > 0) { total += copy; }"
+        , "return total;"
+        , "}"
+        , "int First(_ int value) { return value; }"
+        , "int Second(_ int value) { return value; }"
+        , "int Third(_ int value) { return value - 1; }"
+        , "int Unused() { return 9; }"
+        , "}"
+        ]
+
+-- | Specialize @Box<string>@ for its @Entry@ member only.
+templatePlan :: Maybe TemplateSpecializationPlan
+templatePlan = case analyzeSemantics (CompilerInput "assignment-template.tala" templateSource) of
+    Right artifacts ->
+        either
+            (const Nothing)
+            Just
+            ( planTemplateSpecializations
+                defaultTemplateSpecializationLimits
+                (semanticTypedAST artifacts)
+                [ TemplateSpecializationDemand
+                    (TemplateApplication (QualifiedName [Identifier "Box"]) [TypeTemplateArgument stringType])
+                    (TemplateMemberDemand [Identifier "Entry"])
+                    "assignment-template"
+                ]
+            )
+    Left _ -> Nothing
+
+specializedMembers :: [Declaration ResolvedName Type]
+specializedMembers = case templatePlan of
+    Just plan ->
+        concat
+            [ members
+            | TypeDeclaration {typeMembers = members} <-
+                map templateSpecializationDeclaration (plannedTemplateSpecializations plan)
+            ]
+    Nothing -> []
+
+specializedNames :: [String]
+specializedNames = map (identifierText . resolvedSpelling . declarationName) specializedMembers
+
+-- The instantiated declaration is a fresh, closed copy of the template
+-- member, so both new nodes must have been copied by every template pass.
+templateInstantiates :: Bool
+templateInstantiates =
+    let closed = show specializedMembers
+     in all (`isInfixOf` closed) ["AssignmentExpression", "IncrementExpression"]
+
+templateReachesAssignedCalls :: Bool
+templateReachesAssignedCalls = all (`elem` specializedNames) ["Entry", "First", "Second", "Third"]
+
+closureSource :: String
+closureSource =
+    unlines
+        [ "class Program {"
+        , "    public static int Run(_ int seed) {"
+        , "        auto step = \\(int value) -> { int local = value; return (local += 1) + local++; };"
+        , "        return step(seed);"
+        , "    }"
+        , "}"
+        ]
+
+captureSource :: String
+captureSource =
+    unlines
+        [ "class Program {"
+        , "    public static int Run(_ int seed) {"
+        , "        int counter = seed;"
+        , "        auto read = [held = counter++] \\ -> held;"
+        , "        return read() + counter;"
+        , "    }"
+        , "}"
+        ]
+ test/BranchingDiagnosticTests.hs view
@@ -0,0 +1,317 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Where the diagnostics of @match@, @if@ expressions, @guard@ and nested
+blocks point, and that damaged spellings of those forms are always answered.
+
+An editor underlines the range a diagnostic carries, so the tests pin the
+start of every range: the arm that can never be selected and not the whole
+match, the pattern that binds a duplicate name and not its arm. The
+robustness tests feed the frontend every prefix of a program that uses the
+new forms, and the same program with each token removed in turn. The parser
+has loops of its own for subjects, arms and patterns; a damaged input must
+end each of them with a diagnostic or a tree, never with a hang or an
+exception. The last group pins how the forms behave in a template body and
+on an @else if@ chain far longer than any test above the frontend used
+before.
+-}
+module BranchingDiagnosticTests (branchingDiagnosticTests) where
+
+import CoreInterpreter
+import Data.List (intercalate)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+
+branchingDiagnosticTests :: [(String, Bool)]
+branchingDiagnosticTests = positionTests ++ robustnessTests ++ templateTests ++ chainTests
+
+-- ------------------------------------------------------------- positions
+
+{- | A method whose body starts on line 3, column 1, so the columns in the
+expectations below are the columns of the statement text itself.
+-}
+method :: [String] -> String
+method statements =
+    unlines
+        ( [ "class Program {"
+          , "public static int Evaluate(_ bool flag, _ int left, _ int right) {"
+          ]
+            ++ statements
+            ++ ["}", "}"]
+        )
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "branching-diagnostic.tala" text)
+
+-- | The one-based line and column at which each diagnostic with the code starts.
+startsOf :: String -> String -> [(Int, Int)]
+startsOf code text = case compileSource text of
+    Right _ -> []
+    Left problems ->
+        [ (sourceLine start, sourceColumn start)
+        | problem <- problems
+        , diagnosticCode problem == code
+        , Just spanValue <- [diagnosticSpan problem]
+        , let start = sourceStart spanValue
+        ]
+
+-- | Whether the code is reported exactly once, at the given position.
+reportedAt :: String -> (Int, Int) -> [String] -> Bool
+reportedAt code position statements = startsOf code (method statements) == [position]
+
+positionTests :: [(String, Bool)]
+positionTests =
+    [
+        ( "an incomplete match expression is reported at the match"
+        , reportedAt "TAT0052" (3, 8) ["return match (left) {", "1 -> 10", "};"]
+        )
+    ,
+        ( "an arm that can never be selected is reported at that arm"
+        , reportedAt "TAT0053" (5, 1) ["match (left) {", "1 -> { },", "1 -> { },", "_ -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "every arm that can never be selected is reported"
+        , startsOf "TAT0053" (method ["match (left) {", "_ -> { },", "1 -> { },", "2 -> { }", "}", "return 0;"])
+            == [(5, 1), (6, 1)]
+        )
+    ,
+        ( "a wrong number of patterns is reported at the arm"
+        , reportedAt "TAT0048" (4, 1) ["match (left), (right) {", "1 -> { },", "(_), (_) -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "a guard of the wrong type is reported at the guard expression"
+        , reportedAt "TAT0049" (4, 6) ["match (left) {", "1 if \"text\" -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "arms of different types are reported at the match"
+        , reportedAt "TAT0050" (4, 10) ["long wide = 3;", "long r = match (left) {", "1 -> wide,", "_ -> left", "};", "return 0;"]
+        )
+    ,
+        ( "a literal pattern of the wrong type is reported at the pattern"
+        , reportedAt "TAT0054" (4, 1) ["match (left) {", "\"text\" -> { }", "}", "return 0;"]
+        )
+    , ("a null pattern is reported at the pattern", reportedAt "TAT0055" (4, 1) ["match (left) {", "null -> { }", "}", "return 0;"])
+    , ("an enum case pattern is reported at the pattern", reportedAt "TAT0056" (4, 1) ["match (left) {", ".Ready -> { }", "}", "return 0;"])
+    ,
+        ( "a type pattern of another type is reported at the pattern"
+        , reportedAt "TAT0057" (4, 6) ["match (left), (right) {", "(_), (long other) -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "a subject of an unsupported type is reported at the subject"
+        , reportedAt "TAT0058" (3, 16) ["match (left), (\\(int v) -> v) {", "(_), (_) -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "a duplicate pattern binding is reported at the second pattern"
+        , reportedAt "TAR0008" (4, 14) ["match (left), (right) {", "(int value), (int value) -> { }", "}", "return 0;"]
+        )
+    ,
+        ( "a value block without a value is reported at the block"
+        , reportedAt "TAT0046" (3, 19) ["int r = if (flag) { } else { 2 };", "return r;"]
+        )
+    ,
+        ( "a return of the wrong type in a value block is reported at the return"
+        , reportedAt "TAT0005" (3, 21) ["int r = if (flag) { return true; } else { 3 };", "return r;"]
+        )
+    ,
+        ( "a break without a loop in a value block is reported at the break"
+        , reportedAt "TAT0025" (3, 21) ["int r = if (flag) { break; } else { 2 };", "return r;"]
+        )
+    ,
+        ( "a return of the wrong type in a value block of a loop expression is reported at the return"
+        , reportedAt
+            "TAT0005"
+            (4, 21)
+            ["int r = while (true) {", "int q = if (flag) { return true; } else { 2 };", "break q;", "};", "return r;"]
+        )
+    ,
+        ( "a break without a value out of a loop expression is reported at the break"
+        , reportedAt
+            "TAT0040"
+            (4, 21)
+            ["int r = while (true) {", "int q = if (flag) { break; } else { 2 };", "break q;", "};", "return r;"]
+        )
+    ,
+        ( "a continue inside a callable in a loop is reported at the continue"
+        , reportedAt
+            "TAT0027"
+            (4, 37)
+            ["while (flag) {", "auto f = \\(int v) -> { if (v > 0) { continue; } return v; };", "}", "return 0;"]
+        )
+    ,
+        ( "a value-carrying break in the update of a loop statement is reported at the break"
+        , reportedAt "TAT0026" (3, 41) ["for (int i = 0; i < 3; i += if (flag) { break 1; } else { 1 }) { }", "return 0;"]
+        )
+    ,
+        ( "a guard block that does not leave is reported at the guard"
+        , reportedAt "TAT0061" (3, 1) ["guard (flag) else {", "int unused = left;", "}", "return 0;"]
+        )
+    ,
+        ( "a guard condition of the wrong type is reported at the guard"
+        , reportedAt "TAT0060" (3, 1) ["guard (\"text\") else { return 0; }", "return 1;"]
+        )
+    ,
+        ( "a redeclaration in a nested block is reported at the inner binding"
+        , reportedAt "TAR0003" (5, 1) ["int a = left;", "{", "int a = right;", "}", "return a;"]
+        )
+    ]
+
+-- ------------------------------------------------------------ robustness
+
+-- | A program that uses every new form.
+specimen :: String
+specimen =
+    intercalate
+        " "
+        [ "class Program { public static int Evaluate(_ bool flag, _ int left, _ int right) {"
+        , "guard (left >= 0) else { return 0; }"
+        , "{ int scoped = left; }"
+        , "int kind = match (left), (flag) { (0), (_) -> 10, (1), (true) -> 20,"
+        , "(int low), (_) if low < 5 -> { int doubled = low * 2; doubled } (_), (_) -> 30 };"
+        , "int total = 0;"
+        , "for (int index = 0; index < right; index++) {"
+        , "match (index % 3) { 0 -> { continue; }, 1 -> total += 10, _ -> { total += index; } }"
+        , "guard (total < 40) else { break; } }"
+        , "return if (kind > total) { kind - total } else { total - kind }; } }"
+        ]
+
+-- | Lex and parse; the lexer never fails on these inputs' prefixes it accepts.
+parseOutcome :: String -> Either [Diagnostic] ParsedAST
+parseOutcome text = do
+    tokens <- runLexer defaultLexer (LexerInput "branching-robustness.tala" text)
+    runParser defaultParser (ParserInput "branching-robustness.tala" tokens)
+
+-- | Whether the frontend answers: a tree, artifacts, or diagnostics.
+answered :: String -> Bool
+answered text = case parseOutcome text of
+    Left problems -> not (null problems)
+    Right _ -> case compileSource text of
+        Left problems -> not (null problems)
+        Right _ -> True
+
+-- | The specimen with the token at the given index removed.
+withoutToken :: [String] -> Int -> String
+withoutToken tokens index = unwords (take index tokens ++ drop (index + 1) tokens)
+
+-- | The specimen with the token at the given index written twice.
+withRepeatedToken :: [String] -> Int -> String
+withRepeatedToken tokens index = unwords (take (index + 1) tokens ++ drop index tokens)
+
+robustnessTests :: [(String, Bool)]
+robustnessTests =
+    [ ("the specimen itself compiles", either (const False) (const True) (compileSource specimen))
+    , ("every prefix of the specimen is answered", all (answered . (`take` specimen)) [0 .. length specimen])
+    ,
+        ( "no proper prefix of the specimen is accepted as a complete program with a body"
+        , all (not . compiles . (`take` specimen)) [length "class Program { " .. length specimen - 1]
+        )
+    , ("the specimen without any one token is answered", all (answered . withoutToken tokens) [0 .. length tokens - 1])
+    , ("the specimen with any one token repeated is answered", all (answered . withRepeatedToken tokens) [0 .. length tokens - 1])
+    ]
+    where
+        tokens = words specimen
+        compiles text = either (const False) (const True) (compileSource text)
+
+-- ------------------------------------------------------------- templates
+
+templateTests :: [(String, Bool)]
+templateTests =
+    [
+        ( "a template member may match over values of concrete types"
+        , accepted
+            [ "template<typename T> class Box {"
+            , "public static int Kind(_ T value, _ int code) {"
+            , "guard (code >= 0) else { return 0; }"
+            , "return match (code) { 1 -> 10, int other if other > 5 -> other, _ -> if (code > 2) { 1 } else { 2 } };"
+            , "}"
+            , "}"
+            ]
+        )
+    , -- A value of a type parameter has no known representation in the open
+      -- template body, so it is rejected like a conditional result of that
+      -- type, which reports TAT0039.
+        ( "a match result of a type parameter is not lowered yet"
+        , rejectedWith
+            "TAT0051"
+            [ "template<typename T> class Box {"
+            , "public static T Same(_ T value, _ int code) {"
+            , "return match (code) { 1 -> value, _ -> value };"
+            , "}"
+            , "}"
+            ]
+        )
+    ,
+        ( "a match subject of a type parameter is not lowered yet"
+        , rejectedWith
+            "TAT0058"
+            [ "template<typename T> class Box {"
+            , "public static int Kind(_ T value) {"
+            , "return match (value) { T other -> 1 };"
+            , "}"
+            , "}"
+            ]
+        )
+    ,
+        ( "a conditional result of a type parameter reports the conditional code"
+        , rejectedWith
+            "TAT0039"
+            [ "template<typename T> class Box {"
+            , "public static T Pick(_ bool flag, _ T first, _ T second) {"
+            , "return flag ? first : second;"
+            , "}"
+            , "}"
+            ]
+        )
+    ]
+    where
+        accepted source = either (const False) (const True) (compileSource (unlines source))
+        rejectedWith code source = case compileSource (unlines source) of
+            Left problems -> any ((== code) . diagnosticCode) problems
+            Right _ -> False
+
+-- ----------------------------------------------------------------- chains
+
+{- | An @else if@ chain of the given length: link @index@ returns
+@index * 3 + 1@ and the statement after the chain returns 0.
+-}
+chainProgram :: Int -> String
+chainProgram links =
+    unlines
+        [ "class Program {"
+        , "public static int Evaluate(_ int value) {"
+        , concat
+            [ (if index == 0 then "if" else " else if")
+                ++ " (value == "
+                ++ show index
+                ++ ") { return "
+                ++ show (index * 3 + 1)
+                ++ "; }"
+            | index <- [0 .. links - 1]
+            ]
+        , "return 0;"
+        , "}"
+        , "}"
+        ]
+
+chainValue :: (FrontendArtifacts -> CoreModule) -> Int -> Integer -> Maybe Integer
+chainValue select links subject = case compileSource (chainProgram links) of
+    Right artifacts -> case runFunction (select artifacts) "Evaluate" [IntegerValue subject] of
+        Just (IntegerValue value) -> Just value
+        _ -> Nothing
+    Left _ -> Nothing
+
+-- Core nests one level per link; the frontend and its evaluator walk that
+-- depth on the growable Haskell stack, so only the answers are pinned here.
+-- The native stages after Core have their own tests for the same chain.
+chainTests :: [(String, Bool)]
+chainTests =
+    [ ( "an else-if chain of " ++ show links ++ " links selects subject " ++ show subject ++ " " ++ mode
+      , chainValue select links subject == Just (if subject < toInteger links then subject * 3 + 1 else 0)
+      )
+    | links <- [150, 400]
+    , subject <- [0, toInteger links - 1, toInteger links]
+    , (mode, select) <- [("unoptimized", artifactCore), ("optimized", artifactOptimizedCore)]
+    ]
+ test/BranchingEvaluationCases.hs view
@@ -0,0 +1,466 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | The programs that "BranchingTests" runs, with the value each must return.
+
+Generated by @go -C helpers run ./cmd/execution-cases generate@. Do not edit:
+the cases are written in the files under @Talac/Fuzzing/Cases@, which also
+generate the tables the native smoke program @source_execution_smoke@ runs
+through LLVM.
+
+A case is the body of @int Run(bool flag, bool other, int left, int right)@
+and the runs of that body. The expected values are written by hand from the
+language rules.
+-}
+module BranchingEvaluationCases
+    ( EvaluationCase
+    , evaluationCases
+    , selectionCases
+    , leavingCases
+    ) where
+
+-- | A body, and for each run its arguments and the value it must return.
+type EvaluationCase = (String, [((Bool, Bool, Integer, Integer), Integer)])
+
+-- | Every case, in the order the tests report them.
+evaluationCases :: [EvaluationCase]
+evaluationCases = selectionCases ++ leavingCases
+
+-- | Which arm, block or body is selected, and how often each part runs. From @Talac/Fuzzing/Cases/Selection.cases@.
+selectionCases :: [EvaluationCase]
+selectionCases =
+    [
+        ( "return match (left) { 1 -> 10, 2 -> 20, _ -> 30 };"
+        , [((False, False, 1, 0), 10), ((False, False, 2, 0), 20), ((False, False, 5, 0), 30)]
+        )
+    , -- Negative constants.
+        ( "int v = 0 - left; return match (v) { -1 -> 10, -2 -> 20, 0 -> 5, _ -> 7 };"
+        , [((False, False, 1, 0), 10), ((False, False, 2, 0), 20), ((False, False, 0, 0), 5), ((False, False, 3, 0), 7)]
+        )
+    ,
+        ( "int v = 0 - left - 9223372036854775807; return match (v) { -9223372036854775808 -> 1, -9223372036854775807 -> 2, _ -> 0 };"
+        , [((False, False, 1, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ,
+        ( "int v = right - left; return match (v), (flag) { (-1), (true) -> 1, (-1), (_) -> 2, (_), (_) -> 3 };"
+        , [((True, False, 3, 2), 1), ((False, False, 3, 2), 2), ((True, False, 2, 2), 3)]
+        )
+    , -- Two bool subjects covered without a catch-all arm.
+        ( "return match (flag), (other) { (true), (_) -> 1, (false), (true) -> 2, (false), (false) -> 3 };"
+        , [((True, True, 0, 0), 1), ((True, False, 0, 0), 1), ((False, True, 0, 0), 2), ((False, False, 0, 0), 3)]
+        )
+    ,
+        ( "return match (flag), (other) { (true), (true) -> 3, (true), (_) -> 2, (_), (true) -> 1, (_), (_) -> 0 };"
+        , [((True, True, 0, 0), 3), ((True, False, 0, 0), 2), ((False, True, 0, 0), 1), ((False, False, 0, 0), 0)]
+        )
+    , -- The subject is evaluated once, whichever arm accepts.
+        ( "int n = left; int r = match (n += 1) { 1 -> 100, 2 -> 200, _ -> 300 }; return r + n;"
+        , [((False, False, 0, 0), 101), ((False, False, 1, 0), 202), ((False, False, 5, 0), 306)]
+        )
+    , -- Subjects are evaluated left to right, before any arm is tested.
+        ( "int n = left; return match (n += 1), (n * 10) { (2), (20) -> 1, (_), (_) -> 0 };"
+        , [((False, False, 1, 0), 1), ((False, False, 2, 0), 0)]
+        )
+    , -- A guard runs only when the patterns of its arm accept.
+        ( "int calls = 0; int r = match (left) { 1 if (calls += 1) > 0 -> 10, 2 if (calls += 10) > 0 -> 20, _ -> 30 }; return r * 100 + calls;"
+        , [((False, False, 1, 0), 1001), ((False, False, 2, 0), 2010), ((False, False, 3, 0), 3000)]
+        )
+    , -- A guard that is false passes the value on to the arms after it.
+        ( "return match (left) { 1 if flag -> 1, 1 -> 2, _ -> 3 };"
+        , [((True, False, 1, 0), 1), ((False, False, 1, 0), 2), ((True, False, 9, 0), 3)]
+        )
+    , -- A numeric guard is tested in Boolean context.
+        ( "return match (left) { 1 if right -> 1, _ -> 0 };"
+        , [((False, False, 1, 5), 1), ((False, False, 1, 0), 0), ((False, False, 2, 5), 0)]
+        )
+    , -- Only the body of the accepting arm runs.
+        ( "int n = 0; int r = match (left) { 1 -> (n += 1), 2 -> (n += 10), _ -> (n += 100) }; return r * 1000 + n;"
+        , [((False, False, 1, 0), 1001), ((False, False, 2, 0), 10010), ((False, False, 3, 0), 100100)]
+        )
+    ,
+        ( "return match (Twice(left)) { 4 -> 1, 6 -> 2, _ -> 0 };"
+        , [((False, False, 2, 0), 1), ((False, False, 3, 0), 2), ((False, False, 4, 0), 0)]
+        )
+    ,
+        ( "long wide = 5; return match (wide) { 5 -> 1, _ -> 0 };"
+        , [((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "long wide = match (left) { 1 -> 10, _ -> 20 }; return wide > 15 ? 1 : 0;"
+        , [((False, False, 1, 0), 0), ((False, False, 2, 0), 1)]
+        )
+    ,
+        ( "int r = 0; match (left) { 1 -> r = 5, _ -> r = Twice(left) } return r;"
+        , [((False, False, 1, 0), 5), ((False, False, 4, 0), 8)]
+        )
+    ,
+        ( "return match (left) { 1 -> if (flag) { 5 } else { 6 }, _ -> match (right) { 0 -> 7, _ -> 8 } };"
+        , [((True, False, 1, 0), 5), ((False, False, 1, 0), 6), ((False, False, 2, 0), 7), ((False, False, 2, 3), 8)]
+        )
+    ,
+        ( "return match (left) { 1 -> { int t = right * 2; t + 1 }, _ -> { int t = right * 3; t - 1 } };"
+        , [((False, False, 1, 4), 9), ((False, False, 2, 4), 11)]
+        )
+    , -- A statement arm may continue or leave the enclosing loop.
+        ( "int total = 0; for (int i = 0; i < left; i++) { match (i) { 2 -> { continue; }, 5 -> { break; }, _ -> { total += i; } } } return total;"
+        , [((False, False, 10, 0), 8), ((False, False, 3, 0), 1), ((False, False, 0, 0), 0)]
+        )
+    , -- A statement arm may supply the value of an enclosing loop expression.
+        ( "int n = 0; int r = while (true) { n += 1; match (n) { 4 -> { break n * 10; }, _ -> { } } }; return r;"
+        , [((False, False, 0, 0), 40)]
+        )
+    ,
+        ( "match (left) { 1 -> { return 100; }, _ -> { } } return 5;"
+        , [((False, False, 1, 0), 100), ((False, False, 2, 0), 5)]
+        )
+    ,
+        ( "match (left) { } return 5;"
+        , [((False, False, 1, 0), 5)]
+        )
+    ,
+        ( "int r = if (left > right) { left } else { right }; return r;"
+        , [((False, False, 3, 5), 5), ((False, False, 9, 2), 9)]
+        )
+    ,
+        ( "int r = if (flag) { int t = left * 2; t + 1 } else { int t = right * 3; t - 1 }; return r;"
+        , [((True, False, 4, 0), 9), ((False, False, 0, 5), 14)]
+        )
+    , -- Only the selected block of an if expression runs.
+        ( "int n = 0; int r = if (flag) { n += 1; 10 } else { n += 100; 20 }; return r * 1000 + n;"
+        , [((True, False, 0, 0), 10001), ((False, False, 0, 0), 20100)]
+        )
+    ,
+        ( "return (if (flag) { left } else { right }) + (if (other) { 100 } else { 200 });"
+        , [((True, True, 1, 2), 101), ((False, False, 1, 2), 202)]
+        )
+    ,
+        ( "guard (left > 0) else { return 0 - 1; } return left * 2;"
+        , [((False, False, 3, 0), 6), ((False, False, 0, 0), -1)]
+        )
+    ,
+        ( "int total = 0; for (int i = 0; i < left; i++) { guard (i % 2 == 0) else { continue; } total += i; } return total;"
+        , [((False, False, 6, 0), 6), ((False, False, 1, 0), 0)]
+        )
+    ,
+        ( "int n = 0; while (true) { guard (n < left) else { break; } n += 1; } return n;"
+        , [((False, False, 4, 0), 4), ((False, False, 0, 0), 0)]
+        )
+    ,
+        ( "int total = 0; { int part = left * 2; total += part; } { int part = right * 3; total += part; } return total;"
+        , [((False, False, 2, 3), 13), ((False, False, 0, 0), 0)]
+        )
+    ,
+        ( "int n = 0; while (true) { { n += 1; if (n > left) { break; } } } { { return n * 10; } }"
+        , [((False, False, 3, 0), 40), ((False, False, 0, 0), 10)]
+        )
+    ,
+        ( "int total = 0; for (int i = 0; i < left; i++) { { if (i == 1) { continue; } } { int step = i * 2; total += step; } } return total;"
+        , [((False, False, 4, 0), 10), ((False, False, 1, 0), 0)]
+        )
+    ,
+        ( "return match (left) { 1 -> 10 2 -> 20 _ -> 30 };"
+        , [((False, False, 1, 0), 10), ((False, False, 2, 0), 20), ((False, False, 9, 0), 30)]
+        )
+    , -- A pattern binding is a local of its arm and may be assigned.
+        ( "return match (left) { int value if value > 2 -> { value = value * 2; value }, int value -> { value += 1; value } };"
+        , [((False, False, 5, 0), 10), ((False, False, 1, 0), 2)]
+        )
+    , -- Assigning a binding does not change the subject the later arms test.
+        ( "int r = 0; match (left) { int value if (value = 7) > 9 -> { r = 1; }, 3 -> { r = 2; }, _ -> { r = 3; } } return r;"
+        , [((False, False, 3, 0), 2), ((False, False, 4, 0), 3)]
+        )
+    ,
+        ( "int r = if (flag) { if (other) { 1 } else { 2 } } else { match (left) { 1 -> 10, _ -> 20 } }; return r;"
+        , [((True, True, 0, 0), 1), ((True, False, 0, 0), 2), ((False, False, 1, 0), 10), ((False, False, 2, 0), 20)]
+        )
+    ,
+        ( "int n = 0; int r = if (flag) { if (other) { n += 1; } else { n += 2; } match (left) { 1 -> { n += 10; } } n } else { 0 }; return r;"
+        , [((True, True, 1, 0), 11), ((True, False, 2, 0), 2), ((False, False, 1, 0), 0)]
+        )
+    ,
+        ( "match (left) { 1 -> { match (right) { 2 -> { return 12; } } }, _ -> { } } return 5;"
+        , [((False, False, 1, 2), 12), ((False, False, 1, 3), 5), ((False, False, 2, 2), 5)]
+        )
+    , -- A guard condition with a store runs once, before the block decision.
+        ( "int n = left; guard ((n += 1) > 3) else { return n * 10; } return n;"
+        , [((False, False, 5, 0), 6), ((False, False, 1, 0), 20)]
+        )
+    ]
+
+-- | Expressions that leave instead of yielding a value. From @Talac/Fuzzing/Cases/Leaving.cases@.
+leavingCases :: [EvaluationCase]
+leavingCases =
+    [ -- An expression every branch of which leaves never yields a value.
+        ( "int r = if (flag) { return 1; } else { return 2; }; return r + 50;"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ,
+        ( "int r = match (left) { 0 -> { return 10; }, _ -> { return 20; } }; return r + 1;"
+        , [((False, False, 0, 0), 10), ((False, False, 5, 0), 20)]
+        )
+    ,
+        ( "return Twice(if (flag) { return 7; } else { return 9; });"
+        , [((True, False, 0, 0), 7), ((False, False, 0, 0), 9)]
+        )
+    ,
+        ( "int r = 1 + (if (flag) { return 1; } else { return 2; }); return r;"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < 5) { i += 1; int q = if (i > left) { break; } else { continue; }; t += q; } return t * 10 + i;"
+        , [((False, False, 2, 0), 3), ((False, False, 9, 0), 5)]
+        )
+    ,
+        ( "bool b = flag && (if (left > 0) { return 1; } else { return 2; }); return b ? 3 : 4;"
+        , [((True, False, 5, 0), 1), ((True, False, 0, 0), 2), ((False, False, 5, 0), 4)]
+        )
+    ,
+        ( "bool b = flag || (if (left > 0) { return 1; } else { return 2; }); return b ? 3 : 4;"
+        , [((True, False, 5, 0), 3), ((False, False, 5, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ,
+        ( "int n = left ?: (if (flag) { return 100; } else { return 200; }); return n;"
+        , [((False, False, 5, 0), 5), ((True, False, 0, 0), 100), ((False, False, 0, 0), 200)]
+        )
+    ,
+        ( "int r = if (left > 0) { 5 } else { if (flag) { return 1; } else { return 2; } }; return r;"
+        , [((False, False, 3, 0), 5), ((True, False, 0, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ,
+        ( "int n = left; do { n += 1; } while (if (n > 3) { return n; } else { return 0 - n; }); return 99;"
+        , [((False, False, 5, 0), 6), ((False, False, 1, 0), -2)]
+        )
+    , -- The condition of a do/while that never completes still runs after
+      -- the body and after a continue, with its effects, and is skipped by
+      -- a break.
+        ( "int t = 0; int n = 0; do { n += 1; if (n == left) { continue; } t += 10; } while (if ((t += 1) > 100) { return 1; } else { return t * 100 + n; }); return 99;"
+        , [((False, False, 1, 0), 101), ((False, False, 5, 0), 1101)]
+        )
+    ,
+        ( "int t = 0; do { if (left > 0) { break; } t += 5; } while (if ((t += 1) > 0) { return t; } else { return 0; }); return t + 1000;"
+        , [((False, False, 1, 0), 1000), ((False, False, 0, 0), 6)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; i < 3; i += 1) { do { t += 1; } while (if (t > left) { return t * 10 + i; } else { return t; }); } return 77;"
+        , [((False, False, 0, 0), 10), ((False, False, 5, 0), 1)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < 3) { i += 1; int j = 0; do { j += 1; if (j < 3) { continue; } t += j; } while (if (i > left) { return t * 10 + i; } else { j < 4 }); } return t;"
+        , [((False, False, 0, 0), 1), ((False, False, 9, 0), 21)]
+        )
+    , -- A transfer in a value block targets the innermost loop only.
+        ( "int t = 0; int i = 0; while (i < 5) { i += 1; int j = 0; while (j < 3) { j += 1; t += if (j == 2) { break; } else { 1 }; } } return t * 10 + i;"
+        , [((False, False, 0, 0), 55)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < 5) { i += 1; int j = 0; while (j < 3) { j += 1; t += if (j == 2) { continue; } else { 1 }; } } return t * 10 + i;"
+        , [((False, False, 0, 0), 105)]
+        )
+    ,
+        ( "int t = 0; while (t < 10) { t += 1; int q = while (true) { break t; }; if (q > left) { break; } } return t;"
+        , [((False, False, 3, 0), 4), ((False, False, 20, 0), 10)]
+        )
+    , -- A short-circuit operator evaluates its left operand, with its
+      -- effects, and reaches a right operand that never completes only
+      -- when the left one does not decide.
+        ( "int t = 0; bool b = (t += 1) > 0 && (if (left > 0) { return t * 10; } else { return t * 100; }); return 7;"
+        , [((False, False, 1, 0), 10), ((False, False, 0, 0), 100)]
+        )
+    ,
+        ( "int t = 0; bool b = (t += 1) > 5 && (if (left > 0) { return t * 10; } else { return t * 100; }); return t + (b ? 1000 : 0);"
+        , [((False, False, 1, 0), 1), ((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "int t = 0; bool b = (t += left) > 0 || (if (flag) { return t * 10; } else { return 50 + t; }); return b ? t + 1 : 7;"
+        , [((False, False, 3, 0), 4), ((True, False, 0, 0), 0), ((False, False, 0, 0), 50)]
+        )
+    ,
+        ( "int t = 0; int q = (t += left) ? t * 2 : (if (flag) { return 0 - 1; } else { return 0 - 2; }); return q + t;"
+        , [((False, False, 4, 0), 12), ((True, False, 0, 0), -1), ((False, False, 0, 0), -2)]
+        )
+    , -- Operands are evaluated left to right up to the one that never
+      -- completes; the ones after it are not evaluated.
+        ( "int t = 0; int r = (t += 1) + (if (flag) { return t * 10; } else { return t * 100; }) + (t += 50); return r;"
+        , [((True, False, 0, 0), 10), ((False, False, 0, 0), 100)]
+        )
+    ,
+        ( "int t = 0; return Twice(Twice(t += 3) + (if (flag) { return t; } else { return t * 2; }));"
+        , [((True, False, 0, 0), 3), ((False, False, 0, 0), 6)]
+        )
+    ,
+        ( "int t = left; t = Twice(t += 1) + (if (flag) { return t; } else { return t + 100; }); return 0 - 1;"
+        , [((True, False, 4, 0), 5), ((False, False, 4, 0), 105)]
+        )
+    , -- A guard runs only when its patterns accept, and a guarded arm that
+      -- leaves does not make the match leave.
+        ( "int t = 0; int r = match (left) { 0 if (t += 1) > 5 -> { return 1; }, 0 -> { return 10 + t; }, _ -> { return 20 + t; } }; return r;"
+        , [((False, False, 0, 0), 11), ((False, False, 3, 0), 20)]
+        )
+    ,
+        ( "int r = match (left) { _ if flag -> { return 1; }, _ -> 5 }; return r + 1;"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 6)]
+        )
+    , -- A break in the condition of a loop leaves that loop, once, with the
+      -- effects of the condition up to it.
+        ( "int n = 0; while (if (n >= left) { break; } else { true }) { n += 1; } return n * 10 + 1;"
+        , [((False, False, 3, 0), 31), ((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "int c = 0; int n = 0; while (if ((c += 1) > left) { break; } else { true }) { n += 1; } return c * 100 + n;"
+        , [((False, False, 2, 0), 302), ((False, False, 0, 0), 100)]
+        )
+    ,
+        ( "int n = 0; do { n += 1; } while (if (n >= left) { break; } else { true }); return n;"
+        , [((False, False, 3, 0), 3), ((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; if (i > left) { break; } else { i < 10 }; i += 1) { t += i; } return t;"
+        , [((False, False, 2, 0), 3), ((False, False, 20, 0), 45)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; i < 3; i += 1) { int j = 0; while (if (j == 2) { break; } else { true }) { j += 1; t += 1; } t += 10; } return t;"
+        , [((False, False, 0, 0), 36)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < 4) { i += 1; int j = 0; while (if (j >= i) { break; } else { true }) { j += 1; if (j == 2) { continue; } t += 1; } } return t * 10 + i;"
+        , [((False, False, 0, 0), 74)]
+        )
+    , -- A continue in the update of a loop ends the update; the condition
+      -- is tested next, and the update is not run again for that pass.
+        ( "int t = 0; int skips = 0; for (int i = 0; i < 6; i += if (i == left && skips == 0) { skips += 1; continue; } else { 1 }) { t += 1; } return t * 10 + skips;"
+        , [((False, False, 2, 0), 71), ((False, False, 9, 0), 60)]
+        )
+    ,
+        ( "int t = 0; int u = 0; for (int i = 0; i < 4; i += 1, u += if (i == left) { continue; } else { 10 }) { t += 1; } return u * 10 + t;"
+        , [((False, False, 2, 0), 304), ((False, False, 9, 0), 404)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; i < 3; i += 1) { int c = 0; for (int j = 0; j < 4; j += if (c == 0 && j == left) { c += 1; continue; } else { 1 }) { t += 1; } t += c * 100; } return t;"
+        , [((False, False, 1, 0), 315), ((False, False, 7, 0), 12)]
+        )
+    , -- A continue in the condition of a loop abandons the rest of the
+      -- condition and evaluates the condition of that loop again. The body
+      -- does not run in between, and neither does the update of a for.
+        ( "int c = 0; int n = 0; while (if ((c += 1) < left) { continue; } else { n < 2 }) { n += 1; } return c * 100 + n;"
+        , [((False, False, 3, 0), 502), ((False, False, 0, 0), 302)]
+        )
+    ,
+        ( "int u = 0; int c = 0; int t = 0; for (int i = 0; if ((c += 1) == left) { continue; } else { i < 3 }; i += 1, u += 1) { t += 10; } return c * 1000 + u * 100 + t;"
+        , [((False, False, 2, 0), 5330), ((False, False, 9, 0), 4330)]
+        )
+    ,
+        ( "int c = 0; int n = 0; do { n += 1; } while (if ((c += 1) < left) { continue; } else { n < 3 }); return c * 100 + n;"
+        , [((False, False, 3, 0), 503), ((False, False, 0, 0), 303)]
+        )
+    , -- It targets the loop the condition belongs to, not one around it.
+        ( "int t = 0; int c = 0; for (int i = 0; i < 3; i += 1) { int j = 0; int once = 0; while (if (once == 0) { once = 1; c += 1; continue; } else { j < 2 }) { j += 1; t += 1; } t += 10; } return t * 100 + c;"
+        , [((False, False, 0, 0), 3603)]
+        )
+    , -- A condition every path of which leaves: it repeats until it breaks.
+        ( "int c = 0; while (if ((c += 1) < left) { continue; } else { break; }) { c += 100; } return c;"
+        , [((False, False, 4, 0), 4), ((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "int c = 0; int u = 0; for (int i = 0; if ((c += 1) < left) { continue; } else { break; }; i += 1, u += 1) { c += 100; } return c * 10 + u;"
+        , [((False, False, 4, 0), 40), ((False, False, 0, 0), 10)]
+        )
+    , -- A break in the update of a loop leaves that loop, with the effects
+      -- of the update up to it and without the rest of the update.
+        ( "int t = 0; for (int i = 0; i < 10; i += if (i == left) { break; } else { 1 }) { t += 1; } return t;"
+        , [((False, False, 2, 0), 3), ((False, False, 20, 0), 10)]
+        )
+    ,
+        ( "int t = 0; int u = 0; for (int i = 0; i < 10; u += 1, i += if (i == left) { break; } else { 1 }, u += 100) { t += 1; } return u * 10 + t;"
+        , [((False, False, 1, 0), 1022), ((False, False, 20, 0), 10110)]
+        )
+    , -- A break and a continue in one update clause.
+        ( "int t = 0; int s = 0; for (int i = 0; i < 10; i += if (i == left) { break; } else { 1 }, s += if (i == 2) { continue; } else { 1 }, s += 10) { t += 1; } return s * 100 + t;"
+        , [((False, False, 4, 0), 3305), ((False, False, 20, 0), 9910)]
+        )
+    , -- It leaves the loop the update belongs to, not one around it.
+        ( "int t = 0; for (int a = 0; a < 3; a += 1) { for (int b = 0; b < 5; b += if (b == 1) { break; } else { 1 }) { t += 1; } t += 10; } return t;"
+        , [((False, False, 0, 0), 36)]
+        )
+    , -- In the update of a loop used as an expression it carries the value.
+        ( "int r = for (int i = 0; ; i += if (i == left) { break i * 10; } else { 1 }) { if (i > 5) { break 99; } }; return r;"
+        , [((False, False, 3, 0), 30), ((False, False, 9, 0), 99)]
+        )
+    , -- A value block may carry a value out of the loop expression around
+      -- it; the value goes to that loop, not to one around it.
+        ( "int r = while (true) { int q = if (flag) { break left + 1; } else { 2 }; break q; }; return r;"
+        , [((True, False, 4, 0), 5), ((False, False, 4, 0), 2)]
+        )
+    ,
+        ( "int r = while (true) { int q = match (left) { 0 -> { break 100; }, int n -> n * 2 }; break q; }; return r;"
+        , [((False, False, 0, 0), 100), ((False, False, 4, 0), 8)]
+        )
+    ,
+        ( "int t = 0; int r = for (int i = 0; ; i += 1) { int inner = while (true) { t += 1; int q = if (t > left) { break t * 2; } else { 0 }; t += q; }; if (inner > 0) { break inner + i; } }; return r * 10 + t;"
+        , [((False, False, 2, 0), 63), ((False, False, 0, 0), 21)]
+        )
+    ,
+        ( "int t = 0; int r = Twice(while (true) { t += 1; int q = (t += 10) + (if (t > left) { break t; } else { 1 }); t += q; });  return r * 100 + t;"
+        , [((False, False, 5, 0), 2211), ((False, False, 30, 0), 6834)]
+        )
+    , -- A return leaves the method from a loop expression, directly and
+      -- through a value block.
+        ( "int r = while (true) { int q = if (flag) { return 77; } else { 2 }; break q + left; }; return r;"
+        , [((True, False, 0, 0), 77), ((False, False, 3, 0), 5)]
+        )
+    ,
+        ( "int r = while (true) { if (flag) { return 1; } break 2; }; return r + 10;"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 12)]
+        )
+    ,
+        ( "int r = while (true) { return left; }; return r + 1;"
+        , [((False, False, 6, 0), 6)]
+        )
+    ,
+        ( "int t = 0; int r = while (true) { t += 1; int q = for (int i = 0; ; i += 1) { if (i + t > left) { return i * 10 + t; } if (i == 2) { break i; } }; if (t == 3) { break q; } }; return 0 - r;"
+        , [((False, False, 1, 0), 11), ((False, False, 2, 0), 21), ((False, False, 99, 0), -2)]
+        )
+    , -- A block used as a value may leave instead of yielding one.
+        ( "int r = if (left > 5) { return 100; } else { left * 2 }; return r + 1;"
+        , [((False, False, 9, 0), 100), ((False, False, 3, 0), 7)]
+        )
+    ,
+        ( "int r = match (left) { 0 -> { return 50; }, int n -> n + 1 }; return r * 2;"
+        , [((False, False, 0, 0), 50), ((False, False, 4, 0), 10)]
+        )
+    ,
+        ( "return Twice(if (flag) { return 7; } else { left });"
+        , [((True, False, 4, 0), 7), ((False, False, 4, 0), 8)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < 10) { i += 1; t += if (i > left) { break; } else { i }; } return t * 100 + i;"
+        , [((False, False, 3, 0), 604), ((False, False, 0, 0), 1)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; i < 6; i += 1) { t += match (i) { 2 -> { continue; }, int n -> { if (n == left) { continue; } else { n } } }; } return t;"
+        , [((False, False, 4, 0), 9), ((False, False, 9, 0), 13)]
+        )
+    , -- A guard block that leaves through a match, and through continue.
+        ( "guard (left > 0) else { match (flag) { true -> { return 1; }, false -> { return 2; } } } return left + 10;"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 2), ((False, False, 5, 0), 15)]
+        )
+    ,
+        ( "int t = 0; int i = 0; while (i < left) { i += 1; guard (i \\= 2) else { continue; } t += i; } return t;"
+        , [((False, False, 4, 0), 8), ((False, False, 1, 0), 1)]
+        )
+    ,
+        ( "int r = 0; match (left) { 1 -> { r = 10; }, 2 -> { r = 20; } } return r;"
+        , [((False, False, 1, 0), 10), ((False, False, 2, 0), 20), ((False, False, 7, 0), 0)]
+        )
+    ,
+        ( "return match (left) { int n if n > 10 -> n * 2, int n if n > 5 -> n + 1, _ -> 0 };"
+        , [((False, False, 20, 0), 40), ((False, False, 7, 0), 8), ((False, False, 3, 0), 0)]
+        )
+    ,
+        ( "return match (left), (right) { (1), (1) -> 11, (1), (_) -> 10, (_), (1) -> 1, (_), (_) -> 0 };"
+        , [((False, False, 1, 1), 11), ((False, False, 1, 5), 10), ((False, False, 4, 1), 1), ((False, False, 4, 4), 0)]
+        )
+    ,
+        ( "return match (flag) { true -> 1, false -> 2 };"
+        , [((True, False, 0, 0), 1), ((False, False, 0, 0), 2)]
+        )
+    ]
+ test/BranchingOracleTests.hs view
@@ -0,0 +1,350 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Differential tests for the lowering of @match@.
+
+A match over scalars means the same as a chain of @if@ statements that tests
+the arms in order: the first arm whose literals equal the subjects and whose
+guard holds supplies the result, and a guard is evaluated only when the
+literals of its arm are equal. The @if@ chain is lowered by code that existed
+before @match@ did and has its own tests, so it serves as the oracle here.
+
+The tests generate families of arm lists from a fixed seed, write each family
+once as a match and once as the chain, and require the two programs to return
+the same value for every argument in a small domain, on the unoptimized and
+on the optimized Core. Every guard also adds a distinct weight to a counter
+that is part of the result, so a guard that runs when it should not, or does
+not run when it should, changes the value even when the selected arm is the
+same.
+-}
+module BranchingOracleTests (branchingOracleTests) where
+
+import CoreInterpreter
+import Data.List (intercalate)
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+
+branchingOracleTests :: [(String, Bool)]
+branchingOracleTests =
+    generatorTests
+        ++ concatMap familyTests (zip [0 :: Int ..] singleSubjectFamilies)
+        ++ concatMap pairFamilyTests (zip [0 :: Int ..] pairSubjectFamilies)
+        ++ concatMap wideTests [15, 16, 17, 33, 100]
+
+-- ------------------------------------------------------------- generator
+
+{- | A deterministic stream of small numbers.
+
+The constants are those of the linear congruential generator in the C
+standard's example, reduced to the bits whose period is long enough for the
+few hundred draws made here. The stream is fixed, so a failing family has the
+same index on every run.
+-}
+draws :: Int -> [Int]
+draws seed = map (\value -> (value `div` 65536) `mod` 32768) (drop 1 (iterate step seed))
+    where
+        step value = (value * 1103515245 + 12345) `mod` 2147483648
+
+-- | What a pattern requires of its subject.
+data Test
+    = -- | Accepts every value.
+      Anything
+    | -- | Accepts the value equal to the literal.
+      Exactly Integer
+    | -- | Accepts every value and binds it; the guard compares the binding.
+      Bound
+    deriving stock (Eq, Show)
+
+{- | One generated arm for one subject: its test, an optional guard, and the
+value of its body. A guard adds its weight to the counter and then holds when
+the counter exceeds the threshold, or, for a bound value, when that value
+exceeds it.
+-}
+data Arm = Arm
+    { armTest :: Test
+    , armGuard :: Maybe (Integer, Integer)
+    , armValue :: Integer
+    }
+    deriving stock (Eq, Show)
+
+{- | Arms for the literals @0@ up to the given count, then a catch-all.
+
+A guarded literal arm may be followed by an unguarded arm for the same
+literal, which is the case where a false guard must pass the value on. No
+unguarded arm repeats a literal, and nothing follows the catch-all, so the
+type checker accepts every generated list.
+-}
+generateArms :: Int -> [Int] -> [Arm]
+generateArms count stream = go 0 stream
+    where
+        go index values
+            | index >= count = [Arm Anything Nothing 99]
+            | otherwise = case values of
+                kind : weight : threshold : value : remaining ->
+                    let literal = toInteger index
+                        result = toInteger (10 + value `mod` 80)
+                        guard = (toInteger (1 + weight `mod` 9) * (10 ^ (index `mod` 3)), toInteger (threshold `mod` 4))
+                     in case kind `mod` 5 of
+                            0 -> Arm (Exactly literal) Nothing result : go (index + 1) remaining
+                            1 -> Arm (Exactly literal) (Just guard) result : go (index + 1) remaining
+                            2 ->
+                                Arm (Exactly literal) (Just guard) result
+                                    : Arm (Exactly literal) Nothing (result + 100)
+                                    : go (index + 1) remaining
+                            3 -> Arm Bound (Just (0, toInteger (threshold `mod` 6))) result : go (index + 1) remaining
+                            _ -> Arm Anything (Just guard) result : go (index + 1) remaining
+                _ -> [Arm Anything Nothing 99]
+
+singleSubjectFamilies :: [[Arm]]
+singleSubjectFamilies =
+    [ generateArms (2 + family `mod` 4) (drop (family * 37) (draws 20261004))
+    | family <- [0 .. 23 :: Int]
+    ]
+
+-- | One generated arm for two subjects.
+data PairArm = PairArm
+    { pairTests :: (Test, Test)
+    , pairGuard :: Maybe (Integer, Integer)
+    , pairValue :: Integer
+    }
+    deriving stock (Eq, Show)
+
+{- | Arms over two subjects: every pair of literals below the count in a
+generated order, some replaced by a wildcard in one position, then the
+catch-all. Each pair of tests appears at most once, and an arm that an
+earlier one would shadow is dropped, so every list is accepted.
+-}
+generatePairArms :: Int -> [Int] -> [PairArm]
+generatePairArms count stream = distinct [] (go candidates stream) ++ [PairArm (Anything, Anything) Nothing 99]
+    where
+        candidates = [(toInteger left, toInteger right) | left <- [0 .. count - 1], right <- [0 .. count - 1]]
+        go [] _ = []
+        go ((left, right) : remaining) values = case values of
+            shape : weight : threshold : value : later ->
+                let result = toInteger (10 + value `mod` 80)
+                    guard = (toInteger (1 + weight `mod` 9), toInteger (threshold `mod` 3))
+                    tests = case shape `mod` 6 of
+                        0 -> (Anything, Exactly right)
+                        1 -> (Exactly left, Anything)
+                        _ -> (Exactly left, Exactly right)
+                    guarded = if shape `mod` 4 == 3 then Just guard else Nothing
+                 in PairArm tests guarded result : go remaining later
+            _ -> []
+        distinct _ [] = []
+        distinct seen (arm : remaining)
+            | any (`covers` pairTests arm) seen = distinct seen remaining
+            | otherwise =
+                arm : distinct (if pairGuard arm == Nothing then pairTests arm : seen else seen) remaining
+        covers (first, second) (otherFirst, otherSecond) = accepts first otherFirst && accepts second otherSecond
+        accepts earlier later = earlier == Anything || earlier == later
+
+pairSubjectFamilies :: [[PairArm]]
+pairSubjectFamilies =
+    [ generatePairArms (2 + family `mod` 2) (drop (family * 53) (draws 4102026))
+    | family <- [0 .. 11 :: Int]
+    ]
+
+generatorTests :: [(String, Bool)]
+generatorTests =
+    [ ("the generated draws are deterministic", take 5 (draws 1) == take 5 (draws 1))
+    , ("every single-subject family ends with the catch-all", all ((== Arm Anything Nothing 99) . last) singleSubjectFamilies)
+    , ("the single-subject families use guards", any (any ((/= Nothing) . armGuard)) singleSubjectFamilies)
+    , ("the single-subject families bind values", any (any ((== Bound) . armTest)) singleSubjectFamilies)
+    ,
+        ( "the single-subject families repeat a literal after a guard"
+        , any hasRepeatedLiteral singleSubjectFamilies
+        )
+    , ("every pair family ends with the catch-all", all ((== (Anything, Anything)) . pairTests . last) pairSubjectFamilies)
+    , ("the pair families use wildcards beside literals", any (any mixed) pairSubjectFamilies)
+    ]
+    where
+        hasRepeatedLiteral arms = or (zipWith sameLiteral arms (drop 1 arms))
+        sameLiteral first second = case (armTest first, armTest second) of
+            (Exactly left, Exactly right) -> left == right
+            _ -> False
+        mixed arm = case pairTests arm of
+            (Anything, Exactly _) -> True
+            (Exactly _, Anything) -> True
+            _ -> False
+
+-- --------------------------------------------------------------- sources
+
+program :: String -> String
+program statements =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "}"
+        ]
+
+-- | The condition a guard stands for, over the given bound value.
+guardSource :: String -> (Integer, Integer) -> String
+guardSource bound (weight, threshold)
+    | weight == 0 = bound ++ " > " ++ show threshold
+    | otherwise = "(hits += " ++ show weight ++ ") > " ++ show threshold
+
+matchSource :: [Arm] -> String
+matchSource arms =
+    "int hits = 0; int result = match (left) { "
+        ++ intercalate ", " (zipWith armSource [0 :: Int ..] arms)
+        ++ " }; return result * 1000 + hits;"
+    where
+        armSource index arm =
+            let name = "bound" ++ show index
+                patternSource = case armTest arm of
+                    Anything -> "_"
+                    Exactly value -> show value
+                    Bound -> "int " ++ name
+             in patternSource
+                    ++ maybe "" ((" if " ++) . guardSource name) (armGuard arm)
+                    ++ " -> "
+                    ++ show (armValue arm)
+
+{- | The same arms as a chain of @if@ statements. A bound value is the
+subject itself, and a guard follows the literal test behind @&&@, which
+evaluates it only when the literal is equal.
+-}
+chainSource :: [Arm] -> String
+chainSource arms = "int hits = 0; int result = 0; " ++ chain arms ++ " return result * 1000 + hits;"
+    where
+        chain [] = ""
+        chain (arm : remaining) = case conditionOf arm of
+            Nothing -> "{ result = " ++ show (armValue arm) ++ "; }"
+            Just condition ->
+                "if ("
+                    ++ condition
+                    ++ ") { result = "
+                    ++ show (armValue arm)
+                    ++ "; }"
+                    ++ if null remaining then "" else " else " ++ chain remaining
+        conditionOf arm =
+            let literal = case armTest arm of
+                    Exactly value -> Just ("left == " ++ show value)
+                    _ -> Nothing
+                guard = fmap (guardSource "left") (armGuard arm)
+             in case (literal, guard) of
+                    (Nothing, Nothing) -> Nothing
+                    (Just test, Nothing) -> Just test
+                    (Nothing, Just test) -> Just test
+                    (Just test, Just condition) -> Just (test ++ " && " ++ condition)
+
+pairMatchSource :: [PairArm] -> String
+pairMatchSource arms =
+    "int hits = 0; int result = match (left), (right) { "
+        ++ intercalate ", " (map armSource arms)
+        ++ " }; return result * 1000 + hits;"
+    where
+        armSource arm =
+            let (first, second) = pairTests arm
+             in "("
+                    ++ testSource first
+                    ++ "), ("
+                    ++ testSource second
+                    ++ ")"
+                    ++ maybe "" ((" if " ++) . guardSource "left") (pairGuard arm)
+                    ++ " -> "
+                    ++ show (pairValue arm)
+        testSource test = case test of
+            Exactly value -> show value
+            _ -> "_"
+
+pairChainSource :: [PairArm] -> String
+pairChainSource arms = "int hits = 0; int result = 0; " ++ chain arms ++ " return result * 1000 + hits;"
+    where
+        chain [] = ""
+        chain (arm : remaining) = case conditions arm of
+            [] -> "{ result = " ++ show (pairValue arm) ++ "; }"
+            tests ->
+                "if ("
+                    ++ intercalate " && " tests
+                    ++ ") { result = "
+                    ++ show (pairValue arm)
+                    ++ "; }"
+                    ++ if null remaining then "" else " else " ++ chain remaining
+        conditions arm =
+            let (first, second) = pairTests arm
+             in literalTest "left" first ++ literalTest "right" second ++ maybe [] ((: []) . guardSource "left") (pairGuard arm)
+        literalTest name test = case test of
+            Exactly value -> [name ++ " == " ++ show value]
+            _ -> []
+
+-- ---------------------------------------------------------------- oracle
+
+compileSource :: String -> Either String FrontendArtifacts
+compileSource text = either (Left . show) Right (compileToCorePrep (CompilerInput "branching-oracle.tala" text))
+
+-- | The values a program returns over the argument domain, or nothing when it does not compile or run.
+results :: (FrontendArtifacts -> CoreModule) -> String -> Maybe [Integer]
+results select statements = case compileSource (program statements) of
+    Left _ -> Nothing
+    Right artifacts ->
+        mapM
+            ( \(left, right) -> case runFunction (select artifacts) "Evaluate" [IntegerValue left, IntegerValue right] of
+                Just (IntegerValue value) -> Just value
+                _ -> Nothing
+            )
+            domain
+
+-- Subjects below, at, and above every generated literal, in both positions.
+domain :: [(Integer, Integer)]
+domain = [(left, right) | left <- [0 .. 7], right <- [0 .. 3]]
+
+agree :: String -> String -> [(String, Bool)]
+agree matchText chainText =
+    let oracle = results artifactCore chainText
+     in [ ("the if chain compiles and runs: " ++ chainText, oracle /= Nothing)
+        , ("unoptimized match agrees with the if chain: " ++ matchText, results artifactCore matchText == oracle)
+        , ("optimized match agrees with the if chain: " ++ matchText, results artifactOptimizedCore matchText == oracle)
+        , ("optimized if chain agrees with itself: " ++ chainText, results artifactOptimizedCore chainText == oracle)
+        ]
+
+{- | Matches around and beyond the nesting bound of the lowering, where the
+arms are split into groups. Every sixth literal has a guarded arm before its
+unguarded one, and half of those guards are false, so a false guard also
+passes its value on inside and across groups. The subject domain reaches the
+last literal and the catch-all.
+-}
+wideArms :: Int -> [Arm]
+wideArms count =
+    concat
+        [ [Arm (Exactly literal) (Just (1, if index `mod` 4 == 0 then 0 else 1)) (literal * 7 + 3) | index `mod` 6 == 0]
+            ++ [Arm (Exactly literal) Nothing (literal * 5 + 1)]
+        | index <- [0 .. count - 1]
+        , let literal = toInteger index
+        ]
+        ++ [Arm Anything Nothing 99]
+
+wideResults :: (FrontendArtifacts -> CoreModule) -> String -> Int -> Maybe [Integer]
+wideResults select statements count = case compileSource (program statements) of
+    Left _ -> Nothing
+    Right artifacts ->
+        mapM
+            ( \left -> case runFunction (select artifacts) "Evaluate" [IntegerValue left, IntegerValue 0] of
+                Just (IntegerValue value) -> Just value
+                _ -> Nothing
+            )
+            [0 .. toInteger count + 1]
+
+wideTests :: Int -> [(String, Bool)]
+wideTests count =
+    let arms = wideArms count
+        oracle = wideResults artifactCore (chainSource arms) count
+        label = "a match of " ++ show (length arms) ++ " arms"
+     in [ (label ++ " has an if chain that compiles and runs", oracle /= Nothing)
+        , (label ++ " agrees with the if chain unoptimized", wideResults artifactCore (matchSource arms) count == oracle)
+        , (label ++ " agrees with the if chain optimized", wideResults artifactOptimizedCore (matchSource arms) count == oracle)
+        ]
+
+familyTests :: (Int, [Arm]) -> [(String, Bool)]
+familyTests (index, arms) =
+    [ ("family " ++ show index ++ ": " ++ label, outcome)
+    | (label, outcome) <- agree (matchSource arms) (chainSource arms)
+    ]
+
+pairFamilyTests :: (Int, [PairArm]) -> [(String, Bool)]
+pairFamilyTests (index, arms) =
+    [ ("pair family " ++ show index ++ ": " ++ label, outcome)
+    | (label, outcome) <- agree (pairMatchSource arms) (pairChainSource arms)
+    ]
+ test/BranchingTests.hs view
@@ -0,0 +1,1231 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Vertical tests for @match@, for @if@ used as an expression, for
+@guard@, and for a block written as a statement.
+
+The parser tests pin the shape of each form and the diagnostics of the
+spellings that are recognized but not accepted. The type tests pin every rule
+that decides whether a match has a value: one type for all arms, an arm that
+always accepts, no arm that can never be selected. The evaluation tests state
+the value each program must compute and run the reference evaluator of
+"CoreInterpreter" on the unoptimized and on the optimized Core, so a lowering
+that selects the wrong arm, evaluates a subject twice, or runs a guard that
+should not run is observed as a wrong result.
+-}
+module BranchingTests (branchingTests) where
+
+import BranchingEvaluationCases (evaluationCases)
+import CoreInterpreter
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Specialization
+
+branchingTests :: [(String, Bool)]
+branchingTests = parserTests ++ typeTests ++ loweringTests ++ evaluationTests ++ pipelineTests
+
+-- ---------------------------------------------------------------- sources
+
+body :: String -> String
+body statements =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ bool flag, _ bool other, _ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "    public static int Twice(_ int value) { return value + value; }"
+        , "}"
+        ]
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource text = do
+    tokens <- runLexer defaultLexer (LexerInput "branching.tala" text)
+    runParser defaultParser (ParserInput "branching.tala" tokens)
+
+parses :: String -> Bool
+parses text = either (const False) (const True) (parseSource text)
+
+-- | Whether parsing stops with the given diagnostic.
+parseFailsWith :: String -> String -> Bool
+parseFailsWith code text = case parseSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+firstStatements :: String -> Maybe [Statement Identifier ()]
+firstStatements text = case parseSource text of
+    Right (ParsedAST (SyntaxTree _ (TypeDeclaration {typeMembers = member : _} : _))) -> case member of
+        FunctionDeclaration {declarationBody = Block statements} -> Just statements
+        _ -> Nothing
+    _ -> Nothing
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "branching.tala" text)
+
+accepted :: String -> Bool
+accepted text = either (const False) (const True) (compileSource text)
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- | The codes a source is rejected with, or none when it is accepted.
+codesOf :: String -> [String]
+codesOf text = either (map diagnosticCode) (const []) (compileSource text)
+
+-- ---------------------------------------------------------------- parser
+
+parserTests :: [(String, Bool)]
+parserTests =
+    [ ("a match at the start of a statement is a terminated expression statement", matchStatementParses)
+    , ("a match in a binding initializer is a match expression", matchExpressionParses)
+    , ("every subject of a match has its own parentheses", multipleSubjectsParse)
+    , ("an arm has one pattern for each subject", multiplePatternsParse)
+    , ("a guard follows the patterns of its arm", guardedArmParses)
+    , ("a type pattern binds a name or discards the value", typePatternsParse)
+    , ("the null and enum case patterns are recognized", otherPatternsParse)
+    , ("a block body is a block expression and any other body is the expression itself", armBodiesParse)
+    , ("the comma after a block body is optional", parses (body "match (left) { 1 -> { } 2 -> { } } return 0;"))
+    , ("the comma after the last arm is optional", parses (body "return match (left) { 1 -> 10, _ -> 20, };"))
+    , ("a match without arms parses", parses (body "match (left) { } return 0;"))
+    , ("the comma after an expression body is optional", parses (body "return match (left) { 1 -> 10 2 -> 20 _ -> 30 };"))
+    ,
+        ( "a parenthesized pattern after an expression body without a comma is read as a call"
+        , parseFailsWith "TAP0038" (body "return match (left) { 1 -> Twice (2) -> 20, _ -> 30 };")
+        )
+    , ("a comma keeps a parenthesized pattern apart from the body before it", parses (body "return match (left) { 1 -> Twice(left), (2) -> 20, _ -> 30 };"))
+    , ("an if in last position of a value block is the value of the block", nestedIfIsValue)
+    , ("a match in last position of a value block is the value of the block", nestedMatchIsValue)
+    , ("an if that is not last in a value block is a statement", ifInsideValueBlockIsStatement)
+    , ("a match that is not last in a value block is a statement", matchInsideValueBlockIsStatement)
+    ,
+        ( "a statement block inside a value block may end with a match"
+        , parses (body "int r = if (flag) { if (other) { match (left) { 1 -> { Twice(left); } } } 1 } else { 2 }; return r;")
+        )
+    ,
+        ( "a value in a block that is not used as a value needs its semicolon"
+        , parseFailsWith "TAP0006" (body "int r = if (flag) { if (other) { 1 } 2 } else { 3 }; return r;")
+        )
+    , ("a bare name is not a pattern", parseFailsWith "TAP0036" (body "match (left) { right -> { } } return 0;"))
+    , ("a minus sign and a numeric literal are a pattern", parses (body "match (left) { -1 -> { } } return 0;"))
+    , ("a negative pattern may stand in parentheses", parses (body "match (left), (right) { (-1), (- 2) -> { } } return 0;"))
+    , ("a negative floating literal is a pattern", parses (body "double d = 1.5; return match (d) { -1.5 -> 1, _ -> 0 };"))
+    , ("a minus sign before a name is not a pattern", parseFailsWith "TAP0036" (body "match (left) { -right -> { } } return 0;"))
+    , ("a minus sign before a Boolean literal is not a pattern", parseFailsWith "TAP0036" (body "match (left) { -true -> { } } return 0;"))
+    , ("a minus sign before a parenthesis is not a pattern", parseFailsWith "TAP0036" (body "match (left) { -(1) -> { } } return 0;"))
+    , ("two minus signs are not a pattern", parseFailsWith "TAP0036" (body "match (left) { - -1 -> { } } return 0;"))
+    , ("an arithmetic expression is not a pattern", not (parses (body "match (left) { 1 + 1 -> { } } return 0;")))
+    , ("a plus sign before a literal is not a pattern", not (parses (body "match (left) { +1 -> { } } return 0;")))
+    ,
+        ( "an if whose first block leaves is the value of a value block it ends"
+        , parses (body "int r = match (left) { 0 -> { if (flag) { return 9; } else { 4 } }, _ -> 1 }; return r;")
+        )
+    , -- The body of a method is not a block used as a value.
+        ( "an if in last position of a method body stays a statement"
+        , parseFailsWith "TAP0006" (body "if (flag) { 1 } else { 2 }")
+        )
+    , ("an unterminated match is reported", not (parses (body "match (left) { 1 -> { }")))
+    , ("an if in operand position is a conditional over two value blocks", ifExpressionParses)
+    , ("an if at the start of a statement stays an if statement", ifStatementKeepsItsNode)
+    ,
+        ( "an if used as an expression requires an else branch"
+        , parseFailsWith "TAP0033" (body "int r = if (flag) { 1 }; return r;")
+        )
+    ,
+        ( "the else branch of an if expression must be a block"
+        , parseFailsWith "TAP0034" (body "int r = if (flag) { 1 } else if (other) { 2 } else { 3 }; return r;")
+        )
+    , ("a guard is a statement with a condition and an else block", guardParses)
+    , ("a guard requires an else block", parseFailsWith "TAP0037" (body "guard (flag) { return 0; } return 1;"))
+    ,
+        ( "a binding condition in an if is recognized and rejected"
+        , parseFailsWith "TAP0035" (body "if (auto user = Twice(left)) { return 1; } return 0;")
+        )
+    ,
+        ( "a binding condition in a guard is recognized and rejected"
+        , parseFailsWith "TAP0035" (body "guard (int user = Twice(left)) else { return 1; } return 0;")
+        )
+    ,
+        ( "a binding condition in a while is recognized and rejected"
+        , parseFailsWith "TAP0035" (body "while (auto user = Twice(left)) { return 1; } return 0;")
+        )
+    ,
+        ( "a binding condition in an if expression is recognized and rejected"
+        , parseFailsWith "TAP0035" (body "int r = if (auto user = Twice(left)) { 1 } else { 2 }; return r;")
+        )
+    , ("an assignment in a condition is still an expression", parses (body "int n = 0; if (n = left) { return 1; } return 0;"))
+    , ("a brace at the start of a statement opens a nested block", nestedBlockParses)
+    , ("nested blocks nest", parses (body "{ { { return 1; } } }"))
+    , ("an empty nested block parses", parses (body "{ } return 0;"))
+    ,
+        ( "an unterminated nested block is reported"
+        , parseFailsWith "TAP0002" "class Program { public static int Evaluate() { { int a = 1;"
+        )
+    , ("match is reserved and cannot name a local", not (parses (body "int match = 1; return match;")))
+    , ("guard is reserved and cannot name a local", not (parses (body "int guard = 1; return guard;")))
+    ]
+
+matchStatementParses :: Bool
+matchStatementParses = case firstStatements (body "match (left) { 1 -> { return 1; }, _ -> { return 2; } }") of
+    Just [ExpressionStatement _ (MatchExpression _ [NameExpression _ (Identifier "left") ()] [first, second] ()) True] ->
+        length (matchArmPatterns first) == 1 && length (matchArmPatterns second) == 1
+    _ -> False
+
+matchExpressionParses :: Bool
+matchExpressionParses = case firstStatements (body "int r = match (left) { 1 -> 10, _ -> 20 }; return r;") of
+    Just [BindingStatement _ _ _ (Identifier "r") () (MatchExpression _ [_] [_, _] ()), _] -> True
+    _ -> False
+
+multipleSubjectsParse :: Bool
+multipleSubjectsParse = case firstStatements (body "match (left), (right + 1), (flag) { } return 0;") of
+    Just [ExpressionStatement _ (MatchExpression _ [NameExpression {}, BinaryExpression {}, NameExpression {}] [] ()) _, _] -> True
+    _ -> False
+
+multiplePatternsParse :: Bool
+multiplePatternsParse = case firstStatements (body "match (left), (right) { (1), (2) -> { }, 3, _ -> { } } return 0;") of
+    Just [ExpressionStatement _ (MatchExpression _ _ [first, second] ()) _, _] ->
+        case (matchArmPatterns first, matchArmPatterns second) of
+            ( [MatchLiteralPattern _ (IntegerLiteral 1) (), MatchLiteralPattern _ (IntegerLiteral 2) ()]
+                , [MatchLiteralPattern _ (IntegerLiteral 3) (), MatchWildcardPattern _ ()]
+                ) -> True
+            _ -> False
+    _ -> False
+
+guardedArmParses :: Bool
+guardedArmParses = case firstStatements (body "match (left) { 2 if flag -> { }, _ -> { } } return 0;") of
+    Just [ExpressionStatement _ (MatchExpression _ _ [first, second] ()) _, _] ->
+        case (matchArmGuard first, matchArmGuard second) of
+            (Just (NameExpression _ (Identifier "flag") ()), Nothing) -> True
+            _ -> False
+    _ -> False
+
+typePatternsParse :: Bool
+typePatternsParse = case firstStatements (body "match (left) { int value -> { }, int _ -> { } } return 0;") of
+    Just [ExpressionStatement _ (MatchExpression _ _ [first, second] ()) _, _] ->
+        case (matchArmPatterns first, matchArmPatterns second) of
+            ( [MatchTypePattern _ (ExplicitType (Identifier "int")) (Just (Identifier "value")) ()]
+                , [MatchTypePattern _ (ExplicitType (Identifier "int")) Nothing ()]
+                ) -> True
+            _ -> False
+    _ -> False
+
+otherPatternsParse :: Bool
+otherPatternsParse = case firstStatements (body "match (left) { null -> { }, .Ready -> { }, true -> { }, 'a' -> { } } return 0;") of
+    Just [ExpressionStatement _ (MatchExpression _ _ arms ()) _, _] -> case map matchArmPatterns arms of
+        [ [MatchNullPattern _ ()]
+            , [MatchCasePattern _ (Identifier "Ready") ()]
+            , [MatchLiteralPattern _ (BooleanLiteral True) ()]
+            , [MatchLiteralPattern _ (CharacterLiteral 97) ()]
+            ] -> True
+        _ -> False
+    _ -> False
+
+armBodiesParse :: Bool
+armBodiesParse = case firstStatements (body "int r = match (left) { 1 -> { int t = 2; t }, _ -> left + 1 }; return r;") of
+    Just [BindingStatement _ _ _ _ () (MatchExpression _ _ [first, second] ()), _] ->
+        case (matchArmBody first, matchArmBody second) of
+            (BlockExpression _ (Block [BindingStatement {}, ExpressionStatement _ _ False]) (), BinaryExpression {}) -> True
+            _ -> False
+    _ -> False
+
+ifExpressionParses :: Bool
+ifExpressionParses = case firstStatements (body "int r = if (flag) { 1 } else { int t = 2; t }; return r;") of
+    Just
+        [ BindingStatement
+                _
+                _
+                _
+                _
+                ()
+                ( ConditionalExpression
+                        _
+                        (NameExpression _ (Identifier "flag") ())
+                        (BlockExpression _ (Block [ExpressionStatement _ _ False]) ())
+                        (BlockExpression _ (Block [BindingStatement {}, ExpressionStatement _ _ False]) ())
+                        ()
+                    )
+            , _
+            ] ->
+        True
+    _ -> False
+
+ifStatementKeepsItsNode :: Bool
+ifStatementKeepsItsNode = case firstStatements (body "if (flag) { return 1; } else { return 2; }") of
+    Just [IfStatement _ _ _ (Just _)] -> True
+    _ -> False
+
+nestedBlockParses :: Bool
+nestedBlockParses = case firstStatements (body "int a = 1; { int b = 2; a = b; } return a;") of
+    Just [BindingStatement {}, BlockStatement _ (Block [BindingStatement {}, AssignmentStatement {}]), ReturnStatement {}] -> True
+    _ -> False
+
+-- The statements of the first block of `int r = if (flag) { ... } else { 0 };`.
+innerBlock :: String -> Maybe [Statement Identifier ()]
+innerBlock inner = case firstStatements (body ("int r = if (flag) { " ++ inner ++ " } else { 0 }; return r;")) of
+    Just (BindingStatement _ _ _ _ () (ConditionalExpression _ _ (BlockExpression _ (Block statements) ()) _ ()) : _) ->
+        Just statements
+    _ -> Nothing
+
+nestedIfIsValue :: Bool
+nestedIfIsValue = case innerBlock "if (other) { 1 } else { 2 }" of
+    Just [ExpressionStatement _ (ConditionalExpression _ _ (BlockExpression {}) (BlockExpression {}) ()) False] -> True
+    _ -> False
+
+nestedMatchIsValue :: Bool
+nestedMatchIsValue = case innerBlock "match (left) { 1 -> 10, _ -> 20 }" of
+    Just [ExpressionStatement _ (MatchExpression {}) False] -> True
+    _ -> False
+
+ifInsideValueBlockIsStatement :: Bool
+ifInsideValueBlockIsStatement = case innerBlock "if (other) { Twice(left); } else { Twice(right); } 1" of
+    Just [IfStatement _ _ (Block [ExpressionStatement _ _ True]) (Just (Block [ExpressionStatement _ _ True])), ExpressionStatement _ _ False] -> True
+    _ -> False
+
+matchInsideValueBlockIsStatement :: Bool
+matchInsideValueBlockIsStatement = case innerBlock "match (left) { 1 -> { Twice(left); } } 1" of
+    Just [ExpressionStatement _ (MatchExpression {}) True, ExpressionStatement _ _ False] -> True
+    _ -> False
+
+guardParses :: Bool
+guardParses = case firstStatements (body "guard (left > 0) else { return 0; } return 1;") of
+    Just [GuardStatement _ (BinaryExpression _ GreaterThan _ _ ()) (Block [ReturnStatement {}]), _] -> True
+    _ -> False
+
+-- ---------------------------------------------------------------- types
+
+typeTests :: [(String, Bool)]
+typeTests =
+    [ ("a match expression has the type of its arms", accepted (body "int r = match (left) { 1 -> 10, _ -> 20 }; return r;"))
+    , ("a match expression types its literals from the receiver", accepted (body "long wide = match (left) { 1 -> 10, _ -> 20 }; return wide > 15 ? 1 : 0;"))
+    , ("a literal arm takes its type from an earlier arm", accepted (body "long wide = 3; long r = match (left) { 1 -> wide, _ -> 20 }; return r > 15 ? 1 : 0;"))
+    , ("a literal pattern is typed from its subject", accepted (body "long wide = 5; return match (wide) { 5 -> 1, _ -> 0 };"))
+    , ("a bool subject is covered by true and false", accepted (body "return match (flag) { true -> 1, false -> 2 };"))
+    , ("a type pattern that binds accepts every value", accepted (body "return match (left) { int value -> value + 1 };"))
+    , ("a statement match needs no arm that always accepts", accepted (body "match (left) { 1 -> { return 1; } } return 0;"))
+    , ("an accepted match reports nothing", codesOf (body "return match (left) { 1 -> 10, _ -> 20 };") == [])
+    ,
+        ( "a match expression that may accept nothing is rejected"
+        , rejectedWith "TAT0052" (body "return match (left) { 1 -> 10, 2 -> 20 };")
+        )
+    ,
+        ( "a guarded catch-all does not make a match expression complete"
+        , rejectedWith "TAT0052" (body "return match (left) { _ if flag -> 1 };")
+        )
+    ,
+        ( "one bool literal does not cover a bool subject"
+        , rejectedWith "TAT0052" (body "return match (flag) { true -> 1 };")
+        )
+    ,
+        ( "a guarded bool literal does not cover its value"
+        , rejectedWith "TAT0052" (body "return match (flag) { true if other -> 1, false -> 2 };")
+        )
+    , ("a match expression without arms is rejected", rejectedWith "TAT0052" (body "return match (left) { };"))
+    ,
+        ( "two bool subjects are covered by their four combinations"
+        , accepted
+            (body "return match (flag), (other) { (true), (true) -> 3, (true), (false) -> 2, (false), (true) -> 1, (false), (false) -> 0 };")
+        )
+    ,
+        ( "a wildcard covers both values of its bool subject"
+        , accepted (body "return match (flag), (other) { (true), (_) -> 1, (false), (true) -> 2, (false), (false) -> 3 };")
+        )
+    ,
+        ( "a missing combination of two bool subjects is rejected"
+        , rejectedWith
+            "TAT0052"
+            (body "return match (flag), (other) { (true), (true) -> 3, (true), (false) -> 2, (false), (true) -> 1 };")
+        )
+    ,
+        ( "a guarded combination does not count as covered"
+        , rejectedWith
+            "TAT0052"
+            (body "return match (flag), (other) { (true), (_) -> 1, (false), (true) -> 2, (false), (false) if left > 0 -> 3 };")
+        )
+    ,
+        ( "a bool subject beside an int subject needs a catch-all"
+        , rejectedWith "TAT0052" (body "return match (flag), (left) { (true), (_) -> 1, (false), (1) -> 2 };")
+        )
+    ,
+        ( "arms of different types are rejected"
+        , rejectedWith "TAT0050" (body "long wide = 3; long r = match (left) { 1 -> wide, _ -> left }; return 0;")
+        )
+    ,
+        ( "a match selects one of several strings"
+        , accepted (body "auto text = match (left) { 1 -> \"a\", _ -> \"b\" }; return 0;")
+        )
+    ,
+        ( "a callable match result is not lowered yet"
+        , rejectedWith "TAT0051" (body "auto pick = match (left) { 1 -> \\(int v) -> v + 1, _ -> \\(int v) -> v }; return 0;")
+        )
+    ,
+        ( "a string and a number are not arms of one match"
+        , rejectedWith "TAT0050" (body "auto text = match (left) { 1 -> \"a\", _ -> 2 }; return 0;")
+        )
+    ,
+        ( "a string is a match subject"
+        , accepted (body "match (\"a\") { \"a\" -> { }, _ -> { } } return 0;")
+        )
+    ,
+        ( "a callable subject is not lowered yet"
+        , rejectedWith "TAT0058" (body "match (\\(int v) -> v) { _ -> { } } return 0;")
+        )
+    ,
+        ( "a string subject is not matched by a number"
+        , rejectedWith "TAT0054" (body "match (\"a\") { 1 -> { }, _ -> { } } return 0;")
+        )
+    ,
+        ( "a numeric subject is not matched by a string"
+        , rejectedWith "TAT0054" (body "match (left) { \"a\" -> { }, _ -> { } } return 0;")
+        )
+    ,
+        ( "string patterns alone do not cover every string"
+        , rejectedWith "TAT0052" (body "int r = match (\"a\") { \"a\" -> 1, \"b\" -> 2 }; return r;")
+        )
+    ,
+        ( "a string pattern written twice can never be selected"
+        , rejectedWith "TAT0053" (body "int r = match (\"a\") { \"a\" -> 1, \"a\" -> 2, _ -> 3 }; return r;")
+        )
+    ,
+        ( "an arm with too few patterns is rejected"
+        , rejectedWith "TAT0048" (body "match (left), (right) { 1 -> { } } return 0;")
+        )
+    ,
+        ( "an arm with too many patterns is rejected"
+        , rejectedWith "TAT0048" (body "match (left) { 1, 2 -> { } } return 0;")
+        )
+    ,
+        ( "a second arm for the same literal can never be selected"
+        , rejectedWith "TAT0053" (body "match (left) { 1 -> { }, 1 -> { } } return 0;")
+        )
+    ,
+        ( "an arm after a catch-all can never be selected"
+        , rejectedWith "TAT0053" (body "match (left) { _ -> { }, 1 -> { } } return 0;")
+        )
+    ,
+        ( "an arm after a binding catch-all can never be selected"
+        , rejectedWith "TAT0053" (body "match (left) { int value -> { }, _ -> { } } return 0;")
+        )
+    ,
+        ( "an arm shadowed in every position can never be selected"
+        , rejectedWith "TAT0053" (body "match (left), (right) { (1), (_) -> { }, (1), (2) -> { } } return 0;")
+        )
+    ,
+        ( "a guarded arm does not shadow a later arm"
+        , accepted (body "match (left) { 1 if flag -> { }, 1 -> { } } return 0;")
+        )
+    ,
+        ( "an arm that differs in one position is reachable"
+        , accepted (body "match (left), (right) { (1), (2) -> { }, (1), (_) -> { } } return 0;")
+        )
+    , ("a null pattern needs a reference subject", rejectedWith "TAT0055" (body "match (left) { null -> { } } return 0;"))
+    , ("an enum case pattern needs enum declarations", rejectedWith "TAT0056" (body "match (left) { .Ready -> { } } return 0;"))
+    ,
+        ( "a type pattern must name the type of its subject"
+        , rejectedWith "TAT0057" (body "match (left) { long value -> { } } return 0;")
+        )
+    , -- No numeric conversion is applied to a type pattern.
+        ( "a wider scalar type pattern does not make a match complete by conversion"
+        , rejectedWith "TAT0057" (body "return match (left) { long value -> 1 };")
+        )
+    ,
+        ( "a narrower scalar type pattern is rejected as well"
+        , rejectedWith "TAT0057" (body "return match (left) { short value -> 1, _ -> 0 };")
+        )
+    , ("a type pattern of the subject's own type accepts every value", accepted (body "return match (left) { int value -> value };"))
+    , -- Negative constants are checked against the type of the subject.
+      ("the most negative value of a type is a pattern", accepted (body "return match (left) { -9223372036854775808 -> 1, _ -> 0 };"))
+    ,
+        ( "a negative literal below the range of the subject is rejected"
+        , rejectedWith "TAT0016" (body "return match (left) { -9223372036854775809 -> 1, _ -> 0 };")
+        )
+    ,
+        ( "a positive literal above the range of the subject is still rejected"
+        , rejectedWith "TAT0016" (body "return match (left) { 9223372036854775808 -> 1, _ -> 0 };")
+        )
+    , ("the range of a narrow signed subject includes its minimum", accepted (body "byte small = 1; return match (small) { -128 -> 1, 127 -> 2, _ -> 0 };"))
+    ,
+        ( "a negative literal below the range of a narrow subject is rejected"
+        , rejectedWith "TAT0016" (body "byte small = 1; return match (small) { -129 -> 1, _ -> 0 };")
+        )
+    ,
+        ( "a positive literal above the range of a narrow subject is rejected"
+        , rejectedWith "TAT0016" (body "byte small = 1; return match (small) { 128 -> 1, _ -> 0 };")
+        )
+    ,
+        ( "a negative literal is outside every unsigned type"
+        , rejectedWith "TAT0016" (body "ubyte small = 1; return match (small) { -1 -> 1, _ -> 0 };")
+        )
+    , ("a second arm for the same negative value is unreachable", rejectedWith "TAT0053" (body "return match (left) { -1 -> 1, -1 -> 2, _ -> 0 };"))
+    , ("minus zero is the value zero", rejectedWith "TAT0053" (body "return match (left) { 0 -> 1, -0 -> 2, _ -> 0 };"))
+    ,
+        ( "a negative literal does not make a match complete"
+        , rejectedWith "TAT0052" (body "return match (left) { -1 -> 1, 0 -> 2, 1 -> 3 };")
+        )
+    , ("a literal pattern of another type is rejected", rejectedWith "TAT0054" (body "match (left) { \"a\" -> { } } return 0;"))
+    , ("a guard must be bool or numeric", rejectedWith "TAT0049" (body "match (left) { 1 if \"x\" -> { } } return 0;"))
+    , ("a numeric guard is accepted", accepted (body "match (left) { 1 if right -> { } } return 0;"))
+    ,
+        ( "a pure expression body in a statement match is rejected"
+        , rejectedWith "TAT0013" (body "match (left) { 1 -> 5 } return 0;")
+        )
+    ,
+        ( "an effectful expression body in a statement match is accepted"
+        , accepted (body "int r = 0; match (left) { 1 -> r = 5, _ -> r = Twice(left) } return r;")
+        )
+    , ("a pattern binding may be assigned", accepted (body "match (left) { int value -> { value = 5; return value; } } return 0;"))
+    ,
+        ( "a nested if in last position supplies the value of its block"
+        , accepted (body "int r = if (flag) { if (other) { 1 } else { 2 } } else { 3 }; return r;")
+        )
+    ,
+        ( "a match in last position supplies the value of its block"
+        , accepted (body "int r = if (flag) { match (left) { 1 -> 10, _ -> 20 } } else { 3 }; return r;")
+        )
+    ,
+        ( "a match in last position of a value block must accept every value"
+        , rejectedWith "TAT0052" (body "int r = if (flag) { match (left) { 1 -> 10 } } else { 3 }; return r;")
+        )
+    ,
+        ( "a match in last position of a statement arm is a statement"
+        , accepted (body "match (left) { 1 -> { match (right) { 2 -> { return 2; } } } } return 0;")
+        )
+    ,
+        ( "a pattern binding is in scope in its guard"
+        , accepted (body "return match (left) { int value if value > 3 -> value, _ -> 0 };")
+        )
+    ,
+        ( "a pattern binding is not in scope after the match"
+        , not (accepted (body "match (left) { int value -> { } } return value;"))
+        )
+    ,
+        ( "a pattern binding is not in scope in a later arm"
+        , not (accepted (body "return match (left) { int value if flag -> value, _ -> value };"))
+        )
+    ,
+        ( "two arms may bind the same name"
+        , accepted (body "return match (left) { int value if flag -> value, int value -> value + 1 };")
+        )
+    ,
+        ( "one arm cannot bind a name twice"
+        , rejectedWith "TAR0008" (body "match (left), (right) { (int value), (int value) -> { } } return 0;")
+        )
+    ,
+        ( "a pattern binding cannot reuse a name in scope"
+        , rejectedWith "TAR0008" (body "match (left) { int flag -> { } } return 0;")
+        )
+    , ("an if expression has the type of its blocks", accepted (body "int r = if (flag) { left } else { right }; return r;"))
+    , ("an if expression types its literals from the receiver", accepted (body "long wide = if (flag) { 1 } else { 2 }; return wide > 1 ? 1 : 0;"))
+    ,
+        ( "the blocks of an if expression must have one type"
+        , rejectedWith "TAT0037" (body "long wide = 3; long r = if (flag) { wide } else { left }; return 0;")
+        )
+    , ("the test of an if expression must be bool or numeric", rejectedWith "TAT0036" (body "int r = if (\"s\") { 1 } else { 2 }; return r;"))
+    ,
+        ( "a value block must end with an expression"
+        , rejectedWith "TAT0046" (body "int r = if (flag) { Twice(left); } else { 2 }; return r;")
+        )
+    , ("an empty value block has no value", rejectedWith "TAT0046" (body "int r = if (flag) { } else { 2 }; return r;"))
+    ,
+        ( "a value block may return from the method"
+        , accepted (body "int r = if (flag) { return 1; } else { 3 }; return r;")
+        )
+    ,
+        ( "a return nested inside a value block is accepted"
+        , accepted (body "int r = match (left) { 1 -> { if (flag) { return 1; } 2 }, _ -> 3 }; return r;")
+        )
+    ,
+        ( "a return in a value block must carry the return type of the method"
+        , rejectedWith "TAT0005" (body "int r = if (flag) { return true; } else { 3 }; return r;")
+        )
+    ,
+        ( "a value block that leaves gives the if expression the type of the other block"
+        , rejectedWith "TAT0002" (body "bool r = if (flag) { return 1; } else { left }; return 0;")
+        )
+    ,
+        ( "an if expression whose blocks both leave is valid"
+        , accepted (body "int r = if (flag) { return 1; } else { return 2; }; return r;")
+        )
+    ,
+        ( "a match expression whose arms all leave is valid"
+        , accepted (body "int r = match (left) { 1 -> { return 1; }, _ -> { return 2; } }; return r;")
+        )
+    ,
+        ( "an expression that never completes is not held to the type of its receiver"
+        , accepted (body "bool r = if (flag) { return 1; } else { return 2; }; return r ? 1 : 0;")
+        )
+    ,
+        ( "an inferred binding may be initialized by an expression that never completes"
+        , accepted (body "auto r = if (flag) { return 1; } else { return 2; }; return 0;")
+        )
+    ,
+        ( "the returns of an expression that never completes are still checked"
+        , rejectedWith "TAT0005" (body "int r = if (flag) { return true; } else { return 2; }; return r;")
+        )
+    ,
+        ( "the statements after an expression that never completes are still checked"
+        , rejectedWith "TAT0012" (body "int r = if (flag) { return 1; } else { return 2; }; int s = r + true; return s;")
+        )
+    ,
+        ( "a match whose arms all leave must still be exhaustive"
+        , rejectedWith "TAT0052" (body "int r = match (left) { 1 -> { return 1; } }; return r;")
+        )
+    ,
+        ( "an expression that never completes may be an operand"
+        , accepted (body "int r = 1 + (if (flag) { return 1; } else { return 2; }); return r;")
+        )
+    ,
+        ( "an expression that never completes may be a condition"
+        , accepted (body "if (if (flag) { return 1; } else { return 2; }) { return 3; } return 4;")
+        )
+    ,
+        ( "a block that ends with an expression that never completes leaves"
+        , accepted (body "int r = if (left > 0) { 5 } else { if (flag) { return 1; } else { return 2; } }; return r;")
+        )
+    ,
+        ( "a right operand of && that never completes does not end the statement"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { bool b = flag && (if (other) { return 1; } else { return 2; }); } return 5;")
+        )
+    ,
+        ( "a right operand of || that never completes does not end the statement"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { bool b = flag || (if (other) { return 1; } else { return 2; }); } return 5;")
+        )
+    ,
+        ( "a conditional result that never completes does not end the statement"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { int q = flag ? 1 : (if (other) { return 1; } else { return 2; }); } return 5;")
+        )
+    ,
+        ( "a coalescing fallback that never completes does not end the statement"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { int q = right ?: (if (other) { return 1; } else { return 2; }); } return 5;")
+        )
+    ,
+        ( "a left operand of && that never completes ends the statement"
+        , accepted (body "guard (left > 0) else { bool b = (if (other) { return 1; } else { return 2; }) && flag; } return 5;")
+        )
+    ,
+        ( "an argument that never completes ends the statement"
+        , accepted (body "guard (left > 0) else { int q = Twice(if (other) { return 1; } else { return 2; }); } return 5;")
+        )
+    ,
+        ( "a match whose only leaving arm is guarded completes"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { match (right) { _ if flag -> { return 1; } } } return 5;")
+        )
+    ,
+        ( "a match with a guarded arm and a catch-all that both leave does not complete"
+        , accepted (body "guard (left > 0) else { match (right) { 1 if flag -> { return 1; }, _ -> { return 2; } } } return 5;")
+        )
+    ,
+        ( "an if without an else whose block leaves completes"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { if (flag) { return 1; } } return 5;")
+        )
+    ,
+        ( "a loop with a condition completes even when its body leaves"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { while (flag) { return 1; } } return 5;")
+        )
+    ,
+        ( "a break of an inner loop expression does not leave the endless loop around it"
+        , accepted (body "guard (left > 0) else { while (true) { int q = while (true) { break 1; }; } } return 5;")
+        )
+    ,
+        ( "a break in a value block of an inner loop does not leave the endless loop around it"
+        , accepted (body "guard (left > 0) else { while (true) { while (flag) { int q = if (other) { break; } else { 1 }; } } } return 5;")
+        )
+    ,
+        ( "a break in a match arm leaves the endless loop it stands in"
+        , rejectedWith "TAT0061" (body "guard (left > 0) else { while (true) { match (right) { 1 -> { break; }, _ -> { } } } } return 5;")
+        )
+    , -- The statements kept and dropped around an operand that never completes.
+        ( "the operands before one that never completes keep their effects and the ones after it are dropped"
+        , case loweredBody (body "int t = 0; int r = Twice(t += 3) + (if (flag) { return t; } else { return 2; }) + Twice(9); return r;") of
+            Just [CoreBind _, CoreAssign _ _, CoreEvaluate (CoreApply _ _ _), CoreIf _ [CoreReturn _] [CoreReturn _]] -> True
+            _ -> False
+        )
+    ,
+        ( "a right operand of && that never completes lowers to a conditional without a slot"
+        , case loweredBody (body "bool b = flag && (if (other) { return 1; } else { return 2; }); return b ? 3 : 4;") of
+            Just (CoreIf _ [CoreIf _ [CoreReturn _] [CoreReturn _]] [] : rest) -> not (any bindsResultSlot rest)
+            _ -> False
+        )
+    ,
+        ( "a guard block may leave through an initializer that never completes"
+        , accepted (body "guard (left > 0) else { int q = if (flag) { return 1; } else { return 2; }; } return 5;")
+        )
+    , -- Nothing is stored for a value that does not exist: no slot, no
+      -- binding, and none of the statements that are never reached.
+        ( "an initializer that never completes lowers to its branches alone"
+        , case loweredBody (body "int r = if (flag) { return 1; } else { return 2; }; return r;") of
+            Just [CoreIf _ [CoreReturn _] [CoreReturn _]] -> True
+            _ -> False
+        )
+    ,
+        ( "a returned expression that never completes lowers to its branches alone"
+        , case loweredBody (body "return Twice(if (flag) { return 1; } else { return 2; });") of
+            Just [CoreIf _ [CoreReturn _] [CoreReturn _]] -> True
+            _ -> False
+        )
+    ,
+        ( "a match that never completes lowers without a result slot"
+        , case loweredBody (body "int r = match (left) { 1 -> { return 1; }, _ -> { return 2; } }; return r;") of
+            Just statements -> not (any bindsResultSlot statements)
+            Nothing -> False
+        )
+    ,
+        ( "a value block that can complete must end with its value"
+        , rejectedWith "TAT0046" (body "int r = if (flag) { if (other) { return 1; } } else { 3 }; return r;")
+        )
+    ,
+        ( "an arm that leaves does not make a match complete"
+        , rejectedWith "TAT0052" (body "int r = match (left) { 1 -> { return 1; }, 2 -> 5 }; return r;")
+        )
+    ,
+        ( "a value block may break out of the loop around its expression"
+        , accepted (body "while (true) { int r = if (flag) { break; } else { 2 }; } return 0;")
+        )
+    ,
+        ( "a value block may continue the loop around its expression"
+        , accepted (body "while (flag) { int r = match (left) { 1 -> { continue; }, _ -> 2 }; } return 0;")
+        )
+    , ("a break in a value block needs a loop", rejectedWith "TAT0025" (body "int r = if (flag) { break; } else { 2 }; return r;"))
+    , ("a continue in a value block needs a loop", rejectedWith "TAT0027" (body "int r = if (flag) { continue; } else { 2 }; return r;"))
+    ,
+        ( "a break in a value block does not leave a loop from inside a closure"
+        , not (accepted (body "while (flag) { auto f = \\() -> { int r = if (other) { break; } else { 2 }; return r; }; } return 0;"))
+        )
+    ,
+        ( "a break in the condition of a loop leaves that loop"
+        , accepted (body "int n = 0; while (if (n > left) { break; } else { true }) { n += 1; } return n;")
+        )
+    , ("a break in the condition of a loop needs no loop around it", accepted (body "while (if (flag) { break; } else { true }) { } return 0;"))
+    ,
+        ( "a continue in the update of a loop is accepted"
+        , accepted (body "int t = 0; for (int i = 0; i < 3; i += if (t > 9) { t = 0; continue; } else { 1 }) { t += 5; } return t;")
+        )
+    ,
+        ( "a value block may carry a value out of the loop expression around it"
+        , accepted (body "int r = while (true) { int q = if (flag) { break 1; } else { 2 }; break q; }; return r;")
+        )
+    ,
+        ( "a value block may return from a loop expression"
+        , accepted (body "int r = while (true) { int q = if (flag) { return 1; } else { 2 }; break q; }; return r;")
+        )
+    , -- Real misuse of the same forms.
+        ( "a break that leaves a statement loop from a value block carries no value"
+        , rejectedWith "TAT0026" (body "while (flag) { int q = if (other) { break 1; } else { 2 }; } return 0;")
+        )
+    ,
+        ( "a break that leaves a loop expression from a value block must carry a value"
+        , rejectedWith "TAT0040" (body "int r = while (true) { int q = if (flag) { break; } else { 2 }; break q; }; return r;")
+        )
+    ,
+        ( "the values carried out of value blocks must have the type of the other break values"
+        , rejectedWith "TAT0043" (body "long wide = 3; int r = while (true) { int q = if (flag) { break wide; } else { 2 }; break q; }; return r;")
+        )
+    ,
+        ( "a break in the condition of a statement loop carries no value"
+        , rejectedWith "TAT0026" (body "while (if (flag) { break 1; } else { true }) { } return 0;")
+        )
+    ,
+        ( "a return in a value block of a loop expression carries the return type of the method"
+        , rejectedWith "TAT0005" (body "int r = while (true) { int q = if (flag) { return true; } else { 2 }; break q; }; return r;")
+        )
+    ,
+        ( "a break in a value block inside a closure does not reach a loop expression around the closure"
+        , not (accepted (body "int r = while (true) { auto f = \\() -> { int q = if (flag) { break 1; } else { 2 }; return q; }; break f(); }; return r;"))
+        )
+    , -- The condition of a loop expression is the constant true, so a break
+      -- cannot stand in it.
+        ( "a loop expression whose condition is not the constant true is still rejected"
+        , rejectedWith "TAT0041" (body "int t = 0; int r = while (if (t > left) { break t; } else { true }) { t += 1; }; return r;")
+        )
+    , -- Return types are inferred through expressions, and not across the
+      -- edge of a nested callable.
+        ( "a callable infers its result from a return in a value block"
+        , accepted (body "auto f = \\(int v) -> { int q = if (v > 0) { return 1; } else { 2 }; return q; }; return f(left);")
+        )
+    ,
+        ( "the returns of a callable with an inferred result must agree, also through a value block"
+        , rejectedWith "TAT0062" (body "auto f = \\(int v) -> { int q = if (v > 0) { return true; } else { 2 }; return q; }; return 0;")
+        )
+    ,
+        ( "the returns of a callable do not count for the method that creates it"
+        , accepted (body "auto g = \\(int w) -> { bool b = if (w > 0) { return true; } else { false }; return b; }; return g(left) ? 1 : 2;")
+        )
+    ,
+        ( "the returns of a nested callable do not count for the callable around it"
+        , "TAT0062"
+            `notElem` codesOf
+                (body "auto f = \\(int v) -> { auto g = \\(int w) -> { return w > 0; }; int q = if (v > 0) { return 1; } else { 2 }; return q; }; return f(left);")
+        )
+    ,
+        ( "a method with an inferred return type infers it from a return in a value block"
+        , accepted
+            ( unlines
+                [ "class Program {"
+                , "    public static auto Pick(_ int v) { int q = if (v > 0) { return 1; } else { 2 }; return q; }"
+                , "}"
+                ]
+            )
+        )
+    , -- Callables are compiled and verified, through Core and CorePrep, but
+      -- not run: neither the reference evaluator nor the JIT of the native
+      -- smoke program executes closures.
+        ( "a callable that returns from a loop expression is accepted"
+        , accepted (body "auto f = \\(int v) -> { int q = while (true) { if (v > 3) { return 7; } break 2; }; return q + v; }; return f(left);")
+        )
+    ,
+        ( "a callable every path of which returns from a value block is accepted"
+        , accepted (body "auto f = \\(int v) -> { int q = if (v > 0) { return v * 2; } else { return 0 - v; }; return q; }; return f(left);")
+        )
+    ,
+        ( "a break in a value block does not leave a loop from inside a callable in that loop"
+        , rejectedWith "TAT0025" (body "while (flag) { auto f = \\(int v) -> { int q = if (v > 0) { break; } else { 2 }; return q; }; } return 0;")
+        )
+    ,
+        ( "a continue in a value block does not reach a loop from inside a callable in that loop"
+        , rejectedWith "TAT0027" (body "while (flag) { auto f = \\(int v) -> { int q = if (v > 0) { continue; } else { 2 }; return q; }; } return 0;")
+        )
+    ,
+        ( "a continue in the condition of a loop is accepted"
+        , accepted (body "int n = 0; while (if ((n += 1) < left) { continue; } else { n < right }) { } return n;")
+        )
+    ,
+        ( "a break in the update of a loop is accepted"
+        , accepted (body "for (int i = 0; i < 3; i += if (flag) { break; } else { 1 }) { } return 0;")
+        )
+    ,
+        ( "a break in the update of a loop statement carries no value"
+        , rejectedWith "TAT0026" (body "for (int i = 0; i < 3; i += if (flag) { break 1; } else { 1 }) { } return 0;")
+        )
+    ,
+        ( "a break in the update of a loop expression carries a value"
+        , rejectedWith "TAT0040" (body "int r = for (int i = 0; ; i += if (flag) { break; } else { 1 }) { break 2; }; return r;")
+        )
+    ,
+        ( "a continue in the condition of a loop needs that loop and no other"
+        , rejectedWith "TAT0027" (body "int q = if (flag) { continue; } else { 2 }; return q;")
+        )
+    ,
+        ( "a loop inside a value block may still be left"
+        , accepted (body "int r = if (flag) { int n = 0; while (n < left) { n += 1; if (n > 3) { break; } } n } else { 2 }; return r;")
+        )
+    , ("a guard that returns is accepted", accepted (body "guard (left > 0) else { return 0; } return left;"))
+    , ("a guard condition must be bool or numeric", rejectedWith "TAT0060" (body "guard (\"s\") else { return 0; } return 1;"))
+    ,
+        ( "the else block of a guard must leave"
+        , rejectedWith "TAT0061" (body "guard (flag) else { int unused = Twice(left); } return 0;")
+        )
+    , ("an empty guard block does not leave", rejectedWith "TAT0061" (body "guard (flag) else { } return 0;"))
+    ,
+        ( "a guard block that leaves on both branches is accepted"
+        , accepted (body "guard (flag) else { if (other) { return 1; } else { return 2; } } return 0;")
+        )
+    ,
+        ( "a guard block that leaves on one branch only is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { if (other) { return 1; } } return 0;")
+        )
+    ,
+        ( "a guard may leave a loop"
+        , accepted (body "int n = 0; while (true) { guard (n < left) else { break; } n += 1; } return n;")
+        )
+    , ("a guard cannot break outside a loop", rejectedWith "TAT0025" (body "guard (flag) else { break; } return 0;"))
+    , ("a guard block may end with a nested block that leaves", accepted (body "guard (flag) else { { return 1; } } return 0;"))
+    , -- The rule is about control flow, not about the last statement.
+      ("a guard block may leave before its last statement", accepted (body "int n = left; guard (flag) else { return 1; n += 1; } return n;"))
+    , ("a guard block that never ends is accepted", accepted (body "guard (flag) else { while (true) { } } return 0;"))
+    , ("a guard block with an endless for loop is accepted", accepted (body "guard (flag) else { for (int i = 0; ; i += 1) { } } return 0;"))
+    ,
+        ( "a guard block whose loop can be left is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { while (true) { if (other) { break; } } } return 0;")
+        )
+    ,
+        ( "a guard block whose loop is left from a value block is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { while (true) { int q = if (other) { break; } else { 1 }; } } return 0;")
+        )
+    ,
+        ( "a break of an inner loop does not leave the outer endless loop"
+        , accepted (body "guard (flag) else { while (true) { while (other) { break; } } } return 0;")
+        )
+    ,
+        ( "a guard block whose loop has a condition is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { while (other) { } } return 0;")
+        )
+    ,
+        ( "a guard block may end with a match all of whose arms leave"
+        , accepted (body "guard (flag) else { match (other) { true -> { return 1; }, false -> { return 2; } } } return 0;")
+        )
+    ,
+        ( "a guard block that ends with a match that may select no arm is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { match (left) { 1 -> { return 1; } } } return 0;")
+        )
+    ,
+        ( "a guard block that ends with a match one arm of which completes is rejected"
+        , rejectedWith "TAT0061" (body "guard (flag) else { match (left) { 1 -> { return 1; }, _ -> { } } } return 0;")
+        )
+    , ("a guard may continue a loop", accepted (body "int n = 0; while (n < left) { n += 1; guard (n > 1) else { continue; } } return n;"))
+    , ("a guard cannot continue outside a loop", rejectedWith "TAT0027" (body "guard (flag) else { continue; } return 0;"))
+    , ("a binding in a guard condition is recognized and not implemented", parseFailsWith "TAP0035" (body "guard (auto n = left) else { return 0; } return 1;"))
+    , ("a nested block sees the names declared before it", accepted (body "int a = left; { a += 1; } return a;"))
+    ,
+        ( "a name declared in a nested block ends with the block"
+        , not (accepted (body "{ int inner = left; } return inner;"))
+        )
+    ,
+        ( "sibling blocks may declare the same name"
+        , accepted (body "int total = 0; { int part = left; total += part; } { int part = right; total += part; } return total;")
+        )
+    ,
+        ( "a nested block cannot redeclare a name that is in scope"
+        , rejectedWith "TAR0003" (body "int a = left; { int a = right; } return a;")
+        )
+    ,
+        ( "a return in a nested block is checked against the function type"
+        , rejectedWith "TAT0005" (body "{ return flag; } return 0;")
+        )
+    , ("a break in a nested block needs a loop", rejectedWith "TAT0025" (body "{ break; } return 0;"))
+    ,
+        ( "a break value in a nested block supplies the loop value"
+        , accepted (body "int r = while (true) { { break left; } }; return r;")
+        )
+    ]
+
+-- ---------------------------------------------------------------- lowering
+
+loweringTests :: [(String, Bool)]
+loweringTests =
+    [ ("a match expression lowers to a subject, a result slot and nested tests", matchExpressionLowers)
+    , ("a statement match lowers without a result slot", matchStatementLowers)
+    , ("a catch-all arm ends the chain without a test", catchAllEndsChain)
+    , ("patterns for several subjects are tested together", severalSubjectsLower)
+    , ("a guard after a literal is the last operand of the arm's test", guardedLiteralLowers)
+    , ("a guard that stores is decided in its own slot", storingGuardLowers)
+    , ("a guard on a catch-all is the test itself", guardedCatchAllLowers)
+    , ("a pattern binding is bound to the subject before its guard", bindingLowers)
+    , ("an if expression over pure blocks is one lazy conditional", pureIfExpressionLowers)
+    , ("an if expression whose block has statements selects into a slot", ifExpressionWithStatementsLowers)
+    , ("a guard runs its block when the condition is false", guardLowers)
+    , ("the statements of a nested block join the enclosing sequence", nestedBlockLowers)
+    , ("a match of any width is one chain of conditionals", all (isOneChain . wideMatch) [2, 17, 40, 200])
+    , ("a chain of arms is as deep as one arm", all ((== 1) . nestingOf . wideMatch) [2, 17, 40, 200])
+    , ("the names of all arms are bound before the chain", bindingsPrecedeChain)
+    ]
+
+-- | A match expression with the given number of literal arms and a catch-all.
+wideMatch :: Int -> String
+wideMatch count =
+    body
+        ( "return match (left) { "
+            ++ concat [show index ++ " -> " ++ show (index * 3 + 1) ++ ", " | index <- [0 .. count - 2]]
+            ++ "_ -> 0 };"
+        )
+
+-- | Whether the body is a subject, a result slot, one chain of conditionals and a return.
+isOneChain :: String -> Bool
+isOneChain text = case loweredBody text of
+    Just [CoreBind _, CoreBind _, chain@CoreIf {}, CoreReturn _] -> linksOnly chain
+    _ -> False
+    where
+        -- Every false branch is the next link until the body of the catch-all.
+        linksOnly statement = case statement of
+            CoreIf _ [CoreAssign _ _] [next@CoreIf {}] -> linksOnly next
+            CoreIf _ [CoreAssign _ _] [CoreAssign _ _] -> True
+            _ -> False
+
+{- | The deepest nesting of conditional statements in the lowered body, where
+the links of a chain share one level: a false branch that is exactly one
+conditional continues the chain, as it does in every stage after Core.
+-}
+nestingOf :: String -> Int
+nestingOf text = maybe 0 depth (loweredBody text)
+    where
+        depth :: [CoreStatement] -> Int
+        depth statements = maximum (0 : map statementDepth statements)
+        statementDepth statement = case statement of
+            CoreIf _ whenTrue [next@CoreIf {}] -> max (1 + depth whenTrue) (statementDepth next)
+            CoreIf _ whenTrue whenFalse -> 1 + max (depth whenTrue) (depth whenFalse)
+            _ -> 0
+
+bindingsPrecedeChain :: Bool
+bindingsPrecedeChain = case loweredBody (body "return match (left) { int low if low < 3 -> low, int high if high > 9 -> high, _ -> 0 };") of
+    Just
+        [ CoreBind (CoreBinding subject _ False _)
+            , CoreBind (CoreBinding first _ True (CoreVariable firstSource _))
+            , CoreBind (CoreBinding second _ True (CoreVariable secondSource _))
+            , CoreBind (CoreBinding slot _ True _)
+            , CoreIf _ [CoreAssign _ _] [CoreIf _ [CoreAssign _ _] [CoreAssign _ _]]
+            , CoreReturn _
+            ] ->
+        map spelling [first, second] == ["low", "high"]
+            && all (== subject) [firstSource, secondSource]
+            && generated "$matched" slot
+    _ -> False
+
+-- | Whether a statement binds the result slot of a match or of a choice.
+bindsResultSlot :: CoreStatement -> Bool
+bindsResultSlot statement = case statement of
+    CoreBind binding -> any (`isPrefixOf` identifierText (resolvedSpelling (coreBindingName binding))) ["$matched", "$selected"]
+    _ -> False
+
+loweredBody :: String -> Maybe [CoreStatement]
+loweredBody text = case compileSource text of
+    Right artifacts -> functionBody (artifactCore artifacts)
+    Left _ -> Nothing
+
+functionBody :: CoreModule -> Maybe [CoreStatement]
+functionBody moduleValue =
+    case [ coreFunctionBody function
+         | function <- coreModuleFunctions moduleValue
+         , spelling (coreFunctionName function) == "Evaluate"
+         ] of
+        statements : _ -> Just statements
+        [] -> Nothing
+
+spelling :: ResolvedName -> String
+spelling = identifierText . resolvedSpelling
+
+generated :: String -> ResolvedName -> Bool
+generated prefix name = prefix `isPrefixOf` spelling name
+
+-- | Whether an expression compares the given local with the given integer.
+comparesWith :: ResolvedName -> Integer -> CoreExpression -> Bool
+comparesWith subject value expression = case expression of
+    CorePrimitive CoreEqual [CoreVariable name _, CoreLiteral (CoreInteger literal) _] _ -> name == subject && literal == value
+    _ -> False
+
+matchExpressionLowers :: Bool
+matchExpressionLowers = case loweredBody (body "return match (left) { 1 -> 10, 2 -> 20, _ -> 30 };") of
+    Just
+        [ CoreBind (CoreBinding subject _ False (CoreVariable source _))
+            , CoreBind (CoreBinding slot _ True _)
+            , CoreIf first [CoreAssign firstStore _] [CoreIf second [CoreAssign secondStore _] [CoreAssign lastStore _]]
+            , CoreReturn (CoreVariable result _)
+            ] ->
+        generated "$subject" subject
+            && spelling source == "left"
+            && generated "$matched" slot
+            && comparesWith subject 1 first
+            && comparesWith subject 2 second
+            && all (== slot) [firstStore, secondStore, lastStore, result]
+    _ -> False
+
+matchStatementLowers :: Bool
+matchStatementLowers = case loweredBody (body "match (left) { 1 -> { return 1; }, 2 -> { return 2; } } return 0;") of
+    Just [CoreBind (CoreBinding subject _ False _), CoreIf first [CoreReturn _] [CoreIf second [CoreReturn _] []], CoreReturn _] ->
+        generated "$subject" subject && comparesWith subject 1 first && comparesWith subject 2 second
+    _ -> False
+
+catchAllEndsChain :: Bool
+-- The match never completes, so the statement after it is not lowered.
+catchAllEndsChain = case loweredBody (body "match (left) { _ -> { return 7; } } return 0;") of
+    Just [CoreBind (CoreBinding subject _ False _), CoreReturn _] -> generated "$subject" subject
+    _ -> False
+
+severalSubjectsLower :: Bool
+severalSubjectsLower = case loweredBody (body "match (left), (right) { (1), (2) -> { return 1; }, (_), (3) -> { return 2; } } return 0;") of
+    Just
+        [ CoreBind (CoreBinding first _ False _)
+            , CoreBind (CoreBinding second _ False _)
+            , CoreIf (CorePrimitive CoreLogicalAnd [firstTest, secondTest] _) [CoreReturn _] [CoreIf laterTest [CoreReturn _] []]
+            , CoreReturn _
+            ] ->
+        comparesWith first 1 firstTest && comparesWith second 2 secondTest && comparesWith second 3 laterTest
+    _ -> False
+
+-- The guard is the last operand of the arm's test, behind the short-circuit
+-- conjunction, so it is evaluated only when the comparison holds.
+guardedLiteralLowers :: Bool
+guardedLiteralLowers = case loweredBody (body "match (left) { 1 if flag -> { return 1; } } return 0;") of
+    Just
+        [ CoreBind (CoreBinding subject _ False _)
+            , CoreIf (CorePrimitive CoreLogicalAnd [test, CoreVariable guard _] _) [CoreReturn _] []
+            , CoreReturn _
+            ] ->
+        comparesWith subject 1 test && spelling guard == "flag"
+    _ -> False
+
+-- A guard that stores into a local runs as statements, and only when the
+-- comparison holds: the decision is taken in a slot of its own.
+storingGuardLowers :: Bool
+storingGuardLowers = case loweredBody (body "int hits = 0; match (left) { 1 if (hits += 1) > 0 -> { return 1; } } return hits;") of
+    Just
+        [ _
+            , CoreBind (CoreBinding subject _ False _)
+            , CoreBind (CoreBinding decision _ True (CoreLiteral (CoreBoolean False) _))
+            , CoreIf test [CoreAssign stored _, CoreIf _ [CoreAssign decided _] []] []
+            , CoreIf (CoreVariable taken _) [CoreReturn _] []
+            , CoreReturn _
+            ] ->
+        generated "$accepted" decision
+            && comparesWith subject 1 test
+            && spelling stored == "hits"
+            && decided == decision
+            && taken == decision
+    _ -> False
+
+guardedCatchAllLowers :: Bool
+guardedCatchAllLowers = case loweredBody (body "match (left) { _ if flag -> { return 1; } } return 0;") of
+    Just [CoreBind _, CoreIf (CoreVariable guard _) [CoreReturn _] [], CoreReturn _] -> spelling guard == "flag"
+    _ -> False
+
+bindingLowers :: Bool
+bindingLowers = case loweredBody (body "match (left) { int value if value > 3 -> { return value; } } return 0;") of
+    Just
+        [ CoreBind (CoreBinding subject _ False _)
+            , CoreBind (CoreBinding bound _ True (CoreVariable source _))
+            , CoreIf (CorePrimitive CoreGreaterThan [CoreVariable tested _, _] _) [CoreReturn (CoreVariable returned _)] []
+            , CoreReturn _
+            ] ->
+        spelling bound == "value" && source == subject && tested == bound && returned == bound
+    _ -> False
+
+pureIfExpressionLowers :: Bool
+pureIfExpressionLowers = case loweredBody (body "return if (flag) { left } else { right };") of
+    Just [CoreReturn (CoreConditional (CoreVariable test _) (CoreVariable first _) (CoreVariable second _) _)] ->
+        map spelling [test, first, second] == ["flag", "left", "right"]
+    _ -> False
+
+ifExpressionWithStatementsLowers :: Bool
+ifExpressionWithStatementsLowers = case loweredBody (body "return if (flag) { int doubled = left * 2; doubled } else { right };") of
+    Just
+        [ CoreBind (CoreBinding slot _ True _)
+            , CoreIf _ [CoreBind (CoreBinding local _ _ _), CoreAssign first (CoreVariable stored _)] [CoreAssign second _]
+            , CoreReturn (CoreVariable result _)
+            ] ->
+        generated "$selected" slot && spelling local == "doubled" && stored == local && all (== slot) [first, second, result]
+    _ -> False
+
+guardLowers :: Bool
+guardLowers = case loweredBody (body "guard (flag) else { return 0; } return 1;") of
+    Just [CoreIf (CoreVariable condition _) [] [CoreReturn _], CoreReturn _] -> spelling condition == "flag"
+    _ -> False
+
+nestedBlockLowers :: Bool
+nestedBlockLowers = case loweredBody (body "int a = left; { int b = a + 1; a = b; } return a;") of
+    Just [CoreBind (CoreBinding outer _ _ _), CoreBind (CoreBinding inner _ _ _), CoreAssign stored _, CoreReturn _] ->
+        spelling outer == "a" && spelling inner == "b" && stored == outer
+    _ -> False
+
+-- ------------------------------------------------------------- evaluation
+
+evaluationTests :: [(String, Bool)]
+evaluationTests =
+    concat
+        [ [ ("unoptimized Core computes " ++ label, runs artifactCore statements arguments expected)
+          , ("optimized Core computes " ++ label, runs artifactOptimizedCore statements arguments expected)
+          ]
+        | (statements, runsOfCase) <- evaluationCases
+        , (arguments, expected) <- runsOfCase
+        , let label = show expected ++ " for " ++ show arguments ++ ": " ++ statements
+        ]
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> (Bool, Bool, Integer, Integer) -> Integer -> Bool
+runs select statements (flag, other, left, right) expected = case compileSource (body statements) of
+    Right artifacts ->
+        runFunction
+            (select artifacts)
+            "Evaluate"
+            [BooleanValue flag, BooleanValue other, IntegerValue left, IntegerValue right]
+            == Just (IntegerValue expected)
+    Left _ -> False
+
+-- ---------------------------------------------------------------- pipeline
+
+pipelineTests :: [(String, Bool)]
+pipelineTests =
+    [ ("every lowered case verifies as Core", all (verifies artifactCore) (map fst evaluationCases))
+    , ("every optimized case verifies as Core", all (verifies artifactOptimizedCore) (map fst evaluationCases))
+    , ("every case verifies as CorePrep", all corePrepVerifies (map fst evaluationCases))
+    , ("Core wire round-trips every lowered case", all wireRoundTrips (map fst evaluationCases))
+    , ("a template member keeps its match when instantiated", templateInstantiates)
+    , ("a call inside a match arm is reachable from the template member", templateReachesArmCalls)
+    , ("a closure body may contain a match and a guard", accepted closureSource)
+    ]
+
+verifies :: (FrontendArtifacts -> CoreModule) -> String -> Bool
+verifies select statements = case compileSource (body statements) of
+    Right artifacts -> verifyCore (select artifacts) == Right (select artifacts)
+    Left _ -> False
+
+corePrepVerifies :: String -> Bool
+corePrepVerifies statements = case compileSource (body statements) of
+    Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+
+wireRoundTrips :: String -> Bool
+wireRoundTrips statements = case compileSource (body statements) of
+    Right artifacts ->
+        let core = artifactCore artifacts
+         in (encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits) == Right core
+    Left _ -> False
+
+templateSource :: String
+templateSource =
+    unwords
+        [ "template<typename T> class Box {"
+        , "int Entry(_ int seed) {"
+        , "guard (seed > 0) else { return Fallback(); }"
+        , "return match (seed) { 1 -> Step(seed), int other if other > 9 -> Scale(other), _ -> if (seed > 5) { Step(seed) } else { 0 } };"
+        , "}"
+        , "int Step(_ int value) { return value + 4; }"
+        , "int Scale(_ int value) { return value * 2; }"
+        , "int Fallback() { return 0; }"
+        , "int Unused() { return 9; }"
+        , "}"
+        ]
+
+templatePlan :: Maybe TemplateSpecializationPlan
+templatePlan = case analyzeSemantics (CompilerInput "branching-template.tala" templateSource) of
+    Right artifacts ->
+        either
+            (const Nothing)
+            Just
+            ( planTemplateSpecializations
+                defaultTemplateSpecializationLimits
+                (semanticTypedAST artifacts)
+                [ TemplateSpecializationDemand
+                    (TemplateApplication (QualifiedName [Identifier "Box"]) [TypeTemplateArgument stringType])
+                    (TemplateMemberDemand [Identifier "Entry"])
+                    "branching-template"
+                ]
+            )
+    Left _ -> Nothing
+
+specializedMembers :: [Declaration ResolvedName Type]
+specializedMembers = case templatePlan of
+    Just plan ->
+        concat
+            [ members
+            | TypeDeclaration {typeMembers = members} <-
+                map templateSpecializationDeclaration (plannedTemplateSpecializations plan)
+            ]
+    Nothing -> []
+
+specializedNames :: [String]
+specializedNames = map (identifierText . resolvedSpelling . declarationName) specializedMembers
+
+templateInstantiates :: Bool
+templateInstantiates =
+    all (`isInfixOf` show specializedMembers) ["MatchExpression", "GuardStatement", "BlockExpression"]
+
+templateReachesArmCalls :: Bool
+templateReachesArmCalls =
+    all (`elem` specializedNames) ["Entry", "Step", "Scale", "Fallback"] && "Unused" `notElem` specializedNames
+
+closureSource :: String
+closureSource =
+    unlines
+        [ "class Program {"
+        , "    public static int Run(_ int seed) {"
+        , "        auto pick = \\(int value) -> { guard (value > 0) else { return 0; } return match (value) { 1 -> 10, int other -> other * 2 }; };"
+        , "        return pick(seed);"
+        , "    }"
+        , "}"
+        ]
+ test/ClosureTests.hs view
@@ -0,0 +1,617 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | End-to-end tests for callable literals and closure conversion.
+
+These tests intentionally inspect every compiler boundary.  A parser-only
+assertion would miss capture identity bugs, while a CorePrep-only assertion
+would make source regressions unnecessarily hard to diagnose.
+-}
+module ClosureTests (closureTests) where
+
+import Data.List (find, isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+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
+
+closureTests :: [(String, Bool)]
+closureTests =
+    [ ("lexer recognizes callable and capture delimiters", lexerRecognizesDelimiters)
+    , ("typed callable parameters preserve their declared types", typedParameters)
+    , ("parenthesized inferred callable parameters are accepted", parenthesizedInferredParameters)
+    , ("bare inferred callable parameters are accepted", bareInferredParameters)
+    , ("parenthesized zero-parameter callable is accepted", parenthesizedZeroParameters)
+    , ("bare zero-parameter callable is accepted", bareZeroParameters)
+    , ("callable expression body is retained", expressionBody)
+    , ("callable block body is retained", blockBody)
+    , ("empty explicit capture list is distinct from implicit capture", emptyExplicitCaptureList)
+    , ("normal explicit capture is parsed", normalCapture)
+    , ("weak explicit capture is parsed", weakCapture)
+    , ("unowned explicit capture is parsed", unownedCapture)
+    , ("capture aliases retain their initializer", captureAlias)
+    , ("capture source order remains stable", captureOrder)
+    , ("duplicate explicit capture is diagnosed", duplicateCapture)
+    , ("an omitted explicit capture is rejected", omittedExplicitCapture)
+    , ("callable parameters receive private symbols", privateParameterSymbols)
+    , ("captured bindings receive private symbols", privateCaptureSymbols)
+    , ("typed callable expression produces a function type", callableType)
+    , ("callable invocation checks argument count", callableArity)
+    , ("callable invocation checks argument types", callableArgumentType)
+    , ("weak primitive capture is rejected", weakPrimitiveCapture)
+    , ("weak capture rejects every canonical scalar family", weakScalarCaptures)
+    , ("unowned primitive capture is rejected", unownedPrimitiveCapture)
+    , ("weak String capture is accepted", weakStringCapture)
+    , ("weak callable capture is accepted", weakCallableCapture)
+    , ("implicit capture is discovered during Core lowering", implicitCaptureDiscovery)
+    , ("explicit capture lowers its initializer", explicitCaptureLowering)
+    , ("capture alias evaluates outside the callable body", aliasLowering)
+    , ("closure conversion lifts a CorePrep function", liftedFunction)
+    , ("closure creation records a CorePrep operation", makeClosureOperation)
+    , ("lifted closure receives hidden capture parameters first", hiddenCaptureParameters)
+    , ("closure result remains callable after CorePrep", closureResultType)
+    , ("nested callable conversion reaches a fixed point", nestedClosureConversion)
+    , ("Core verifier accepts a well-formed closure", coreVerifierAcceptsClosure)
+    , ("Core verifier rejects mismatched closure type", coreVerifierRejectsTypeMismatch)
+    , ("Core verifier rejects capture initializer mismatch", coreVerifierRejectsCaptureMismatch)
+    , ("Core wire v10 round-trips closure values and ownership", coreWireClosureRoundTrip)
+    , ("CorePrep wire v7 round-trips closure creation and ownership", corePrepWireClosureRoundTrip)
+    , ("CorePrep verifier accepts converted closure", corePrepVerifierAcceptsClosure)
+    , ("CorePrep verifier rejects primitive weak capture", corePrepVerifierRejectsWeakPrimitive)
+    ]
+
+lexerRecognizesDelimiters :: Bool
+lexerRecognizesDelimiters = case lexText "[weak owner] \\(int value) -> value" of
+    Right tokens -> all (`elem` map tokenText tokens) ["[", "]", "\\", "(", ")", "->"]
+    Left _ -> False
+
+typedParameters :: Bool
+typedParameters = case parsedCallable "\\(int left, int right) -> left + right" of
+    Just (CallableExpression _ False [] parameters _ _) ->
+        map parameterTypeSyntax parameters
+            == [ExplicitType (Identifier "int"), ExplicitType (Identifier "int")]
+    _ -> False
+
+parenthesizedInferredParameters :: Bool
+parenthesizedInferredParameters = case parsedCallable "\\(left, right) -> left" of
+    Just (CallableExpression _ False [] parameters _ _) ->
+        map parameterTypeSyntax parameters == [AutoType, AutoType]
+    _ -> False
+
+bareInferredParameters :: Bool
+bareInferredParameters = case parsedCallable "\\left, right -> right" of
+    Just (CallableExpression _ False [] parameters _ _) ->
+        map (identifierText . parameterName) parameters == ["left", "right"]
+    _ -> False
+
+parenthesizedZeroParameters :: Bool
+parenthesizedZeroParameters = callableParameterCount "\\() -> 42" == Just 0
+
+bareZeroParameters :: Bool
+bareZeroParameters = callableParameterCount "\\ -> 42" == Just 0
+
+expressionBody :: Bool
+expressionBody = case parsedCallable "\\(int value) -> value + 1" of
+    Just CallableExpression {} -> case callableBodyOf (parsedCallable "\\(int value) -> value + 1") of
+        Just CallableExpressionBody {} -> True
+        _ -> False
+    _ -> False
+
+blockBody :: Bool
+blockBody = case callableBodyOf (parsedCallable "\\(int value) -> { int next = value + 1; next }") of
+    Just (CallableBlockBody (Block statements)) -> length statements == 2
+    _ -> False
+
+emptyExplicitCaptureList :: Bool
+emptyExplicitCaptureList = case parsedCallable "[] \\ -> 42" of
+    Just (CallableExpression _ explicit captures _ _ _) -> explicit && null captures
+    _ -> False
+
+normalCapture :: Bool
+normalCapture = captureModes "[value] \\ -> value" == Just [StrongCapture]
+
+weakCapture :: Bool
+weakCapture = captureModes "[weak owner] \\ -> owner" == Just [WeakCapture]
+
+unownedCapture :: Bool
+unownedCapture = captureModes "[unowned owner] \\ -> owner" == Just [UnownedCapture]
+
+captureAlias :: Bool
+captureAlias = case parsedCallable "[answer = seed + 1] \\ -> answer" of
+    Just (CallableExpression _ True [Capture _ StrongCapture (Identifier "answer") _ (Just BinaryExpression {})] _ _ _) -> True
+    _ -> False
+
+captureOrder :: Bool
+captureOrder = case parsedCallable "[first = one, second = two, third = three] \\ -> first" of
+    Just (CallableExpression _ True captures _ _ _) ->
+        map (identifierText . captureName) captures == ["first", "second", "third"]
+    _ -> False
+
+duplicateCapture :: Bool
+duplicateCapture = hasDiagnostic "TAR0005" (compileSource "int seed = 1; auto f = [seed, seed] \\ -> seed;")
+
+omittedExplicitCapture :: Bool
+omittedExplicitCapture =
+    hasDiagnostic "TAN0001" (compileSource "int first = 1; int second = 2; auto f = [first] \\ -> first + second;")
+
+privateParameterSymbols :: Bool
+privateParameterSymbols = case resolvedCallable "auto f = \\(int value) -> value;" of
+    Just (CallableExpression _ _ _ [parameter] body _) ->
+        resolvedSymbol (parameterName parameter) `elem` expressionSymbols body
+    _ -> False
+
+privateCaptureSymbols :: Bool
+privateCaptureSymbols = case resolvedCallable "int seed = 1; auto f = [seed] \\ -> seed;" of
+    Just (CallableExpression _ True [capture] _ body _) ->
+        resolvedSymbol (captureName capture) `elem` expressionSymbols body
+    _ -> False
+
+callableType :: Bool
+callableType = case typedCallable "auto f = \\(int value) -> value + 1;" of
+    Just (CallableExpression _ _ _ _ _ (FunctionType [parameter] result)) ->
+        parameter == intType && result == intType
+    _ -> False
+
+callableArity :: Bool
+callableArity = hasDiagnostic "TAT0008" (compileSource "auto f = \\(int value) -> value; int result = f(1, 2);")
+
+callableArgumentType :: Bool
+callableArgumentType = hasDiagnostic "TAT0009" (compileSource "auto f = \\(int value) -> value; int result = f(true);")
+
+weakPrimitiveCapture :: Bool
+weakPrimitiveCapture = hasDiagnostic "TAT0014" (compileSource "int value = 1; auto f = [weak value] \\ -> value;")
+
+weakScalarCaptures :: Bool
+weakScalarCaptures = all rejectsWeak scalarTypes
+    where
+        rejectsWeak scalar = hasDiagnostic "TAT0014" (compileSource (scalarSource scalar))
+        scalarSource scalar =
+            scalarTypeName scalar
+                ++ " value = "
+                ++ scalarInitializer scalar
+                ++ "; auto f = [weak value] \\ -> value;"
+        scalarInitializer CharacterScalar = "'A'"
+        scalarInitializer BooleanScalar = "true"
+        scalarInitializer scalar
+            | scalarTypeFamily scalar == FloatingFamily = "1.0"
+            | otherwise = "1"
+
+unownedPrimitiveCapture :: Bool
+unownedPrimitiveCapture = hasDiagnostic "TAT0015" (compileSource "int value = 1; auto f = [unowned value] \\ -> value;")
+
+weakStringCapture :: Bool
+weakStringCapture =
+    not (hasDiagnostic "TAT0014" (compileSource "String value = \"Wolf\"; auto f = [weak value] \\ -> value;"))
+
+weakCallableCapture :: Bool
+weakCallableCapture =
+    not (hasDiagnostic "TAT0014" (compileSource "auto first = \\ -> 1; auto second = [weak first] \\ -> first;"))
+
+implicitCaptureDiscovery :: Bool
+implicitCaptureDiscovery = case coreClosure "int seed = 1; auto f = \\ -> seed;" of
+    Just (CoreClosure captures [] _ _ _) -> map (identifierText . resolvedSpelling . coreCaptureName) captures == ["seed"]
+    _ -> False
+
+explicitCaptureLowering :: Bool
+explicitCaptureLowering = case coreClosure "int seed = 1; auto f = [seed] \\ -> seed;" of
+    Just (CoreClosure [capture] [] _ _ _) ->
+        coreCaptureMode capture == StrongCapture && expressionType (coreCaptureValue capture) == intType
+    _ -> False
+
+aliasLowering :: Bool
+aliasLowering = case coreClosure "int seed = 1; auto f = [answer = seed + 1] \\ -> answer;" of
+    Just (CoreClosure [capture] [] _ _ _) -> case coreCaptureValue capture of
+        CorePrimitive CoreAdd [CoreVariable _ _, CoreLiteral (CoreInteger 1) _] _ -> True
+        _ -> False
+    _ -> False
+
+liftedFunction :: Bool
+liftedFunction = case compiled "int seed = 1; auto f = \\ -> seed;" of
+    Right artifacts -> length (corePrepModuleFunctions (artifactCorePrep artifacts)) == 2
+    Left _ -> False
+
+makeClosureOperation :: Bool
+makeClosureOperation = case preparedClosure "int seed = 1; auto f = \\ -> seed;" of
+    Just (CorePrepMakeClosure _ [_]) -> True
+    _ -> False
+
+hiddenCaptureParameters :: Bool
+hiddenCaptureParameters = case compiled "int seed = 1; auto f = \\(int value) -> seed + value;" of
+    Right artifacts -> case drop 1 (corePrepModuleFunctions (artifactCorePrep artifacts)) of
+        [function] -> map (identifierText . resolvedSpelling . fst) (corePrepFunctionParameters function) == ["seed", "value"]
+        _ -> False
+    Left _ -> False
+
+closureResultType :: Bool
+closureResultType = case preparedBinding "auto f = \\(int value) -> value; f(1);" of
+    Just (CorePrepBind _ valueType _ CorePrepMakeClosure {}) ->
+        valueType == FunctionType [intType] intType
+    _ -> False
+
+nestedClosureConversion :: Bool
+nestedClosureConversion = case compiled "int seed = 1; auto outer = \\ -> \\ -> seed;" of
+    Right artifacts -> length (corePrepModuleFunctions (artifactCorePrep artifacts)) == 3
+    Left _ -> False
+
+coreVerifierAcceptsClosure :: Bool
+coreVerifierAcceptsClosure = case compiled "int seed = 1; auto f = \\ -> seed;" of
+    Right artifacts -> verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+    Left _ -> False
+
+coreVerifierRejectsTypeMismatch :: Bool
+coreVerifierRejectsTypeMismatch = isLeftDiagnostics (verifyCore invalidClosureType)
+
+coreVerifierRejectsCaptureMismatch :: Bool
+coreVerifierRejectsCaptureMismatch = isLeftDiagnostics (verifyCore invalidCaptureType)
+
+coreWireClosureRoundTrip :: Bool
+coreWireClosureRoundTrip = case compiled "int seed = 1; auto f = \\ -> seed;" of
+    Right artifacts ->
+        let value = artifactOptimizedCore artifacts
+         in (encodeCore defaultCoreWireLimits value >>= decodeCore defaultCoreWireLimits) == Right value
+    Left _ -> False
+
+corePrepWireClosureRoundTrip :: Bool
+corePrepWireClosureRoundTrip = case compiled "int seed = 1; auto f = \\ -> seed;" of
+    Right artifacts ->
+        let value = artifactCorePrep artifacts
+         in (encodeCorePrep value >>= decodeCorePrep) == Right value
+    Left _ -> False
+
+corePrepVerifierAcceptsClosure :: Bool
+corePrepVerifierAcceptsClosure = case compiled "int seed = 1; auto f = \\ -> seed;" of
+    Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+
+corePrepVerifierRejectsWeakPrimitive :: Bool
+corePrepVerifierRejectsWeakPrimitive = isLeftDiagnostics (verifyCorePrep invalidPreparedWeakCapture)
+
+-- Test source is wrapped in the smallest valid declaration context.  Statements
+-- are injected before a final unit-producing return so callable values may be
+-- exercised without conflating closure behavior with entry-point validation.
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource = compiled
+
+compiled :: String -> Either [Diagnostic] FrontendArtifacts
+compiled statements =
+    compileToCorePrep
+        ( CompilerInput
+            "closure-test.tala"
+            ( unlines
+                [ "namespace ClosureTests;"
+                , "class Program {"
+                , "  public static void Main() {"
+                , statements
+                , "    return;"
+                , "  }"
+                , "}"
+                ]
+            )
+        )
+
+parseOnly :: String -> Either [Diagnostic] ParsedAST
+parseOnly statements = do
+    tokens <- lexText (source statements)
+    runParser defaultParser (ParserInput "closure-test.tala" tokens)
+
+lexText :: String -> Either [Diagnostic] [Token]
+lexText = runLexer defaultLexer . LexerInput "closure-test.tala"
+
+source :: String -> String
+source statements =
+    unlines
+        [ "namespace ClosureTests;"
+        , "class Program {"
+        , "  public static void Main() {"
+        , "    auto callable = " ++ statements ++ ";"
+        , "    return;"
+        , "  }"
+        , "}"
+        ]
+
+parsedCallable :: String -> Maybe (Expression Identifier ())
+parsedCallable value = either (const Nothing) findParsedCallable (parseOnly value)
+
+findParsedCallable :: ParsedAST -> Maybe (Expression Identifier ())
+findParsedCallable (ParsedAST tree) = firstCallableInTree tree
+
+firstCallableInTree :: SyntaxTree name annotation -> Maybe (Expression name annotation)
+firstCallableInTree tree = firstJust (map declarationCallable (syntaxDeclarations tree))
+
+declarationCallable :: Declaration name annotation -> Maybe (Expression name annotation)
+declarationCallable declaration = case declaration of
+    TypeDeclaration {typeMembers = members} -> firstJust (map declarationCallable members)
+    TemplateTypeDeclaration {typeMembers = members} -> firstJust (map declarationCallable members)
+    FunctionDeclaration {declarationBody = body} -> blockCallable body
+    EnumDeclaration {} -> Nothing
+
+blockCallable :: Block name annotation -> Maybe (Expression name annotation)
+blockCallable (Block statements) = firstJust (map statementCallable statements)
+
+statementCallable :: Statement name annotation -> Maybe (Expression name annotation)
+statementCallable statement = case statement of
+    BindingStatement _ _ _ _ _ value -> expressionCallable value
+    AssignmentStatement _ _ _ value -> expressionCallable value
+    ReturnStatement _ value -> value >>= expressionCallable
+    IfStatement _ condition yes no ->
+        expressionCallable condition `orElse` blockCallable yes `orElse` (no >>= blockCallable)
+    WhileStatement _ condition body -> expressionCallable condition `orElse` blockCallable body
+    DoWhileStatement _ body condition -> blockCallable body `orElse` expressionCallable condition
+    ForStatement _ initializer condition updates body ->
+        (initializer >>= statementCallable)
+            `orElse` (condition >>= expressionCallable)
+            `orElse` blockCallable body
+            `orElse` firstJust (map statementCallable updates)
+    ForEachStatement _ _ _ _ _ collection body -> expressionCallable collection `orElse` blockCallable body
+    IncrementStatement {} -> Nothing
+    CompoundAssignmentStatement _ _ _ _ value -> expressionCallable value
+    DiscardStatement _ value -> expressionCallable value
+    BreakStatement _ value -> value >>= expressionCallable
+    ContinueStatement {} -> Nothing
+    GuardStatement _ condition block -> expressionCallable condition `orElse` blockCallable block
+    BlockStatement _ block -> blockCallable block
+    ExpressionStatement _ value _ -> expressionCallable value
+
+expressionCallable :: Expression name annotation -> Maybe (Expression name annotation)
+expressionCallable expression@CallableExpression {} = Just expression
+expressionCallable expression = case expression of
+    CallExpression _ callee arguments _ -> expressionCallable callee `orElse` firstJust (map expressionCallable arguments)
+    UnaryExpression _ _ value _ -> expressionCallable value
+    BinaryExpression _ _ left right _ -> expressionCallable left `orElse` expressionCallable right
+    _ -> Nothing
+
+callableParameterCount :: String -> Maybe Int
+callableParameterCount value = case parsedCallable value of
+    Just (CallableExpression _ _ _ parameters _ _) -> Just (length parameters)
+    _ -> Nothing
+
+callableBodyOf :: Maybe (Expression name annotation) -> Maybe (CallableBody name annotation)
+callableBodyOf (Just (CallableExpression _ _ _ _ body _)) = Just body
+callableBodyOf _ = Nothing
+
+captureModes :: String -> Maybe [CaptureMode]
+captureModes value = case parsedCallable value of
+    Just (CallableExpression _ _ captures _ _ _) -> Just (map captureMode captures)
+    _ -> Nothing
+
+resolvedCallable :: String -> Maybe (Expression ResolvedName ())
+resolvedCallable statements = case compiledFrontend statements of
+    Left _ -> Nothing
+    Right artifacts -> firstCallableInTree (resolvedSyntaxTree (artifactResolvedAST artifacts))
+
+typedCallable :: String -> Maybe (Expression ResolvedName Type)
+typedCallable statements = case compiledFrontend statements of
+    Left _ -> Nothing
+    Right artifacts -> firstCallableInTree (typedSyntaxTree (artifactTypedAST artifacts))
+
+compiledFrontend :: String -> Either [Diagnostic] FrontendArtifacts
+compiledFrontend = compiled
+
+expressionSymbols :: CallableBody ResolvedName annotation -> [SymbolId]
+expressionSymbols body = case body of
+    CallableExpressionBody expression -> symbols expression
+    CallableBlockBody block -> blockSymbols block
+
+symbols :: Expression ResolvedName annotation -> [SymbolId]
+symbols expression = case expression of
+    NameExpression _ name _ -> [resolvedSymbol name]
+    LiteralExpression {} -> []
+    MemberAccessExpression _ receiver _ _ -> symbols receiver
+    MethodReferenceExpression _ receiver _ _ -> symbols receiver
+    CallExpression _ callee arguments _ -> symbols callee ++ concatMap symbols arguments
+    UnaryExpression _ _ value _ -> symbols value
+    BinaryExpression _ _ left right _ -> symbols left ++ symbols right
+    IsPatternExpression _ value _ _ -> symbols value
+    ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap symbols [condition, whenTrue, whenFalse]
+    CoalesceExpression _ left fallback _ -> symbols left ++ symbols fallback
+    AssignmentExpression _ _ name value _ -> resolvedSymbol name : symbols value
+    IncrementExpression _ _ name _ -> [resolvedSymbol name]
+    LoopExpression {} -> []
+    BlockExpression _ block _ -> blockSymbols block
+    MatchExpression _ subjects arms _ -> concatMap symbols (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ captures parameters body _ ->
+        map (resolvedSymbol . captureName) captures
+            ++ map (resolvedSymbol . parameterName) parameters
+            ++ expressionSymbols body
+
+blockSymbols :: Block ResolvedName annotation -> [SymbolId]
+blockSymbols (Block statements) = concatMap statementSymbols statements
+
+statementSymbols :: Statement ResolvedName annotation -> [SymbolId]
+statementSymbols statement = case statement of
+    BindingStatement _ _ _ name _ value -> resolvedSymbol name : symbols value
+    AssignmentStatement _ name _ value -> resolvedSymbol name : symbols value
+    ReturnStatement _ value -> maybe [] symbols value
+    IfStatement _ condition yes no -> symbols condition ++ blockSymbols yes ++ maybe [] blockSymbols no
+    WhileStatement _ condition body -> symbols condition ++ blockSymbols body
+    DoWhileStatement _ body condition -> blockSymbols body ++ symbols condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementSymbols initializer
+            ++ maybe [] symbols condition
+            ++ blockSymbols body
+            ++ concatMap statementSymbols updates
+    ForEachStatement _ _ _ name _ collection body ->
+        resolvedSymbol name : symbols collection ++ blockSymbols body
+    IncrementStatement _ name _ -> [resolvedSymbol name]
+    CompoundAssignmentStatement _ _ name _ value -> resolvedSymbol name : symbols value
+    DiscardStatement _ value -> symbols value
+    BreakStatement _ value -> maybe [] symbols value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> symbols condition ++ blockSymbols block
+    BlockStatement _ block -> blockSymbols block
+    ExpressionStatement _ value _ -> symbols value
+
+coreClosure :: String -> Maybe CoreExpression
+coreClosure statements = case compiled statements of
+    Left _ -> Nothing
+    Right artifacts -> findCoreClosure (artifactOptimizedCore artifacts)
+
+findCoreClosure :: CoreModule -> Maybe CoreExpression
+findCoreClosure moduleValue =
+    firstJust
+        [ firstJust (map statementCoreClosure (coreFunctionBody function))
+        | function <- coreModuleFunctions moduleValue
+        ]
+
+statementCoreClosure :: CoreStatement -> Maybe CoreExpression
+statementCoreClosure statement = case statement of
+    CoreBind binding -> expressionCoreClosure (coreBindingValue binding)
+    CoreAssign _ value -> expressionCoreClosure value
+    CoreReturn value -> expressionCoreClosure value
+    CoreIf condition yes no ->
+        expressionCoreClosure condition
+            `orElse` firstJust (map statementCoreClosure yes)
+            `orElse` firstJust (map statementCoreClosure no)
+    CoreEvaluate value -> expressionCoreClosure value
+    CoreWhile condition body -> expressionCoreClosure condition `orElse` firstJust (map statementCoreClosure body)
+    CoreDoWhile body condition ->
+        firstJust (map statementCoreClosure body) `orElse` expressionCoreClosure condition
+    CoreFor condition body update ->
+        expressionCoreClosure condition
+            `orElse` firstJust (map statementCoreClosure body)
+            `orElse` firstJust (map statementCoreClosure update)
+    CoreBreak -> Nothing
+    CoreContinue -> Nothing
+
+expressionCoreClosure :: CoreExpression -> Maybe CoreExpression
+expressionCoreClosure expression@CoreClosure {} = Just expression
+expressionCoreClosure expression = case expression of
+    CoreApply callee arguments _ -> expressionCoreClosure callee `orElse` firstJust (map expressionCoreClosure arguments)
+    CorePrimitive _ arguments _ -> firstJust (map expressionCoreClosure arguments)
+    _ -> Nothing
+
+preparedClosure :: String -> Maybe CorePrepOperation
+preparedClosure statements = preparedBinding statements >>= operationOf
+
+preparedBinding :: String -> Maybe CorePrepInstruction
+preparedBinding statements = case compiled statements of
+    Left _ -> Nothing
+    Right artifacts ->
+        firstJust
+            [ find isClosureBinding (corePrepBlockInstructions block)
+            | function <- corePrepModuleFunctions (artifactCorePrep artifacts)
+            , block <- corePrepFunctionBlocks function
+            ]
+
+isClosureBinding :: CorePrepInstruction -> Bool
+isClosureBinding (CorePrepBind _ _ _ CorePrepMakeClosure {}) = True
+isClosureBinding (CorePrepEvaluate CorePrepMakeClosure {}) = True
+isClosureBinding _ = False
+
+operationOf :: CorePrepInstruction -> Maybe CorePrepOperation
+operationOf (CorePrepBind _ _ _ operation) = Just operation
+operationOf (CorePrepEvaluate operation) = Just operation
+operationOf _ = Nothing
+
+hasDiagnostic :: String -> Either [Diagnostic] value -> Bool
+hasDiagnostic code result = case result of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+isLeftDiagnostics :: Either [Diagnostic] value -> Bool
+isLeftDiagnostics (Left (_ : _)) = True
+isLeftDiagnostics _ = False
+
+orElse :: Maybe value -> Maybe value -> Maybe value
+orElse (Just value) _ = Just value
+orElse Nothing right = right
+
+firstJust :: [Maybe value] -> Maybe value
+firstJust = foldr orElse Nothing
+
+testName :: Int -> String -> ResolvedName
+testName value spelling = ResolvedName (SymbolId value) (Identifier spelling)
+
+invalidClosureType :: CoreModule
+invalidClosureType =
+    CoreModule
+        (QualifiedName [Identifier "ClosureTests"])
+        [ CoreFunction
+            (testName 1 "Main")
+            []
+            unitType
+            [ CoreBind
+                ( CoreBinding
+                    (testName 2 "callable")
+                    intType
+                    False
+                    (CoreClosure [] [] intType [CoreReturn (CoreLiteral (CoreInteger 1) intType)] intType)
+                )
+            , CoreReturn (CoreLiteral CoreUnit unitType)
+            ]
+        ]
+
+invalidCaptureType :: CoreModule
+invalidCaptureType =
+    CoreModule
+        (QualifiedName [Identifier "ClosureTests"])
+        [ CoreFunction
+            (testName 1 "Main")
+            []
+            unitType
+            [ CoreBind (CoreBinding (testName 2 "seed") intType True (CoreLiteral (CoreInteger 1) intType))
+            , CoreBind
+                ( CoreBinding
+                    (testName 3 "callable")
+                    (FunctionType [] intType)
+                    False
+                    ( CoreClosure
+                        [CoreCapture StrongCapture (testName 4 "seed") intType (CoreLiteral (CoreBoolean True) boolType)]
+                        []
+                        intType
+                        [CoreReturn (CoreVariable (testName 4 "seed") intType)]
+                        (FunctionType [] intType)
+                    )
+                )
+            , CoreReturn (CoreLiteral CoreUnit unitType)
+            ]
+        ]
+
+invalidPreparedWeakCapture :: CorePrepModule
+invalidPreparedWeakCapture =
+    CorePrepModule
+        (QualifiedName [Identifier "ClosureTests"])
+        [ CorePrepFunction
+            (testName 1 "Main")
+            ""
+            []
+            unitType
+            0
+            [ CorePrepBlock
+                0
+                [ CorePrepBind
+                    (testName 2 "callable")
+                    (FunctionType [] intType)
+                    False
+                    ( CorePrepMakeClosure
+                        (testName 3 "lifted")
+                        [CorePrepCapture WeakCapture (testName 4 "value") intType (CorePrepLiteral (CoreInteger 1) intType)]
+                    )
+                ]
+                (CorePrepReturn (CorePrepLiteral CoreUnit unitType))
+            ]
+        , CorePrepFunction
+            (testName 3 "lifted")
+            ""
+            [(testName 4 "value", intType)]
+            intType
+            0
+            [CorePrepBlock 0 [] (CorePrepReturn (CorePrepVariable (testName 4 "value") intType))]
+        ]
+        []
+
+-- Keep a textual assertion near the wire tests so failures caused by an
+-- accidental version rollback explain themselves in the test output.
+_wireVersionContext :: String
+_wireVersionContext = "closures require Core wire version 10 and CorePrep wire version 8"
+
+_diagnosticContext :: Diagnostic -> Bool
+_diagnosticContext diagnostic = "closure" `isInfixOf` diagnosticMessage diagnostic
+ test/CompileTimeParityTests.hs view
@@ -0,0 +1,168 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CompileTimeParityTests (compileTimeParityTests) where
+
+import Data.Bits (shiftL)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.ConstantEvaluation
+import Progmasoft.Tala.TemplateValue
+
+compileTimeParityTests :: [(String, Bool)]
+compileTimeParityTests =
+    [ ("constant and template evaluators agree across compile-time magnitude boundaries", boundaryBinaryParity)
+    , ("constant and template evaluators agree for unary boundary operations", boundaryUnaryParity)
+    , ("constant and template evaluators map arithmetic failures consistently", failureParity)
+    ]
+
+-- The front-end uses two evaluator entry points for different semantic
+-- contexts. This matrix compares their externally visible values and failure
+-- classes without requiring either evaluator to call the other.
+boundaryBinaryParity :: Bool
+boundaryBinaryParity =
+    and
+        [ normalizeConstant valueResultType (evaluateConstantInteger expression)
+            == normalizeTemplate (evaluateTemplateValue template)
+        | (operator, _) <- binaryCases
+        , left <- boundaryValues
+        , right <- boundaryValues
+        , let valueResultType = binaryResultType operator
+        , let expression = BinaryExpression testSpan operator (integerExpression left) (integerExpression right) valueResultType
+        , let template = TemplateBinarySyntax testSpan operator (integerSyntax left) (integerSyntax right)
+        ]
+        && length binaryCases == 20
+
+boundaryUnaryParity :: Bool
+boundaryUnaryParity =
+    and
+        [ normalizeConstant valueResultType (evaluateConstantInteger expression)
+            == normalizeTemplate (evaluateTemplateValue template)
+        | (operator, valueResultType) <- unaryCases
+        , value <- boundaryValues
+        , let expression = UnaryExpression testSpan operator (integerExpression value) valueResultType
+        , let template = TemplateUnarySyntax testSpan operator (integerSyntax value)
+        ]
+
+failureParity :: Bool
+failureParity =
+    and
+        [ sameBinaryResult Divide 8 0 DivisionByZeroOutcome
+        , sameBinaryResult FloorDivide 8 0 FloorDivisionByZeroOutcome
+        , sameBinaryResult Remainder 8 0 RemainderByZeroOutcome
+        , sameBinaryResult Power 2 (-1) NegativeExponentOutcome
+        , sameBinaryResult Add (compileTimeLimit - 1) 1 LimitExceededOutcome
+        ]
+    where
+        sameBinaryResult operator left right expected =
+            normalizeConstant (binaryResultType operator) (evaluateConstantInteger (binaryExpression operator left right))
+                == expected
+                && normalizeTemplate (evaluateTemplateValue (binarySyntax operator left right)) == expected
+
+binaryCases :: [(BinaryOperator, Type)]
+binaryCases =
+    [ (Add, intType)
+    , (Subtract, intType)
+    , (Multiply, intType)
+    , (Divide, intType)
+    , (FloorDivide, intType)
+    , (Remainder, intType)
+    , (Power, intType)
+    , (ShiftLeft, intType)
+    , (ShiftRight, intType)
+    , (BitwiseAnd, intType)
+    , (BitwiseXor, intType)
+    , (BitwiseOr, intType)
+    , (LessThan, boolType)
+    , (LessEqual, boolType)
+    , (GreaterThan, boolType)
+    , (GreaterEqual, boolType)
+    , (Equal, boolType)
+    , (NotEqual, boolType)
+    , (LogicalAnd, boolType)
+    , (LogicalOr, boolType)
+    ]
+
+unaryCases :: [(UnaryOperator, Type)]
+unaryCases = [(UnaryPlus, intType), (UnaryNegate, intType), (LogicalNot, boolType), (BitwiseNot, intType)]
+
+binaryResultType :: BinaryOperator -> Type
+binaryResultType operation = case operation of
+    LessThan -> boolType
+    LessEqual -> boolType
+    GreaterThan -> boolType
+    GreaterEqual -> boolType
+    Equal -> boolType
+    NotEqual -> boolType
+    LogicalAnd -> boolType
+    LogicalOr -> boolType
+    _ -> intType
+
+boundaryValues :: [Integer]
+boundaryValues =
+    [ negate (compileTimeLimit - 1)
+    , negate (compileTimeLimit `quot` 2)
+    , -2
+    , -1
+    , 0
+    , 1
+    , 2
+    , compileTimeLimit `quot` 2
+    , compileTimeLimit - 1
+    ]
+
+normalizeConstant :: Type -> Either ConstantIntegerError (Maybe Integer) -> NormalizedOutcome
+normalizeConstant valueType result = case result of
+    Right (Just value)
+        | valueType == boolType -> BooleanOutcome (value /= 0)
+        | otherwise -> IntegerOutcome value
+    Right Nothing -> NonconstantOutcome
+    Left ConstantDivisionByZero -> DivisionByZeroOutcome
+    Left ConstantFloorDivisionByZero -> FloorDivisionByZeroOutcome
+    Left ConstantRemainderByZero -> RemainderByZeroOutcome
+    Left ConstantNegativeExponent -> NegativeExponentOutcome
+    Left ConstantEvaluationLimitExceeded -> LimitExceededOutcome
+
+normalizeTemplate :: Either TemplateValueError TemplateValue -> NormalizedOutcome
+normalizeTemplate result = case result of
+    Right (IntegerTemplateValue value) -> IntegerOutcome value
+    Right (BooleanTemplateValue value) -> BooleanOutcome value
+    Right (CharacterTemplateValue value) -> CharacterOutcome value
+    Right _ -> OtherFailureOutcome
+    Left TemplateValueDivisionByZero -> DivisionByZeroOutcome
+    Left TemplateValueFloorDivisionByZero -> FloorDivisionByZeroOutcome
+    Left TemplateValueRemainderByZero -> RemainderByZeroOutcome
+    Left (TemplateValueNegativeExponent _) -> NegativeExponentOutcome
+    Left TemplateValueEvaluationLimitExceeded -> LimitExceededOutcome
+    Left _ -> OtherFailureOutcome
+
+data NormalizedOutcome
+    = IntegerOutcome Integer
+    | BooleanOutcome Bool
+    | CharacterOutcome Integer
+    | DivisionByZeroOutcome
+    | FloorDivisionByZeroOutcome
+    | RemainderByZeroOutcome
+    | NegativeExponentOutcome
+    | LimitExceededOutcome
+    | NonconstantOutcome
+    | OtherFailureOutcome
+    deriving (Eq, Ord, Read, Show)
+
+integerExpression :: Integer -> Expression name Type
+integerExpression value = LiteralExpression testSpan (IntegerLiteral value) intType
+
+binaryExpression :: BinaryOperator -> Integer -> Integer -> Expression name Type
+binaryExpression operator left right =
+    BinaryExpression testSpan operator (integerExpression left) (integerExpression right) (binaryResultType operator)
+
+integerSyntax :: Integer -> TemplateValueSyntax
+integerSyntax = TemplateIntegerSyntax testSpan
+
+binarySyntax :: BinaryOperator -> Integer -> Integer -> TemplateValueSyntax
+binarySyntax operator left right = TemplateBinarySyntax testSpan operator (integerSyntax left) (integerSyntax right)
+
+compileTimeLimit :: Integer
+compileTimeLimit = 1 `shiftL` 65536
+
+testSpan :: SourceSpan
+testSpan = SourceSpan "compile-time-parity.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+ test/ConditionalExpressionTests.hs view
@@ -0,0 +1,924 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Vertical tests for the conditional forms and the statement forms that
+landed with them: @condition ? first : second@, @left ?: fallback@, compound
+assignment, and the discard statement.
+
+Shape assertions read the unoptimized Core wherever a call must stay visible,
+because the optimizer is free to fold a pure callee. Laziness is asserted on
+CorePrep blocks, which is the representation the native backend executes.
+-}
+module ConditionalExpressionTests (conditionalExpressionTests) where
+
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Specialization
+
+conditionalExpressionTests :: [(String, Bool)]
+conditionalExpressionTests =
+    lexerTests
+        ++ parserTests
+        ++ typeTests
+        ++ loweringTests
+        ++ verifierTests
+        ++ optimizerTests
+        ++ corePrepTests
+
+-- ---------------------------------------------------------------- lexer
+
+lexerTests :: [(String, Bool)]
+lexerTests =
+    [ ("a spaced conditional lexes '?' and ':' separately", symbolsOf "a ? b : c" == ["?", ":"])
+    , ("the omitted-middle operator is one token", symbolsOf "a ?: b" == ["?:"])
+    , ("a conditional with a spaced empty middle is not the coalescing token", symbolsOf "a ? : b" == ["?", ":"])
+    , ("null coalescing lexes as its own token", symbolsOf "a ?? b" == ["??"])
+    , ("null-coalescing assignment lexes as its own token", symbolsOf "a ??= b" == ["??="])
+    , ("every compound assignment operator is one token", all compoundIsOneToken compoundSpellings)
+    , ("rounded-division assignment is not a division followed by '/='", symbolsOf "a //= b" == ["//="])
+    , ("power assignment is not a multiplication followed by '*='", symbolsOf "a **= b" == ["**="])
+    , ("shift assignments are single tokens", symbolsOf "a <<= b >>= c" == ["<<=", ">>="])
+    , ("relational operators keep their spelling next to assignments", symbolsOf "a <= b >= c" == ["<=", ">="])
+    , ("nested generic closers stay separate tokens", symbolsOf "A<B<C>> value" == ["<", "<", ">", ">"])
+    , ("a line comment still starts at a spaced '--'", symbolsOf "a -= b -- c ? d : e" == ["-="])
+    ]
+
+compoundSpellings :: [String]
+compoundSpellings = ["+=", "-=", "*=", "/=", "//=", "%=", "**=", "<<=", ">>=", "&=", "^=", "|="]
+
+compoundIsOneToken :: String -> Bool
+compoundIsOneToken operatorText = symbolsOf ("target " ++ operatorText ++ " 1") == [operatorText]
+
+symbolsOf :: String -> [String]
+symbolsOf text = case runLexer defaultLexer (LexerInput "conditional.tala" text) of
+    Right tokens -> [tokenText token | token <- tokens, tokenKind token == SymbolToken]
+    Left _ -> ["<lexer error>"]
+
+-- ---------------------------------------------------------------- parser
+
+parserTests :: [(String, Bool)]
+parserTests =
+    [ ("a conditional parses into its three operands", conditionalParses)
+    , ("chained conditionals group to the right", conditionalIsRightAssociative)
+    , ("a conditional may nest in its first result without parentheses", conditionalNestsInFirstResult)
+    , ("logical OR binds more tightly than a conditional", logicalOrBindsTighter)
+    , ("the conditional is the weakest expression level in a call argument", conditionalIsCallArgument)
+    , ("truthy coalescing parses into its two operands", coalesceParses)
+    , ("a spaced empty middle is the same omitted-middle form", spacedCoalesceParses)
+    , ("chained truthy coalescing groups to the right", coalesceIsRightAssociative)
+    , ("coalescing in a conditional result stays inside that result", coalesceNestsInConditional)
+    , ("a conditional without ':' is a dedicated syntax error", parseRejectedWith "TAP0029" (returning "flag ? 1"))
+    , ("null coalescing is rejected until nullable types exist", parseRejectedWith "TAP0030" (returning "left ?? right"))
+    , ("null-coalescing assignment is rejected until nullable types exist", parseRejectedWith "TAP0031" (body "left ??= right; return 0;"))
+    , ("every compound operator parses to its binary operator", all compoundParses compoundOperators)
+    , ("a compound assignment needs a named target", parseRejectedWith "TAP0003" (body "Next() += 1; return 0;"))
+    , ("a compound assignment needs its terminator", parseRejected (body "left += 1 return left;"))
+    , ("a for update may be a compound assignment", forUpdateIsCompound)
+    , ("a for update list may mix increments and compound assignments", forUpdateListParses)
+    , ("the discard statement parses as its own statement", discardParses)
+    , ("discard is not an expression", parseRejected (returning "(_ = Next())"))
+    , ("discard needs its terminator", parseRejected (body "_ = Next() return 0;"))
+    , ("a conditional is not a template value argument", parseRejectedWith "TAP0018" templateConditionalArgument)
+    ]
+
+compoundOperators :: [(String, BinaryOperator)]
+compoundOperators =
+    [ ("+=", Add)
+    , ("-=", Subtract)
+    , ("*=", Multiply)
+    , ("/=", Divide)
+    , ("//=", FloorDivide)
+    , ("%=", Remainder)
+    , ("**=", Power)
+    , ("<<=", ShiftLeft)
+    , (">>=", ShiftRight)
+    , ("&=", BitwiseAnd)
+    , ("^=", BitwiseXor)
+    , ("|=", BitwiseOr)
+    ]
+
+conditionalParses :: Bool
+conditionalParses = case returned "flag ? left : right" of
+    Just (ConditionalExpression _ condition first second ()) ->
+        isName "flag" condition && isName "left" first && isName "right" second
+    _ -> False
+
+conditionalIsRightAssociative :: Bool
+conditionalIsRightAssociative = case returned "flag ? 1 : other ? 2 : 3" of
+    Just (ConditionalExpression _ condition _ (ConditionalExpression _ inner _ _ ()) ()) ->
+        isName "flag" condition && isName "other" inner
+    _ -> False
+
+conditionalNestsInFirstResult :: Bool
+conditionalNestsInFirstResult = case returned "flag ? other ? 1 : 2 : 3" of
+    Just (ConditionalExpression _ condition (ConditionalExpression _ inner _ _ ()) third ()) ->
+        isName "flag" condition && isName "other" inner && isInteger 3 third
+    _ -> False
+
+logicalOrBindsTighter :: Bool
+logicalOrBindsTighter = case returned "flag || other ? 1 : 2" of
+    Just (ConditionalExpression _ (BinaryExpression _ LogicalOr _ _ ()) first second ()) ->
+        isInteger 1 first && isInteger 2 second
+    _ -> False
+
+conditionalIsCallArgument :: Bool
+conditionalIsCallArgument = case returned "Twice(flag ? 1 : 2)" of
+    Just (CallExpression _ _ [ConditionalExpression {}] ()) -> True
+    _ -> False
+
+coalesceParses :: Bool
+coalesceParses = case returned "left ?: right" of
+    Just (CoalesceExpression _ left fallback ()) -> isName "left" left && isName "right" fallback
+    _ -> False
+
+spacedCoalesceParses :: Bool
+spacedCoalesceParses = case returned "left ? : right" of
+    Just (CoalesceExpression _ left fallback ()) -> isName "left" left && isName "right" fallback
+    _ -> False
+
+coalesceIsRightAssociative :: Bool
+coalesceIsRightAssociative = case returned "left ?: right ?: 9" of
+    Just (CoalesceExpression _ left (CoalesceExpression _ middle final ()) ()) ->
+        isName "left" left && isName "right" middle && isInteger 9 final
+    _ -> False
+
+coalesceNestsInConditional :: Bool
+coalesceNestsInConditional = case returned "flag ? left ?: 1 : right ?: 2" of
+    Just (ConditionalExpression _ _ CoalesceExpression {} CoalesceExpression {} ()) -> True
+    _ -> False
+
+compoundParses :: (String, BinaryOperator) -> Bool
+compoundParses (operatorText, operator) =
+    case firstStatements (body ("left " ++ operatorText ++ " right; return left;")) of
+        Just (CompoundAssignmentStatement _ parsed (Identifier "left") () value : _) ->
+            parsed == operator && isName "right" value
+        _ -> False
+
+forUpdateIsCompound :: Bool
+forUpdateIsCompound =
+    case firstStatements (body "for (int index = 0; index < left; index += 2) {} return left;") of
+        Just (ForStatement _ _ _ [CompoundAssignmentStatement _ Add (Identifier "index") () _] _ : _) -> True
+        _ -> False
+
+forUpdateListParses :: Bool
+forUpdateListParses =
+    case firstStatements (body "for (int index = 0; index < left; index++, right -= 1) {} return left;") of
+        Just (ForStatement _ _ _ [IncrementStatement {}, CompoundAssignmentStatement _ Subtract _ () _] _ : _) -> True
+        _ -> False
+
+discardParses :: Bool
+discardParses = case firstStatements (body "_ = Next(); return 0;") of
+    Just (DiscardStatement _ CallExpression {} : _) -> True
+    _ -> False
+
+templateConditionalArgument :: String
+templateConditionalArgument =
+    unlines
+        [ "template<int N> class Buffer {}"
+        , "class Program {"
+        , "    public static int Evaluate() {"
+        , "        final Buffer<+(1 ? 2 : 3)> value = 0;"
+        , "        return 0;"
+        , "    }"
+        , "}"
+        ]
+
+isName :: String -> Expression Identifier () -> Bool
+isName expected expression = case expression of
+    NameExpression _ (Identifier actual) () -> actual == expected
+    _ -> False
+
+isInteger :: Integer -> Expression Identifier () -> Bool
+isInteger expected expression = case expression of
+    LiteralExpression _ (IntegerLiteral actual) () -> actual == expected
+    _ -> False
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource text = do
+    tokens <- runLexer defaultLexer (LexerInput "conditional.tala" text)
+    runParser defaultParser (ParserInput "conditional.tala" tokens)
+
+parseRejected :: String -> Bool
+parseRejected text = either (const True) (const False) (parseSource text)
+
+parseRejectedWith :: String -> String -> Bool
+parseRejectedWith code text = case parseSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- | Statements of the first method of the first class in a parsed unit.
+firstStatements :: String -> Maybe [Statement Identifier ()]
+firstStatements text = case parseSource text of
+    Right (ParsedAST (SyntaxTree _ (TypeDeclaration {typeMembers = member : _} : _))) -> case member of
+        FunctionDeclaration {declarationBody = Block statements} -> Just statements
+        _ -> Nothing
+    _ -> Nothing
+
+returned :: String -> Maybe (Expression Identifier ())
+returned expression = case firstStatements (returning expression) of
+    Just [ReturnStatement _ value] -> value
+    _ -> Nothing
+
+-- ---------------------------------------------------------------- sources
+
+-- | One method whose parameters give every test non-constant operands.
+body :: String -> String
+body statements =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ bool flag, _ bool other, _ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "    public static int Next() { return 7; }"
+        , "    public static int Twice(_ int value) { return value + value; }"
+        , "    public static void Touch() { }"
+        , "    public static int Countdown(_ int value) { return value > 0 ? Countdown(value - 1) : 0; }"
+        , "}"
+        ]
+
+returning :: String -> String
+returning expression = body ("return " ++ expression ++ ";")
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "conditional.tala" text)
+
+accepted :: String -> Bool
+accepted text = either (const False) (const True) (compileSource text)
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- ---------------------------------------------------------------- type checker
+
+typeTests :: [(String, Bool)]
+typeTests =
+    [ ("a conditional over int operands is accepted", accepted (returning "flag ? left : right"))
+    , ("a conditional test may be numeric", accepted (returning "left ? left : right"))
+    , ("a conditional test must be bool or numeric", rejectedWith "TAT0036" (returning "\"text\" ? left : right"))
+    , ("conditional results must have one type", rejectedWith "TAT0037" (returning "flag ? left : other"))
+    , ("a conditional selects one of two strings", accepted (body "bool same = (flag ? \"a\" : \"b\") == \"a\"; return 0;"))
+    , ("a string and a number are not results of one conditional", rejectedWith "TAT0037" (body "bool same = (flag ? \"a\" : 1) == \"a\"; return 0;"))
+    , ("a void call is not a conditional result", rejected (body "flag ? Touch() : Touch(); return 0;"))
+    , ("a literal first result takes the type of the second", accepted (longBody "long chosen = flag ? 1 : wide; return 0;"))
+    , ("a literal second result takes the type of the first", accepted (longBody "long chosen = flag ? wide : 1; return 0;"))
+    , ("an untyped literal pair takes the expected type", accepted (longBody "long chosen = flag ? 1 : 2; return 0;"))
+    , ("an expected type does not convert a computed result", rejected (longBody "long chosen = flag ? narrow : 2; return 0;"))
+    , ("a conditional result out of the target range is diagnosed", rejectedWith "TAT0016" (body "byte small = flag ? 300 : 1; return 0;"))
+    , ("a floating conditional is accepted", accepted (body "double ratio = flag ? 1.5 : 2.5; return 0;"))
+    , ("a Boolean conditional is accepted", accepted (body "bool chosen = flag ? other : false; return 0;"))
+    , ("a conditional with a call is an effect-capable statement", accepted (body "flag ? Next() : 0; return 0;"))
+    , ("a call-free conditional statement is a pure value statement", rejectedWith "TAT0013" (body "flag ? left : right; return 0;"))
+    , ("truthy coalescing over int operands is accepted", accepted (returning "left ?: right"))
+    , ("truthy coalescing over bool operands is accepted", accepted (body "bool chosen = flag ?: other; return 0;"))
+    , ("truthy coalescing operands must have one type", rejectedWith "TAT0038" (returning "left ?: flag"))
+    , ("truthy coalescing needs a condition-compatible left operand", rejectedWith "TAT0039" (body "bool same = (\"a\" ?: \"b\") == \"a\"; return 0;"))
+    , ("a literal left operand takes the type of the fallback", accepted (longBody "long chosen = 0 ?: wide; return 0;"))
+    , ("a literal fallback takes the type of the left operand", accepted (longBody "long chosen = wide ?: 5; return 0;"))
+    , ("every arithmetic compound assignment type-checks on int", all compoundAccepted ["+=", "-=", "*=", "/=", "//=", "%=", "**=", "<<=", ">>=", "&=", "^=", "|="])
+    , ("a compound literal operand takes the target type", accepted (longBody "wide += 1; return 0;"))
+    , ("a compound assignment cannot modify an immutable binding", rejectedWith "TAT0003" (body "final int fixed = 1; fixed += 1; return fixed;"))
+    , ("compound operands must match the target type", rejectedWith "TAT0012" (longBody "narrow += wide; return 0;"))
+    , ("a bitwise compound assignment needs an integer target", rejectedWith "TAT0012" (body "double ratio = 1.5; ratio &= 2.5; return 0;"))
+    , ("a compound assignment needs a numeric target", rejectedWith "TAT0012" (body "bool state = flag; state += other; return 0;"))
+    , ("a compound result must have the target type", rejectedWith "TAT0035" (body "double ratio = 7.5; ratio //= 2.0; return 0;"))
+    , ("a compound assignment to an unknown name is a resolution error", rejected (body "missing += 1; return 0;"))
+    , ("discard accepts a call", accepted (body "_ = Next(); return 0;"))
+    , ("discard accepts a void call", accepted (body "_ = Touch(); return 0;"))
+    , ("discard accepts a pure value", accepted (body "_ = left + right; return 0;"))
+    , ("discard still type-checks its operand", rejectedWith "TAT0012" (body "_ = left + flag; return 0;"))
+    , ("the new forms survive template instantiation", templateInstantiates)
+    , ("template reachability follows calls in both conditional results", templateReachesBothResults)
+    , ("template reachability follows calls in a coalescing fallback and a discard", templateReachesFallbackAndDiscard)
+    , ("a closure body may use the conditional forms", accepted closureSource)
+    ]
+
+rejected :: String -> Bool
+rejected = not . accepted
+
+compoundAccepted :: String -> Bool
+compoundAccepted operatorText = accepted (body ("int value = left; value " ++ operatorText ++ " 2; return value;"))
+
+longBody :: String -> String
+longBody statements = body ("long wide = 5; int narrow = 3; " ++ statements)
+
+templateSource :: String
+templateSource =
+    unwords
+        [ "template<typename T> class Box {"
+        , "int Entry(_ bool flag, _ int seed) {"
+        , "int value = seed;"
+        , "value += 2;"
+        , "_ = Audit(value);"
+        , "return flag ? First(value) : Second(seed) ?: Fallback();"
+        , "}"
+        , "int First(_ int value) { return value; }"
+        , "int Second(_ int value) { return value; }"
+        , "int Fallback() { return 7; }"
+        , "int Audit(_ int value) { return value; }"
+        , "int Unused() { return 9; }"
+        , "}"
+        ]
+
+-- | Specialize @Box<string>@ for its @Entry@ member only.
+templatePlan :: Maybe TemplateSpecializationPlan
+templatePlan = case analyzeSemantics (CompilerInput "conditional-template.tala" templateSource) of
+    Right artifacts ->
+        either
+            (const Nothing)
+            Just
+            ( planTemplateSpecializations
+                defaultTemplateSpecializationLimits
+                (semanticTypedAST artifacts)
+                [ TemplateSpecializationDemand
+                    (TemplateApplication (QualifiedName [Identifier "Box"]) [TypeTemplateArgument stringType])
+                    (TemplateMemberDemand [Identifier "Entry"])
+                    "conditional-template"
+                ]
+            )
+    Left _ -> Nothing
+
+specializedMembers :: [Declaration ResolvedName Type]
+specializedMembers = case templatePlan of
+    Just plan ->
+        concat
+            [ members
+            | TypeDeclaration {typeMembers = members} <-
+                map templateSpecializationDeclaration (plannedTemplateSpecializations plan)
+            ]
+    Nothing -> []
+
+specializedNames :: [String]
+specializedNames = map (identifierText . resolvedSpelling . declarationName) specializedMembers
+
+-- The instantiated declaration is a fresh, closed copy of the template
+-- member. Every new node must have been copied by the template passes.
+templateInstantiates :: Bool
+templateInstantiates =
+    let closed = show specializedMembers
+     in all
+            (`isInfixOf` closed)
+            ["ConditionalExpression", "CoalesceExpression", "CompoundAssignmentStatement", "DiscardStatement"]
+
+templateReachesBothResults :: Bool
+templateReachesBothResults = all (`elem` specializedNames) ["Entry", "First", "Second"]
+
+templateReachesFallbackAndDiscard :: Bool
+templateReachesFallbackAndDiscard =
+    all (`elem` specializedNames) ["Fallback", "Audit"] && "Unused" `notElem` specializedNames
+
+closureSource :: String
+closureSource =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ bool flag, _ int left, _ int right) {"
+        , "        auto choose = \\ -> flag ? left : right ?: 1;"
+        , "        return choose();"
+        , "    }"
+        , "}"
+        ]
+
+-- ---------------------------------------------------------------- lowering
+
+loweringTests :: [(String, Bool)]
+loweringTests =
+    [ ("a conditional lowers to one Core conditional", conditionalLowers)
+    , ("truthy coalescing binds its left operand once", coalesceBindsLeftOnce)
+    , ("truthy coalescing reads the bound value as test and result", coalesceReadsBinding)
+    , ("every compound operator lowers to its primitive", all compoundLowers compoundPrimitives)
+    , ("a compound assignment reads its target as the left operand", compoundReadsTarget)
+    , ("a for update compound assignment lowers into the update region", forUpdateLowers)
+    , ("discard lowers to an evaluated expression", discardLowers)
+    , ("the lowered conditional forms verify as Core", loweredCoreVerifies)
+    , ("Core wire round-trips the conditional forms", wireRoundTrips)
+    , ("a truncated conditional payload is rejected by the wire reader", wireRejectsTruncation)
+    , ("a closure captures the operands of its conditional body", closureCapturesOperands)
+    ]
+
+compoundPrimitives :: [(String, CorePrimitive)]
+compoundPrimitives =
+    [ ("+=", CoreAdd)
+    , ("-=", CoreSubtract)
+    , ("*=", CoreMultiply)
+    , ("/=", CoreDivide)
+    , ("//=", CoreFloorDivide)
+    , ("%=", CoreRemainder)
+    , ("**=", CorePower)
+    , ("<<=", CoreShiftLeft)
+    , (">>=", CoreShiftRight)
+    , ("&=", CoreBitwiseAnd)
+    , ("^=", CoreBitwiseXor)
+    , ("|=", CoreBitwiseOr)
+    ]
+
+-- | Unoptimized Core body of the method named @Evaluate@.
+loweredBody :: String -> Maybe [CoreStatement]
+loweredBody text = case compileSource text of
+    Right artifacts -> evaluateBody (artifactCore artifacts)
+    Left _ -> Nothing
+
+evaluateBody :: CoreModule -> Maybe [CoreStatement]
+evaluateBody moduleValue =
+    case [coreFunctionBody function | function <- coreModuleFunctions moduleValue, named "Evaluate" function] of
+        statements : _ -> Just statements
+        [] -> Nothing
+
+named :: String -> CoreFunction -> Bool
+named expected function = identifierText (resolvedSpelling (coreFunctionName function)) == expected
+
+conditionalLowers :: Bool
+conditionalLowers = case loweredBody (returning "flag ? left : right") of
+    Just [CoreReturn (CoreConditional (CoreVariable condition _) (CoreVariable first _) (CoreVariable second _) valueType)] ->
+        map spelling [condition, first, second] == ["flag", "left", "right"] && valueType == intType
+    _ -> False
+
+coalesceBindsLeftOnce :: Bool
+coalesceBindsLeftOnce = case loweredBody (returning "Next() ?: right") of
+    Just [CoreReturn expression@(CoreLet _ _ CoreApply {} _ _)] -> countCalls expression == 1
+    _ -> False
+
+coalesceReadsBinding :: Bool
+coalesceReadsBinding = case loweredBody (returning "Next() ?: right") of
+    Just [CoreReturn (CoreLet bound _ _ (CoreConditional (CoreVariable test _) (CoreVariable first _) (CoreVariable second _) _) _)] ->
+        resolvedSymbol test == resolvedSymbol bound
+            && resolvedSymbol first == resolvedSymbol bound
+            && spelling second == "right"
+            && "$coalesce" `isPrefixOf` spelling bound
+    _ -> False
+
+compoundLowers :: (String, CorePrimitive) -> Bool
+compoundLowers (text, primitive) =
+    case loweredBody (body ("int value = left; value " ++ text ++ " right; return value;")) of
+        Just [_, CoreAssign target (CorePrimitive actual [_, _] valueType), _] ->
+            spelling target == "value" && actual == primitive && valueType == intType
+        _ -> False
+
+compoundReadsTarget :: Bool
+compoundReadsTarget = case loweredBody (body "int value = left; value -= right; return value;") of
+    Just [_, CoreAssign target (CorePrimitive CoreSubtract [CoreVariable current _, CoreVariable operand _] _), _] ->
+        resolvedSymbol current == resolvedSymbol target && spelling operand == "right"
+    _ -> False
+
+forUpdateLowers :: Bool
+forUpdateLowers =
+    case loweredBody (body "int total = 0; for (int index = 0; index < left; index += 2) { total += index; } return total;") of
+        Just [_, _, CoreFor _ [CoreAssign _ (CorePrimitive CoreAdd _ _)] [CoreAssign index (CorePrimitive CoreAdd _ _)], _] ->
+            spelling index == "index"
+        _ -> False
+
+discardLowers :: Bool
+discardLowers = case loweredBody (body "_ = Next(); return 0;") of
+    Just [CoreEvaluate CoreApply {}, CoreReturn _] -> True
+    _ -> False
+
+allFormsSource :: String
+allFormsSource =
+    body $
+        unwords
+            [ "int value = flag ? left : right;"
+            , "value += other ? 1 : 2;"
+            , "value = Twice(value) ?: Next();"
+            , "_ = Countdown(value);"
+            , "for (int index = 0; index < left; index += 1) { value -= index ?: 1; }"
+            , "return flag ? other ? value : left : right ?: 3;"
+            ]
+
+loweredCoreVerifies :: Bool
+loweredCoreVerifies = case compileSource allFormsSource of
+    Right artifacts ->
+        verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+            && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+    Left _ -> False
+
+wireRoundTrips :: Bool
+wireRoundTrips = case compileSource allFormsSource of
+    Right artifacts ->
+        let core = artifactCore artifacts
+         in moduleHasConditional core
+                && (encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits) == Right core
+    Left _ -> False
+
+wireRejectsTruncation :: Bool
+wireRejectsTruncation = case encodeCore defaultCoreWireLimits conditionalModule of
+    Right bytes ->
+        -- Every proper prefix must fail: a conditional has no count that a
+        -- shorter payload could satisfy.
+        all (isLeft . decodeCore defaultCoreWireLimits) [take size bytes | size <- [0 .. length bytes - 1]]
+            && decodeCore defaultCoreWireLimits bytes == Right conditionalModule
+    Left _ -> False
+    where
+        isLeft = either (const True) (const False)
+
+closureCapturesOperands :: Bool
+closureCapturesOperands = case compileSource closureSource of
+    Right artifacts ->
+        let captured =
+                [ spelling (coreCaptureName capture)
+                | function <- coreModuleFunctions (artifactCore artifacts)
+                , CoreClosure captures _ _ _ _ <- concatMap statementExpressions (coreFunctionBody function)
+                , capture <- captures
+                ]
+         in captured == ["flag", "left", "right"]
+    Left _ -> False
+
+spelling :: ResolvedName -> String
+spelling = identifierText . resolvedSpelling
+
+-- | Top-level expressions of a statement list, without descending into them.
+statementExpressions :: CoreStatement -> [CoreExpression]
+statementExpressions statement = case statement of
+    CoreBind binding -> [coreBindingValue binding]
+    CoreAssign _ value -> [value]
+    CoreReturn value -> [value]
+    CoreEvaluate value -> [value]
+    CoreIf condition yes no -> condition : concatMap statementExpressions (yes ++ no)
+    CoreWhile condition loopBody -> condition : concatMap statementExpressions loopBody
+    CoreDoWhile loopBody condition -> concatMap statementExpressions loopBody ++ [condition]
+    CoreFor condition loopBody update -> condition : concatMap statementExpressions (loopBody ++ update)
+    CoreBreak -> []
+    CoreContinue -> []
+
+-- | Every expression node reachable from an expression, itself included.
+subexpressions :: CoreExpression -> [CoreExpression]
+subexpressions expression =
+    expression : case expression of
+        CoreVariable {} -> []
+        CoreLiteral {} -> []
+        CoreApply callee arguments _ -> concatMap subexpressions (callee : arguments)
+        CorePrimitive _ arguments _ -> concatMap subexpressions arguments
+        CoreLet _ _ value letBody _ -> subexpressions value ++ subexpressions letBody
+        CoreConditional condition whenTrue whenFalse _ -> concatMap subexpressions [condition, whenTrue, whenFalse]
+        CoreClosure captures _ _ closureBody _ ->
+            concatMap (subexpressions . coreCaptureValue) captures
+                ++ concatMap subexpressions (concatMap statementExpressions closureBody)
+
+moduleExpressions :: CoreModule -> [CoreExpression]
+moduleExpressions moduleValue =
+    concatMap
+        subexpressions
+        (concatMap statementExpressions (concatMap coreFunctionBody (coreModuleFunctions moduleValue)))
+
+countCalls :: CoreExpression -> Int
+countCalls expression = length [() | CoreApply {} <- subexpressions expression]
+
+isConditional :: CoreExpression -> Bool
+isConditional expression = case expression of
+    CoreConditional {} -> True
+    _ -> False
+
+moduleHasConditional :: CoreModule -> Bool
+moduleHasConditional = any isConditional . moduleExpressions
+
+-- ---------------------------------------------------------------- Core verifier
+
+verifierTests :: [(String, Bool)]
+verifierTests =
+    [ ("the Core verifier accepts a well-typed conditional", verifyCore conditionalModule == Right conditionalModule)
+    , ("the Core verifier rejects a non-condition test", coreRejectedWith "TAC1067" (conditionalOf text one two intType))
+    , ("the Core verifier rejects a mistyped first result", coreRejectedWith "TAC1068" (conditionalOf flagRead truth two intType))
+    , ("the Core verifier rejects a mistyped second result", coreRejectedWith "TAC1069" (conditionalOf flagRead one truth intType))
+    , ("the Core verifier rejects a non-scalar result", coreRejectedWith "TAC1070" (conditionalOf flagRead text text stringType))
+    , ("the Core verifier rejects a void result", coreRejectedWith "TAC1070" (conditionalOf flagRead unit unit unitType))
+    , ("the Core verifier checks names inside both results", coreRejectedWith "TAC1020" (conditionalOf flagRead one missing intType))
+    , ("the Core verifier checks names inside the test", coreRejectedWith "TAC1020" (conditionalOf missingFlag one two intType))
+    , ("a let inside a result does not leak into the other result", coreRejectedWith "TAC1020" leakingLet)
+    ]
+    where
+        text = CoreLiteral (CoreString "text") stringType
+        truth = CoreLiteral (CoreBoolean True) boolType
+        unit = CoreLiteral CoreUnit unitType
+        missing = CoreVariable (ResolvedName (SymbolId 90) (Identifier "missing")) intType
+        missingFlag = CoreVariable (ResolvedName (SymbolId 91) (Identifier "missing")) boolType
+        scoped = ResolvedName (SymbolId 50) (Identifier "scoped")
+        leakingLet =
+            conditionalOf
+                flagRead
+                (CoreLet scoped intType one (CoreVariable scoped intType) intType)
+                (CoreVariable scoped intType)
+                intType
+
+flagName :: ResolvedName
+flagName = ResolvedName (SymbolId 2) (Identifier "flag")
+
+flagRead :: CoreExpression
+flagRead = CoreVariable flagName boolType
+
+one, two :: CoreExpression
+one = CoreLiteral (CoreInteger 1) intType
+two = CoreLiteral (CoreInteger 2) intType
+
+-- | A one-function module returning the given conditional.
+conditionalOf :: CoreExpression -> CoreExpression -> CoreExpression -> Type -> CoreModule
+conditionalOf condition whenTrue whenFalse valueType =
+    CoreModuleWithSources
+        (QualifiedName [Identifier "Conditional"])
+        [ CoreFunction
+            (ResolvedName (SymbolId 1) (Identifier "Choose"))
+            [(flagName, boolType)]
+            valueType
+            [CoreReturn (CoreConditional condition whenTrue whenFalse valueType)]
+        ]
+        []
+        []
+
+conditionalModule :: CoreModule
+conditionalModule = conditionalOf flagRead one two intType
+
+coreRejectedWith :: String -> CoreModule -> Bool
+coreRejectedWith code moduleValue = case verifyCore moduleValue of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- ---------------------------------------------------------------- optimizer
+
+optimizerTests :: [(String, Bool)]
+optimizerTests =
+    [ ("a constant true test leaves only the first result", foldsTo "true ? left : right" "left")
+    , ("a constant false test leaves only the second result", foldsTo "false ? left : right" "right")
+    , ("a numeric constant test selects by truth", foldsTo "0 ? left : right" "right")
+    , ("a non-constant test keeps the conditional", optimizedHasConditional (returning "flag ? left : right"))
+    , ("a truthy constant left operand removes the fallback", coalesceKeepsConstantLeft)
+    , ("a zero left operand selects the fallback", coalesceSelectsFallback)
+    , ("constants fold inside a kept conditional", constantsFoldInResults)
+    , ("a pure discarded value disappears", optimizedBodyLength (body "_ = left + right; return 0;") == Just 1)
+    , ("a recursive call stays behind its conditional", optimizedKeepsRecursiveCall)
+    , ("the optimized conditional forms still verify", optimizedVerifies)
+    ]
+
+optimizedBody :: String -> Maybe [CoreStatement]
+optimizedBody text = case compileSource text of
+    Right artifacts -> evaluateBody (artifactOptimizedCore artifacts)
+    Left _ -> Nothing
+
+optimizedBodyLength :: String -> Maybe Int
+optimizedBodyLength = fmap length . optimizedBody
+
+foldsTo :: String -> String -> Bool
+foldsTo expression expected = case optimizedBody (returning expression) of
+    Just [CoreReturn (CoreVariable name _)] -> spelling name == expected
+    _ -> False
+
+-- The constant folder propagates the bound literal into the body but keeps
+-- the now unread binding; only the conditional itself must be gone.
+coalesceKeepsConstantLeft :: Bool
+coalesceKeepsConstantLeft = case optimizedBody (returning "5 ?: right") of
+    Just [CoreReturn (CoreLet _ _ (CoreLiteral (CoreInteger 5) _) (CoreLiteral (CoreInteger 5) _) _)] -> True
+    _ -> False
+
+coalesceSelectsFallback :: Bool
+coalesceSelectsFallback = case optimizedBody (returning "0 ?: right") of
+    Just [CoreReturn (CoreLet _ _ (CoreLiteral (CoreInteger 0) _) (CoreVariable name _) _)] -> spelling name == "right"
+    _ -> False
+
+optimizedHasConditional :: String -> Bool
+optimizedHasConditional text = case optimizedBody text of
+    Just statements -> any isConditional (concatMap subexpressions (concatMap statementExpressions statements))
+    Nothing -> False
+
+constantsFoldInResults :: Bool
+constantsFoldInResults = case optimizedBody (returning "flag ? 2 + 3 : 4 * 5") of
+    Just [CoreReturn (CoreConditional _ (CoreLiteral (CoreInteger 5) _) (CoreLiteral (CoreInteger 20) _) _)] -> True
+    _ -> False
+
+optimizedKeepsRecursiveCall :: Bool
+optimizedKeepsRecursiveCall = case compileSource (returning "Countdown(left)") of
+    Right artifacts ->
+        case [coreFunctionBody function | function <- coreModuleFunctions (artifactOptimizedCore artifacts), named "Countdown" function] of
+            [[CoreReturn (CoreConditional _ whenTrue (CoreLiteral (CoreInteger 0) _) _)]] -> countCalls whenTrue == 1
+            _ -> False
+    Left _ -> False
+
+optimizedVerifies :: Bool
+optimizedVerifies = case compileSource allFormsSource of
+    Right artifacts -> verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+    Left _ -> False
+
+-- ---------------------------------------------------------------- CorePrep
+
+corePrepTests :: [(String, Bool)]
+corePrepTests =
+    [ ("a conditional lowers to a branch, two arms and a join", conditionalHasFourBlocks)
+    , ("both arms assign the result slot and jump to the join", armsAssignAndJoin)
+    , ("the result slot has exactly one mutable seed", slotHasOneSeed)
+    , ("the join returns the result slot", joinReturnsSlot)
+    , ("a numeric test is compared with zero before the branch", numericTestIsBooleanized)
+    , ("a Boolean test is branched on directly", booleanTestHasNoComparison)
+    , ("a floating result slot starts from a floating zero", floatingSlotSeed)
+    , ("a Boolean result slot starts from false", booleanSlotSeed)
+    , ("a call in a result is emitted only in that arm", callStaysInItsArm)
+    , ("nested conditionals create one region each", nestedConditionalRegions)
+    , ("truthy coalescing evaluates its fallback behind a branch", coalesceFallbackIsConditional)
+    , ("a conditional in a loop condition keeps the back-edge", conditionalInLoopCondition)
+    , ("every generated block id is unique", all blockIdsAreUnique (preparedFunctions allFormsSource))
+    , ("every branch and jump target exists", all targetsExist (preparedFunctions allFormsSource))
+    , ("CorePrep of the conditional forms survives its verifier", preparedVerifies)
+    , ("a discarded call stays one result-dropping instruction", discardedCallIsEvaluated)
+    , ("a discarded non-call value is computed into a temporary", discardedOperatorIsBound)
+    , ("a discarded division still executes", discardedDivisionIsBound)
+    , ("a conditional statement leaves no result-dropping copy", conditionalStatementHasNoEvaluate)
+    , ("a discarded closure creation is bound, not dropped", discardedClosureIsBound)
+    ]
+
+preparedFunctions :: String -> [CorePrepFunction]
+preparedFunctions text = case compileSource text of
+    Right artifacts -> corePrepModuleFunctions (artifactCorePrep artifacts)
+    Left _ -> []
+
+preparedNamed :: String -> String -> Maybe CorePrepFunction
+preparedNamed name text =
+    case [function | function <- preparedFunctions text, spelling (corePrepFunctionName function) == name] of
+        function : _ -> Just function
+        [] -> Nothing
+
+prepared :: String -> Maybe CorePrepFunction
+prepared = preparedNamed "Evaluate"
+
+blocksOf :: String -> [CorePrepBlock]
+blocksOf = maybe [] corePrepFunctionBlocks . prepared
+
+instructionsOf :: String -> [CorePrepInstruction]
+instructionsOf = concatMap corePrepBlockInstructions . blocksOf
+
+simple :: String
+simple = returning "flag ? left : right"
+
+conditionalHasFourBlocks :: Bool
+conditionalHasFourBlocks = case blocksOf simple of
+    [entry, whenTrue, whenFalse, joinBlock] ->
+        corePrepBlockTerminator entry
+            `isBranchTo` (corePrepBlockId whenTrue, corePrepBlockId whenFalse)
+            && corePrepBlockTerminator whenTrue == CorePrepJump (corePrepBlockId joinBlock)
+            && corePrepBlockTerminator whenFalse == CorePrepJump (corePrepBlockId joinBlock)
+    _ -> False
+
+isBranchTo :: CorePrepTerminator -> (Int, Int) -> Bool
+isBranchTo terminator (expectedTrue, expectedFalse) = case terminator of
+    CorePrepBranch _ actualTrue actualFalse -> actualTrue == expectedTrue && actualFalse == expectedFalse
+    _ -> False
+
+armsAssignAndJoin :: Bool
+armsAssignAndJoin = case blocksOf simple of
+    [_, whenTrue, whenFalse, _] ->
+        map assignedFrom (corePrepBlockInstructions whenTrue) == [Just "left"]
+            && map assignedFrom (corePrepBlockInstructions whenFalse) == [Just "right"]
+    _ -> False
+    where
+        assignedFrom instruction = case instruction of
+            CorePrepAssign target (CorePrepVariable value _) | isSlot target -> Just (spelling value)
+            _ -> Nothing
+
+isSlot :: ResolvedName -> Bool
+isSlot name = "$conditional" `isPrefixOf` spelling name
+
+slotSeeds :: [CorePrepInstruction] -> [(Type, CorePrepOperation)]
+slotSeeds instructions =
+    [(valueType, operation) | CorePrepBind name valueType True operation <- instructions, isSlot name]
+
+slotHasOneSeed :: Bool
+slotHasOneSeed = case blocksOf simple of
+    entry : rest ->
+        slotSeeds (corePrepBlockInstructions entry) == [(intType, CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType))]
+            && null (slotSeeds (concatMap corePrepBlockInstructions rest))
+    [] -> False
+
+joinReturnsSlot :: Bool
+joinReturnsSlot = case reverse (blocksOf simple) of
+    joinBlock : _ -> case corePrepBlockTerminator joinBlock of
+        CorePrepReturn (CorePrepVariable name _) -> isSlot name
+        _ -> False
+    [] -> False
+
+comparisons :: [CorePrepInstruction] -> Int
+comparisons instructions =
+    length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreNotEqual _) <- instructions]
+
+numericTestIsBooleanized :: Bool
+numericTestIsBooleanized = comparisons (instructionsOf (returning "left ? left : right")) == 1
+
+booleanTestHasNoComparison :: Bool
+booleanTestHasNoComparison = comparisons (instructionsOf simple) == 0
+
+floatingSlotSeed :: Bool
+floatingSlotSeed =
+    map snd (slotSeeds (instructionsOf (body "double ratio = flag ? 1.5 : 2.5; return ratio > 2.0 ? 1 : 0;")))
+        == [ CorePrepCopy (CorePrepLiteral (CoreFloating "0") doubleType)
+           , CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType)
+           ]
+    where
+        doubleType = namedType "double"
+
+booleanSlotSeed :: Bool
+booleanSlotSeed =
+    map snd (slotSeeds (instructionsOf (body "bool chosen = flag ? other : left > right; return chosen ? 1 : 0;")))
+        == [ CorePrepCopy (CorePrepLiteral (CoreBoolean False) boolType)
+           , CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType)
+           ]
+
+isCall :: CorePrepInstruction -> Bool
+isCall instruction = case instruction of
+    CorePrepBind _ _ _ CorePrepCall {} -> True
+    _ -> False
+
+callStaysInItsArm :: Bool
+callStaysInItsArm = case maybe [] corePrepFunctionBlocks (preparedNamed "Countdown" simple) of
+    [entry, whenTrue, whenFalse, joinBlock] ->
+        not (any isCall (corePrepBlockInstructions entry))
+            && length (filter isCall (corePrepBlockInstructions whenTrue)) == 1
+            && not (any isCall (corePrepBlockInstructions whenFalse))
+            && not (any isCall (corePrepBlockInstructions joinBlock))
+    _ -> False
+
+branches :: [CorePrepBlock] -> Int
+branches blocks = length [() | CorePrepBranch {} <- map corePrepBlockTerminator blocks]
+
+nestedConditionalRegions :: Bool
+nestedConditionalRegions =
+    let blocks = blocksOf (returning "flag ? other ? left : right : 0")
+     in branches blocks == 2
+            && length (slotSeeds (concatMap corePrepBlockInstructions blocks)) == 2
+            && length blocks == 7
+
+coalesceFallbackIsConditional :: Bool
+coalesceFallbackIsConditional =
+    case blocksOf (returning "left ?: Countdown(right)") of
+        [entry, whenTrue, whenFalse, _] ->
+            not (any isCall (corePrepBlockInstructions entry))
+                && not (any isCall (corePrepBlockInstructions whenTrue))
+                && length (filter isCall (corePrepBlockInstructions whenFalse)) == 1
+        _ -> False
+
+conditionalInLoopCondition :: Bool
+conditionalInLoopCondition =
+    let blocks = blocksOf (body "int value = left; while (flag ? value > 0 : value > right) { value -= 1; } return value;")
+        backward block = case corePrepBlockTerminator block of
+            CorePrepJump target -> target < corePrepBlockId block
+            _ -> False
+     in branches blocks == 2 && length (filter backward blocks) == 1
+
+evaluations :: [CorePrepInstruction] -> [CorePrepOperation]
+evaluations instructions = [operation | CorePrepEvaluate operation <- instructions]
+
+discardedCallIsEvaluated :: Bool
+discardedCallIsEvaluated =
+    case evaluations (instructionsOf (body "_ = Countdown(left); return 0;")) of
+        [CorePrepCall {}] -> True
+        _ -> False
+
+-- `_ = !Countdown(left);` once reached later stages as a discarded bitwise
+-- instruction, which only calls and closure creations may be.
+discardedOperatorIsBound :: Bool
+discardedOperatorIsBound =
+    let instructions = instructionsOf (body "_ = !Countdown(left); return 0;")
+     in null (evaluations instructions)
+            && length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreBitwiseNot _) <- instructions] == 1
+            && length (filter isCall instructions) == 1
+
+discardedDivisionIsBound :: Bool
+discardedDivisionIsBound =
+    let instructions = instructionsOf (body "_ = 12 / left; return 0;")
+     in null (evaluations instructions)
+            && length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreDivide _) <- instructions] == 1
+
+conditionalStatementHasNoEvaluate :: Bool
+conditionalStatementHasNoEvaluate =
+    let blocks = blocksOf (body "flag ? Countdown(left) : 0; return 0;")
+        instructions = concatMap corePrepBlockInstructions blocks
+     in null (evaluations instructions) && branches blocks == 1 && length (filter isCall instructions) == 1
+
+discardedClosureIsBound :: Bool
+discardedClosureIsBound =
+    let instructions = instructionsOf (body "_ = \\ -> left; return 0;")
+     in null (evaluations instructions)
+            && length [() | CorePrepBind _ _ _ CorePrepMakeClosure {} <- instructions] == 1
+
+blockIdsAreUnique :: CorePrepFunction -> Bool
+blockIdsAreUnique function =
+    let identifiers = map corePrepBlockId (corePrepFunctionBlocks function)
+     in all (\identifier -> length (filter (== identifier) identifiers) == 1) identifiers
+
+targetsExist :: CorePrepFunction -> Bool
+targetsExist function =
+    let blocks = corePrepFunctionBlocks function
+        identifiers = map corePrepBlockId blocks
+        targets block = case corePrepBlockTerminator block of
+            CorePrepJump target -> [target]
+            CorePrepBranch _ whenTrue whenFalse -> [whenTrue, whenFalse]
+            _ -> []
+     in all (`elem` identifiers) (concatMap targets blocks)
+
+preparedVerifies :: Bool
+preparedVerifies = case compileSource allFormsSource of
+    Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+ test/ConsoleTests.hs view
@@ -0,0 +1,517 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Console output, strings and the output format grammar.
+
+@Spec\/StandardLibrary\/IO\/ConsoleIO.tala@ specifies the console. A program
+writes with @Console.Print@, @Console.Println@, @Console.Printf@ and
+@Console.Printfn@, to standard error with @Console.Error@ and its three
+siblings, and builds a string with @Console.Format@. A format is a
+compile-time string, checked against its arguments when the program is
+compiled: a conversion that does not exist, a flag a conversion does not
+take, a missing argument and an argument of the wrong type are errors of the
+program and not of a run.
+
+Every program here is run in the reference Core evaluator, on the Core the
+frontend lowered and on the Core the optimizer left, and what it wrote is
+compared with a string written by hand. The line terminator of the
+reference is a line feed.
+
+Output is an effect. The second half of this module holds what follows from
+that under evaluation by need: an expression that writes is evaluated where
+it stands and in the order it is written, whether or not its value is ever
+needed, while a value that only computes is still computed by need.
+-}
+module ConsoleTests (consoleTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf)
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Diagnostic
+
+consoleTests :: [(String, Bool)]
+consoleTests =
+    concat
+        [ [ ("unoptimized Core writes " ++ show expected ++ " for " ++ body, written artifactCore body == Just (expected, ""))
+          , ("optimized Core writes " ++ show expected ++ " for " ++ body, written artifactOptimizedCore body == Just (expected, ""))
+          , ("every stage verifies " ++ body, verifies body)
+          ]
+        | (body, expected) <- outputs
+        ]
+        ++ [ ("standard error receives " ++ show expected ++ " for " ++ body, written artifactCore body == Just ("", expected))
+           | (body, expected) <- errors
+           ]
+        ++ [ ("rejected with " ++ code ++ ": " ++ body, rejectedWith code body)
+           | (body, code) <- rejections
+           ]
+        ++ [ ("both streams keep their own text", written artifactCore mixed == Just ("ac", "b\n"))
+           , ("the optimizer keeps both streams", written artifactOptimizedCore mixed == Just ("ac", "b\n"))
+           , -- A program that writes nothing has no runtime call in it.
+             ("a program without output has no runtime call", not (mentions "CoreRuntimeCall" artifactCore "int x = 1;"))
+           , ("a write is a runtime call", mentions "CoreRuntimeCall" artifactCore "Console.Print(\"a\");")
+           -- The equality primitive on two strings would compare objects.
+           , ("a string pattern of is is not the equality primitive", not (mentions "CoreEqual" artifactCore "Console.Println(Name() is \"x\");"))
+           , ("a string pattern of match is not the equality primitive", not (mentions "CoreEqual" artifactCore "Console.Println(match (Name()) { \"x\" -> 1, _ -> 2 });"))
+           , ("a string is not ordered by a pattern", rejectedWith "TAT0023" "bool b = Name() is > \"A\";")
+           , ("a string is not matched by a number in is", rejectedWith "TAT0021" "bool b = Name() is 5;")
+           , -- The optimizer may not drop a write whose result nothing uses:
+             -- the write is the point.
+             ("the optimizer keeps a write", mentions "CoreRuntimeCall" artifactOptimizedCore "Console.Print(\"a\");")
+           , ("the optimizer keeps a write in a method nothing reads the result of", written artifactOptimizedCore "Log(4);" == Just ("4\n", ""))
+           , -- A value that only computes is still by need beside output.
+             ("a value that is never needed is not computed beside output", written artifactCore unusedFailure == Just ("ok\n", ""))
+           , ("the same holds after optimization", written artifactOptimizedCore unusedFailure == Just ("ok\n", ""))
+           , -- A binding whose initializer writes has no flag: it is not deferred.
+             ("a binding that writes is not deferred", not (mentions "$known" artifactCore "int x = Log(1); Console.Println(2);"))
+           , ("a binding that only computes is deferred as before", mentions "$known" artifactCore "int x = Half(8); if (Zero() > 0) { Console.Println(x); }")
+           , -- An argument that writes is not suspended; one that only
+             -- computes still is.
+             ("an argument that writes is not suspended", not (mentions "CoreMemoize" artifactCore "Console.Println(Pick(0, Log(5)));"))
+           , ("an argument that only computes is suspended as before", mentions "CoreMemoize" artifactCore "Console.Println(Pick(0, 8 / Zero()));")
+           , -- A class of the program named Console is the program's own.
+             ("a class named Console shadows the console", shadowed)
+           ]
+    where
+        mixed = "Console.Print(\"a\"); Console.Errorln(\"b\"); Console.Print(\"c\");"
+        unusedFailure = "int z = 1 / Zero(); Console.Println(\"ok\");"
+
+{- | Programs and what they write to standard output. The body is the body of
+@Main@; the helpers are the methods of 'program'.
+-}
+outputs :: [(String, String)]
+outputs =
+    -- Plain output. Print writes the text; Println ends the line after it.
+    [ ("Console.Print(\"a\"); Console.Print(\"b\");", "ab")
+    , ("Console.Println(\"Hello\");", "Hello\n")
+    , ("Console.Println(\"\");", "\n")
+    , ("Console.Print(\"\");", "")
+    , ("Console.Println(\"one\"); Console.Println(\"two\");", "one\ntwo\n")
+    , ("System.Console.Println(\"qualified\");", "qualified\n")
+    , -- A percent sign has no meaning outside a format.
+      ("Console.Println(\"%d\");", "%d\n")
+    , ("Console.Println(\"100%\");", "100%\n")
+    , -- A value that is not a string is written as text.
+      ("Console.Println(42);", "42\n")
+    , ("Console.Println(0);", "0\n")
+    , ("Console.Println(0 - 7);", "-7\n")
+    , ("Console.Println(true);", "true\n")
+    , ("Console.Println(false);", "false\n")
+    , ("Console.Println('x');", "x\n")
+    , ("Console.Println(Half(10));", "5\n")
+    , ("int n = 9223372036854775807; Console.Println(n);", "9223372036854775807\n")
+    , ("uint u = 18446744073709551615; Console.Println(u);", "18446744073709551615\n")
+    , ("byte small = 100; Console.Println(small);", "100\n")
+    , ("ushort medium = 65535; Console.Println(medium);", "65535\n")
+    , -- A string is a value: bound, passed, returned.
+      ("String s = \"kept\"; Console.Println(s);", "kept\n")
+    , ("Console.Println(Name());", "Tala\n")
+    , ("Console.Println(Twice(\"ab\"));", "abab\n")
+    , -- + joins two strings, and writes a value that is not one as text.
+      ("Console.Println(\"Hello from \" + \"Tala\" + \"!\");", "Hello from Tala!\n")
+    , ("String language = \"Tala\"; Console.Println(\"Hello from \" + language + \"!\");", "Hello from Tala!\n")
+    , ("Console.Println(\"n=\" + 5);", "n=5\n")
+    , ("Console.Println(5 + \"n\");", "5n\n")
+    , ("Console.Println(\"b=\" + true);", "b=true\n")
+    , ("Console.Println(\"c=\" + 'z');", "c=z\n")
+    , ("Console.Println(\"\" + (1 + 2));", "3\n")
+    , -- + associates to the left: the string takes the 1, then the 2.
+      ("Console.Println(\"a\" + 1 + 2);", "a12\n")
+    , ("Console.Println(1 + 2 + \"a\");", "3a\n")
+    , ("uint u = 7; Console.Println(\"u=\" + u);", "u=7\n")
+    , ("Console.Println(\"\" + \"\");", "\n")
+    , ("String t = \"x\"; t += \"y\"; t += 3; Console.Println(t);", "xy3\n")
+    , ("String t = \"\"; for (int i = 0; i < 4; i += 1) { t += i; } Console.Println(t);", "0123\n")
+    , -- Strings are compared by what they hold.
+      ("Console.Println(\"a\" == \"a\");", "true\n")
+    , ("Console.Println(\"a\" == \"b\");", "false\n")
+    , ("Console.Println(\"a\" \\= \"b\");", "true\n")
+    , ("Console.Println(\"a\" \\= \"a\");", "false\n")
+    , ("String a = \"ab\"; String b = \"a\" + \"b\"; Console.Println(a == b);", "true\n")
+    , ("String a = \"ab\"; String b = \"a\" + \"c\"; Console.Println(a == b);", "false\n")
+    , ("Console.Println(\"\" == \"\");", "true\n")
+    , ("Console.Println(\"a\" == \"ab\");", "false\n")
+    , ("if (Name() == \"Tala\") { Console.Println(\"same\"); } else { Console.Println(\"other\"); }", "same\n")
+    , -- Printf writes its format; Printfn ends the line after it.
+      ("Console.Printf(\"plain\");", "plain")
+    , ("Console.Printfn(\"plain\");", "plain\n")
+    , ("Console.Printf(\"\");", "")
+    , ("Console.Printfn(\"\");", "\n")
+    , ("Console.Printf(\"%d\", 42);", "42")
+    , ("Console.Printfn(\"%d\", 42);", "42\n")
+    , ("Console.Printf(\"Count: %d\", 3);", "Count: 3")
+    , ("Console.Printf(\"%d and %d\", 1, 2);", "1 and 2")
+    , ("Console.Printfn(\"gcd(%d, %d) = %d\", 1071, 462, 21);", "gcd(1071, 462) = 21\n")
+    , -- %d: a signed integer in decimal.
+      ("Console.Printf(\"%d\", 0 - 255);", "-255")
+    , ("Console.Printf(\"%5d|\", 42);", "   42|")
+    , ("Console.Printf(\"%-5d|\", 42);", "42   |")
+    , ("Console.Printf(\"%05d\", 42);", "00042")
+    , ("Console.Printf(\"%05d\", 0 - 42);", "-0042")
+    , ("Console.Printf(\"%08d\", 123);", "00000123")
+    , ("Console.Printf(\"%+d\", 42);", "+42")
+    , ("Console.Printf(\"%+d\", 0 - 42);", "-42")
+    , ("Console.Printf(\"% d\", 42);", " 42")
+    , ("Console.Printf(\"%2d\", 12345);", "12345")
+    , -- The ' flag groups the integer digits in threes with apostrophes.
+      ("Console.Printf(\"%'d\", 1234567);", "1'234'567")
+    , ("Console.Printf(\"%'d\", 123);", "123")
+    , ("Console.Printf(\"%'d\", 1234);", "1'234")
+    , ("Console.Printf(\"%'d\", 0 - 1234567);", "-1'234'567")
+    , ("byte small = 0 - 100; Console.Printf(\"%d\", small);", "-100")
+    , -- %x: hexadecimal. A negative number is a minus sign and its
+      -- magnitude, never the bit pattern of its representation.
+      ("Console.Printf(\"%x\", 255);", "ff")
+    , ("Console.Printf(\"%x\", 0 - 255);", "-ff")
+    , ("Console.Printf(\"%#x\", 255);", "0xff")
+    , ("Console.Printf(\"%#x\", 0 - 255);", "-0xff")
+    , ("Console.Printf(\"%08x\", 48879);", "0000beef")
+    , ("Console.Printf(\"%#08x\", 255);", "0x0000ff")
+    , ("uint u = 4294967295; Console.Printf(\"%x\", u);", "ffffffff")
+    , ("Console.Printf(\"%x\", 0);", "0")
+    , -- %u: an unsigned integer.
+      ("uint u = 42; Console.Printf(\"%u\", u);", "42")
+    , ("Console.Printf(\"%u\", 42);", "42")
+    , ("ubyte tiny = 255; Console.Printf(\"%u\", tiny);", "255")
+    , ("uint u = 1234567; Console.Printf(\"%'u\", u);", "1'234'567")
+    , -- %s and %c.
+      ("Console.Printf(\"%s\", \"abc\");", "abc")
+    , ("Console.Printf(\"%10s|\", \"abc\");", "       abc|")
+    , ("Console.Printf(\"%-10s|\", \"abc\");", "abc       |")
+    , ("Console.Printf(\"%.2s\", \"abcdef\");", "ab")
+    , ("Console.Printf(\"%.5s\", \"abc\");", "abc")
+    , ("Console.Printf(\"%5.1s|\", \"abc\");", "    a|")
+    , ("Console.Printf(\"%s: %d\", \"total\", 7);", "total: 7")
+    , ("Console.Printf(\"%s\", Name());", "Tala")
+    , ("Console.Printf(\"%c\", 'q');", "q")
+    , ("Console.Printf(\"%3c|\", 'q');", "  q|")
+    , ("Console.Printf(\"%-3c|\", 'q');", "q  |")
+    , -- %b: a Boolean.
+      ("Console.Printf(\"%b\", true);", "true")
+    , ("Console.Printf(\"%b\", 1 > 2);", "false")
+    , ("Console.Printf(\"%6b|\", false);", " false|")
+    , ("Console.Printf(\"%-6b|\", true);", "true  |")
+    , -- %% is a percent sign and %n the line terminator; neither takes an
+      -- argument.
+      ("Console.Printf(\"100%%\");", "100%")
+    , ("Console.Printf(\"%d%%\", 50);", "50%")
+    , ("Console.Printf(\"A%nB\");", "A\nB")
+    , ("Console.Printf(\"%n\");", "\n")
+    , -- %f: a fixed number of digits after the point, six unless a
+      -- precision says otherwise; a tie rounds to the even digit.
+      ("Console.Printf(\"%f\", 12.5);", "12.500000")
+    , ("Console.Printf(\"%.2f\", 12.5);", "12.50")
+    , ("Console.Printf(\"%.0f\", 2.5);", "2")
+    , ("Console.Printf(\"%.0f\", 3.5);", "4")
+    , ("Console.Printf(\"%.1f\", 0.25);", "0.2")
+    , ("Console.Printf(\"%.1f\", 0.75);", "0.8")
+    , ("Console.Printf(\"%.3f\", 3.14159);", "3.142")
+    , ("Console.Printf(\"%10.3f|\", 3.14159);", "     3.142|")
+    , ("Console.Printf(\"%-10.3f|\", 3.14159);", "3.142     |")
+    , ("Console.Printf(\"%010.3f\", 3.14159);", "000003.142")
+    , ("Console.Printf(\"%+.1f\", 1.5);", "+1.5")
+    , ("Console.Printf(\"%'.2f\", 1234567.5);", "1'234'567.50")
+    , ("Console.Printf(\"%.2f\", 0.0);", "0.00")
+    , -- The digits are those of the binary number, not of the literal.
+      ("Console.Printf(\"%.20f\", 0.1);", "0.10000000000000000555")
+    , ("Console.Printf(\"%.1f\", 1000000.0);", "1000000.0")
+    , ("float price = 19.99; Console.Printfn(\"Price: %.2f\", price);", "Price: 19.99\n")
+    , -- A width or a precision written as * is the int argument before
+      -- the value.
+      ("Console.Printf(\"%*d|\", 5, 42);", "   42|")
+    , ("Console.Printf(\"%-*d|\", 5, 42);", "42   |")
+    , ("Console.Printf(\"%*s|\", 6, \"ab\");", "    ab|")
+    , ("Console.Printf(\"%.*f\", 2, 3.14159);", "3.14")
+    , ("Console.Printf(\"%*.*f|\", 8, 2, 3.14159);", "    3.14|")
+    , ("int width = 4; Console.Printf(\"%*d|\", width, 7);", "   7|")
+    , -- Console.Format applies a format and writes nothing.
+      ("String t = Console.Format(\"%d-%d\", 1, 2); Console.Print(t);", "1-2")
+    , ("String t = Console.Format(\"Name: %s, Age: %d\", \"Ada\", 36); Console.Println(t);", "Name: Ada, Age: 36\n")
+    , ("String t = Console.Format(\"plain\"); Console.Print(t + \"!\");", "plain!")
+    , ("Console.Print(Console.Format(\"%05d\", 42) + Console.Format(\"%x\", 255));", "00042ff")
+    , ("String t = Console.Format(\"%d\", 1); Console.Println(\"x\");", "x\n")
+    , -- Output in loops, branches and methods.
+      ("for (int i = 0; i < 3; i += 1) { Console.Print(i); }", "012")
+    , ("for (int i = 1; i <= 3; i += 1) { Console.Printfn(\"%d squared is %d\", i, i * i); }", "1 squared is 1\n2 squared is 4\n3 squared is 9\n")
+    , ("if (Half(4) == 2) { Console.Println(\"yes\"); } else { Console.Println(\"no\"); }", "yes\n")
+    , ("Greet(\"Ada\"); Greet(\"Alan\");", "Hello, Ada!\nHello, Alan!\n")
+    , ("Console.Println(Fizz(3)); Console.Println(Fizz(5)); Console.Println(Fizz(15)); Console.Println(Fizz(7));", "Fizz\nBuzz\nFizzBuzz\n7\n")
+    , -- Output is an effect: it happens where it is written and in the
+      -- order it is written, whether or not a value is needed.
+      ("int x = Log(1); Console.Println(2);", "1\n2\n")
+    , ("int x = Log(1); int y = Log(2); Console.Println(y + x);", "1\n2\n3\n")
+    , ("int x = Log(1) + Log(2);", "1\n2\n")
+    , ("Console.Println(Pick(0, Log(5)));", "5\n7\n")
+    , ("Console.Println(Pick(1, Log(5)));", "5\n5\n")
+    , ("Console.Printf(\"%d %d\", Log(1), Log(2));", "1\n2\n1 2")
+    , -- The width of a conversion is written before its value and is
+      -- evaluated before it.
+      ("Console.Printf(\"%*d\", Log(3), Log(4));", "3\n4\n  4")
+    , ("Console.Println(\"a\" + Log(1) + Log(2));", "1\n2\na12\n")
+    , ("_ = Log(8);", "8\n")
+    , ("Log(8);", "8\n")
+    , ("int v = Zero() > 0 ? Log(1) : Log(2);", "2\n")
+    , ("bool both = Log(0) > 0 && Log(1) > 0;", "0\n")
+    , ("auto say = \\(int v) -> Log(v); int x = say(1); Console.Println(9);", "1\n9\n")
+    , ("int total = 0; for (int i = 0; i < 3; i += 1) { total += Log(i); } Console.Println(total);", "0\n1\n2\n3\n")
+    , ("int x = Twofold(3); Console.Println(\"after\");", "3\n3\nafter\n")
+    , -- A callable may write itself, and reads what it captured.
+      ("auto say = \\(int v) -> Console.Println(v); say(4); say(5);", "4\n5\n")
+    , ("auto say = \\(String s) -> Console.Printfn(\"<%s>\", s); say(\"a\");", "<a>\n")
+    , ("int base = 3; auto say = \\(int v) -> { Console.Println(v + base); return v; }; int x = say(1); Console.Println(\"end\");", "4\nend\n")
+    , -- A value that only computes is still computed by need.
+      ("int z = 1 / Zero(); Console.Println(\"ok\");", "ok\n")
+    , ("int z = 8 / Zero(); Console.Println(Pick(0, z));", "7\n")
+    , ("String s = \"x\"; int z = 8 / Zero(); Console.Println(s);", "x\n")
+    , -- Strings that callables take, capture, make and return.
+      ("auto greet = \\(String who) -> \"Hi \" + who; Console.Println(greet(\"Ada\"));", "Hi Ada\n")
+    , ("String prefix = Name() + \": \"; auto label = \\(int n) -> prefix + n; Console.Println(label(1)); Console.Println(label(2));", "Tala: 1\nTala: 2\n")
+    , ("auto twice = \\(int v) -> { Console.Printf(\"%d,\", v); return v * 2; }; Console.Println(twice(twice(1)));", "1,2,4\n")
+    , ("auto make = \\(int n) -> Console.Format(\"<%03d>\", n); String all = \"\"; for (int i = 0; i < 3; i += 1) { all += make(i); } Console.Println(all);", "<000><001><002>\n")
+    , ("String kept = Twice(\"ab\"); auto never = \\(int n) -> kept + n; Console.Println(\"x\");", "x\n")
+    , -- A method is a callable value: it writes where its call stands.
+      ("auto f = Log; int x = f(1); Console.Println(9);", "1\n9\n")
+    , -- Appending makes a new string; another name keeps the old one.
+      ("String s = \"a\"; String t = s; s += \"b\"; Console.Println(s + t);", "aba\n")
+    , ("String s = \"\"; for (int i = 0; i < 5; i += 1) { String old = s; s += i; if (old == s) { s += \"!\"; } } Console.Println(s);", "01234\n")
+    , -- String operations in arguments that are and are not needed.
+      ("Console.Println(Pick(1, Half(8)) + Name());", "4Tala\n")
+    , ("String made = Twice(Name() + \"!\"); Console.Println(Pick(0, 8 / Zero()));", "7\n")
+    , ("Console.Println(Console.Format(\"[%s]\", Console.Format(\"%5s\", Console.Format(\"%d\", 42))));", "[   42]\n")
+    , ("Console.Println(Twice(Twice(\"ab\")) == \"abababab\");", "true\n")
+    , -- A conditional selects one of two strings; the other is never made.
+      ("String s = Zero() > 0 ? \"a\" : \"b\"; Console.Println(s);", "b\n")
+    , ("Console.Println(Zero() == 0 ? Name() : Twice(\"x\"));", "Tala\n")
+    , ("String a = \"x\"; String b = \"y\"; String c = Zero() > 0 ? a : b; Console.Println(c + a + b);", "yxy\n")
+    , ("Console.Println(Zero() > 0 ? \"a\" : Zero() == 0 ? \"b\" : \"c\");", "b\n")
+    , ("String s = Zero() > 0 ? \"\" + Log(1) : \"\" + Log(2); Console.Println(s);", "2\n2\n")
+    , ("String s = \"\"; for (int i = 0; i < 4; i += 1) { s += i % 2 == 0 ? \"e\" : \"o\"; } Console.Println(s);", "eoeo\n")
+    , ("Console.Println(\"<\" + (Zero() == 0 ? \"yes\" : \"no\") + \">\");", "<yes>\n")
+    , ("Greet(Zero() == 0 ? Name() : \"nobody\");", "Hello, Tala!\n")
+    , ("String s = \"keep\"; s = Zero() > 0 ? \"lost\" : s; Console.Println(s);", "keep\n")
+    , -- A match, an if used as a value and a loop yield strings the same
+      -- way: one is selected, and the others are never made.
+      ("String s = match (Zero()) { 0 -> \"zero\", 1 -> \"one\", _ -> \"many\" }; Console.Println(s);", "zero\n")
+    , ("String s = match (Half(8)) { 0 -> \"zero\", int n if n > 3 -> \"big \" + n, _ -> \"small\" }; Console.Println(s);", "big 4\n")
+    , ("String s = if (Zero() > 0) { \"pos\" } else { Name() + \"!\" }; Console.Println(s);", "Tala!\n")
+    , ("int i = 0; String s = while (true) { i += 1; if (i > 2) { break \"done\" + i; } }; Console.Println(s);", "done3\n")
+    , ("Console.Println(match (Zero()) { 0 -> Name(), _ -> \"x\" } + (if (Zero() == 0) { \"a\" } else { \"b\" }));", "Talaa\n")
+    , ("String s = \"k\"; s = match (Zero()) { 1 -> \"lost\", _ -> s }; Console.Println(s);", "k\n")
+    , ("String s = match (Zero()) { 0 -> \"\" + Log(1), _ -> \"\" + Log(2) }; Console.Println(s);", "1\n1\n")
+    , ("String s = \"\"; for (int i = 0; i < 4; i += 1) { s += match (i % 3) { 0 -> \"a\", 1 -> \"b\", _ -> \"c\" }; } Console.Println(s);", "abca\n")
+    , ("Greet(match (Zero()) { 0 -> Name(), _ -> \"nobody\" });", "Hello, Tala!\n")
+    , ("String s = match (Zero()) { 0 -> match (Half(2)) { 1 -> \"inner\", _ -> \"other\" }, _ -> \"outer\" }; Console.Println(s);", "inner\n")
+    , -- A string is a subject of match and of `is`. It is matched by the
+      -- characters it holds: none of these subjects is the object of the
+      -- literal it is compared with.
+      ("Console.Println(match (Name()) { \"x\" -> 1, \"Tala\" -> 2, _ -> 3 });", "2\n")
+    , ("Console.Println(match (Twice(\"ab\")) { \"ab\" -> 1, \"abab\" -> 2, _ -> 3 });", "2\n")
+    , ("Console.Println(match (Fizz(15)) { \"Fizz\" -> 3, \"Buzz\" -> 5, \"FizzBuzz\" -> 15, _ -> 0 });", "15\n")
+    , ("Console.Println(match (Name()) { \"x\" -> \"no\", String other if other == Twice(\"a\") -> \"twice\", String other -> \"is \" + other });", "is Tala\n")
+    , ("match (Fizz(3)), (Fizz(5)) { (\"Fizz\"), (\"Buzz\") -> { Console.Println(\"both\"); }, _, _ -> { Console.Println(\"none\"); } }", "both\n")
+    , ("match (Fizz(3)), (Fizz(7)) { (\"Fizz\"), (\"Buzz\") -> { Console.Println(\"both\"); }, _, _ -> { Console.Println(\"none\"); } }", "none\n")
+    , ("int t = 0; for (int i = 1; i < 16; i += 1) { t += match (Fizz(i)) { \"Fizz\" -> 1, \"Buzz\" -> 10, \"FizzBuzz\" -> 100, _ -> 0 }; } Console.Println(t);", "124\n")
+    , ("Console.Println(Name() is \"Tala\");", "true\n")
+    , ("Console.Println(Name() is not \"Tala\");", "false\n")
+    , ("Console.Println(Twice(\"ab\") is \"ab\" or \"abab\");", "true\n")
+    , ("Console.Println(Twice(\"ab\") is \"ab\" or \"ba\");", "false\n")
+    , ("Console.Println(Name() is == \"Tala\"); Console.Println(Name() is \\= \"Tala\");", "true\nfalse\n")
+    , ("String s = Fizz(3); if (s is \"Fizz\" && Name() is not \"x\") { Console.Println(\"ok\"); }", "ok\n")
+    ]
+
+-- | Programs and what they write to standard error.
+errors :: [(String, String)]
+errors =
+    [ ("Console.Error(\"e\");", "e")
+    , ("Console.Errorln(\"failed\");", "failed\n")
+    , ("Console.Error(\"a\"); Console.Errorln(\"b\");", "ab\n")
+    , ("Console.Errorf(\"Code: %d\", 7);", "Code: 7")
+    , ("Console.Errorfn(\"Code: %d\", 7);", "Code: 7\n")
+    , ("Console.Errorln(404);", "404\n")
+    , ("System.Console.Errorln(\"q\");", "q\n")
+    ]
+
+-- | Programs the type checker rejects, with the code it reports.
+rejections :: [(String, String)]
+rejections =
+    -- The console has the methods the specification gives it.
+    [ ("Console.Shout(\"x\");", "TAT0071")
+    , ("Console.println(\"x\");", "TAT0071")
+    , ("System.Console.Nothing();", "TAT0071")
+    , -- Print takes one value.
+      ("Console.Print();", "TAT0072")
+    , ("Console.Println();", "TAT0072")
+    , ("Console.Print(\"a\", \"b\");", "TAT0072")
+    , ("Console.Printf();", "TAT0072")
+    , ("Console.Format();", "TAT0072")
+    , -- What cannot be written as text.
+      ("auto f = \\(int v) -> v; Console.Println(f);", "TAT0073")
+    , ("auto f = \\(int v) -> v; String s = \"a\" + f;", "TAT0073")
+    , -- A format is a string literal.
+      ("String f = \"%d\"; Console.Printf(f, 1);", "TAT0074")
+    , ("Console.Printf(Name());", "TAT0074")
+    , ("Console.Printf(\"%d\" + \"%d\", 1, 2);", "TAT0074")
+    , ("Console.Format(42);", "TAT0074")
+    , -- A conversion that does not exist, or is not finished.
+      ("Console.Printf(\"%q\", 1);", "TAT0075")
+    , ("Console.Printf(\"%\");", "TAT0075")
+    , ("Console.Printf(\"abc%\");", "TAT0075")
+    , ("Console.Printf(\"%5\");", "TAT0075")
+    , ("Console.Printf(\"%.d\", 1);", "TAT0075")
+    , ("Console.Printf(\"%D\", 1);", "TAT0075")
+    , ("Console.Printf(\"%i\", 1);", "TAT0075")
+    , ("Console.Printf(\"%e\", 1.5);", "TAT0075")
+    , ("Console.Printf(\"%1234567890d\", 1);", "TAT0075")
+    , -- A flag a conversion does not take, and flags that exclude each
+      -- other; examples 16 to 18 of the specification file.
+      ("Console.Printf(\"%+s\", \"a\");", "TAT0076")
+    , ("Console.Printf(\"%0s\", \"a\");", "TAT0076")
+    , ("Console.Printf(\"%#d\", 1);", "TAT0076")
+    , ("Console.Printf(\"%'x\", 1);", "TAT0076")
+    , ("Console.Printf(\"%-08d\", 1);", "TAT0076")
+    , ("Console.Printf(\"%+ d\", 1);", "TAT0076")
+    , ("Console.Printf(\"%++d\", 1);", "TAT0076")
+    , ("Console.Printf(\"%+u\", 1);", "TAT0076")
+    , ("Console.Printf(\"% u\", 1);", "TAT0076")
+    , ("Console.Printf(\"%#s\", \"a\");", "TAT0076")
+    , ("Console.Printf(\"%'s\", \"a\");", "TAT0076")
+    , ("Console.Printf(\"%0c\", 'a');", "TAT0076")
+    , ("Console.Printf(\"%+b\", true);", "TAT0076")
+    , ("Console.Printf(\"%#f\", 1.5);", "TAT0076")
+    , ("Console.Printf(\"%.2d\", 1);", "TAT0076")
+    , ("Console.Printf(\"%.2x\", 1);", "TAT0076")
+    , ("Console.Printf(\"%.1c\", 'a');", "TAT0076")
+    , ("Console.Printf(\"%.1b\", true);", "TAT0076")
+    , ("Console.Printf(\"%5%\");", "TAT0076")
+    , ("Console.Printf(\"%-n\");", "TAT0076")
+    , -- The arguments are the ones the format names, no more and no fewer.
+      ("Console.Printf(\"%d\");", "TAT0077")
+    , ("Console.Printf(\"%d\", 1, 2);", "TAT0077")
+    , ("Console.Printf(\"plain\", 1);", "TAT0077")
+    , ("Console.Printf(\"%d %d\", 1);", "TAT0077")
+    , ("Console.Printf(\"%*d\", 1);", "TAT0077")
+    , ("Console.Printf(\"%*.*f\", 1, 2);", "TAT0077")
+    , ("Console.Printf(\"%n\", 1);", "TAT0077")
+    , ("Console.Printf(\"%%\", 1);", "TAT0077")
+    , ("Console.Format(\"%s\");", "TAT0077")
+    , -- An argument has the type its conversion takes. Nothing is
+      -- converted for formatting: examples 11 and 12.
+      ("uint value = 42; Console.Printf(\"%d\", value);", "TAT0078")
+    , ("int value = 42; Console.Printf(\"%u\", value);", "TAT0078")
+    , ("int value = 42; Console.Printf(\"%f\", value);", "TAT0078")
+    , ("float value = 1.5; Console.Printf(\"%d\", value);", "TAT0078")
+    , ("Console.Printf(\"%s\", 42);", "TAT0078")
+    , ("Console.Printf(\"%d\", \"text\");", "TAT0078")
+    , ("Console.Printf(\"%c\", 65);", "TAT0078")
+    , ("Console.Printf(\"%c\", \"a\");", "TAT0078")
+    , ("Console.Printf(\"%b\", 1);", "TAT0078")
+    , ("Console.Printf(\"%d\", true);", "TAT0078")
+    , ("Console.Printf(\"%x\", 1.5);", "TAT0078")
+    , ("Console.Printf(\"%x\", \"ff\");", "TAT0078")
+    , ("Console.Printf(\"%*d\", true, 1);", "TAT0078")
+    , ("Console.Printf(\"%*d\", \"5\", 1);", "TAT0078")
+    , ("uint width = 5; Console.Printf(\"%*d\", width, 1);", "TAT0078")
+    , ("Console.Printf(\"%.*f\", 1.5, 1.5);", "TAT0078")
+    , -- Specified, and not implemented yet.
+      ("Console.Printf(\"%A\", 1);", "TAT0079")
+    , ("Console.Printf(\"%O\", 1);", "TAT0079")
+    , ("Console.Println(1.5);", "TAT0079")
+    , ("String s = \"x\" + 1.5;", "TAT0079")
+    , ("longint wide = 1; Console.Println(wide);", "TAT0079")
+    , ("longint wide = 1; Console.Printf(\"%d\", wide);", "TAT0079")
+    , ("ulongint wide = 1; Console.Printf(\"%u\", wide);", "TAT0079")
+    , ("double wide = 1.5; Console.Printf(\"%f\", wide);", "TAT0079")
+    , ("Console.Stdout();", "TAT0079")
+    , ("Console.Stdin();", "TAT0079")
+    , ("Console.Stderr();", "TAT0079")
+    , -- Strings have + and the two equalities, and no other operator.
+      ("String s = \"a\" - \"b\";", "TAT0012")
+    , ("String s = \"a\" * 2;", "TAT0012")
+    , ("bool b = \"a\" < \"b\";", "TAT0012")
+    , -- An immutable string is not appended to.
+      ("final String s = \"a\"; s += \"b\";", "TAT0003")
+    ]
+
+{- | A class of the program named @Console@ is found before the console of
+the language: its method is called, and nothing is written.
+-}
+shadowed :: Bool
+shadowed = case compileSource text of
+    Right artifacts ->
+        not (any (("CoreRuntimeCall" `isInfixOf`) . show . coreFunctionBody) (coreModuleFunctions (artifactCore artifacts)))
+            && (runFunctionWriting 10000 (artifactCore artifacts) "Main" [] == Just (UnitValue, Written "" ""))
+    Left _ -> False
+    where
+        text =
+            unlines
+                [ "namespace Demo;"
+                , "public class Console {"
+                , "    public static int Println(_ int value) { return value + 1; }"
+                , "}"
+                , "public class Program {"
+                , "    public static void Main() { int kept = Console.Println(41); }"
+                , "}"
+                ]
+
+program :: String -> String
+program body =
+    unlines
+        [ "namespace Demo;"
+        , "public class Program {"
+        , "    public static int Zero() { return 0; }"
+        , "    public static int Half(_ int v) { return v / 2; }"
+        , "    public static int Pick(_ int flag, _ int value) { if (flag > 0) { return value; } return 7; }"
+        , "    public static int Log(_ int v) { Console.Println(v); return v; }"
+        , "    public static int Twofold(_ int v) { return Log(v) + Log(v); }"
+        , "    public static String Name() { return \"Tala\"; }"
+        , "    public static String Twice(_ String s) { return s + s; }"
+        , "    public static void Greet(_ String who) { Console.Println(\"Hello, \" + who + \"!\"); }"
+        , "    public static String Fizz(_ int n) {"
+        , "        if (n % 15 == 0) { return \"FizzBuzz\"; }"
+        , "        if (n % 3 == 0) { return \"Fizz\"; }"
+        , "        if (n % 5 == 0) { return \"Buzz\"; }"
+        , "        return \"\" + n;"
+        , "    }"
+        , "    public static void Main() {"
+        , "        " ++ body
+        , "    }"
+        , "}"
+        ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "console.tala" text)
+
+-- | What @Main@ writes to standard output and to standard error.
+written :: (FrontendArtifacts -> CoreModule) -> String -> Maybe (String, String)
+written select body = case compileSource (program body) of
+    Right artifacts -> case runFunctionWriting 200000 (select artifacts) "Main" [] of
+        Just (_, Written output failure) -> Just (output, failure)
+        Nothing -> Nothing
+    Left _ -> Nothing
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code body = case compileSource (program body) of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+-- | Whether the Core of @Main@ mentions the given constructor or generated name.
+mentions :: String -> (FrontendArtifacts -> CoreModule) -> String -> Bool
+mentions needle select body = case compileSource (program body) of
+    Right artifacts ->
+        any
+            ((needle `isInfixOf`) . show . coreFunctionBody)
+            [ function
+            | function <- coreModuleFunctions (select artifacts)
+            , "Main" `isInfixOf` show (coreFunctionName function)
+            ]
+    Left _ -> False
+
+verifies :: String -> Bool
+verifies body = case compileSource (program body) of
+    Right artifacts ->
+        verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+            && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+            && verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+ test/CoreInliningTests.hs view
@@ -0,0 +1,372 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CoreInliningTests (coreInliningTests) where
+
+import Data.List (find)
+import Progmasoft.Tala.AST hiding (parameterName)
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Verifier (verifyCore)
+
+-- Core-level fixtures isolate substitution safety from frontend lowering. The
+-- integration suite separately proves that source calls reach this pass.
+coreInliningTests :: [(String, Bool)]
+coreInliningTests =
+    [ ("nullary literal return is inlined", nullaryLiteral)
+    , ("literal parameter is substituted and folded", literalParameter)
+    , ("variable parameter is substituted by SymbolId", variableParameter)
+    , ("multiple parameters retain their positions", multipleParameters)
+    , ("repeated literal parameters remain safe", repeatedLiteral)
+    , ("repeated variable reads remain safe", repeatedVariable)
+    , ("unused literal parameter may disappear", unusedLiteral)
+    , ("unused variable parameter may disappear", unusedVariable)
+    , ("primitive arguments are evaluated once behind a fresh let", primitiveArgumentPreserved)
+    , ("a nested pure call becomes a safe inline argument", nestedCallBecomesSafe)
+    , ("closure arguments are allocated once behind a fresh let", closureArgumentPreserved)
+    , ("allocating functions are not candidates", allocatingCalleeRejected)
+    , ("possibly failing functions are not candidates", failingCalleeRejected)
+    , ("indirectly calling functions are not candidates", indirectCalleeRejected)
+    , ("self recursion is never inlined", selfRecursionRejected)
+    , ("mutual recursion is never inlined", mutualRecursionRejected)
+    , ("candidate node budget is enforced", candidateBudget)
+    , ("expanded node budget is enforced", expansionBudget)
+    , ("zero budget normalizes to one node", zeroBudget)
+    , ("inlining can be disabled", disabledInlining)
+    , ("effect inference disablement disables inlining", disabledEffects)
+    , ("inlining precedes constant folding", inlineThenFold)
+    , ("pure call chains converge", pureCallChain)
+    , ("branch conditions are inlined", branchCondition)
+    , ("closure captures are inlined", closureCapture)
+    , ("closure bodies are inlined", closureBody)
+    , ("typed reports count candidates and rewrites", reportCounts)
+    , ("typed reports count budget skips", reportBudgetSkip)
+    , ("disabled pass emits no inline report", disabledReport)
+    , ("pass reports start with inlining", passOrder)
+    , ("function declaration order stays stable", stableFunctionOrder)
+    , ("optimized output verifies", outputVerifies)
+    , ("optimized output is idempotent", outputIdempotent)
+    ]
+
+moduleName :: QualifiedName
+moduleName = QualifiedName [Identifier "Inlining"]
+
+entryName, helperName, leafName, parameterName, otherName, callerParameterName :: ResolvedName
+entryName = named 1 "Entry"
+helperName = named 2 "Helper"
+leafName = named 3 "Leaf"
+parameterName = named 10 "value"
+otherName = named 11 "other"
+callerParameterName = named 12 "input"
+
+named :: Int -> String -> ResolvedName
+named value spelling = ResolvedName (SymbolId value) (Identifier spelling)
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+boolean :: Bool -> CoreExpression
+boolean value = CoreLiteral (CoreBoolean value) boolType
+
+variable :: ResolvedName -> Type -> CoreExpression
+variable = CoreVariable
+
+primitive :: CorePrimitive -> [CoreExpression] -> Type -> CoreExpression
+primitive = CorePrimitive
+
+call :: ResolvedName -> [Type] -> Type -> [CoreExpression] -> CoreExpression
+call name parameterTypes resultType arguments =
+    CoreApply (variable name (FunctionType parameterTypes resultType)) arguments resultType
+
+closure :: [CoreCapture] -> Type -> [CoreStatement] -> CoreExpression
+closure captures resultType body = CoreClosure captures [] resultType body (FunctionType [] resultType)
+
+returning :: ResolvedName -> [(ResolvedName, Type)] -> Type -> CoreExpression -> CoreFunction
+returning name parameters resultType value = CoreFunction name parameters resultType [CoreReturn value]
+
+entry :: [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreFunction
+entry = CoreFunction entryName
+
+moduleWith :: [CoreFunction] -> CoreModule
+moduleWith = CoreModule moduleName
+
+run :: CoreModule -> Maybe OptimizationResult
+run value = either (const Nothing) Just (optimizeCoreWith defaultOptimizerOptions value)
+
+runWith :: OptimizerOptions -> CoreModule -> Maybe OptimizationResult
+runWith options value = either (const Nothing) Just (optimizeCoreWith options value)
+
+functionBody :: ResolvedName -> OptimizationResult -> Maybe [CoreStatement]
+functionBody name result =
+    coreFunctionBody
+        <$> find ((== resolvedSymbol name) . resolvedSymbol . coreFunctionName) (coreModuleFunctions (optimizedCore result))
+
+returnValue :: OptimizationResult -> Maybe CoreExpression
+returnValue result = do
+    body <- functionBody entryName result
+    case body of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+optimizedReturn :: CoreModule -> Maybe CoreExpression
+optimizedReturn value = run value >>= returnValue
+
+literalCall :: CoreExpression
+literalCall = call helperName [] intType []
+
+literalFixture :: CoreModule
+literalFixture = moduleWith [entry [] intType [CoreReturn literalCall], returning helperName [] intType (integer 42)]
+
+identityFixture :: CoreExpression -> [(ResolvedName, Type)] -> CoreModule
+identityFixture argument callerParameters =
+    moduleWith
+        [ entry callerParameters intType [CoreReturn invocation]
+        , returning helperName [(parameterName, intType)] intType (variable parameterName intType)
+        ]
+    where
+        invocation = call helperName [intType] intType [argument]
+
+nullaryLiteral :: Bool
+nullaryLiteral = optimizedReturn literalFixture == Just (integer 42)
+
+literalParameter :: Bool
+literalParameter = optimizedReturn (identityFixture (integer 42) []) == Just (integer 42)
+
+variableParameter :: Bool
+variableParameter =
+    let argument = variable callerParameterName intType
+     in optimizedReturn (identityFixture argument [(callerParameterName, intType)]) == Just argument
+
+multipleParameters :: Bool
+multipleParameters =
+    let result = primitive CoreSubtract [variable parameterName intType, variable otherName intType] intType
+        helper = returning helperName [(parameterName, intType), (otherName, intType)] intType result
+        invocation = call helperName [intType, intType] intType [integer 50, integer 8]
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn invocation], helper]) == Just (integer 42)
+
+repeatedLiteral :: Bool
+repeatedLiteral = repeatedArgument (integer 21) [] (integer 42)
+
+repeatedVariable :: Bool
+repeatedVariable =
+    let argument = variable callerParameterName intType
+        expected = primitive CoreAdd [argument, argument] intType
+     in repeatedArgument argument [(callerParameterName, intType)] expected
+
+repeatedArgument :: CoreExpression -> [(ResolvedName, Type)] -> CoreExpression -> Bool
+repeatedArgument argument callerParameters expected =
+    let result = primitive CoreAdd [variable parameterName intType, variable parameterName intType] intType
+        helper = returning helperName [(parameterName, intType)] intType result
+        invocation = call helperName [intType] intType [argument]
+     in optimizedReturn (moduleWith [entry callerParameters intType [CoreReturn invocation], helper]) == Just expected
+
+unusedLiteral :: Bool
+unusedLiteral = unusedArgument (integer 99) []
+
+unusedVariable :: Bool
+unusedVariable = unusedArgument (variable callerParameterName intType) [(callerParameterName, intType)]
+
+unusedArgument :: CoreExpression -> [(ResolvedName, Type)] -> Bool
+unusedArgument argument callerParameters =
+    let helper = returning helperName [(parameterName, intType)] intType (integer 42)
+        invocation = call helperName [intType] intType [argument]
+     in optimizedReturn (moduleWith [entry callerParameters intType [CoreReturn invocation], helper]) == Just (integer 42)
+
+primitiveArgumentPreserved :: Bool
+primitiveArgumentPreserved =
+    let argument = primitive CoreAdd [integer 20, integer 22] intType
+        options = defaultOptimizerOptions {optimizerConstantPropagation = False}
+     in case runWith options (identityFixture argument []) >>= returnValue of
+            Just (CoreLet fresh bindingType value (CoreVariable used resultType) letType) ->
+                bindingType == intType
+                    && value == argument
+                    && resolvedSymbol fresh == resolvedSymbol used
+                    && resultType == intType
+                    && letType == intType
+            _ -> False
+
+nestedCallBecomesSafe :: Bool
+nestedCallBecomesSafe =
+    let argument = call leafName [] intType []
+        invocation = call helperName [intType] intType [argument]
+        helper = returning helperName [(parameterName, intType)] intType (variable parameterName intType)
+        leaf = returning leafName [] intType (integer 42)
+        options = defaultOptimizerOptions {optimizerMaximumIterations = 1, optimizerConstantPropagation = False}
+     in (runWith options (moduleWith [entry [] intType [CoreReturn invocation], helper, leaf]) >>= returnValue)
+            == Just (integer 42)
+
+closureArgumentPreserved :: Bool
+closureArgumentPreserved =
+    let callableType = FunctionType [] intType
+        argument = closure [] intType [CoreReturn (integer 42)]
+        invocation = call helperName [callableType] callableType [argument]
+        helper = returning helperName [(parameterName, callableType)] callableType (variable parameterName callableType)
+        options =
+            defaultOptimizerOptions
+                { optimizerConstantPropagation = False
+                , optimizerDeadCodeElimination = False
+                }
+     in case runWith options (moduleWith [entry [] callableType [CoreReturn invocation], helper]) >>= returnValue of
+            Just (CoreLet fresh bindingType value (CoreVariable used resultType) letType) ->
+                bindingType == callableType
+                    && value == argument
+                    && resolvedSymbol fresh == resolvedSymbol used
+                    && resultType == callableType
+                    && letType == callableType
+            _ -> False
+
+allocatingCalleeRejected :: Bool
+allocatingCalleeRejected =
+    let callableType = FunctionType [] intType
+        helper = returning helperName [] callableType (closure [] intType [CoreReturn (integer 42)])
+        invocation = call helperName [] callableType []
+     in optimizedReturn (moduleWith [entry [] callableType [CoreReturn invocation], helper]) == Just invocation
+
+failingCalleeRejected :: Bool
+failingCalleeRejected =
+    let divisor = variable parameterName intType
+        result = primitive CoreDivide [integer 42, divisor] intType
+        helper = returning helperName [(parameterName, intType)] intType result
+        invocation = call helperName [intType] intType [integer 0]
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn invocation], helper]) == Just invocation
+
+indirectCalleeRejected :: Bool
+indirectCalleeRejected =
+    let callableType = FunctionType [] intType
+        indirectName = named 20 "indirect"
+        helper =
+            returning helperName [(indirectName, callableType)] intType (CoreApply (variable indirectName callableType) [] intType)
+        argument = closure [] intType [CoreReturn (integer 42)]
+        invocation = call helperName [callableType] intType [argument]
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn invocation], helper]) == Just invocation
+
+selfRecursionRejected :: Bool
+selfRecursionRejected =
+    let invocation = call helperName [] intType []
+        helper = returning helperName [] intType invocation
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn invocation], helper]) == Just invocation
+
+mutualRecursionRejected :: Bool
+mutualRecursionRejected =
+    let firstCall = call helperName [] intType []
+        secondCall = call leafName [] intType []
+        first = returning helperName [] intType secondCall
+        second = returning leafName [] intType firstCall
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn firstCall], first, second]) == Just firstCall
+
+candidateBudget :: Bool
+candidateBudget = budgetResult 2 complexResult == Just complexInvocation
+
+expansionBudget :: Bool
+expansionBudget = budgetResult 2 repeatedResult == Just repeatedInvocation
+
+zeroBudget :: Bool
+zeroBudget =
+    let options = defaultOptimizerOptions {optimizerMaximumInlineExpressionNodes = 0}
+     in (runWith options literalFixture >>= returnValue) == Just (integer 42)
+
+complexResult, complexInvocation, repeatedResult, repeatedInvocation :: CoreExpression
+complexResult = primitive CoreAdd [primitive CoreAdd [variable parameterName intType, integer 1] intType, integer 2] intType
+complexInvocation = call helperName [intType] intType [integer 39]
+repeatedResult = primitive CoreAdd [variable parameterName intType, variable parameterName intType] intType
+repeatedInvocation = call helperName [intType] intType [integer 21]
+
+budgetResult :: Int -> CoreExpression -> Maybe CoreExpression
+budgetResult budget result =
+    let invocation = if result == complexResult then complexInvocation else repeatedInvocation
+        helper = returning helperName [(parameterName, intType)] intType result
+        options = defaultOptimizerOptions {optimizerMaximumInlineExpressionNodes = budget, optimizerConstantPropagation = False}
+     in runWith options (moduleWith [entry [] intType [CoreReturn invocation], helper]) >>= returnValue
+
+disabledInlining :: Bool
+disabledInlining = optionRetains defaultOptimizerOptions {optimizerInlining = False}
+
+disabledEffects :: Bool
+disabledEffects = optionRetains defaultOptimizerOptions {optimizerInterproceduralEffects = False}
+
+optionRetains :: OptimizerOptions -> Bool
+optionRetains options = (runWith options literalFixture >>= returnValue) == Just literalCall
+
+inlineThenFold :: Bool
+inlineThenFold =
+    let result = primitive CoreAdd [variable parameterName intType, integer 1] intType
+        helper = returning helperName [(parameterName, intType)] intType result
+        invocation = call helperName [intType] intType [integer 41]
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn invocation], helper]) == Just (integer 42)
+
+pureCallChain :: Bool
+pureCallChain =
+    let helper = returning helperName [] intType (call leafName [] intType [])
+        leaf = returning leafName [] intType (integer 42)
+     in optimizedReturn (moduleWith [entry [] intType [CoreReturn literalCall], helper, leaf]) == Just (integer 42)
+
+branchCondition :: Bool
+branchCondition =
+    let invocation = call helperName [] boolType []
+        helper = returning helperName [] boolType (boolean True)
+        caller = entry [] intType [CoreIf invocation [CoreReturn (integer 42)] [CoreReturn (integer 0)]]
+     in optimizedReturn (moduleWith [caller, helper]) == Just (integer 42)
+
+closureCapture :: Bool
+closureCapture =
+    let captureName = named 30 "captured"
+        capture = CoreCapture StrongCapture captureName intType literalCall
+        value = closure [capture] intType [CoreReturn (variable captureName intType)]
+        helper = returning helperName [] intType (integer 42)
+     in case optimizedReturn (moduleWith [entry [] (expressionType value) [CoreReturn value], helper]) of
+            Just (CoreClosure [changed] _ _ _ _) -> coreCaptureValue changed == integer 42
+            _ -> False
+
+closureBody :: Bool
+closureBody =
+    let value = closure [] intType [CoreReturn literalCall]
+        helper = returning helperName [] intType (integer 42)
+     in case optimizedReturn (moduleWith [entry [] (expressionType value) [CoreReturn value], helper]) of
+            Just (CoreClosure _ _ _ [CoreReturn result] _) -> result == integer 42
+            _ -> False
+
+reportCounts :: Bool
+reportCounts = case run literalFixture of
+    Just result -> case optimizationInlineReports result of
+        report : _ -> inlineCandidateCount report == 2 && inlineRewrittenCalls report == 1
+        [] -> False
+    Nothing -> False
+
+reportBudgetSkip :: Bool
+reportBudgetSkip =
+    let helper = returning helperName [(parameterName, intType)] intType complexResult
+        options = defaultOptimizerOptions {optimizerMaximumInlineExpressionNodes = 2}
+     in case runWith options (moduleWith [entry [] intType [CoreReturn complexInvocation], helper]) of
+            Just result -> case optimizationInlineReports result of
+                report : _ -> inlineCandidateCount report == 2 && inlineSkippedOversized report == 1
+                [] -> False
+            Nothing -> False
+
+disabledReport :: Bool
+disabledReport =
+    let options = defaultOptimizerOptions {optimizerInlining = False}
+     in maybe False (null . optimizationInlineReports) (runWith options literalFixture)
+
+passOrder :: Bool
+passOrder = case run literalFixture of
+    Just result -> case optimizationPassReports result of
+        report : _ -> passReportPass report == InliningPass
+        [] -> False
+    Nothing -> False
+
+stableFunctionOrder :: Bool
+stableFunctionOrder =
+    let functions = coreModuleFunctions literalFixture
+     in case run literalFixture of
+            Just result -> map coreFunctionName (coreModuleFunctions (optimizedCore result)) == map coreFunctionName functions
+            Nothing -> False
+
+outputVerifies :: Bool
+outputVerifies = case run literalFixture of
+    Just result -> either (const False) (const True) (verifyCore (optimizedCore result))
+    Nothing -> False
+
+outputIdempotent :: Bool
+outputIdempotent = case run literalFixture of
+    Just first -> (optimizedCore <$> run (optimizedCore first)) == Just (optimizedCore first)
+    Nothing -> False
+ test/CoreInterpreter.hs view
@@ -0,0 +1,480 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | A small reference evaluator for scalar Core, used only by tests.
+
+It executes Core statements directly, without CorePrep, the optimizer or the
+native backend, so a test can state what a source program must compute and
+check that against the Core the Desugarer produced and, separately, against
+the Core the optimizer left. It is deliberately independent of both: it
+shares no lowering or rewriting code with the compiler.
+
+The subset is the one the tests need: integer and Boolean values, the
+arithmetic, comparison, bitwise and logical primitives, expression-local
+bindings, conditional expressions, direct calls of module functions,
+closures, callables that remember their result, strings, the runtime calls
+for text and the console, and all statement forms. What a program writes to
+the console is kept and returned with its result. Integers are unbounded; a test that depends on overflow
+belongs to the native execution tests instead. Anything outside the subset,
+and any run that exceeds its step budget, yields 'Nothing' rather than a
+guess.
+-}
+module CoreInterpreter
+    ( Value (..)
+    , Written (..)
+    , runFunction
+    , runFunctionWithBudget
+    , runFunctionWriting
+    ) where
+
+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
+
+-- | A runtime value of the supported subset.
+data Value
+    = IntegerValue Integer
+    | BooleanValue Bool
+    | UnitValue
+    | {- | A closure: the values its captures had when it was created, the
+      symbols of its parameters, and its body. A capture initializer is
+      evaluated once, where the closure is created.
+      -}
+      ClosureValue [(Int, Value)] [Int] [CoreStatement]
+    | {- | A callable that remembers its result: the cell that holds the
+      result once there is one, and the callable that computes it. Copies of
+      the value name the same cell, which is how they share the result.
+      -}
+      MemoValue Int Value
+    | -- | A string.
+      TextValue String
+    | {- | A floating-point number: whether it is negative zero, and its exact
+      value, which is that of the binary number its type holds.
+      -}
+      FloatingValue Bool Rational
+    deriving (Eq, Show)
+
+-- | What a run wrote to the console, in the order it was written.
+data Written = Written
+    { writtenOutput :: String
+    -- ^ Standard output.
+    , writtenError :: String
+    -- ^ Standard error.
+    }
+    deriving (Eq, Show)
+
+-- Local values keyed by symbol identity. Core symbols are unique within a
+-- function, so one flat table is a faithful model of its locals.
+type Locals = [(Int, Value)]
+
+{- | What a run carries from step to step besides its locals.
+
+The steps left bound every run, so a lowering error that turns a loop into
+an endless one fails the test instead of hanging the suite. The cells hold
+the results that callables have remembered; they belong to the run and not
+to a function, because a callable is passed between functions and must find
+its result wherever it is called.
+-}
+data Budget = Budget
+    { stepsLeft :: Int
+    , cells :: [(Int, Value)]
+    , nextCell :: Int
+    , writes :: [(Bool, String)]
+    -- ^ Every console write, latest first: whether it went to standard
+    -- error, and the text.
+    }
+
+-- | Whether no step is left.
+exhausted :: Budget -> Bool
+exhausted budget = stepsLeft budget <= 0
+
+-- | The budget after one step.
+step :: Budget -> Budget
+step budget = budget {stepsLeft = stepsLeft budget - 1}
+
+data Flow
+    = Proceed
+    | BreakLoop
+    | ContinueLoop
+    | ReturnValue Value
+
+-- | Run a function by name with the default step budget.
+runFunction :: CoreModule -> String -> [Value] -> Maybe Value
+runFunction = runFunctionWithBudget 200000
+
+{- | Run a function by name; 'Nothing' when it is missing, leaves the
+supported subset, or exhausts the budget.
+-}
+runFunctionWithBudget :: Int -> CoreModule -> String -> [Value] -> Maybe Value
+runFunctionWithBudget steps moduleValue name arguments = fst <$> runFunctionWriting steps moduleValue name arguments
+
+{- | Run a function by name and return what it wrote to the console with its
+result. Nothing is returned for a run that does not finish: what such a run
+wrote before it stopped is not a result a test may rely on here.
+-}
+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
+    let inOrder = reverse (writes budget)
+    Just
+        ( value
+        , Written
+            (concat [written | (False, written) <- inOrder])
+            (concat [written | (True, written) <- inOrder])
+        )
+
+functionSpelling :: CoreFunction -> String
+functionSpelling = identifierText . resolvedSpelling . coreFunctionName
+
+firstJust :: [value] -> Maybe value
+firstJust values = case values of
+    value : _ -> Just value
+    [] -> Nothing
+
+symbolOf :: ResolvedName -> Int
+symbolOf = symbolIdValue . resolvedSymbol
+
+call :: CoreModule -> Budget -> CoreFunction -> [Value] -> Maybe (Value, Budget)
+call moduleValue budget function arguments
+    | exhausted budget = Nothing
+    | length arguments /= length (coreFunctionParameters function) = Nothing
+    | otherwise = do
+        let locals = zip (map (symbolOf . fst) (coreFunctionParameters function)) arguments
+        (flow, _, remaining) <- executeAll moduleValue (step budget) locals (coreFunctionBody function)
+        case flow of
+            ReturnValue value -> Just (value, remaining)
+            Proceed -> Just (UnitValue, remaining)
+            _ -> Nothing
+
+executeAll :: CoreModule -> Budget -> Locals -> [CoreStatement] -> Maybe (Flow, Locals, Budget)
+executeAll _ budget locals [] = Just (Proceed, locals, budget)
+executeAll moduleValue budget locals (statement : remaining) = do
+    (flow, nextLocals, nextBudget) <- execute moduleValue budget locals statement
+    case flow of
+        Proceed -> executeAll moduleValue nextBudget nextLocals remaining
+        _ -> Just (flow, nextLocals, nextBudget)
+
+execute :: CoreModule -> Budget -> Locals -> CoreStatement -> Maybe (Flow, Locals, Budget)
+execute moduleValue budget locals statement
+    | exhausted budget = Nothing
+    | otherwise = case statement of
+        CoreBind binding -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals (coreBindingValue binding)
+            Just (Proceed, store (symbolOf (coreBindingName binding)) value afterLocals, afterBudget)
+        CoreAssign name expression -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
+            -- An assignment never introduces a local: a missing target is a
+            -- lowering error, not something to paper over.
+            _ <- lookup (symbolOf name) afterLocals
+            Just (Proceed, store (symbolOf name) value afterLocals, afterBudget)
+        CoreReturn expression -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
+            Just (ReturnValue value, afterLocals, afterBudget)
+        CoreEvaluate expression -> do
+            (_, afterLocals, afterBudget) <- evaluate moduleValue spent locals expression
+            Just (Proceed, afterLocals, afterBudget)
+        CoreIf condition whenTrue whenFalse -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue spent locals condition
+            selected <- truth value
+            executeAll moduleValue afterBudget afterLocals (if selected then whenTrue else whenFalse)
+        CoreWhile condition body -> loop moduleValue spent locals (Just condition) body [] True
+        CoreDoWhile body condition -> loop moduleValue spent locals (Just condition) body [] False
+        CoreFor condition body update -> loop moduleValue spent locals (Just condition) body update True
+        CoreBreak -> Just (BreakLoop, locals, spent)
+        CoreContinue -> Just (ContinueLoop, locals, spent)
+    where
+        spent = step budget
+
+{- | Run one structured loop.
+
+The condition is tested before the body when @testFirst@ holds and after it
+otherwise. @continue@ ends the body, runs the update clause and then tests
+the condition, in every loop form.
+-}
+loop ::
+    CoreModule ->
+    Budget ->
+    Locals ->
+    Maybe CoreExpression ->
+    [CoreStatement] ->
+    [CoreStatement] ->
+    Bool ->
+    Maybe (Flow, Locals, Budget)
+loop moduleValue budget locals condition body update testFirst
+    | exhausted budget = Nothing
+    | testFirst = do
+        (enter, afterLocals, afterBudget) <- test budget locals
+        if enter then iteration afterBudget afterLocals else Just (Proceed, afterLocals, afterBudget)
+    | otherwise = iteration budget locals
+    where
+        test currentBudget currentLocals = case condition of
+            Nothing -> Just (True, currentLocals, currentBudget)
+            Just expression -> do
+                (value, afterLocals, afterBudget) <- evaluate moduleValue currentBudget currentLocals expression
+                selected <- truth value
+                Just (selected, afterLocals, afterBudget)
+        iteration currentBudget currentLocals = do
+            (flow, afterBody, bodyBudget) <- executeAll moduleValue (step currentBudget) currentLocals body
+            case flow of
+                BreakLoop -> Just (Proceed, afterBody, bodyBudget)
+                ReturnValue _ -> Just (flow, afterBody, bodyBudget)
+                _ -> do
+                    (updateFlow, afterUpdate, updateBudget) <- executeAll moduleValue bodyBudget afterBody update
+                    case updateFlow of
+                        Proceed -> loop moduleValue updateBudget afterUpdate condition body update True
+                        -- A break in the update clause leaves the loop,
+                        -- and a return leaves the function.
+                        BreakLoop -> Just (Proceed, afterUpdate, updateBudget)
+                        ReturnValue _ -> Just (updateFlow, afterUpdate, updateBudget)
+                        -- The Core verifier rejects a continue placed
+                        -- directly in an update clause.
+                        ContinueLoop -> Nothing
+
+store :: Int -> Value -> Locals -> Locals
+store symbol value locals = (symbol, value) : filter ((/= symbol) . fst) locals
+
+truth :: Value -> Maybe Bool
+truth value = case value of
+    BooleanValue flag -> Just flag
+    IntegerValue number -> Just (number /= 0)
+    UnitValue -> Nothing
+    ClosureValue {} -> Nothing
+    MemoValue {} -> Nothing
+    TextValue {} -> Nothing
+    FloatingValue {} -> Nothing
+
+evaluate :: CoreModule -> Budget -> Locals -> CoreExpression -> Maybe (Value, Locals, Budget)
+evaluate moduleValue budget locals expression
+    | exhausted budget = Nothing
+    | otherwise = case expression of
+        CoreVariable name _ -> case lookup (symbolOf name) locals of
+            Just value -> Just (value, locals, budget)
+            -- A method named where a value is expected is a closure
+            -- without captures.
+            Nothing -> do
+                method <-
+                    firstJust
+                        [ candidate
+                        | candidate <- coreModuleFunctions moduleValue
+                        , symbolOf (coreFunctionName candidate) == symbolOf name
+                        ]
+                Just
+                    ( ClosureValue [] (map (symbolOf . fst) (coreFunctionParameters method)) (coreFunctionBody method)
+                    , locals
+                    , budget
+                    )
+        CoreLiteral literal valueType -> do
+            value <- literalValue literal valueType
+            Just (value, locals, budget)
+        CoreLet name _ bound body _ -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals bound
+            evaluate moduleValue afterBudget (store (symbolOf name) value afterLocals) body
+        CoreConditional condition whenTrue whenFalse _ -> do
+            (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals condition
+            selected <- truth value
+            evaluate moduleValue afterBudget afterLocals (if selected then whenTrue else whenFalse)
+        CorePrimitive CoreLogicalAnd [left, right] _ -> shortCircuit False left right
+        CorePrimitive CoreLogicalOr [left, right] _ -> shortCircuit True left right
+        -- Remembering takes a cell that no other callable has. Nothing is
+        -- computed here: the callable is not called.
+        CorePrimitive CoreMemoize [operand] _ -> do
+            (target, afterLocals, afterBudget) <- evaluate moduleValue budget locals operand
+            case target of
+                ClosureValue _ [] _ ->
+                    Just
+                        ( MemoValue (nextCell afterBudget) target
+                        , afterLocals
+                        , afterBudget {nextCell = nextCell afterBudget + 1}
+                        )
+                _ -> Nothing
+        -- A runtime call: the function its first operand names, applied
+        -- to the values of the operands after it.
+        CorePrimitive CoreRuntimeCall (CoreLiteral (CoreInteger identity) _ : operands) _ -> do
+            function <- runtimeFunctionOf identity
+            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals operands
+            (value, afterCall) <- applyRuntime function values afterBudget
+            Just (value, afterLocals, afterCall)
+        CorePrimitive primitive operands valueType -> do
+            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals operands
+            value <- applyPrimitive primitive valueType values
+            Just (value, afterLocals, afterBudget)
+        CoreApply (CoreVariable callee _) arguments _
+            | function : _ <-
+                [ candidate
+                | candidate <- coreModuleFunctions moduleValue
+                , symbolOf (coreFunctionName candidate) == symbolOf callee
+                ] -> do
+                (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals arguments
+                (value, callBudget) <- call moduleValue afterBudget function values
+                Just (value, afterLocals, callBudget)
+        -- The callee is evaluated before the arguments, like any operand.
+        CoreApply callee arguments _ -> do
+            (target, calleeLocals, calleeBudget) <- evaluate moduleValue budget locals callee
+            (values, afterLocals, afterBudget) <- evaluateMany moduleValue calleeBudget calleeLocals arguments
+            (value, callBudget) <- callClosure target values afterBudget
+            Just (value, afterLocals, callBudget)
+        CoreClosure captures parameters _ body _ -> do
+            (values, afterLocals, afterBudget) <- evaluateMany moduleValue budget locals (map coreCaptureValue captures)
+            Just
+                ( ClosureValue (zip (map (symbolOf . coreCaptureName) captures) values) (map (symbolOf . fst) parameters) body
+                , afterLocals
+                , afterBudget
+                )
+    where
+        -- A closure runs on its captures and its arguments alone: it sees
+        -- no local of the function that calls it.
+        callClosure target values currentBudget = case target of
+            ClosureValue captured parameters body
+                | not (exhausted currentBudget) && length parameters == length values -> do
+                    (flow, _, remaining) <-
+                        executeAll moduleValue (step currentBudget) (zip parameters values ++ captured) body
+                    case flow of
+                        ReturnValue value -> Just (value, remaining)
+                        Proceed -> Just (UnitValue, remaining)
+                        _ -> Nothing
+            -- The first call computes the result and keeps it in the cell;
+            -- every later call, of any copy, reads the cell. A call that
+            -- does not finish leaves the cell empty.
+            MemoValue cell computation
+                | not (exhausted currentBudget) && null values -> case lookup cell (cells currentBudget) of
+                    Just remembered -> Just (remembered, step currentBudget)
+                    Nothing -> do
+                        (value, remaining) <- callClosure computation [] (step currentBudget)
+                        Just (value, remaining {cells = (cell, value) : cells remaining})
+            _ -> Nothing
+        -- The right operand runs only when the left one does not decide.
+        shortCircuit decidingValue left right = do
+            (leftValue, afterLeft, leftBudget) <- evaluate moduleValue budget locals left
+            leftTruth <- truth leftValue
+            if leftTruth == decidingValue
+                then Just (BooleanValue decidingValue, afterLeft, leftBudget)
+                else do
+                    (rightValue, afterRight, rightBudget) <- evaluate moduleValue leftBudget afterLeft right
+                    rightTruth <- truth rightValue
+                    Just (BooleanValue rightTruth, afterRight, rightBudget)
+
+evaluateMany :: CoreModule -> Budget -> Locals -> [CoreExpression] -> Maybe ([Value], Locals, Budget)
+evaluateMany _ budget locals [] = Just ([], locals, budget)
+evaluateMany moduleValue budget locals (expression : remaining) = do
+    (value, afterLocals, afterBudget) <- evaluate moduleValue budget locals expression
+    (values, finalLocals, finalBudget) <- evaluateMany moduleValue afterBudget afterLocals remaining
+    Just (value : values, finalLocals, finalBudget)
+
+literalValue :: CoreLiteral -> Type -> Maybe Value
+literalValue literal valueType = case literal of
+    CoreInteger number
+        | valueType == boolType -> Just (BooleanValue (number /= 0))
+        | otherwise -> Just (IntegerValue number)
+    CoreBoolean flag -> Just (BooleanValue flag)
+    CoreUnit -> Just UnitValue
+    CoreFloating spelling -> floatingValue valueType spelling
+    CoreString value -> Just (TextValue value)
+    CoreNull -> Nothing
+
+{- | The value a floating-point literal has in its type: the decimal number
+that was written, rounded to the nearest number the type holds. Only the
+plain decimal spellings the tests use are read.
+-}
+floatingValue :: Type -> String -> Maybe Value
+floatingValue valueType spelling = do
+    written <- decimal (filter (/= '_') spelling)
+    case valueType of
+        NamedType (QualifiedName [Identifier "float"]) [] ->
+            Just (FloatingValue False (toRational (fromRational written :: Double)))
+        NamedType (QualifiedName [Identifier "lfloat"]) [] ->
+            Just (FloatingValue False (toRational (fromRational written :: Float)))
+        _ -> Nothing
+    where
+        decimal value =
+            let (whole, afterWhole) = span isDigit value
+                (fraction, afterFraction) = case afterWhole of
+                    '.' : rest -> span isDigit rest
+                    _ -> ("", afterWhole)
+                mantissa = read ('0' : whole ++ fraction) :: Integer
+                scaled = mantissa % (10 ^ length fraction)
+             in case afterFraction of
+                    [] | not (null whole) -> Just scaled
+                    'e' : exponentText -> scaleBy scaled exponentText
+                    'E' : exponentText -> scaleBy scaled exponentText
+                    _ -> Nothing
+        scaleBy scaled exponentText = case exponentText of
+            '-' : digits | all isDigit digits, not (null digits) -> Just (scaled / (10 ^ (read digits :: Integer)))
+            '+' : digits | all isDigit digits, not (null digits) -> Just (scaled * (10 ^ (read digits :: Integer)))
+            digits | all isDigit digits, not (null digits) -> Just (scaled * (10 ^ (read digits :: Integer)))
+            _ -> Nothing
+
+{- | Apply a function of the runtime. The text functions are those of
+"RuntimeText"; a console write adds to what the run has written.
+-}
+applyRuntime :: RuntimeFunction -> [Value] -> Budget -> Maybe (Value, Budget)
+applyRuntime function values budget = case (function, values) of
+    (TextConcat, [TextValue left, TextValue right]) -> text (left ++ right)
+    (TextEquals, [TextValue left, TextValue right]) -> Just (BooleanValue (left == right), budget)
+    (TextFromSigned, [IntegerValue value]) -> text (textOfSigned value)
+    (TextFromUnsigned, [IntegerValue value]) -> text (textOfSigned value)
+    (TextFromBool, [BooleanValue value]) -> text (textOfBool value)
+    (TextFromChar, [IntegerValue value]) -> text [chr (fromInteger value)]
+    (TextFormatSigned, [IntegerValue flags, IntegerValue width, IntegerValue precision, IntegerValue value]) ->
+        text (formatInteger (Conversion flags width precision) value)
+    (TextFormatUnsigned, [IntegerValue flags, IntegerValue width, IntegerValue precision, IntegerValue value]) ->
+        text (formatInteger (Conversion flags width precision) value)
+    (TextFormatFloating, [IntegerValue flags, IntegerValue width, IntegerValue precision, FloatingValue negativeZero value]) ->
+        text (formatFloating (Conversion flags width precision) negativeZero value)
+    (TextFormatString, [IntegerValue flags, IntegerValue width, IntegerValue precision, TextValue value]) ->
+        text (formatText (Conversion flags width precision) value)
+    (TextFormatChar, [IntegerValue flags, IntegerValue width, IntegerValue _, IntegerValue value]) ->
+        text (formatText (Conversion flags width absent) [chr (fromInteger value)])
+    (TextNewline, []) -> text lineTerminator
+    (ConsoleWrite, [TextValue value, IntegerValue target])
+        | target `elem` [consoleOutput, consoleOutputLine, consoleError, consoleErrorLine] ->
+            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})
+    _ -> Nothing
+    where
+        text value = Just (TextValue value, budget)
+
+applyPrimitive :: CorePrimitive -> Type -> [Value] -> Maybe Value
+applyPrimitive primitive valueType values = case (primitive, values) of
+    (CoreAdd, [IntegerValue left, IntegerValue right]) -> integer (left + right)
+    (CoreSubtract, [IntegerValue left, IntegerValue right]) -> integer (left - right)
+    (CoreMultiply, [IntegerValue left, IntegerValue right]) -> integer (left * right)
+    (CoreDivide, [IntegerValue left, IntegerValue right]) | right /= 0 -> integer (left `quot` right)
+    (CoreRemainder, [IntegerValue left, IntegerValue right]) | right /= 0 -> integer (left `rem` right)
+    (CoreNegate, [IntegerValue operand]) -> integer (negate operand)
+    (CoreShiftLeft, [IntegerValue left, IntegerValue right]) | inShiftRange right -> integer (left `shiftL` fromInteger right)
+    (CoreShiftRight, [IntegerValue left, IntegerValue right]) | inShiftRange right -> integer (left `shiftR` fromInteger right)
+    (CoreBitwiseAnd, [IntegerValue left, IntegerValue right]) -> integer (left .&. right)
+    (CoreBitwiseXor, [IntegerValue left, IntegerValue right]) -> integer (left `xor` right)
+    (CoreBitwiseOr, [IntegerValue left, IntegerValue right]) -> integer (left .|. right)
+    (CoreBitwiseNot, [IntegerValue operand]) -> integer (complement operand)
+    (CoreLessThan, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left < right))
+    (CoreLessEqual, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left <= right))
+    (CoreGreaterThan, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left > right))
+    (CoreGreaterEqual, [IntegerValue left, IntegerValue right]) -> Just (BooleanValue (left >= right))
+    -- On two strings the equality primitive compares the objects, which
+    -- this evaluator does not model: two strings with the same characters
+    -- may or may not be one object. Equal characters are the runtime
+    -- function's to decide, so a program that reaches here with two
+    -- strings was lowered wrongly, and it fails instead of getting the
+    -- answer the native code would not give.
+    (CoreEqual, [TextValue _, TextValue _]) -> Nothing
+    (CoreNotEqual, [TextValue _, TextValue _]) -> Nothing
+    (CoreEqual, [left, right]) -> Just (BooleanValue (left == right))
+    (CoreNotEqual, [left, right]) -> Just (BooleanValue (left /= right))
+    (CoreLogicalNot, [operand]) -> BooleanValue . not <$> truth operand
+    -- Remembering needs the cells of the run; 'evaluate' handles it.
+    (CoreMemoize, _) -> Nothing
+    _ -> Nothing
+    where
+        -- A primitive whose result type is bool yields a Boolean even when
+        -- its operands are numeric, as the logical forms of numbers do.
+        integer number
+            | valueType == boolType = Just (BooleanValue (number /= 0))
+            | otherwise = Just (IntegerValue number)
+        inShiftRange amount = amount >= 0 && amount < 64
+ test/CoreLinearInliningTests.hs view
@@ -0,0 +1,611 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CoreLinearInliningTests (coreLinearInliningTests) where
+
+import Data.List (find, nub)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep (prepareCore)
+import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Symbols
+import Progmasoft.Tala.Core.Verifier (verifyCore)
+
+-- These tests deliberately construct Core directly. Source tests prove that
+-- frontend lowering reaches Core; this suite owns the optimizer's identity,
+-- evaluation-order, budget, and CorePrep boundary contracts.
+coreLinearInliningTests :: [(String, Bool)]
+coreLinearInliningTests =
+    [ ("linear inliner accepts one immutable local", immutableLocal)
+    , ("linear inliner accepts several immutable locals", severalLocals)
+    , ("linear inliner preserves local dependency order", localDependencyOrder)
+    , ("linear inliner substitutes a literal parameter", literalParameter)
+    , ("linear inliner substitutes a variable parameter", variableParameter)
+    , ("linear inliner binds a primitive argument once", primitiveArgumentOnce)
+    , ("linear inliner binds a direct-call argument once", callArgumentOnce)
+    , ("linear inliner preserves a failing argument", failingArgumentOnce)
+    , ("linear inliner preserves an allocating argument", allocatingArgumentOnce)
+    , ("linear inliner retains left-to-right argument order", argumentOrder)
+    , ("linear inliner retains an unused non-trivial argument", unusedArgument)
+    , ("linear inliner can discard an unused literal", unusedLiteral)
+    , ("linear inliner converts pure evaluation statements", evaluationStep)
+    , ("linear inliner keeps multiple evaluations ordered", evaluationOrder)
+    , ("linear inliner rejects mutable local state", mutableBodyRejected)
+    , ("linear inliner rejects assignment", assignmentBodyRejected)
+    , ("linear inliner rejects branches", branchBodyRejected)
+    , ("linear inliner rejects early returns", earlyReturnRejected)
+    , ("linear inliner rejects missing returns", missingReturnRejectedBeforeOptimization)
+    , ("linear inliner starts fresh ids above the entire module", freshFrontier)
+    , ("separate call sites receive disjoint local ids", disjointCallSites)
+    , ("copied expression lets receive fresh ids", nestedLetFreshened)
+    , ("statement candidate accounting is explicit", statementCandidateReport)
+    , ("expression candidate accounting remains explicit", expressionCandidateReport)
+    , ("non-linear pure bodies are counted", rejectedBodyReport)
+    , ("generated let accounting is explicit", generatedLetReport)
+    , ("alpha-renaming accounting is explicit", alphaRenameReport)
+    , ("expanded body obeys the node budget", expandedBudget)
+    , ("candidate estimate obeys the node budget", candidateBudget)
+    , ("linear inlining output verifies as Core", outputVerifies)
+    , ("linear inlining output prepares successfully", outputPrepares)
+    , ("prepared linear-inlining output verifies", preparedOutputVerifies)
+    , ("linear inlining is deterministic", deterministicOutput)
+    , ("linear inlining reaches an idempotent fixed point", idempotentOutput)
+    , ("symbol inventory includes expression lets", inventoryIncludesLets)
+    , ("symbol inventory includes closure-owned identities", inventoryIncludesClosure)
+    , ("symbol inventory preserves deterministic structural order", inventoryOrder)
+    ]
+
+moduleName :: QualifiedName
+moduleName = QualifiedName [Identifier "LinearInlining"]
+
+entryName, helperName, leafName :: ResolvedName
+entryName = named 1 "Entry"
+helperName = named 2 "Helper"
+leafName = named 3 "Leaf"
+
+parameterA, parameterB, localA, localB, callerValue :: ResolvedName
+parameterA = named 10 "a"
+parameterB = named 11 "b"
+localA = named 20 "first"
+localB = named 21 "second"
+callerValue = named 30 "input"
+
+named :: Int -> String -> ResolvedName
+named value spelling = ResolvedName (SymbolId value) (Identifier spelling)
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+boolean :: Bool -> CoreExpression
+boolean value = CoreLiteral (CoreBoolean value) boolType
+
+variable :: ResolvedName -> Type -> CoreExpression
+variable = CoreVariable
+
+primitive :: CorePrimitive -> [CoreExpression] -> Type -> CoreExpression
+primitive = CorePrimitive
+
+call :: ResolvedName -> [Type] -> Type -> [CoreExpression] -> CoreExpression
+call name parameterTypes resultType arguments =
+    CoreApply (variable name (FunctionType parameterTypes resultType)) arguments resultType
+
+binding :: ResolvedName -> Type -> Bool -> CoreExpression -> CoreStatement
+binding name valueType mutable value = CoreBind (CoreBinding name valueType mutable value)
+
+function :: ResolvedName -> [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreFunction
+function = CoreFunction
+
+entry :: Type -> CoreExpression -> CoreFunction
+entry resultType value = function entryName [] resultType [CoreReturn value]
+
+moduleWith :: [CoreFunction] -> CoreModule
+moduleWith = CoreModule moduleName
+
+invocation :: [Type] -> Type -> [CoreExpression] -> CoreExpression
+invocation = call helperName
+
+inspectionOptions :: OptimizerOptions
+inspectionOptions =
+    defaultOptimizerOptions
+        { optimizerMaximumIterations = 1
+        , optimizerConstantPropagation = False
+        , optimizerControlFlowSimplification = False
+        , optimizerDeadCodeElimination = False
+        }
+
+optimize :: CoreModule -> Maybe OptimizationResult
+optimize value = either (const Nothing) Just (optimizeCoreWith inspectionOptions value)
+
+optimizeWith :: OptimizerOptions -> CoreModule -> Maybe OptimizationResult
+optimizeWith options value = either (const Nothing) Just (optimizeCoreWith options value)
+
+entryReturn :: OptimizationResult -> Maybe CoreExpression
+entryReturn result = do
+    selected <-
+        find ((== resolvedSymbol entryName) . resolvedSymbol . coreFunctionName) (coreModuleFunctions (optimizedCore result))
+    case coreFunctionBody selected of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+optimizedEntry :: CoreModule -> Maybe CoreExpression
+optimizedEntry value = optimize value >>= entryReturn
+
+identityHelper :: CoreFunction
+identityHelper = function helperName [(parameterA, intType)] intType [CoreReturn (variable parameterA intType)]
+
+oneLocalHelper :: CoreFunction
+oneLocalHelper =
+    function
+        helperName
+        [(parameterA, intType)]
+        intType
+        [ binding localA intType False (primitive CoreAdd [variable parameterA intType, integer 1] intType)
+        , CoreReturn (variable localA intType)
+        ]
+
+twoLocalHelper :: CoreFunction
+twoLocalHelper =
+    function
+        helperName
+        [(parameterA, intType)]
+        intType
+        [ binding localA intType False (primitive CoreAdd [variable parameterA intType, integer 1] intType)
+        , binding localB intType False (primitive CoreMultiply [variable localA intType, integer 2] intType)
+        , CoreReturn (variable localB intType)
+        ]
+
+immutableLocal :: Bool
+immutableLocal = case optimizedEntry fixture of
+    Just (CoreLet fresh intBinding value (CoreVariable used intResult) letResult) ->
+        symbolIdValue (resolvedSymbol fresh) > maximumCoreSymbolValue fixture
+            && resolvedSymbol fresh == resolvedSymbol used
+            && intBinding == intType
+            && value == primitive CoreAdd [integer 41, integer 1] intType
+            && intResult == intType
+            && letResult == intType
+    _ -> False
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 41]), oneLocalHelper]
+
+severalLocals :: Bool
+severalLocals = (countLets <$> optimizedEntry fixture) == Just 2
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 20]), twoLocalHelper]
+
+localDependencyOrder :: Bool
+localDependencyOrder = case optimizedEntry fixture of
+    Just (CoreLet first _ _ (CoreLet second _ secondValue (CoreVariable returned _) _) _) ->
+        resolvedSymbol first /= resolvedSymbol second
+            && resolvedSymbol second == resolvedSymbol returned
+            && expressionUses (resolvedSymbol first) secondValue
+    _ -> False
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 20]), twoLocalHelper]
+
+literalParameter :: Bool
+literalParameter = optimizedEntry fixture == Just (integer 42)
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 42]), identityHelper]
+
+variableParameter :: Bool
+variableParameter = optimizedEntry fixture == Just (variable callerValue intType)
+    where
+        caller =
+            function
+                entryName
+                [(callerValue, intType)]
+                intType
+                [CoreReturn (invocation [intType] intType [variable callerValue intType])]
+        fixture = moduleWith [caller, identityHelper]
+
+primitiveArgumentOnce :: Bool
+primitiveArgumentOnce = nonTrivialIdentity argument
+    where
+        argument = primitive CoreAdd [integer 20, integer 22] intType
+
+callArgumentOnce :: Bool
+callArgumentOnce = case optimize fixture >>= entryReturn of
+    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
+        bindingType == intType
+            && value == argument
+            && resolvedSymbol fresh == resolvedSymbol used
+            && resultType == intType
+    _ -> False
+    where
+        callableName = named 31 "producer"
+        callableType = FunctionType [] intType
+        argument = CoreApply (variable callableName callableType) [] intType
+        caller = function entryName [(callableName, callableType)] intType [CoreReturn (invocation [intType] intType [argument])]
+        fixture = moduleWith [caller, identityHelper]
+
+failingArgumentOnce :: Bool
+failingArgumentOnce = nonTrivialIdentity argument
+    where
+        argument = primitive CoreDivide [integer 1, integer 0] intType
+
+allocatingArgumentOnce :: Bool
+allocatingArgumentOnce = case optimizedEntry fixture of
+    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
+        bindingType == callableType
+            && value == closureValue
+            && resolvedSymbol fresh == resolvedSymbol used
+            && resultType == callableType
+    _ -> False
+    where
+        callableType = FunctionType [] intType
+        closureValue = CoreClosure [] [] intType [CoreReturn (integer 42)] callableType
+        helper = function helperName [(parameterA, callableType)] callableType [CoreReturn (variable parameterA callableType)]
+        fixture = moduleWith [entry callableType (invocation [callableType] callableType [closureValue]), helper]
+
+nonTrivialIdentity :: CoreExpression -> Bool
+nonTrivialIdentity argument = case optimizedEntry fixture of
+    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
+        bindingType == intType
+            && value == argument
+            && resolvedSymbol fresh == resolvedSymbol used
+            && resultType == intType
+            && countExpression argument value == 1
+    _ -> False
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), identityHelper, leaf]
+        leaf = function leafName [] intType [CoreReturn (integer 42)]
+
+argumentOrder :: Bool
+argumentOrder = case optimizedEntry fixture of
+    Just (CoreLet first _ firstValue (CoreLet second _ secondValue _ _) _) ->
+        firstValue == firstArgument
+            && secondValue == secondArgument
+            && resolvedSymbol first /= resolvedSymbol second
+    _ -> False
+    where
+        firstArgument = primitive CoreAdd [integer 1, integer 2] intType
+        secondArgument = primitive CoreMultiply [integer 3, integer 4] intType
+        result = primitive CoreSubtract [variable parameterA intType, variable parameterB intType] intType
+        helper = function helperName [(parameterA, intType), (parameterB, intType)] intType [CoreReturn result]
+        fixture = moduleWith [entry intType (invocation [intType, intType] intType [firstArgument, secondArgument]), helper]
+
+unusedArgument :: Bool
+unusedArgument = case optimizedEntry fixture of
+    Just (CoreLet _ _ value result _) -> value == argument && result == integer 42
+    _ -> False
+    where
+        argument = primitive CoreDivide [integer 1, integer 0] intType
+        helper = function helperName [(parameterA, intType)] intType [CoreReturn (integer 42)]
+        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]
+
+unusedLiteral :: Bool
+unusedLiteral = optimizedEntry fixture == Just (integer 42)
+    where
+        helper = function helperName [(parameterA, intType)] intType [CoreReturn (integer 42)]
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 99]), helper]
+
+evaluationStep :: Bool
+evaluationStep = case optimizedEntry fixture of
+    Just (CoreLet _ _ evaluated result _) -> evaluated == integer 1 && result == integer 42
+    _ -> False
+    where
+        helper = function helperName [] intType [CoreEvaluate (integer 1), CoreReturn (integer 42)]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+evaluationOrder :: Bool
+evaluationOrder = case optimizedEntry fixture of
+    Just (CoreLet _ _ first (CoreLet _ _ second result _) _) ->
+        first == integer 1 && second == integer 2 && result == integer 3
+    _ -> False
+    where
+        helper = function helperName [] intType [CoreEvaluate (integer 1), CoreEvaluate (integer 2), CoreReturn (integer 3)]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+mutableBodyRejected :: Bool
+mutableBodyRejected = optimizedEntry fixture == Just original
+    where
+        original = invocation [] intType []
+        helper = function helperName [] intType [binding localA intType True (integer 1), CoreReturn (variable localA intType)]
+        fixture = moduleWith [entry intType original, helper]
+
+assignmentBodyRejected :: Bool
+assignmentBodyRejected = optimizedEntry fixture == Just original
+    where
+        original = invocation [] intType []
+        helper =
+            function
+                helperName
+                []
+                intType
+                [binding localA intType True (integer 1), CoreAssign localA (integer 2), CoreReturn (variable localA intType)]
+        fixture = moduleWith [entry intType original, helper]
+
+branchBodyRejected :: Bool
+branchBodyRejected = optimizedEntry fixture == Just original
+    where
+        original = invocation [] intType []
+        helper = function helperName [] intType [CoreIf (boolean True) [CoreReturn (integer 1)] [CoreReturn (integer 2)]]
+        fixture = moduleWith [entry intType original, helper]
+
+earlyReturnRejected :: Bool
+earlyReturnRejected = optimizedEntry fixture == Just original
+    where
+        original = invocation [] intType []
+        helper = function helperName [] intType [CoreReturn (integer 1), CoreReturn (integer 2)]
+        fixture = moduleWith [entry intType original, helper]
+
+missingReturnRejectedBeforeOptimization :: Bool
+missingReturnRejectedBeforeOptimization = case optimize fixture of
+    Nothing -> True
+    Just _ -> False
+    where
+        helper = function helperName [] intType [CoreEvaluate (integer 1)]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+freshFrontier :: Bool
+freshFrontier = case optimizedEntry fixture of
+    Just value -> all ((> maximumCoreSymbolValue fixture) . symbolIdValue) (definedExpressionSymbols value)
+    Nothing -> False
+    where
+        highLocal = named 900 "high"
+        helper =
+            function helperName [] intType [binding highLocal intType False (integer 42), CoreReturn (variable highLocal intType)]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+disjointCallSites :: Bool
+disjointCallSites = case optimize fixture of
+    Just result -> case functionBody entryName result of
+        Just [CoreBind first, CoreReturn second] ->
+            let firstIds = definedExpressionSymbols (coreBindingValue first)
+                secondIds = definedExpressionSymbols second
+             in not (null firstIds) && not (null secondIds) && null [value | value <- firstIds, value `elem` secondIds]
+        _ -> False
+    Nothing -> False
+    where
+        firstResult = invocation [intType] intType [integer 1]
+        secondResult = invocation [intType] intType [integer 2]
+        caller = function entryName [] intType [binding callerValue intType False firstResult, CoreReturn secondResult]
+        fixture = moduleWith [caller, oneLocalHelper]
+
+nestedLetFreshened :: Bool
+nestedLetFreshened = case optimizedEntry fixture of
+    Just value -> all ((> maximumCoreSymbolValue fixture) . symbolIdValue) (definedExpressionSymbols value)
+    Nothing -> False
+    where
+        nestedName = named 700 "nested"
+        result = CoreLet nestedName intType (integer 42) (variable nestedName intType) intType
+        helper = function helperName [] intType [CoreReturn result]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+statementCandidateReport :: Bool
+statementCandidateReport = maybe False ((> 0) . sum . map inlineStatementCandidateCount . optimizationInlineReports) (optimize fixture)
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 41]), oneLocalHelper]
+
+expressionCandidateReport :: Bool
+expressionCandidateReport = maybe False ((> 0) . sum . map inlineExpressionCandidateCount . optimizationInlineReports) (optimize fixture)
+    where
+        fixture = moduleWith [entry intType (invocation [intType] intType [integer 42]), identityHelper]
+
+rejectedBodyReport :: Bool
+rejectedBodyReport = maybe False ((> 0) . sum . map inlineRejectedNonLinearBodies . optimizationInlineReports) (optimize fixture)
+    where
+        helper = function helperName [] intType [CoreIf (boolean True) [CoreReturn (integer 1)] [CoreReturn (integer 2)]]
+        fixture = moduleWith [entry intType (invocation [] intType []), helper]
+
+generatedLetReport :: Bool
+generatedLetReport = case optimize fixture of
+    Just result -> case optimizationInlineReports result of
+        report : _ ->
+            inlineGeneratedParameterLets report == 1
+                && inlineGeneratedLocalLets report == 1
+                && inlineGeneratedEvaluationLets report == 1
+        [] -> False
+    Nothing -> False
+    where
+        argument = primitive CoreAdd [integer 20, integer 21] intType
+        helper =
+            function
+                helperName
+                [(parameterA, intType)]
+                intType
+                [ CoreEvaluate (integer 0)
+                , binding localA intType False (variable parameterA intType)
+                , CoreReturn (variable localA intType)
+                ]
+        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]
+
+alphaRenameReport :: Bool
+alphaRenameReport = case optimize fixture of
+    Just result -> case optimizationInlineReports result of
+        report : _ -> inlineAlphaRenamedSymbols report == 3
+        [] -> False
+    Nothing -> False
+    where
+        argument = primitive CoreAdd [integer 20, integer 21] intType
+        helper =
+            function
+                helperName
+                [(parameterA, intType)]
+                intType
+                [ CoreEvaluate (integer 0)
+                , binding localA intType False (variable parameterA intType)
+                , CoreReturn (variable localA intType)
+                ]
+        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]
+
+expandedBudget :: Bool
+expandedBudget = case optimizeWith options fixture >>= entryReturn of
+    Just value -> value == original
+    Nothing -> False
+    where
+        argument = primitive CoreAdd [integer 20, integer 22] intType
+        original = invocation [intType] intType [argument]
+        fixture = moduleWith [entry intType original, identityHelper]
+        options = inspectionOptions {optimizerMaximumInlineExpressionNodes = 4}
+
+candidateBudget :: Bool
+candidateBudget = case optimizeWith options fixture >>= entryReturn of
+    Just value -> value == original
+    Nothing -> False
+    where
+        original = invocation [intType] intType [integer 20]
+        fixture = moduleWith [entry intType original, twoLocalHelper]
+        options = inspectionOptions {optimizerMaximumInlineExpressionNodes = 4}
+
+outputVerifies :: Bool
+outputVerifies = case optimize fixture of
+    Just result -> either (const False) (const True) (verifyCore (optimizedCore result))
+    Nothing -> False
+    where
+        fixture =
+            moduleWith
+                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]
+
+outputPrepares :: Bool
+outputPrepares = case optimize fixture of
+    Just result -> either (const False) (const True) (prepareCore (optimizedCore result))
+    Nothing -> False
+    where
+        fixture =
+            moduleWith
+                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]
+
+preparedOutputVerifies :: Bool
+preparedOutputVerifies = case optimize fixture of
+    Just result -> case prepareCore (optimizedCore result) of
+        Right prepared -> either (const False) (const True) (verifyCorePrep prepared)
+        Left _ -> False
+    Nothing -> False
+    where
+        fixture =
+            moduleWith
+                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]
+
+deterministicOutput :: Bool
+deterministicOutput = (optimizedCore <$> optimize fixture) == (optimizedCore <$> optimize fixture)
+    where
+        fixture =
+            moduleWith
+                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]
+
+idempotentOutput :: Bool
+idempotentOutput = case optimize fixture of
+    Just first -> (optimizedCore <$> optimize (optimizedCore first)) == Just (optimizedCore first)
+    Nothing -> False
+    where
+        fixture =
+            moduleWith
+                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]
+
+inventoryIncludesLets :: Bool
+inventoryIncludesLets = map resolvedSymbol (coreExpressionSymbols expression) == [SymbolId 80, SymbolId 80]
+    where
+        letName = named 80 "inside"
+        expression = CoreLet letName intType (integer 1) (variable letName intType) intType
+
+inventoryIncludesClosure :: Bool
+inventoryIncludesClosure = all (`elem` symbols) [SymbolId 81, SymbolId 82, SymbolId 83]
+    where
+        captureName = named 81 "capture"
+        parameterName = named 82 "parameter"
+        localName = named 83 "local"
+        capture = CoreCapture StrongCapture captureName intType (integer 1)
+        expression =
+            CoreClosure
+                [capture]
+                [(parameterName, intType)]
+                intType
+                [binding localName intType False (variable captureName intType), CoreReturn (variable parameterName intType)]
+                (FunctionType [intType] intType)
+        symbols = map resolvedSymbol (coreExpressionSymbols expression)
+
+inventoryOrder :: Bool
+inventoryOrder = map resolvedSymbol (coreModuleSymbols fixture) == expected
+    where
+        fixture =
+            moduleWith
+                [ function
+                    entryName
+                    [(parameterA, intType)]
+                    intType
+                    [binding localA intType False (variable parameterA intType), CoreReturn (variable localA intType)]
+                ]
+        expected = [SymbolId 1, SymbolId 10, SymbolId 20, SymbolId 10, SymbolId 20]
+
+functionBody :: ResolvedName -> OptimizationResult -> Maybe [CoreStatement]
+functionBody name result =
+    coreFunctionBody
+        <$> find ((== resolvedSymbol name) . resolvedSymbol . coreFunctionName) (coreModuleFunctions (optimizedCore result))
+
+countLets :: CoreExpression -> Int
+countLets expression = case expression of
+    CoreLet _ _ value body _ -> 1 + countLets value + countLets body
+    CoreApply callee arguments _ -> countLets callee + sum (map countLets arguments)
+    CorePrimitive _ arguments _ -> sum (map countLets arguments)
+    CoreClosure captures _ _ body _ -> sum (map (countLets . coreCaptureValue) captures) + sum (map countStatementLets body)
+    _ -> 0
+
+countStatementLets :: CoreStatement -> Int
+countStatementLets statement = case statement of
+    CoreBind value -> countLets (coreBindingValue value)
+    CoreAssign _ value -> countLets value
+    CoreReturn value -> countLets value
+    CoreEvaluate value -> countLets value
+    CoreIf condition yes no -> countLets condition + sum (map countStatementLets (yes ++ no))
+    CoreWhile condition body -> countLets condition + sum (map countStatementLets body)
+    CoreDoWhile body condition -> sum (map countStatementLets body) + countLets condition
+    CoreFor condition updates body ->
+        countLets condition + sum (map countStatementLets updates) + sum (map countStatementLets body)
+    CoreBreak -> 0
+    CoreContinue -> 0
+
+expressionUses :: SymbolId -> CoreExpression -> Bool
+expressionUses symbol = elem symbol . map resolvedSymbol . coreExpressionSymbols
+
+definedExpressionSymbols :: CoreExpression -> [SymbolId]
+definedExpressionSymbols expression = nub (go expression)
+    where
+        go value = case value of
+            CoreLet name _ bound body _ -> resolvedSymbol name : go bound ++ go body
+            CoreApply callee arguments _ -> go callee ++ concatMap go arguments
+            CorePrimitive _ arguments _ -> concatMap go arguments
+            CoreClosure captures parameters _ body _ ->
+                map (resolvedSymbol . coreCaptureName) captures
+                    ++ map (resolvedSymbol . fst) parameters
+                    ++ concatMap (go . coreCaptureValue) captures
+                    ++ concatMap statementDefinitions body
+            _ -> []
+        statementDefinitions statement = case statement of
+            CoreBind value -> resolvedSymbol (coreBindingName value) : go (coreBindingValue value)
+            CoreAssign _ value -> go value
+            CoreReturn value -> go value
+            CoreEvaluate value -> go value
+            CoreIf condition yes no -> go condition ++ concatMap statementDefinitions (yes ++ no)
+            CoreWhile condition body -> go condition ++ concatMap statementDefinitions body
+            CoreDoWhile body condition -> concatMap statementDefinitions body ++ go condition
+            CoreFor condition updates body ->
+                go condition ++ concatMap statementDefinitions updates ++ concatMap statementDefinitions body
+            CoreBreak -> []
+            CoreContinue -> []
+
+countExpression :: CoreExpression -> CoreExpression -> Int
+countExpression needle expression =
+    (if needle == expression then 1 else 0)
+        + case expression of
+            CoreApply callee arguments _ -> sum (map (countExpression needle) (callee : arguments))
+            CorePrimitive _ arguments _ -> sum (map (countExpression needle) arguments)
+            CoreLet _ _ value body _ -> countExpression needle value + countExpression needle body
+            CoreClosure captures _ _ body _ ->
+                sum (map (countExpression needle . coreCaptureValue) captures)
+                    + sum (map countStatement body)
+            _ -> 0
+    where
+        countStatement statement = case statement of
+            CoreBind value -> countExpression needle (coreBindingValue value)
+            CoreAssign _ value -> countExpression needle value
+            CoreReturn value -> countExpression needle value
+            CoreEvaluate value -> countExpression needle value
+            CoreIf condition yes no -> countExpression needle condition + sum (map countStatement (yes ++ no))
+            CoreWhile condition body -> countExpression needle condition + sum (map countStatement body)
+            CoreDoWhile body condition -> sum (map countStatement body) + countExpression needle condition
+            CoreFor condition updates body ->
+                countExpression needle condition + sum (map countStatement updates) + sum (map countStatement body)
+            CoreBreak -> 0
+            CoreContinue -> 0
+ test/CoreOptimizerSourceTests.hs view
@@ -0,0 +1,737 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CoreOptimizerSourceTests (coreOptimizerSourceTests) where
+
+import CoreInterpreter
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep (corePrepModuleFunctions)
+import Progmasoft.Tala.Core.CorePrep qualified as CorePrep
+import Progmasoft.Tala.Core.Scalar (coreIntegerTypeNames)
+
+-- These cases begin as Tala text. The constructor-level optimizer suite
+-- can isolate every rewrite, while this group proves that real desugared trees
+-- carry the identities and types those rewrites expect.
+coreOptimizerSourceTests :: [(String, Bool)]
+coreOptimizerSourceTests =
+    [ ("source constants fold through immutable locals", immutableSourceChain)
+    , ("source mutable assignments remain observable", mutableSourceValue)
+    , ("source true conditions select the live branch", sourceTrueBranch)
+    , ("source false conditions select the live branch", sourceFalseBranch)
+    , ("source numeric zero selects the false branch", sourceNumericFalse)
+    , ("source nonzero conditions select the true branch", sourceNumericTrue)
+    , ("source dead scalar locals disappear from optimized Core", sourceDeadLocal)
+    , ("source pure calls disappear with discarded results", sourceDiscardedCall)
+    , ("source code after return disappears", sourceUnreachable)
+    , ("source closure allocation survives a dead binding", sourceDeadClosure)
+    , ("source closure constants fold inside the body", sourceClosureFold)
+    , ("source optimizer output remains accepted by CorePrep", sourceReachesCorePrep)
+    , ("source nullary calls inline into returns", sourceInlineNullary)
+    , ("source parameters substitute and fold", sourceInlineParameter)
+    , ("source multi-parameter calls preserve position", sourceInlineMultiple)
+    , ("source pure call chains converge", sourceInlineChain)
+    , ("source predicates inline before branch selection", sourceInlinePredicate)
+    , ("floating rounded division folds to an int through the source pipeline", sourceFloatingRoundedDivision)
+    , ("unsigned source bitwise complement folds at its declared width", sourceUnsignedBitwiseComplement)
+    , ("unsigned source bitwise complement masks away the sign extension", sourceUnsignedBitwiseZeroComplement)
+    , ("source repeated parameters inline for literals", sourceInlineRepeated)
+    , ("source unused literal arguments may disappear", sourceInlineUnused)
+    , ("source immutable helper locals inline", sourceInlineLocal)
+    , ("source dependent helper locals inline", sourceInlineDependentLocals)
+    , ("source primitive arguments remain single evaluations", sourceInlinePrimitiveArgument)
+    , ("source failing unused arguments are never computed", sourceUnusedFailingArgument)
+    , ("source argument order survives generated lets", sourceInlineArgumentOrder)
+    , ("source mutable helper bodies retain calls", sourceRejectsMutableHelper)
+    , ("source branching helper bodies retain calls", sourceRejectsBranchHelper)
+    , ("source inlined locals receive fresh symbols", sourceInlineFreshSymbols)
+    , ("source inlined locals prepare without calls", sourceInlinePrepares)
+    , ("source separate call sites use separate symbols", sourceInlineDisjointSites)
+    , ("source guard lets a dead division disappear", sourceGuardedDeadDivision)
+    , ("source unguarded dead division remains observable", sourceUnguardedDeadDivision)
+    , ("source assignment replaces a branch range", sourceAssignmentReplacesGuard)
+    , ("source impossible integer branch is removed", sourceImpossibleIntegerBranch)
+    , ("source equality transfers range facts between locals", sourceVariableEqualityGuard)
+    , ("source ordering derives a positive divisor", sourceVariableOrderingGuard)
+    , ("source short-circuit false edge proves nonzero", sourceFalseDisjunctionGuard)
+    , ("source arithmetic keeps a bounded product nonzero", sourceProductRangeGuard)
+    , ("source arithmetic keeps a bounded difference nonzero", sourceDifferenceRangeGuard)
+    , ("source branch join retains common zero exclusion", sourceNonzeroBranchJoin)
+    , ("source branch join drops conflicting zero exclusion", sourceConflictingBranchJoin)
+    , ("source comparison boundaries match the generated integer oracle", sourceComparisonBoundaryMatrix)
+    ]
+
+compiled :: String -> Maybe FrontendArtifacts
+compiled body = case compileToCorePrep (CompilerInput "optimizer-source.tala" (source body)) of
+    Left _ -> Nothing
+    Right artifacts -> Just artifacts
+
+source :: String -> String
+source body =
+    unlines
+        [ "namespace OptimizerSource;"
+        , "class Program {"
+        , "  int Value() {"
+        , body
+        , "  }"
+        , "}"
+        ]
+
+singleBody :: FrontendArtifacts -> Maybe [CoreStatement]
+singleBody artifacts = case coreModuleFunctions (artifactOptimizedCore artifacts) of
+    [function] -> Just (coreFunctionBody function)
+    _ -> Nothing
+
+singleReturn :: FrontendArtifacts -> Maybe CoreExpression
+singleReturn artifacts = do
+    body <- singleBody artifacts
+    case body of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+immutableSourceChain :: Bool
+immutableSourceChain = case compiled "final int base = 40; final int answer = base + 2; return answer;" of
+    Just artifacts -> singleReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+mutableSourceValue :: Bool
+mutableSourceValue = case compiled "int value = 1; value = 2; return value;" of
+    Just artifacts -> case singleBody artifacts of
+        Just [CoreBind binding, CoreAssign target assigned, CoreReturn returned] ->
+            coreBindingMutable binding
+                && resolvedSymbol (coreBindingName binding) == resolvedSymbol target
+                && assigned == integer 2
+                && returned == CoreVariable target intType
+        _ -> False
+    Nothing -> False
+
+sourceTrueBranch :: Bool
+sourceTrueBranch = case compiled "if (true) { return 1; } else { return 2; }" of
+    Just artifacts -> singleReturn artifacts == Just (integer 1)
+    Nothing -> False
+
+sourceFalseBranch :: Bool
+sourceFalseBranch = case compiled "if (false) { return 1; } else { return 2; }" of
+    Just artifacts -> singleReturn artifacts == Just (integer 2)
+    Nothing -> False
+
+sourceNumericFalse :: Bool
+sourceNumericFalse = case compiled "if (0) { return 1; } else { return 2; }" of
+    Just artifacts -> singleReturn artifacts == Just (integer 2)
+    Nothing -> False
+
+sourceNumericTrue :: Bool
+sourceNumericTrue = case compiled "if (-1) { return 1; } else { return 2; }" of
+    Just artifacts -> singleReturn artifacts == Just (integer 1)
+    Nothing -> False
+
+sourceDeadLocal :: Bool
+sourceDeadLocal = case compiled "int unused = 42; return 1;" of
+    Just artifacts -> singleReturn artifacts == Just (integer 1)
+    Nothing -> False
+
+sourceDiscardedCall :: Bool
+sourceDiscardedCall = case compileToCorePrep (CompilerInput "optimizer-call.tala" callSource) of
+    Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
+        [_helper, caller] -> case coreFunctionBody caller of
+            [CoreReturn (CoreLiteral (CoreInteger 1) _)] -> True
+            _ -> False
+        _ -> False
+    Left _ -> False
+    where
+        callSource =
+            unlines
+                [ "namespace OptimizerSource;"
+                , "class Program {"
+                , "  int Helper() { return 42; }"
+                , "  int Value() { Helper(); return 1; }"
+                , "}"
+                ]
+
+sourceUnreachable :: Bool
+sourceUnreachable = case compiled "return 1; int unreachable = 2;" of
+    Just artifacts -> singleReturn artifacts == Just (integer 1)
+    Nothing -> False
+
+sourceDeadClosure :: Bool
+sourceDeadClosure = case compiled "auto callable = \\ -> 42; return 1;" of
+    Just artifacts -> case singleBody artifacts of
+        Just [CoreEvaluate CoreClosure {}, CoreReturn (CoreLiteral (CoreInteger 1) _)] -> True
+        _ -> False
+    Nothing -> False
+
+sourceClosureFold :: Bool
+sourceClosureFold = case compiled "auto callable = \\ -> 40 + 2; return 1;" of
+    Just artifacts -> case singleBody artifacts of
+        Just [CoreEvaluate (CoreClosure _ _ _ [CoreReturn value] _), CoreReturn _] -> value == integer 42
+        _ -> False
+    Nothing -> False
+
+sourceReachesCorePrep :: Bool
+sourceReachesCorePrep = case compiled "final int left = 20; final int right = 22; return left + right;" of
+    Just artifacts ->
+        singleReturn artifacts == Just (integer 42)
+            && not (null (corePrepModuleFunctions (artifactCorePrep artifacts)))
+    Nothing -> False
+
+compiledProgram :: [String] -> Maybe FrontendArtifacts
+compiledProgram members = case compileToCorePrep (CompilerInput "optimizer-inline.tala" text) of
+    Left _ -> Nothing
+    Right artifacts -> Just artifacts
+    where
+        text =
+            unlines
+                ( [ "namespace OptimizerSource;"
+                  , "class Program {"
+                  ]
+                    ++ map ("  " ++) members
+                    ++ ["}"]
+                )
+
+lastReturn :: FrontendArtifacts -> Maybe CoreExpression
+lastReturn artifacts = case reverse (coreModuleFunctions (artifactOptimizedCore artifacts)) of
+    function : _ -> case coreFunctionBody function of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+    [] -> Nothing
+
+sourceInlineNullary :: Bool
+sourceInlineNullary = case compiledProgram ["int Answer() { return 42; }", "int Value() { return Answer(); }"] of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+sourceInlineParameter :: Bool
+sourceInlineParameter = case compiledProgram ["int AddOne(_ int value) { return value + 1; }", "int Value() { return AddOne(41); }"] of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+sourceInlineMultiple :: Bool
+sourceInlineMultiple = case compiledProgram
+    ["int Difference(_ int left, _ int right) { return left - right; }", "int Value() { return Difference(50, 8); }"] of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+sourceInlineChain :: Bool
+sourceInlineChain = case compiledProgram members of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+    where
+        members =
+            [ "int Answer() { return 42; }"
+            , "int Forward() { return Answer(); }"
+            , "int Value() { return Forward(); }"
+            ]
+
+sourceInlinePredicate :: Bool
+sourceInlinePredicate = case compiledProgram members of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+    where
+        members =
+            [ "bool Enabled() { return true; }"
+            , "int Value() { if (Enabled()) { return 42; } else { return 0; } }"
+            ]
+
+sourceFloatingRoundedDivision :: Bool
+sourceFloatingRoundedDivision = case compiled "return 7.8 // 2.0;" of
+    Just artifacts -> singleReturn artifacts == Just (CoreLiteral (CoreInteger 4) intType)
+    Nothing -> False
+
+sourceUnsignedBitwiseComplement :: Bool
+sourceUnsignedBitwiseComplement = case compiledProgram ["ubyte Value() { return !0; }"] of
+    Just artifacts -> lastReturn artifacts == Just (CoreLiteral (CoreInteger 255) (namedType "ubyte"))
+    Nothing -> False
+
+sourceUnsignedBitwiseZeroComplement :: Bool
+sourceUnsignedBitwiseZeroComplement = case compiledProgram ["ubyte Value() { return !255; }"] of
+    Just artifacts -> lastReturn artifacts == Just (CoreLiteral (CoreInteger 0) (namedType "ubyte"))
+    Nothing -> False
+
+sourceInlineRepeated :: Bool
+sourceInlineRepeated = case compiledProgram ["int Twice(_ int value) { return value + value; }", "int Value() { return Twice(21); }"] of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+sourceInlineUnused :: Bool
+sourceInlineUnused = case compiledProgram ["int Answer(_ int ignored) { return 42; }", "int Value() { return Answer(99); }"] of
+    Just artifacts -> lastReturn artifacts == Just (integer 42)
+    Nothing -> False
+
+sourceInlineLocal :: Bool
+sourceInlineLocal = case compiledProgram members of
+    Just artifacts -> maybe False (\value -> not (containsCall value) && terminalResult value == integer 42) (lastReturn artifacts)
+    Nothing -> False
+    where
+        members =
+            [ "int AddOne(_ int value) { final int result = value + 1; return result; }"
+            , "int Value() { return AddOne(41); }"
+            ]
+
+sourceInlineDependentLocals :: Bool
+sourceInlineDependentLocals = case compiledProgram members of
+    Just artifacts -> maybe False (\value -> not (containsCall value) && terminalResult value == integer 42) (lastReturn artifacts)
+    Nothing -> False
+    where
+        members =
+            [ "int Transform(_ int value) { final int doubled = value * 2; final int result = doubled + 2; return result; }"
+            , "int Value() { return Transform(20); }"
+            ]
+
+sourceInlinePrimitiveArgument :: Bool
+sourceInlinePrimitiveArgument = case compiledProgram members of
+    Just artifacts -> case lastReturn artifacts of
+        Just (CoreLet fresh bindingType value body resultType) ->
+            bindingType == intType
+                && value == integer 42
+                && body == integer 42
+                && resultType == intType
+                && symbolIdValue (resolvedSymbol fresh) > 0
+        _ -> False
+    Nothing -> False
+    where
+        members =
+            [ "int Identity(_ int value) { return value; }"
+            , "int Value() { return Identity(20 + 22); }"
+            ]
+
+{- | An argument the method never needs is never computed. The call yields
+the method's result, in the Core the Desugarer produced and in the Core the
+optimizer left: no division is carried out on the way.
+-}
+sourceUnusedFailingArgument :: Bool
+sourceUnusedFailingArgument = case compiledProgram members of
+    Just artifacts ->
+        all
+            (\core -> runFunction core "Value" [] == Just (IntegerValue 42))
+            [artifactCore artifacts, artifactOptimizedCore artifacts]
+    Nothing -> False
+    where
+        members =
+            [ "int Answer(_ int ignored) { return 42; }"
+            , "int Value() { return Answer(1 / 0); }"
+            ]
+
+sourceInlineArgumentOrder :: Bool
+sourceInlineArgumentOrder = case compiledProgram members of
+    Just artifacts -> case lastReturn artifacts of
+        Just (CoreLet first _ firstValue (CoreLet second _ secondValue _ _) _) ->
+            firstValue == integer 3
+                && secondValue == integer 12
+                && resolvedSymbol first /= resolvedSymbol second
+        _ -> False
+    Nothing -> False
+    where
+        members =
+            [ "int Difference(_ int left, _ int right) { return left - right; }"
+            , "int Value() { return Difference(1 + 2, 3 * 4); }"
+            ]
+
+sourceRejectsMutableHelper :: Bool
+sourceRejectsMutableHelper = case compiledProgram members of
+    Just artifacts -> maybe False containsCall (lastReturn artifacts)
+    Nothing -> False
+    where
+        members =
+            [ "int Change(_ int value) { int result = value; result = result + 1; return result; }"
+            , "int Value() { return Change(41); }"
+            ]
+
+sourceRejectsBranchHelper :: Bool
+sourceRejectsBranchHelper = case compiledProgram members of
+    Just artifacts -> maybe False containsCall (lastReturn artifacts)
+    Nothing -> False
+    where
+        members =
+            [ "int Choose(_ int value) { if (value) { return 42; } else { return 0; } }"
+            , "int Value() { return Choose(1); }"
+            ]
+
+sourceInlineFreshSymbols :: Bool
+sourceInlineFreshSymbols = case compiledProgram members of
+    Just artifacts -> case lastReturn artifacts of
+        Just value ->
+            let sourceMaximum = maximumModuleSymbol (artifactCore artifacts)
+                generated = expressionLetSymbols value
+             in not (null generated) && all ((> sourceMaximum) . symbolIdValue) generated
+        Nothing -> False
+    Nothing -> False
+    where
+        members =
+            [ "int Transform(_ int value) { final int doubled = value * 2; return doubled + 2; }"
+            , "int Value() { return Transform(20 + 0); }"
+            ]
+
+sourceInlinePrepares :: Bool
+sourceInlinePrepares = case compiledProgram members of
+    Just artifacts -> all (not . prepFunctionCalls) (corePrepModuleFunctions (artifactCorePrep artifacts))
+    Nothing -> False
+    where
+        members =
+            [ "int Transform(_ int value) { final int doubled = value * 2; return doubled + 2; }"
+            , "int Value() { return Transform(20); }"
+            ]
+
+sourceInlineDisjointSites :: Bool
+sourceInlineDisjointSites = case compiledProgram members of
+    Just artifacts -> case reverse (coreModuleFunctions (artifactOptimizedCore artifacts)) of
+        caller : _ -> case coreFunctionBody caller of
+            [CoreBind first, CoreReturn second] ->
+                let firstSymbols = expressionLetSymbols (coreBindingValue first)
+                    secondSymbols = expressionLetSymbols second
+                 in not (null firstSymbols)
+                        && not (null secondSymbols)
+                        && null [symbol | symbol <- firstSymbols, symbol `elem` secondSymbols]
+            _ -> False
+        [] -> False
+    Nothing -> False
+    where
+        members =
+            [ "int Transform(_ int value) { final int doubled = value * 2; return doubled + 2; }"
+            , "int Value() { final int first = Transform(10); return Transform(first); }"
+            ]
+
+containsCall :: CoreExpression -> Bool
+containsCall expression = case expression of
+    CoreApply {} -> True
+    CorePrimitive _ arguments _ -> any containsCall arguments
+    CoreLet _ _ value body _ -> containsCall value || containsCall body
+    CoreClosure captures _ _ body _ ->
+        any (containsCall . coreCaptureValue) captures || any statementContainsCall body
+    _ -> False
+
+terminalResult :: CoreExpression -> CoreExpression
+terminalResult expression = case expression of
+    CoreLet _ _ _ body _ -> terminalResult body
+    _ -> expression
+
+statementContainsCall :: CoreStatement -> Bool
+statementContainsCall statement = case statement of
+    CoreBind value -> containsCall (coreBindingValue value)
+    CoreAssign _ value -> containsCall value
+    CoreReturn value -> containsCall value
+    CoreEvaluate value -> containsCall value
+    CoreIf condition yes no -> containsCall condition || any statementContainsCall (yes ++ no)
+    CoreWhile condition body -> containsCall condition || any statementContainsCall body
+    CoreDoWhile body condition -> any statementContainsCall body || containsCall condition
+    CoreFor condition updates body ->
+        containsCall condition || any statementContainsCall updates || any statementContainsCall body
+    CoreBreak -> False
+    CoreContinue -> False
+
+expressionLetSymbols :: CoreExpression -> [SymbolId]
+expressionLetSymbols expression = case expression of
+    CoreLet name _ value body _ -> resolvedSymbol name : expressionLetSymbols value ++ expressionLetSymbols body
+    CoreApply callee arguments _ -> concatMap expressionLetSymbols (callee : arguments)
+    CorePrimitive _ arguments _ -> concatMap expressionLetSymbols arguments
+    CoreClosure captures _ _ body _ ->
+        concatMap (expressionLetSymbols . coreCaptureValue) captures
+            ++ concatMap statementLetSymbols body
+    _ -> []
+
+statementLetSymbols :: CoreStatement -> [SymbolId]
+statementLetSymbols statement = case statement of
+    CoreBind value -> expressionLetSymbols (coreBindingValue value)
+    CoreAssign _ value -> expressionLetSymbols value
+    CoreReturn value -> expressionLetSymbols value
+    CoreEvaluate value -> expressionLetSymbols value
+    CoreIf condition yes no -> expressionLetSymbols condition ++ concatMap statementLetSymbols (yes ++ no)
+    CoreWhile condition body -> expressionLetSymbols condition ++ concatMap statementLetSymbols body
+    CoreDoWhile body condition -> concatMap statementLetSymbols body ++ expressionLetSymbols condition
+    CoreFor condition updates body ->
+        expressionLetSymbols condition ++ concatMap statementLetSymbols updates ++ concatMap statementLetSymbols body
+    CoreBreak -> []
+    CoreContinue -> []
+
+maximumModuleSymbol :: CoreModule -> Int
+maximumModuleSymbol moduleValue = maximum (0 : concatMap functionSymbols (coreModuleFunctions moduleValue))
+    where
+        functionSymbols value =
+            symbolIdValue (resolvedSymbol (coreFunctionName value))
+                : map (symbolIdValue . resolvedSymbol . fst) (coreFunctionParameters value)
+                ++ concatMap statementSymbols (coreFunctionBody value)
+        statementSymbols statement = case statement of
+            CoreBind value -> symbolIdValue (resolvedSymbol (coreBindingName value)) : expressionSymbols (coreBindingValue value)
+            CoreAssign name value -> symbolIdValue (resolvedSymbol name) : expressionSymbols value
+            CoreReturn value -> expressionSymbols value
+            CoreEvaluate value -> expressionSymbols value
+            CoreIf condition yes no -> expressionSymbols condition ++ concatMap statementSymbols (yes ++ no)
+            CoreWhile condition body -> expressionSymbols condition ++ concatMap statementSymbols body
+            CoreDoWhile body condition -> concatMap statementSymbols body ++ expressionSymbols condition
+            CoreFor condition updates body ->
+                expressionSymbols condition ++ concatMap statementSymbols updates ++ concatMap statementSymbols body
+            CoreBreak -> []
+            CoreContinue -> []
+        expressionSymbols expression = map (symbolIdValue . resolvedSymbol) (expressionNames expression)
+        expressionNames expression = case expression of
+            CoreVariable name _ -> [name]
+            CoreLiteral {} -> []
+            CoreApply callee arguments _ -> concatMap expressionNames (callee : arguments)
+            CorePrimitive _ arguments _ -> concatMap expressionNames arguments
+            CoreLet name _ value body _ -> name : expressionNames value ++ expressionNames body
+            CoreConditional condition whenTrue whenFalse _ -> concatMap expressionNames [condition, whenTrue, whenFalse]
+            CoreClosure captures parameters _ body _ ->
+                map coreCaptureName captures
+                    ++ map fst parameters
+                    ++ concatMap (expressionNames . coreCaptureValue) captures
+                    ++ concatMap statementNames body
+        statementNames statement = case statement of
+            CoreBind value -> coreBindingName value : expressionNames (coreBindingValue value)
+            CoreAssign name value -> name : expressionNames value
+            CoreReturn value -> expressionNames value
+            CoreEvaluate value -> expressionNames value
+            CoreIf condition yes no -> expressionNames condition ++ concatMap statementNames (yes ++ no)
+            CoreWhile condition body -> expressionNames condition ++ concatMap statementNames body
+            CoreDoWhile body condition -> concatMap statementNames body ++ expressionNames condition
+            CoreFor condition updates body ->
+                expressionNames condition ++ concatMap statementNames updates ++ concatMap statementNames body
+            CoreBreak -> []
+            CoreContinue -> []
+
+prepFunctionCalls :: CorePrep.CorePrepFunction -> Bool
+prepFunctionCalls value = any blockCalls (CorePrep.corePrepFunctionBlocks value)
+    where
+        blockCalls block = any instructionCalls (CorePrep.corePrepBlockInstructions block)
+        instructionCalls instruction = case instruction of
+            CorePrep.CorePrepBind _ _ _ (CorePrep.CorePrepCall _ _) -> True
+            CorePrep.CorePrepEvaluate (CorePrep.CorePrepCall _ _) -> True
+            _ -> False
+
+sourceGuardedDeadDivision :: Bool
+sourceGuardedDeadDivision =
+    case compiledProgram ["void Guarded(_ int divisor) { if (divisor \\= 0) { int result = 10 / divisor; if (result > 0) { } } }"] of
+        Just artifacts ->
+            case coreModuleFunctions (artifactOptimizedCore artifacts) of
+                [function] -> not (containsIntegerDivision (coreFunctionBody function))
+                _ -> False
+        Nothing -> False
+
+sourceUnguardedDeadDivision :: Bool
+sourceUnguardedDeadDivision =
+    case compiledProgram ["void Guarded(_ int divisor) { int result = 10 / divisor; if (result > 0) { } }"] of
+        Just artifacts ->
+            case coreModuleFunctions (artifactOptimizedCore artifacts) of
+                [function] -> containsIntegerDivision (coreFunctionBody function)
+                _ -> False
+        Nothing -> False
+
+sourceAssignmentReplacesGuard :: Bool
+sourceAssignmentReplacesGuard =
+    case compiledProgram
+        ["void Guarded(_ int input) { int divisor = input; if (divisor \\= 0) { divisor = 0; int result = 10 / divisor; if (result > 0) { } } }"] of
+        Just artifacts ->
+            case coreModuleFunctions (artifactOptimizedCore artifacts) of
+                [function] -> containsIntegerDivision (coreFunctionBody function)
+                _ -> False
+        Nothing -> False
+
+sourceImpossibleIntegerBranch :: Bool
+sourceImpossibleIntegerBranch =
+    case compiledProgram ["int Guarded(_ int value) { if (value > 4 && value < 5) { return 24 / value; } else { return 0; } }"] of
+        Just artifacts ->
+            case coreModuleFunctions (artifactOptimizedCore artifacts) of
+                [function] ->
+                    not (containsIntegerDivision (coreFunctionBody function))
+                        && any isZeroReturn (coreFunctionBody function)
+                _ -> False
+        Nothing -> False
+    where
+        isZeroReturn (CoreReturn (CoreLiteral (CoreInteger 0) _)) = True
+        isZeroReturn _ = False
+
+sourceVariableEqualityGuard :: Bool
+sourceVariableEqualityGuard =
+    sourceDivisionExpectation
+        False
+        ["void Guarded(_ int known, _ int divisor) { if (known \\= 0 && divisor == known) { int probed = 24 / divisor; if (probed > 0) { } } }"]
+
+sourceVariableOrderingGuard :: Bool
+sourceVariableOrderingGuard =
+    sourceDivisionExpectation
+        False
+        [ "void Guarded(_ int lowerBound, _ int divisor) { if (lowerBound >= 0 && divisor > lowerBound) { int probed = 24 / divisor; if (probed > 0) { } } }"
+        ]
+
+sourceFalseDisjunctionGuard :: Bool
+sourceFalseDisjunctionGuard =
+    sourceDivisionExpectation
+        False
+        [ "void Guarded(_ int divisor, _ int other) {"
+        , "  if (divisor == 0 || other == 0) { return; }"
+        , "  int probed = 24 / divisor; if (probed > 0) { }"
+        , "}"
+        ]
+
+sourceProductRangeGuard :: Bool
+sourceProductRangeGuard =
+    sourceDivisionExpectation
+        False
+        [ "void Guarded(_ int value) {"
+        , "  if (value > 0 && value < 5) {"
+        , "    int divisor = value * 2;"
+        , "    int probed = 24 / divisor; if (probed > 0) { }"
+        , "  }"
+        , "}"
+        ]
+
+sourceDifferenceRangeGuard :: Bool
+sourceDifferenceRangeGuard =
+    sourceDivisionExpectation
+        False
+        [ "void Guarded(_ int value) {"
+        , "  if (value >= 8) {"
+        , "    int divisor = value - 5;"
+        , "    int probed = 24 / divisor; if (probed > 0) { }"
+        , "  }"
+        , "}"
+        ]
+
+sourceNonzeroBranchJoin :: Bool
+sourceNonzeroBranchJoin =
+    sourceDivisionExpectation
+        False
+        [ "void Guarded(_ int input, _ bool choose) {"
+        , "  if (input \\= 0) {"
+        , "    int divisor = input;"
+        , "    if (choose) { divisor = 7; } else { divisor = -1; }"
+        , "    int probed = 24 / divisor; if (probed > 0) { }"
+        , "  }"
+        , "}"
+        ]
+
+sourceConflictingBranchJoin :: Bool
+sourceConflictingBranchJoin =
+    sourceDivisionExpectation
+        True
+        [ "void Guarded(_ int input, _ bool choose) {"
+        , "  if (input \\= 0) {"
+        , "    int divisor = input;"
+        , "    if (choose) { divisor = 7; } else { divisor = 0; }"
+        , "    int probed = 24 / divisor; if (probed > 0) { }"
+        , "  }"
+        , "}"
+        ]
+
+sourceDivisionExpectation :: Bool -> [String] -> Bool
+sourceDivisionExpectation expectedPresent members =
+    case compiledProgram members of
+        Just artifacts ->
+            case coreModuleFunctions (artifactOptimizedCore artifacts) of
+                [function] -> containsIntegerDivision (coreFunctionBody function) == expectedPresent
+                _ -> False
+        Nothing -> False
+
+-- This whole-program matrix checks parser, typing, lowering, fact refinement,
+-- and DCE together. Each generated method places a dead divide on one selected
+-- comparison edge; the oracle decides whether that edge excludes zero.
+sourceComparisonBoundaryMatrix :: Bool
+sourceComparisonBoundaryMatrix =
+    case compiledProgram (zipWith sourceComparisonCase [0 :: Int ..] comparisons) of
+        Just artifacts ->
+            let actual =
+                    [ containsIntegerDivision (coreFunctionBody function)
+                    | function <- coreModuleFunctions (artifactOptimizedCore artifacts)
+                    ]
+                expected =
+                    [ not (sourceComparisonProvesNonzero operator boundary truth reversed)
+                    | (operator, boundary, truth, reversed) <- comparisons
+                    ]
+             in actual == expected
+        Nothing -> False
+    where
+        operators = [CoreEqual, CoreNotEqual, CoreLessThan, CoreLessEqual, CoreGreaterThan, CoreGreaterEqual]
+        comparisons =
+            [ (operator, boundary, trueEdge, reversed)
+            | operator <- operators
+            , boundary <- [-3 .. 3]
+            , trueEdge <- [False, True]
+            , reversed <- [False, True]
+            ]
+
+sourceComparisonCase :: Int -> (CorePrimitive, Integer, Bool, Bool) -> String
+sourceComparisonCase index (operator, boundary, trueEdge, reversed) =
+    "void FlowCase"
+        ++ show index
+        ++ "(_ int value) { if ("
+        ++ leftOperand
+        ++ " "
+        ++ comparisonToken operator
+        ++ " "
+        ++ rightOperand
+        ++ ") {"
+        ++ trueBody
+        ++ "} else {"
+        ++ falseBody
+        ++ "} }"
+    where
+        (leftOperand, rightOperand) =
+            if reversed
+                then (show boundary, "value")
+                else ("value", show boundary)
+        selectedBody = " int probed = 24 / value; if (probed > 0) { } "
+        trueBody = if trueEdge then selectedBody else ""
+        falseBody = if trueEdge then "" else selectedBody
+
+comparisonToken :: CorePrimitive -> String
+comparisonToken operator = case operator of
+    CoreEqual -> "=="
+    CoreNotEqual -> "\\="
+    CoreLessThan -> "<"
+    CoreLessEqual -> "<="
+    CoreGreaterThan -> ">"
+    CoreGreaterEqual -> ">="
+    _ -> "?"
+
+sourceComparisonProvesNonzero :: CorePrimitive -> Integer -> Bool -> Bool -> Bool
+sourceComparisonProvesNonzero operator boundary trueEdge reversed = case normalized of
+    CoreEqual -> (trueEdge && boundary /= 0) || (not trueEdge && boundary == 0)
+    CoreNotEqual -> (trueEdge && boundary == 0) || (not trueEdge && boundary /= 0)
+    CoreLessThan -> (trueEdge && boundary <= 0) || (not trueEdge && boundary > 0)
+    CoreLessEqual -> (trueEdge && boundary < 0) || (not trueEdge && boundary >= 0)
+    CoreGreaterThan -> (trueEdge && boundary >= 0) || (not trueEdge && boundary < 0)
+    CoreGreaterEqual -> (trueEdge && boundary > 0) || (not trueEdge && boundary <= 0)
+    _ -> False
+    where
+        normalized = if reversed then reverseSourceComparison operator else operator
+
+reverseSourceComparison :: CorePrimitive -> CorePrimitive
+reverseSourceComparison operator = case operator of
+    CoreLessThan -> CoreGreaterThan
+    CoreLessEqual -> CoreGreaterEqual
+    CoreGreaterThan -> CoreLessThan
+    CoreGreaterEqual -> CoreLessEqual
+    _ -> operator
+
+containsIntegerDivision :: [CoreStatement] -> Bool
+containsIntegerDivision = any statementHasDivision
+
+statementHasDivision :: CoreStatement -> Bool
+statementHasDivision statement = case statement of
+    CoreBind binding -> expressionHasDivision (coreBindingValue binding)
+    CoreAssign _ expression -> expressionHasDivision expression
+    CoreReturn expression -> expressionHasDivision expression
+    CoreEvaluate expression -> expressionHasDivision expression
+    CoreIf condition whenTrue whenFalse ->
+        expressionHasDivision condition || containsIntegerDivision whenTrue || containsIntegerDivision whenFalse
+    CoreWhile condition body -> expressionHasDivision condition || containsIntegerDivision body
+    CoreDoWhile body condition -> containsIntegerDivision body || expressionHasDivision condition
+    CoreFor condition updates body ->
+        expressionHasDivision condition || containsIntegerDivision updates || containsIntegerDivision body
+    CoreBreak -> False
+    CoreContinue -> False
+
+expressionHasDivision :: CoreExpression -> Bool
+expressionHasDivision expression = case expression of
+    CoreVariable _ _ -> False
+    CoreLiteral _ _ -> False
+    CoreApply callee arguments _ -> any expressionHasDivision (callee : arguments)
+    CorePrimitive operator arguments valueType ->
+        (elem operator [CoreDivide, CoreFloorDivide, CoreRemainder] && elem valueType (map namedType coreIntegerTypeNames))
+            || any expressionHasDivision arguments
+    CoreLet _ _ value body _ -> expressionHasDivision value || expressionHasDivision body
+    CoreConditional condition whenTrue whenFalse _ -> any expressionHasDivision [condition, whenTrue, whenFalse]
+    CoreClosure captures _ _ body _ ->
+        any (expressionHasDivision . coreCaptureValue) captures || containsIntegerDivision body
+ test/CoreOptimizerTests.hs view
@@ -0,0 +1,1080 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CoreOptimizerTests (coreOptimizerTests) where
+
+import Data.Bits (complement, shiftL, shiftR, xor, (.&.), (.|.))
+import Data.List (nub)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Scalar
+import Progmasoft.Tala.Core.Verifier (verifyCore)
+
+-- This suite exercises the optimizer as a verified Core-to-Core boundary.
+-- Small constructor helpers keep each case focused on the rewrite contract
+-- rather than on incidental SymbolId and module boilerplate.
+coreOptimizerTests :: [(String, Bool)]
+coreOptimizerTests =
+    [ ("Core scalar facts classify every integer type", all isCoreIntegerType integerTypes)
+    , ("Core scalar facts classify every floating type", all isCoreFloatingType floatingTypes)
+    , ("Core scalar facts reject bool as numeric", not (isCoreNumericType boolType))
+    , ("Core scalar spelling rejects applied types", coreTypeSpelling appliedIntType == "")
+    , ("Core scalar width catalog covers signed and unsigned integers", integerWidthsMatchTypes)
+    , ("Core scalar ranges agree with every declared width", integerRangesMatchWidths)
+    ,
+        ( "Core scalar layout lookup rejects floating and applied types"
+        , coreIntegerBitWidth (namedType "float") == Nothing
+            && coreIntegerBitWidth appliedIntType == Nothing
+            && coreIntegerIsSigned (namedType "float") == Nothing
+        )
+    , ("byte accepts its lower boundary", integerFitsCoreType byteType (-128))
+    , ("byte accepts its upper boundary", integerFitsCoreType byteType 127)
+    , ("byte rejects lower overflow", not (integerFitsCoreType byteType (-129)))
+    , ("byte rejects upper overflow", not (integerFitsCoreType byteType 128))
+    , ("ubyte accepts zero", integerFitsCoreType ubyteType 0)
+    , ("ubyte accepts 255", integerFitsCoreType ubyteType 255)
+    , ("ubyte rejects negative values", not (integerFitsCoreType ubyteType (-1)))
+    , ("ubyte rejects 256", not (integerFitsCoreType ubyteType 256))
+    , ("int accepts signed 64-bit minimum", integerFitsCoreType intType (negate (2 ^ (63 :: Int))))
+    , ("int accepts signed 64-bit maximum", integerFitsCoreType intType (2 ^ (63 :: Int) - 1))
+    , ("uint accepts unsigned 64-bit maximum", integerFitsCoreType uintType (2 ^ (64 :: Int) - 1))
+    , ("uint rejects unsigned 64-bit overflow", not (integerFitsCoreType uintType (2 ^ (64 :: Int))))
+    , ("longint accepts signed 128-bit minimum", integerFitsCoreType longintType (negate (2 ^ (127 :: Int))))
+    , ("ulongint accepts unsigned 128-bit maximum", integerFitsCoreType ulongintType (2 ^ (128 :: Int) - 1))
+    , ("floating spelling accepts an integer mantissa", validCoreFloatingSpelling "42")
+    , ("floating spelling accepts a fractional mantissa", validCoreFloatingSpelling "42.125")
+    , ("floating spelling accepts a leading point", validCoreFloatingSpelling ".5")
+    , ("floating spelling accepts a trailing point", validCoreFloatingSpelling "5.")
+    , ("floating spelling accepts a signed exponent", validCoreFloatingSpelling "-1.5e+20")
+    , ("floating spelling accepts infinity", validCoreFloatingSpelling "+inf")
+    , ("floating spelling accepts NaN", validCoreFloatingSpelling "-nan")
+    , ("floating spelling rejects an empty value", not (validCoreFloatingSpelling ""))
+    , ("floating spelling rejects a bare sign", not (validCoreFloatingSpelling "-"))
+    , ("floating spelling rejects a bare point", not (validCoreFloatingSpelling "."))
+    , ("floating spelling rejects a missing exponent", not (validCoreFloatingSpelling "1e"))
+    , ("floating spelling rejects repeated exponents", not (validCoreFloatingSpelling "1e2e3"))
+    , ("floating spelling rejects Unicode decimal digits", not (validCoreFloatingSpelling "١.0"))
+    , ("constant propagation folds an immutable chain", immutableChain)
+    , ("constant propagation does not cross a mutable assignment", mutableInvalidation)
+    , ("integer addition folds", primitiveFolds CoreAdd 20 22 42)
+    , ("integer subtraction folds", primitiveFolds CoreSubtract 20 22 (-2))
+    , ("integer multiplication folds", primitiveFolds CoreMultiply 6 7 42)
+    , ("integer power folds a small nonnegative exponent", typedPrimitiveFolds CorePower intType 3 4 81)
+    , ("power and shifts respect every Core integer width at both edges", integerOperationBoundaryMatrix)
+    , ("integer arithmetic overflow stays explicit for every Core scalar", integerArithmeticOverflowMatrix)
+    , integerOperatorOracleMatrix
+    , ("integer unary folds preserve boundaries for every Core scalar", integerUnaryBoundaryMatrix)
+    , ("integer power folds zero exponent", typedPrimitiveFolds CorePower intType 9 0 1)
+    ,
+        ( "zero raised to a large exponent folds without constructing a power"
+        , typedPrimitiveFolds CorePower intType 0 (2 ^ (63 :: Int) - 1) 0
+        )
+    ,
+        ( "one raised to a maximum-width exponent folds directly"
+        , typedPrimitiveFolds CorePower intType 1 (2 ^ (63 :: Int) - 1) 1
+        )
+    ,
+        ( "negative one power uses exponent parity without large intermediates"
+        , typedPrimitiveFolds CorePower intType (-1) (2 ^ (63 :: Int) - 1) (-1)
+        )
+    ,
+        ( "signed 128-bit minimum is accepted from a bounded power"
+        , typedPrimitiveFolds CorePower longintType (-2) 127 (negate (2 ^ (127 :: Int)))
+        )
+    ,
+        ( "unsigned 128-bit high bit is accepted from a bounded power"
+        , typedPrimitiveFolds CorePower ulongintType 2 127 (2 ^ (127 :: Int))
+        )
+    , ("unsigned 128-bit overflow remains explicit", overflowingPower ulongintType 2 128)
+    , ("untrusted huge exponents remain explicit without materializing the result", hugePowerRemainsExplicit)
+    , ("left shift folds within the integer width", typedPrimitiveFolds CoreShiftLeft intType 3 4 48)
+    , ("right shift preserves sign for a negative integer", typedPrimitiveFolds CoreShiftRight intType (-16) 2 (-4))
+    , ("shift equal to the type width remains explicit", overflowingShift intType CoreShiftLeft 1 64)
+    , ("128-bit shift count cannot wrap through the host Int width", hugeShiftRemainsExplicit)
+    , ("integer division truncates toward zero", primitiveFolds CoreDivide (-7) 3 (-2))
+    , ("integer rounded division chooses nearest", primitiveFolds CoreFloorDivide (-7) 3 (-2))
+    , ("integer rounded division moves halves away from zero", primitiveFolds CoreFloorDivide 7 2 4)
+    , ("integer remainder follows truncating division", primitiveFolds CoreRemainder (-7) 3 (-1))
+    , ("division by zero remains explicit", divisionByZeroRetained CoreDivide)
+    , ("floor division by zero remains explicit", divisionByZeroRetained CoreFloorDivide)
+    , ("remainder by zero remains explicit", divisionByZeroRetained CoreRemainder)
+    , ("integer negation folds", unaryFolds CoreNegate 42 (integer (-42)))
+    , ("bitwise complement matches each integer type's finite width", bitwiseComplementBoundaries)
+    , ("logical not folds boolean values", unaryFolds CoreLogicalNot 1 (boolean False))
+    , ("logical not treats numeric zero as false", unaryFolds CoreLogicalNot 0 (boolean True))
+    , ("less-than comparison folds", comparisonFolds CoreLessThan 1 2 True)
+    , ("less-equal comparison folds", comparisonFolds CoreLessEqual 2 2 True)
+    , ("greater-than comparison folds", comparisonFolds CoreGreaterThan 3 2 True)
+    , ("greater-equal comparison folds", comparisonFolds CoreGreaterEqual 2 2 True)
+    , ("equality comparison folds", comparisonFolds CoreEqual 2 2 True)
+    , ("inequality comparison folds", comparisonFolds CoreNotEqual 2 3 True)
+    , ("logical and folds", logicalFolds CoreLogicalAnd True False False)
+    , ("logical or folds", logicalFolds CoreLogicalOr False True True)
+    , ("overflowing byte addition is retained", overflowingByteAddition)
+    , ("addition by zero returns the live operand", identityRewrite CoreAdd 0)
+    , ("subtraction by zero returns the live operand", identityRewrite CoreSubtract 0)
+    , ("multiplication by one returns the live operand", identityRewrite CoreMultiply 1)
+    , ("division by one returns the live operand", identityRewrite CoreDivide 1)
+    , ("floor division by one returns the live operand", identityRewrite CoreFloorDivide 1)
+    , ("remainder by one becomes zero for a simple operand", remainderIdentity)
+    , ("double arithmetic negation collapses", doubleNegation)
+    , ("double boolean negation collapses", doubleLogicalNot)
+    , ("double logical negation preserves numeric-to-bool conversion", numericDoubleLogicalNot)
+    , ("a true branch selects its then statements", knownBranch True)
+    , ("a false branch selects its else statements", knownBranch False)
+    , ("numeric zero selects an else branch", numericBranch 0 2)
+    , ("nonzero numeric conditions select a then branch", numericBranch (-3) 1)
+    , ("identical pure branches discard their condition", identicalPureBranches)
+    , ("identical branches retain an effectful condition", identicalEffectfulBranches)
+    , ("empty branches retain an effectful condition", emptyEffectfulBranches)
+    , ("statements after return are unreachable", unreachableAfterReturn)
+    , ("a fully returning branch makes following code unreachable", unreachableAfterBranch)
+    , ("unused literal bindings are removed", deadLiteralBinding)
+    , ("unused primitive evaluations are removed", deadPrimitiveEvaluation)
+    , ("unused calls are retained as evaluations", deadCallResultPreserved)
+    , ("unused closure allocations are retained as evaluations", deadClosurePreserved)
+    , ("dead pure assignments are removed", deadAssignment)
+    , ("assignments feeding a return remain live", liveAssignment)
+    , ("an overwritten assignment is removed without losing its declaration", overwrittenAssignment)
+    , ("a branch assignment retains its declaration", branchAssignmentRetainsDeclaration)
+    , ("a dead branch assignment does not retain storage", deadBranchAssignment)
+    , ("effectful dead binding initializers become evaluations", effectfulBindingBecomesEvaluation)
+    , ("closure bodies are optimized as independent regions", closureBodyOptimized)
+    , ("literal captures propagate into closure bodies", closureCapturePropagates)
+    , ("closure parameters shadow capture constants", closureParameterShadowsCapture)
+    , ("disabling all passes preserves the verified module", disabledPipelineIsIdentity)
+    , ("disabling constant propagation retains arithmetic", constantPassCanBeDisabled)
+    , ("disabling control-flow simplification retains branches", controlFlowPassCanBeDisabled)
+    , ("disabling dead-code elimination retains dead bindings", deadCodePassCanBeDisabled)
+    , ("the default pipeline reaches a fixed point", reportsConvergence)
+    , ("a one-iteration limit reports an unfinished fixed point", reportsIterationLimit)
+    , ("optimizer metrics count functions", metricsCountFunctions)
+    , ("optimizer metrics report removed statements", metricsReportReduction)
+    , ("optimizer metrics count calls and closures", metricsCountEffects)
+    , ("pass reports preserve pipeline order", reportsPreservePassOrder)
+    , ("pass reports identify changing passes", reportsIdentifyChanges)
+    , ("pass reports include every completed iteration", reportsCoverIterations)
+    , ("disabled passes are absent from reports", reportsOmitDisabledPasses)
+    , ("pass report metrics form a continuous chain", reportMetricsAreContinuous)
+    , ("optimizer output is idempotent", optimizerIsIdempotent)
+    , ("optimizer output passes the Core verifier", optimizedOutputVerifies)
+    , ("optimizer rejects an invalid input module", invalidInputRejected)
+    ]
+
+name :: Int -> String -> ResolvedName
+name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+moduleName :: QualifiedName
+moduleName = QualifiedName [Identifier "Optimizer", Identifier "Tests"]
+
+entryName, calleeName, predicateName, valueName, secondName, capturedValueName, closureParameterName :: ResolvedName
+entryName = name 1 "Main"
+calleeName = name 2 "Effect"
+predicateName = name 3 "Predicate"
+valueName = name 10 "value"
+secondName = name 11 "second"
+capturedValueName = name 12 "captured"
+closureParameterName = name 13 "parameter"
+
+byteType, ubyteType, uintType, longintType, ulongintType :: Type
+byteType = namedType "byte"
+ubyteType = namedType "ubyte"
+uintType = namedType "uint"
+longintType = namedType "longint"
+ulongintType = namedType "ulongint"
+
+integerTypes, floatingTypes :: [Type]
+integerTypes = map namedType coreIntegerTypeNames
+floatingTypes = map namedType coreFloatingTypeNames
+
+integerWidthsMatchTypes :: Bool
+integerWidthsMatchTypes =
+    map coreIntegerBitWidth integerTypes
+        == map Just [32, 8, 16, 32, 64, 128, 8, 16, 32, 64, 128]
+
+integerRangesMatchWidths :: Bool
+integerRangesMatchWidths = all checkType integerTypes
+    where
+        checkType valueType = case (coreIntegerBitWidth valueType, coreIntegerIsSigned valueType) of
+            (Just width, Just isSigned) ->
+                let signBits = if isSigned then 1 else 0
+                    magnitude = 2 ^ (width - signBits)
+                    minimumValue = if isSigned then negate magnitude else 0
+                    maximumValue = magnitude - 1
+                 in integerFitsCoreType valueType minimumValue
+                        && integerFitsCoreType valueType maximumValue
+                        && not (integerFitsCoreType valueType (minimumValue - 1))
+                        && not (integerFitsCoreType valueType (maximumValue + 1))
+            _ -> False
+
+bitwiseComplementBoundaries :: Bool
+bitwiseComplementBoundaries = all checkType integerTypes
+    where
+        checkType valueType = case (coreIntegerBitWidth valueType, coreIntegerIsSigned valueType) of
+            (Just width, Just isSigned) ->
+                let signBits = if isSigned then 1 else 0
+                    magnitude = 2 ^ (width - signBits)
+                    minimumValue = if isSigned then negate magnitude else 0
+                    maximumValue = magnitude - 1
+                    zeroComplement = if isSigned then -1 else maximumValue
+                 in typedUnaryPrimitiveFolds CoreBitwiseNot valueType 0 zeroComplement
+                        && typedUnaryPrimitiveFolds CoreBitwiseNot valueType minimumValue maximumValue
+                        && typedUnaryPrimitiveFolds CoreBitwiseNot valueType maximumValue minimumValue
+            _ -> False
+
+integerOperationBoundaryMatrix :: Bool
+integerOperationBoundaryMatrix = all checkType integerTypes
+    where
+        checkType valueType = case (coreIntegerBitWidth valueType, coreIntegerIsSigned valueType) of
+            (Just width, Just isSigned) ->
+                let signedBits = if isSigned then 1 else 0
+                    minimumValue = if isSigned then negate (2 ^ (width - 1)) else 0
+                    maximumValue = 2 ^ (width - signedBits) - 1
+                    highestSafeShift = width - signedBits - 1
+                    safeShiftResult = 2 ^ highestSafeShift
+                    minimumPowerExponent = width - 1
+                    minimumPowerResult = negate (2 ^ minimumPowerExponent)
+                    rightShiftInput = if isSigned then minimumValue else maximumValue
+                    rightShiftExpected = if isSigned then -1 else 1
+                 in typedPrimitiveFolds CoreShiftLeft valueType 1 (toInteger highestSafeShift) safeShiftResult
+                        && typedPrimitiveFolds CoreShiftRight valueType rightShiftInput (toInteger (width - 1)) rightShiftExpected
+                        && ( not isSigned
+                                || typedPrimitiveFolds CorePower valueType (-2) (toInteger minimumPowerExponent) minimumPowerResult
+                           )
+                        && overflowingShift valueType CoreShiftLeft 1 (toInteger width)
+                        && overflowingPower valueType 2 (toInteger (width - signedBits))
+            _ -> False
+
+integerArithmeticOverflowMatrix :: Bool
+integerArithmeticOverflowMatrix = all checkType integerTypes
+    where
+        checkType valueType = case (coreIntegerBitWidth valueType, coreIntegerIsSigned valueType) of
+            (Just width, Just isSigned) ->
+                let highestValue = 2 ^ (width - if isSigned then 1 else 0) - 1
+                    overflowing = CorePrimitive CoreAdd [literal highestValue, literal 1] valueType
+                 in returnExpression (moduleWith [entry valueType [CoreReturn overflowing]]) == Just overflowing
+            _ -> False
+            where
+                literal value = CoreLiteral (CoreInteger value) valueType
+
+integerOperatorOracleMatrix :: (String, Bool)
+integerOperatorOracleMatrix = case optimizeCoreWith defaultOptimizerOptions input of
+    Left diagnostics -> ("integer Core oracle module verifies (" ++ show diagnostics ++ ")", False)
+    Right result ->
+        let actualExpressions =
+                [ expression
+                | function <- coreModuleFunctions (optimizedCore result)
+                , [CoreReturn expression] <- [coreFunctionBody function]
+                ]
+            mismatches =
+                [ (show (valueType, operation, left, right), expected, actual)
+                | ((valueType, operation, left, right, expected), actual) <- zip cases actualExpressions
+                , expected /= actual
+                ]
+         in ( "integer Core folding agrees with the width-aware operator oracle " ++ show (take 3 mismatches)
+            , null mismatches && length actualExpressions == length cases
+            )
+    where
+        cases =
+            [ (valueType, operation, left, right, expected)
+            | valueType <- integerTypes
+            , width <- maybeToList (coreIntegerBitWidth valueType)
+            , isSigned <- maybeToList (coreIntegerIsSigned valueType)
+            , let magnitude = 2 ^ (width - if isSigned then 1 else 0)
+            , let minimumValue = if isSigned then negate magnitude else 0
+            , let maximumValue = magnitude - 1
+            , left <- nub [minimumValue, minimumValue + 1, -1, 0, 1, maximumValue - 1, maximumValue]
+            , integerFitsCoreType valueType left
+            , right <- nub [minimumValue, minimumValue + 1, -1, 0, 1, maximumValue - 1, maximumValue]
+            , integerFitsCoreType valueType right
+            , operation <- integerOracleOperators
+            , let expression =
+                    CorePrimitive
+                        operation
+                        [typedLiteral valueType left, typedLiteral valueType right]
+                        (oracleResultType operation valueType)
+            , let expected =
+                    maybe
+                        expression
+                        (\literal -> CoreLiteral literal (oracleResultType operation valueType))
+                        (oracleFold valueType width operation left right)
+            ]
+        input =
+            CoreModule
+                moduleName
+                [ CoreFunction
+                    (name (1000 + index) "IntegerOracle")
+                    []
+                    (oracleResultType operation valueType)
+                    [ CoreReturn
+                        ( CorePrimitive
+                            operation
+                            [typedLiteral valueType left, typedLiteral valueType right]
+                            (oracleResultType operation valueType)
+                        )
+                    ]
+                | (index, (valueType, operation, left, right, _)) <- zip [0 ..] cases
+                ]
+
+integerOracleOperators :: [CorePrimitive]
+integerOracleOperators =
+    [ CoreAdd
+    , CoreSubtract
+    , CoreMultiply
+    , CoreDivide
+    , CoreFloorDivide
+    , CoreRemainder
+    , CorePower
+    , CoreShiftLeft
+    , CoreShiftRight
+    , CoreBitwiseAnd
+    , CoreBitwiseXor
+    , CoreBitwiseOr
+    , CoreLessThan
+    , CoreLessEqual
+    , CoreGreaterThan
+    , CoreGreaterEqual
+    , CoreEqual
+    , CoreNotEqual
+    , CoreLogicalAnd
+    , CoreLogicalOr
+    ]
+
+integerUnaryBoundaryMatrix :: Bool
+integerUnaryBoundaryMatrix = case optimizeCoreWith defaultOptimizerOptions input of
+    Left _ -> False
+    Right result -> actualExpressions (optimizedCore result) == map fifth cases
+    where
+        cases =
+            [ (valueType, operation, operand, resultType, expected)
+            | valueType <- integerTypes
+            , width <- maybeToList (coreIntegerBitWidth valueType)
+            , isSigned <- maybeToList (coreIntegerIsSigned valueType)
+            , let magnitude = 2 ^ (width - if isSigned then 1 else 0)
+            , let minimumValue = if isSigned then negate magnitude else 0
+            , let maximumValue = magnitude - 1
+            , operand <- nub [minimumValue, minimumValue + 1, -1, 0, 1, maximumValue - 1, maximumValue]
+            , integerFitsCoreType valueType operand
+            , operation <- [CoreNegate, CoreBitwiseNot]
+            , let resultType = valueType
+            , let expression = CorePrimitive operation [typedLiteral valueType operand] resultType
+            , let expected = case operation of
+                    CoreNegate ->
+                        if integerFitsCoreType resultType (negate operand)
+                            then CoreLiteral (CoreInteger (negate operand)) resultType
+                            else expression
+                    _ ->
+                        let complemented = complement operand
+                            finiteComplement = if isSigned then complemented else complemented .&. ((1 `shiftL` width) - 1)
+                         in CoreLiteral (CoreInteger finiteComplement) resultType
+            ]
+        input =
+            CoreModule
+                moduleName
+                [ CoreFunction
+                    (name (5000 + index) "IntegerUnaryOracle")
+                    []
+                    resultType
+                    [CoreReturn (CorePrimitive operation [typedLiteral valueType operand] resultType)]
+                | (index, (valueType, operation, operand, resultType, _)) <- zip [0 ..] cases
+                ]
+        actualExpressions optimizedModule =
+            [ expression
+            | function <- coreModuleFunctions optimizedModule
+            , [CoreReturn expression] <- [coreFunctionBody function]
+            ]
+        fifth (_, _, _, _, value) = value
+
+oracleResultType :: CorePrimitive -> Type -> Type
+oracleResultType operation operandType
+    | operation
+        `elem` [CoreLessThan, CoreLessEqual, CoreGreaterThan, CoreGreaterEqual, CoreEqual, CoreNotEqual, CoreLogicalAnd, CoreLogicalOr] =
+        boolType
+    | otherwise = operandType
+
+oracleFold :: Type -> Int -> CorePrimitive -> Integer -> Integer -> Maybe CoreLiteral
+oracleFold valueType width operation left right = case operation of
+    CoreAdd -> integerResult (left + right)
+    CoreSubtract -> integerResult (left - right)
+    CoreMultiply -> integerResult (left * right)
+    CoreDivide
+        | right /= 0 -> integerResult (left `quot` right)
+    CoreFloorDivide
+        | right /= 0 -> integerResult (roundedOracleDivision left right)
+    CoreRemainder
+        | right /= 0 -> integerResult (left `rem` right)
+    CorePower
+        | right < 0 -> Nothing
+        | left == 0 -> integerResult (if right == 0 then 1 else 0)
+        | left == 1 -> integerResult 1
+        | left == -1 -> integerResult (if even right then 1 else -1)
+        | right <= toInteger width -> integerResult (left ^ (fromInteger right :: Int))
+    CoreShiftLeft
+        | right >= 0 && right < toInteger width -> integerResult (shiftL left (fromInteger right))
+    CoreShiftRight
+        | right >= 0 && right < toInteger width -> integerResult (shiftR left (fromInteger right))
+    CoreBitwiseAnd -> integerResult (left .&. right)
+    CoreBitwiseXor -> integerResult (xor left right)
+    CoreBitwiseOr -> integerResult (left .|. right)
+    CoreLessThan -> booleanResult (left < right)
+    CoreLessEqual -> booleanResult (left <= right)
+    CoreGreaterThan -> booleanResult (left > right)
+    CoreGreaterEqual -> booleanResult (left >= right)
+    CoreEqual -> booleanResult (left == right)
+    CoreNotEqual -> booleanResult (left /= right)
+    CoreLogicalAnd -> booleanResult (left /= 0 && right /= 0)
+    CoreLogicalOr -> booleanResult (left /= 0 || right /= 0)
+    _ -> Nothing
+    where
+        integerResult result
+            | integerFitsCoreType valueType result = Just (CoreInteger result)
+            | otherwise = Nothing
+        booleanResult = Just . CoreBoolean
+
+typedLiteral :: Type -> Integer -> CoreExpression
+typedLiteral valueType value = CoreLiteral (CoreInteger value) valueType
+
+roundedOracleDivision :: Integer -> Integer -> Integer
+roundedOracleDivision dividend divisor =
+    let (quotient, remainder) = dividend `quotRem` divisor
+        adjustment = signum dividend * signum divisor
+     in if 2 * abs remainder >= abs divisor then quotient + adjustment else quotient
+
+maybeToList :: Maybe value -> [value]
+maybeToList value = case value of
+    Just item -> [item]
+    Nothing -> []
+
+appliedIntType :: Type
+appliedIntType = NamedType (QualifiedName [Identifier "int"]) [TypeTemplateArgument intType]
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+byte :: Integer -> CoreExpression
+byte value = CoreLiteral (CoreInteger value) byteType
+
+boolean :: Bool -> CoreExpression
+boolean value = CoreLiteral (CoreBoolean value) boolType
+
+unit :: CoreExpression
+unit = CoreLiteral CoreUnit unitType
+
+variable :: ResolvedName -> Type -> CoreExpression
+variable = CoreVariable
+
+primitive :: CorePrimitive -> [CoreExpression] -> Type -> CoreExpression
+primitive = CorePrimitive
+
+binding :: ResolvedName -> Type -> Bool -> CoreExpression -> CoreStatement
+binding bindingName valueType mutable value = CoreBind (CoreBinding bindingName valueType mutable value)
+
+entry :: Type -> [CoreStatement] -> CoreFunction
+entry resultType body = CoreFunction entryName [] resultType body
+
+moduleWith :: [CoreFunction] -> CoreModule
+moduleWith = CoreModule moduleName
+
+unitModule :: [CoreStatement] -> CoreModule
+unitModule statements = moduleWith [entry unitType (statements ++ [CoreReturn unit])]
+
+intModule :: [CoreStatement] -> CoreModule
+intModule statements = moduleWith [entry intType statements]
+
+optimized :: CoreModule -> Maybe OptimizationResult
+optimized input = either (const Nothing) Just (optimizeCoreWith defaultOptimizerOptions input)
+
+optimizedWith :: OptimizerOptions -> CoreModule -> Maybe OptimizationResult
+optimizedWith options input = either (const Nothing) Just (optimizeCoreWith options input)
+
+optimizedBody :: CoreModule -> Maybe [CoreStatement]
+optimizedBody input = do
+    result <- optimized input
+    function <- case coreModuleFunctions (optimizedCore result) of
+        first : _ -> Just first
+        [] -> Nothing
+    pure (coreFunctionBody function)
+
+returnExpression :: CoreModule -> Maybe CoreExpression
+returnExpression input = do
+    body <- optimizedBody input
+    case body of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+immutableChain :: Bool
+immutableChain =
+    returnExpression
+        ( intModule
+            [ binding valueName intType False (integer 40)
+            , binding secondName intType False (primitive CoreAdd [variable valueName intType, integer 2] intType)
+            , CoreReturn (variable secondName intType)
+            ]
+        )
+        == Just (integer 42)
+
+mutableInvalidation :: Bool
+mutableInvalidation =
+    optimizedBody
+        ( intModule
+            [ binding valueName intType True (integer 1)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+        )
+        == Just
+            [ binding valueName intType True (integer 1)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+
+primitiveFolds :: CorePrimitive -> Integer -> Integer -> Integer -> Bool
+primitiveFolds operation left right expected =
+    returnExpression (intModule [CoreReturn (primitive operation [integer left, integer right] intType)])
+        == Just (integer expected)
+
+typedPrimitiveFolds :: CorePrimitive -> Type -> Integer -> Integer -> Integer -> Bool
+typedPrimitiveFolds operation valueType left right expected =
+    let expression = CorePrimitive operation [literal left, literal right] valueType
+     in returnExpression (moduleWith [entry valueType [CoreReturn expression]]) == Just (literal expected)
+    where
+        literal value = CoreLiteral (CoreInteger value) valueType
+
+typedUnaryPrimitiveFolds :: CorePrimitive -> Type -> Integer -> Integer -> Bool
+typedUnaryPrimitiveFolds operation valueType operand expected =
+    let expression = CorePrimitive operation [literal operand] valueType
+     in returnExpression (moduleWith [entry valueType [CoreReturn expression]]) == Just (literal expected)
+    where
+        literal value = CoreLiteral (CoreInteger value) valueType
+
+overflowingPower :: Type -> Integer -> Integer -> Bool
+overflowingPower valueType base exponentValue =
+    let expression = CorePrimitive CorePower [literal base, literal exponentValue] valueType
+     in returnExpression (moduleWith [entry valueType [CoreReturn expression]]) == Just expression
+    where
+        literal value = CoreLiteral (CoreInteger value) valueType
+
+overflowingShift :: Type -> CorePrimitive -> Integer -> Integer -> Bool
+overflowingShift valueType operation value amount =
+    let expression = CorePrimitive operation [literal value, literal amount] valueType
+     in returnExpression (moduleWith [entry valueType [CoreReturn expression]]) == Just expression
+    where
+        literal number = CoreLiteral (CoreInteger number) valueType
+
+hugePowerRemainsExplicit :: Bool
+hugePowerRemainsExplicit = overflowingPower intType 2 (2 ^ (63 :: Int) - 1)
+
+hugeShiftRemainsExplicit :: Bool
+hugeShiftRemainsExplicit = overflowingShift ulongintType CoreShiftLeft 1 (2 ^ (128 :: Int) - 1)
+
+unaryFolds :: CorePrimitive -> Integer -> CoreExpression -> Bool
+unaryFolds operation operand expected =
+    let resultType = expressionType expected
+     in returnExpression (moduleWith [entry resultType [CoreReturn (primitive operation [integer operand] resultType)]])
+            == Just expected
+
+comparisonFolds :: CorePrimitive -> Integer -> Integer -> Bool -> Bool
+comparisonFolds operation left right expected =
+    returnExpression
+        (moduleWith [entry boolType [CoreReturn (primitive operation [integer left, integer right] boolType)]])
+        == Just (boolean expected)
+
+logicalFolds :: CorePrimitive -> Bool -> Bool -> Bool -> Bool
+logicalFolds operation left right expected =
+    returnExpression
+        ( moduleWith
+            [entry boolType [CoreReturn (primitive operation [boolean left, boolean right] boolType)]]
+        )
+        == Just (boolean expected)
+
+divisionByZeroRetained :: CorePrimitive -> Bool
+divisionByZeroRetained operation =
+    let expression = primitive operation [integer 7, integer 0] intType
+     in returnExpression (intModule [CoreReturn expression]) == Just expression
+
+overflowingByteAddition :: Bool
+overflowingByteAddition =
+    let expression = primitive CoreAdd [byte 127, byte 1] byteType
+     in returnExpression (moduleWith [entry byteType [CoreReturn expression]]) == Just expression
+
+identityRewrite :: CorePrimitive -> Integer -> Bool
+identityRewrite operation identity =
+    let parameter = variable valueName intType
+        function =
+            CoreFunction
+                entryName
+                [(valueName, intType)]
+                intType
+                [CoreReturn (primitive operation [parameter, integer identity] intType)]
+     in returnExpression (moduleWith [function]) == Just parameter
+
+remainderIdentity :: Bool
+remainderIdentity =
+    let function = CoreFunction entryName [(valueName, intType)] intType [CoreReturn expression]
+        expression = primitive CoreRemainder [variable valueName intType, integer 1] intType
+     in returnExpression (moduleWith [function]) == Just (integer 0)
+
+doubleNegation :: Bool
+doubleNegation =
+    let parameter = variable valueName intType
+        expression = primitive CoreNegate [primitive CoreNegate [parameter] intType] intType
+        function = CoreFunction entryName [(valueName, intType)] intType [CoreReturn expression]
+     in returnExpression (moduleWith [function]) == Just parameter
+
+doubleLogicalNot :: Bool
+doubleLogicalNot =
+    let parameter = variable valueName boolType
+        expression = primitive CoreLogicalNot [primitive CoreLogicalNot [parameter] boolType] boolType
+        function = CoreFunction entryName [(valueName, boolType)] boolType [CoreReturn expression]
+     in returnExpression (moduleWith [function]) == Just parameter
+
+numericDoubleLogicalNot :: Bool
+numericDoubleLogicalNot =
+    let parameter = variable valueName intType
+        inner = primitive CoreLogicalNot [parameter] boolType
+        expression = primitive CoreLogicalNot [inner] boolType
+        function = CoreFunction entryName [(valueName, intType)] boolType [CoreReturn expression]
+     in returnExpression (moduleWith [function]) == Just expression
+
+knownBranch :: Bool -> Bool
+knownBranch condition =
+    returnExpression
+        ( intModule
+            [ CoreIf
+                (boolean condition)
+                [CoreReturn (integer 1)]
+                [CoreReturn (integer 2)]
+            ]
+        )
+        == Just (integer (if condition then 1 else 2))
+
+numericBranch :: Integer -> Integer -> Bool
+numericBranch condition expected =
+    returnExpression
+        ( intModule
+            [ CoreIf
+                (integer condition)
+                [CoreReturn (integer 1)]
+                [CoreReturn (integer 2)]
+            ]
+        )
+        == Just (integer expected)
+
+identicalPureBranches :: Bool
+identicalPureBranches =
+    let condition = primitive CoreLessThan [integer 1, variable valueName intType] boolType
+        function = CoreFunction entryName [(valueName, intType)] intType [CoreIf condition branch branch]
+        branch = [CoreReturn (integer 7)]
+     in returnExpression (moduleWith [function]) == Just (integer 7)
+
+effectFunction :: CoreFunction
+effectFunction =
+    CoreFunction
+        calleeName
+        []
+        unitType
+        [ CoreEvaluate (closureExpression [] [] unitType [CoreReturn unit])
+        , CoreReturn unit
+        ]
+
+effectCall :: CoreExpression
+effectCall = CoreApply (variable calleeName (FunctionType [] unitType)) [] unitType
+
+predicateFunction :: CoreFunction
+predicateFunction =
+    CoreFunction
+        predicateName
+        []
+        boolType
+        [ CoreEvaluate (closureExpression [] [] unitType [CoreReturn unit])
+        , CoreReturn (boolean True)
+        ]
+
+predicateCall :: CoreExpression
+predicateCall = CoreApply (variable predicateName (FunctionType [] boolType)) [] boolType
+
+identicalEffectfulBranches :: Bool
+identicalEffectfulBranches =
+    optimizedBody
+        ( moduleWith
+            [ entry intType [CoreIf predicateCall branch branch]
+            , predicateFunction
+            ]
+        )
+        == Just [CoreEvaluate predicateCall, CoreReturn (integer 7)]
+    where
+        branch = [CoreReturn (integer 7)]
+
+emptyEffectfulBranches :: Bool
+emptyEffectfulBranches =
+    optimizedBody (moduleWith [entry unitType [CoreIf predicateCall [] [], CoreReturn unit], predicateFunction])
+        == Just [CoreEvaluate predicateCall, CoreReturn unit]
+
+unreachableAfterReturn :: Bool
+unreachableAfterReturn =
+    optimizedBody
+        (moduleWith [entry intType [CoreReturn (integer 1), CoreEvaluate effectCall], effectFunction])
+        == Just [CoreReturn (integer 1)]
+
+unreachableAfterBranch :: Bool
+unreachableAfterBranch =
+    let function = CoreFunction entryName [(valueName, boolType)] intType body
+        body =
+            [ CoreIf
+                (variable valueName boolType)
+                [CoreReturn (integer 1)]
+                [CoreReturn (integer 2)]
+            , CoreEvaluate effectCall
+            ]
+     in optimizedBody (moduleWith [function, effectFunction])
+            == Just
+                [ CoreIf
+                    (variable valueName boolType)
+                    [CoreReturn (integer 1)]
+                    [CoreReturn (integer 2)]
+                ]
+
+deadLiteralBinding :: Bool
+deadLiteralBinding =
+    optimizedBody (unitModule [binding valueName intType False (integer 42)])
+        == Just [CoreReturn unit]
+
+deadPrimitiveEvaluation :: Bool
+deadPrimitiveEvaluation =
+    optimizedBody (unitModule [CoreEvaluate (primitive CoreAdd [integer 1, integer 2] intType)])
+        == Just [CoreReturn unit]
+
+deadCallResultPreserved :: Bool
+deadCallResultPreserved =
+    optimizedBody
+        ( moduleWith
+            [ entry unitType [binding valueName unitType False effectCall, CoreReturn unit]
+            , effectFunction
+            ]
+        )
+        == Just [CoreEvaluate effectCall, CoreReturn unit]
+
+closureExpression :: [CoreCapture] -> [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreExpression
+closureExpression captures parameters resultType body =
+    CoreClosure captures parameters resultType body (FunctionType (map snd parameters) resultType)
+
+deadClosurePreserved :: Bool
+deadClosurePreserved =
+    let closure = closureExpression [] [] unitType [CoreReturn unit]
+     in optimizedBody (unitModule [binding valueName (expressionType closure) False closure])
+            == Just [CoreEvaluate closure, CoreReturn unit]
+
+deadAssignment :: Bool
+deadAssignment =
+    optimizedBody
+        ( unitModule
+            [ binding valueName intType True (integer 1)
+            , CoreAssign valueName (integer 2)
+            ]
+        )
+        == Just [CoreReturn unit]
+
+liveAssignment :: Bool
+liveAssignment =
+    optimizedBody
+        ( intModule
+            [ binding valueName intType True (integer 1)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+        )
+        == Just
+            [ binding valueName intType True (integer 1)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+
+overwrittenAssignment :: Bool
+overwrittenAssignment =
+    optimizedBody
+        ( intModule
+            [ binding valueName intType True (integer 0)
+            , CoreAssign valueName (integer 1)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+        )
+        == Just
+            [ binding valueName intType True (integer 0)
+            , CoreAssign valueName (integer 2)
+            , CoreReturn (variable valueName intType)
+            ]
+
+branchAssignmentRetainsDeclaration :: Bool
+branchAssignmentRetainsDeclaration =
+    let function = CoreFunction entryName [(secondName, boolType)] intType body
+        body =
+            [ binding valueName intType True (integer 0)
+            , CoreIf
+                (variable secondName boolType)
+                [CoreAssign valueName (integer 1)]
+                [CoreAssign valueName (integer 2)]
+            , CoreReturn (variable valueName intType)
+            ]
+     in optimizedBody (moduleWith [function]) == Just body
+
+deadBranchAssignment :: Bool
+deadBranchAssignment =
+    let function = CoreFunction entryName [(secondName, boolType)] unitType body
+        body =
+            [ binding valueName intType True (integer 0)
+            , CoreIf
+                (variable secondName boolType)
+                [CoreAssign valueName (integer 1)]
+                [CoreAssign valueName (integer 2)]
+            , CoreReturn unit
+            ]
+     in optimizedBody (moduleWith [function]) == Just [CoreReturn unit]
+
+effectfulBindingBecomesEvaluation :: Bool
+effectfulBindingBecomesEvaluation =
+    optimizedBody
+        ( moduleWith
+            [ entry unitType [binding valueName unitType False effectCall, CoreReturn unit]
+            , effectFunction
+            ]
+        )
+        == Just [CoreEvaluate effectCall, CoreReturn unit]
+
+closureBodyOptimized :: Bool
+closureBodyOptimized =
+    let closure =
+            closureExpression
+                []
+                []
+                intType
+                [ binding valueName intType False (integer 40)
+                , CoreReturn (primitive CoreAdd [variable valueName intType, integer 2] intType)
+                , CoreEvaluate effectCall
+                ]
+        input = moduleWith [entry (expressionType closure) [CoreReturn closure], effectFunction]
+     in case returnExpression input of
+            Just (CoreClosure _ _ _ body _) -> body == [CoreReturn (integer 42)]
+            _ -> False
+
+closureCapturePropagates :: Bool
+closureCapturePropagates =
+    let capture = CoreCapture StrongCapture capturedValueName intType (integer 40)
+        closure =
+            closureExpression
+                [capture]
+                []
+                intType
+                [CoreReturn (primitive CoreAdd [variable capturedValueName intType, integer 2] intType)]
+     in case returnExpression (moduleWith [entry (expressionType closure) [CoreReturn closure]]) of
+            Just (CoreClosure _ _ _ body _) -> body == [CoreReturn (integer 42)]
+            _ -> False
+
+closureParameterShadowsCapture :: Bool
+closureParameterShadowsCapture =
+    let capture = CoreCapture StrongCapture closureParameterName intType (integer 40)
+        closure =
+            closureExpression
+                [capture]
+                [(closureParameterName, intType)]
+                intType
+                [CoreReturn (variable closureParameterName intType)]
+     in case returnExpression (moduleWith [entry (expressionType closure) [CoreReturn closure]]) of
+            Just (CoreClosure _ _ _ body _) -> body == [CoreReturn (variable closureParameterName intType)]
+            _ -> False
+
+disabledOptions :: OptimizerOptions
+disabledOptions = OptimizerOptions 12 False False False False False 24
+
+disabledPipelineIsIdentity :: Bool
+disabledPipelineIsIdentity =
+    let input = intModule [CoreReturn (primitive CoreAdd [integer 20, integer 22] intType)]
+     in (optimizedCore <$> optimizedWith disabledOptions input) == Just input
+
+constantPassCanBeDisabled :: Bool
+constantPassCanBeDisabled =
+    let input = intModule [CoreReturn expression]
+        expression = primitive CoreAdd [integer 20, integer 22] intType
+        options = defaultOptimizerOptions {optimizerConstantPropagation = False}
+     in returnFromResult (optimizedWith options input) == Just expression
+
+controlFlowPassCanBeDisabled :: Bool
+controlFlowPassCanBeDisabled =
+    let branch = CoreIf (boolean True) [CoreReturn (integer 1)] [CoreReturn (integer 2)]
+        input = intModule [branch]
+        options = defaultOptimizerOptions {optimizerControlFlowSimplification = False}
+     in bodyFromResult (optimizedWith options input) == Just [branch]
+
+deadCodePassCanBeDisabled :: Bool
+deadCodePassCanBeDisabled =
+    let dead = binding valueName intType False (integer 42)
+        input = unitModule [dead]
+        options = defaultOptimizerOptions {optimizerDeadCodeElimination = False}
+     in bodyFromResult (optimizedWith options input) == Just [dead, CoreReturn unit]
+
+bodyFromResult :: Maybe OptimizationResult -> Maybe [CoreStatement]
+bodyFromResult maybeResult = do
+    result <- maybeResult
+    case coreModuleFunctions (optimizedCore result) of
+        function : _ -> Just (coreFunctionBody function)
+        [] -> Nothing
+
+returnFromResult :: Maybe OptimizationResult -> Maybe CoreExpression
+returnFromResult maybeResult = do
+    body <- bodyFromResult maybeResult
+    case body of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+pipelineFixture :: CoreModule
+pipelineFixture =
+    intModule
+        [ binding valueName intType False (integer 40)
+        , binding secondName intType False (primitive CoreAdd [variable valueName intType, integer 2] intType)
+        , CoreIf
+            (primitive CoreEqual [variable secondName intType, integer 42] boolType)
+            [CoreReturn (variable secondName intType)]
+            [CoreReturn (integer 0)]
+        ]
+
+reportsConvergence :: Bool
+reportsConvergence = maybe False optimizationConverged (optimized pipelineFixture)
+
+reportsIterationLimit :: Bool
+reportsIterationLimit =
+    case optimizedWith defaultOptimizerOptions {optimizerMaximumIterations = 1} pipelineFixture of
+        Just result -> optimizationIterations result == 1 && not (optimizationConverged result)
+        Nothing -> False
+
+metricsCountFunctions :: Bool
+metricsCountFunctions =
+    case optimized (moduleWith [entry unitType [CoreReturn unit], effectFunction]) of
+        Just result -> metricFunctions (optimizationBefore result) == 2
+        Nothing -> False
+
+metricsReportReduction :: Bool
+metricsReportReduction =
+    case optimized pipelineFixture of
+        Just result ->
+            metricStatements (optimizationAfter result) < metricStatements (optimizationBefore result)
+                && metricBindings (optimizationAfter result) < metricBindings (optimizationBefore result)
+                && metricBranches (optimizationAfter result) < metricBranches (optimizationBefore result)
+        Nothing -> False
+
+metricsCountEffects :: Bool
+metricsCountEffects =
+    let closure = closureExpression [] [] unitType [CoreReturn unit]
+        input =
+            moduleWith
+                [ entry
+                    unitType
+                    [ CoreEvaluate effectCall
+                    , CoreEvaluate closure
+                    , CoreReturn unit
+                    ]
+                , effectFunction
+                ]
+     in case optimized input of
+            Just result ->
+                metricCalls (optimizationBefore result) == 1
+                    && metricClosures (optimizationBefore result) == 2
+                    && metricCalls (optimizationAfter result) == 1
+                    && metricClosures (optimizationAfter result) == 2
+            Nothing -> False
+
+reportsPreservePassOrder :: Bool
+reportsPreservePassOrder =
+    case optimized pipelineFixture of
+        Just result ->
+            take 4 (map passReportPass (optimizationPassReports result))
+                == [ InliningPass
+                   , ConstantPropagationPass
+                   , ControlFlowSimplificationPass
+                   , DeadCodeEliminationPass
+                   ]
+        Nothing -> False
+
+reportsIdentifyChanges :: Bool
+reportsIdentifyChanges =
+    case optimized pipelineFixture of
+        Just result ->
+            any
+                ( \report ->
+                    passReportPass report == ConstantPropagationPass
+                        && passReportIteration report == 1
+                        && passReportChanged report
+                )
+                (optimizationPassReports result)
+                && any
+                    ( \report ->
+                        passReportPass report == DeadCodeEliminationPass
+                            && passReportIteration report == 1
+                            && passReportChanged report
+                    )
+                    (optimizationPassReports result)
+        Nothing -> False
+
+reportsCoverIterations :: Bool
+reportsCoverIterations =
+    case optimized pipelineFixture of
+        Just result ->
+            let iterations = map passReportIteration (optimizationPassReports result)
+             in not (null iterations)
+                    && minimum iterations == 1
+                    && maximum iterations == optimizationIterations result
+                    && length iterations == optimizationIterations result * 4
+        Nothing -> False
+
+reportsOmitDisabledPasses :: Bool
+reportsOmitDisabledPasses =
+    let options =
+            defaultOptimizerOptions
+                { optimizerConstantPropagation = False
+                , optimizerDeadCodeElimination = False
+                , optimizerInlining = False
+                }
+     in case optimizedWith options pipelineFixture of
+            Just result ->
+                not (null (optimizationPassReports result))
+                    && all
+                        ((== ControlFlowSimplificationPass) . passReportPass)
+                        (optimizationPassReports result)
+            Nothing -> False
+
+reportMetricsAreContinuous :: Bool
+reportMetricsAreContinuous =
+    case optimized pipelineFixture of
+        Just result -> continuous (optimizationPassReports result)
+        Nothing -> False
+    where
+        continuous [] = True
+        continuous [_] = True
+        continuous (first : second : remaining) =
+            passReportAfter first == passReportBefore second
+                && continuous (second : remaining)
+
+optimizerIsIdempotent :: Bool
+optimizerIsIdempotent = doOptimizeTwice pipelineFixture
+
+doOptimizeTwice :: CoreModule -> Bool
+doOptimizeTwice input = case optimized input of
+    Nothing -> False
+    Just first -> case optimized (optimizedCore first) of
+        Nothing -> False
+        Just second -> optimizedCore first == optimizedCore second
+
+optimizedOutputVerifies :: Bool
+optimizedOutputVerifies = case optimized pipelineFixture of
+    Nothing -> False
+    Just result -> case verifyCore (optimizedCore result) of
+        Right _ -> True
+        Left _ -> False
+
+invalidInputRejected :: Bool
+invalidInputRejected =
+    let invalidName = ResolvedName (SymbolId 0) (Identifier "Invalid")
+        invalid = moduleWith [CoreFunction invalidName [] unitType [CoreReturn unit]]
+     in case optimizeCoreWith defaultOptimizerOptions invalid of
+            Left _ -> True
+            Right _ -> False
+ test/CoreVerifierTests.hs view
@@ -0,0 +1,435 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module CoreVerifierTests (coreVerifierTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Diagnostic
+
+-- The optimizer is guarded by the same verifier before and after rewriting.
+-- These focused malformed-tree cases make that boundary explicit and ensure
+-- diagnostics continue to identify the invariant that was violated.
+coreVerifierTests :: [(String, Bool)]
+coreVerifierTests =
+    [ ("Core verifier accepts the minimal unit module", accepted minimalUnitModule)
+    , ("Core verifier rejects an empty module name", rejectedWith "TAC1001" emptyModuleName)
+    , ("Core verifier rejects an empty module-name segment", rejectedWith "TAC1001" emptyModuleSegment)
+    , ("Core verifier rejects duplicate function symbols", rejectedWith "TAC1002" duplicateFunctions)
+    , ("Core verifier rejects an unresolved function result", rejectedWith "TAC1003" unresolvedFunctionResult)
+    , ("Core verifier rejects duplicate parameter symbols", rejectedWith "TAC1004" duplicateParameters)
+    , ("Core verifier rejects a missing value return", rejectedWith "TAC1005" missingValueReturn)
+    , -- A loop that cannot be left never falls through to the end.
+      ("Core verifier accepts a body that ends in a loop that cannot be left", accepted (endsInLoop (CoreWhile trueValue [])))
+    , ("Core verifier accepts an endless do/while at the end of a body", accepted (endsInLoop (CoreDoWhile [] trueValue)))
+    , ("Core verifier accepts an endless for at the end of a body", accepted (endsInLoop (CoreFor trueValue [] [])))
+    ,
+        ( "Core verifier rejects a body that ends in a loop a break leaves"
+        , rejectedWith "TAC1005" (endsInLoop (CoreWhile trueValue [CoreIf trueValue [CoreBreak] []]))
+        )
+    ,
+        ( "Core verifier rejects a body that ends in a for loop whose update breaks"
+        , rejectedWith "TAC1005" (endsInLoop (CoreFor trueValue [] [CoreBreak]))
+        )
+    ,
+        ( "Core verifier rejects a body that ends in a loop with a condition"
+        , rejectedWith "TAC1005" (endsInLoop (CoreWhile (CoreLiteral (CoreBoolean False) boolType) []))
+        )
+    ,
+        ( "a break of a nested loop does not leave the loop around it"
+        , accepted (endsInLoop (CoreWhile trueValue [CoreWhile trueValue [CoreBreak]]))
+        )
+    , ("Core verifier rejects a zero function symbol", rejectedWith "TAC1006" zeroFunctionSymbol)
+    , ("Core verifier rejects a negative parameter symbol", rejectedWith "TAC1006" negativeParameterSymbol)
+    , ("Core verifier rejects an unresolved parameter type", rejectedWith "TAC1007" unresolvedParameterType)
+    , ("Core verifier rejects a zero binding symbol", rejectedWith "TAC1008" zeroBindingSymbol)
+    , ("Core verifier rejects an unresolved binding type", rejectedWith "TAC1009" unresolvedBindingType)
+    , ("Core verifier rejects a redefined local symbol", rejectedWith "TAC1010" duplicateBinding)
+    , ("Core verifier rejects a binding value type mismatch", rejectedWith "TAC1011" bindingTypeMismatch)
+    , ("Core verifier rejects assignment to an undefined symbol", rejectedWith "TAC1012" undefinedAssignment)
+    , ("Core verifier rejects assignment to immutable storage", rejectedWith "TAC1013" immutableAssignment)
+    , ("Core verifier rejects assignment with the wrong value type", rejectedWith "TAC1014" assignmentTypeMismatch)
+    , ("Core verifier rejects a zero assignment symbol", rejectedWith "TAC1015" zeroAssignmentSymbol)
+    , ("Core verifier rejects a return value type mismatch", rejectedWith "TAC1016" returnTypeMismatch)
+    , ("Core verifier rejects a String condition", rejectedWith "TAC1017" stringCondition)
+    , ("Core verifier rejects an unresolved expression type", rejectedWith "TAC1018" unresolvedExpression)
+    , ("Core verifier rejects a zero variable symbol", rejectedWith "TAC1019" zeroVariableSymbol)
+    , ("Core verifier rejects an undefined variable", rejectedWith "TAC1020" undefinedVariable)
+    , ("Core verifier rejects variable type disagreement", rejectedWith "TAC1021" variableTypeMismatch)
+    , ("Core verifier rejects a call with too few arguments", rejectedWith "TAC1022" callArityMismatch)
+    , ("Core verifier rejects a call argument type mismatch", rejectedWith "TAC1023" callArgumentMismatch)
+    , ("Core verifier rejects a call result type mismatch", rejectedWith "TAC1024" callResultMismatch)
+    , ("Core verifier rejects a non-callable target", rejectedWith "TAC1025" nonCallableTarget)
+    , ("Core verifier rejects a primitive arity mismatch", rejectedWith "TAC1026" primitiveArityMismatch)
+    , ("Core verifier rejects String arithmetic", rejectedWith "TAC1027" stringArithmetic)
+    , ("Core verifier rejects mixed numeric operand types", rejectedWith "TAC1027" mixedArithmetic)
+    , ("Core verifier rejects a primitive result mismatch", rejectedWith "TAC1028" primitiveResultMismatch)
+    , ("Core verifier rejects an overflowing byte literal", rejectedWith "TAC1029" overflowingByteLiteral)
+    , ("Core verifier rejects a floating payload on int", rejectedWith "TAC1029" floatingPayloadOnInt)
+    , ("Core verifier rejects malformed floating spelling", rejectedWith "TAC1029" malformedFloatingSpelling)
+    , ("Core verifier rejects duplicate closure captures", rejectedWith "TAC1030" duplicateCaptures)
+    , ("Core verifier rejects duplicate closure parameters", rejectedWith "TAC1031" duplicateClosureParameters)
+    , ("Core verifier rejects a closure missing a return", rejectedWith "TAC1032" closureMissingReturn)
+    , ("Core verifier rejects closure parameter disagreement", rejectedWith "TAC1033" closureParameterMismatch)
+    , ("Core verifier rejects closure callable arity mismatch", rejectedWith "TAC1034" closureArityMismatch)
+    , ("Core verifier rejects closure result disagreement", rejectedWith "TAC1035" closureResultMismatch)
+    , ("Core verifier rejects a non-callable closure type", rejectedWith "TAC1036" closureNotCallable)
+    , ("Core verifier rejects a zero capture symbol", rejectedWith "TAC1037" zeroCaptureSymbol)
+    , ("Core verifier rejects an unresolved capture type", rejectedWith "TAC1038" unresolvedCaptureType)
+    , ("Core verifier rejects a capture initializer mismatch", rejectedWith "TAC1039" captureInitializerMismatch)
+    , ("Core verifier accepts numeric conditions", accepted numericCondition)
+    , ("Core verifier accepts boolean conditions", accepted booleanCondition)
+    , ("Core verifier accepts Boolean equality", accepted (primitiveModule CoreEqual [boolValue, boolValue] boolType))
+    , ("Core verifier accepts Boolean inequality", accepted (primitiveModule CoreNotEqual [boolValue, boolValue] boolType))
+    ,
+        ( "Core verifier rejects mixed Boolean equality"
+        , rejectedWith "TAC1027" (primitiveModule CoreEqual [boolValue, intValue] boolType)
+        )
+    ,
+        ( "Core verifier rejects Boolean ordering"
+        , rejectedWith "TAC1027" (primitiveModule CoreLessThan [boolValue, boolValue] boolType)
+        )
+    ,
+        ( "Core verifier rejects numeric Boolean equality result"
+        , rejectedWith "TAC1028" (primitiveModule CoreEqual [boolValue, boolValue] intType)
+        )
+    , ("Core verifier accepts all-returning branches", accepted allReturningBranch)
+    , ("Core verifier rejects break outside a loop", rejectedWith "TAC1045" (loopModule [CoreBreak]))
+    , ("Core verifier rejects continue outside a loop", rejectedWith "TAC1046" (loopModule [CoreContinue]))
+    , ("Core verifier accepts continue in a for body", accepted (forModule [CoreContinue] []))
+    , ("Core verifier accepts break in a for update region", accepted (forModule [] [CoreBreak]))
+    , ("Core verifier rejects continue in a for update region", rejectedWith "TAC1066" (forModule [] [CoreContinue]))
+    ,
+        ( "Core verifier rejects continue nested in a branch of a for update region"
+        , rejectedWith "TAC1066" (forModule [] [CoreIf boolValue [CoreContinue] []])
+        )
+    ,
+        ( "Core verifier accepts continue in a loop nested in a for update region"
+        , accepted (forModule [] [CoreWhile boolValue [CoreContinue]])
+        )
+    ,
+        ( "Core verifier does not report an outside-loop continue as an update continue"
+        , not (rejectedWith "TAC1066" (loopModule [CoreContinue]))
+        )
+    , ("Core verifier accepts a well-typed direct call", accepted validDirectCall)
+    , ("Core verifier accepts a well-typed closure", accepted validClosure)
+    ]
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+primitiveModule :: CorePrimitive -> [CoreExpression] -> Type -> CoreModule
+primitiveModule primitive arguments result =
+    coreModule [function mainName [] result [CoreReturn (CorePrimitive primitive arguments result)]]
+
+moduleName :: QualifiedName
+moduleName = QualifiedName [Identifier "Verifier", Identifier "Tests"]
+
+mainName, helperName, valueName, captureValueName :: ResolvedName
+mainName = resolved 1 "Main"
+helperName = resolved 2 "Helper"
+valueName = resolved 10 "value"
+captureValueName = resolved 12 "captured"
+
+unitValue, intValue, boolValue, stringValue :: CoreExpression
+unitValue = CoreLiteral CoreUnit unitType
+intValue = CoreLiteral (CoreInteger 1) intType
+boolValue = CoreLiteral (CoreBoolean True) boolType
+stringValue = CoreLiteral (CoreString "value") stringType
+
+function :: ResolvedName -> [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreFunction
+function = CoreFunction
+
+unitFunction :: ResolvedName -> [CoreStatement] -> CoreFunction
+unitFunction functionName body = function functionName [] unitType (body ++ [CoreReturn unitValue])
+
+coreModule :: [CoreFunction] -> CoreModule
+coreModule = CoreModule moduleName
+
+accepted :: CoreModule -> Bool
+accepted moduleValue = verifyCore moduleValue == Right moduleValue
+
+rejectedWith :: String -> CoreModule -> Bool
+rejectedWith code moduleValue = case verifyCore moduleValue of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+minimalUnitModule :: CoreModule
+minimalUnitModule = coreModule [unitFunction mainName []]
+
+emptyModuleName :: CoreModule
+emptyModuleName = CoreModuleWithSources (QualifiedName []) [unitFunction mainName []] [] []
+
+emptyModuleSegment :: CoreModule
+emptyModuleSegment = CoreModuleWithSources (QualifiedName [Identifier "Verifier", Identifier ""]) [unitFunction mainName []] [] []
+
+duplicateFunctions :: CoreModule
+duplicateFunctions = coreModule [unitFunction mainName [], unitFunction mainName []]
+
+unresolvedFunctionResult :: CoreModule
+unresolvedFunctionResult = coreModule [function mainName [] ErrorType [CoreReturn unitValue]]
+
+duplicateParameters :: CoreModule
+duplicateParameters =
+    coreModule [function mainName [(valueName, intType), (valueName, intType)] unitType [CoreReturn unitValue]]
+
+missingValueReturn :: CoreModule
+missingValueReturn = coreModule [function mainName [] intType []]
+
+-- | A function that returns a value and whose body is the given loop alone.
+endsInLoop :: CoreStatement -> CoreModule
+endsInLoop loop = coreModule [function mainName [] intType [loop]]
+
+trueValue :: CoreExpression
+trueValue = CoreLiteral (CoreBoolean True) boolType
+
+zeroFunctionSymbol :: CoreModule
+zeroFunctionSymbol = coreModule [unitFunction (resolved 0 "Main") []]
+
+negativeParameterSymbol :: CoreModule
+negativeParameterSymbol =
+    coreModule [function mainName [(resolved (-1) "value", intType)] unitType [CoreReturn unitValue]]
+
+unresolvedParameterType :: CoreModule
+unresolvedParameterType = coreModule [function mainName [(valueName, ErrorType)] unitType [CoreReturn unitValue]]
+
+zeroBindingSymbol :: CoreModule
+zeroBindingSymbol =
+    coreModule [unitFunction mainName [CoreBind (CoreBinding (resolved 0 "value") intType False intValue)]]
+
+unresolvedBindingType :: CoreModule
+unresolvedBindingType =
+    coreModule [unitFunction mainName [CoreBind (CoreBinding valueName ErrorType False intValue)]]
+
+duplicateBinding :: CoreModule
+duplicateBinding =
+    coreModule
+        [ unitFunction
+            mainName
+            [ CoreBind (CoreBinding valueName intType False intValue)
+            , CoreBind (CoreBinding valueName intType False intValue)
+            ]
+        ]
+
+bindingTypeMismatch :: CoreModule
+bindingTypeMismatch =
+    coreModule [unitFunction mainName [CoreBind (CoreBinding valueName boolType False intValue)]]
+
+undefinedAssignment :: CoreModule
+undefinedAssignment = coreModule [unitFunction mainName [CoreAssign valueName intValue]]
+
+immutableAssignment :: CoreModule
+immutableAssignment =
+    coreModule
+        [ unitFunction
+            mainName
+            [ CoreBind (CoreBinding valueName intType False intValue)
+            , CoreAssign valueName intValue
+            ]
+        ]
+
+assignmentTypeMismatch :: CoreModule
+assignmentTypeMismatch =
+    coreModule
+        [ unitFunction
+            mainName
+            [ CoreBind (CoreBinding valueName intType True intValue)
+            , CoreAssign valueName boolValue
+            ]
+        ]
+
+zeroAssignmentSymbol :: CoreModule
+zeroAssignmentSymbol = coreModule [unitFunction mainName [CoreAssign (resolved 0 "value") intValue]]
+
+returnTypeMismatch :: CoreModule
+returnTypeMismatch = coreModule [function mainName [] boolType [CoreReturn intValue]]
+
+stringCondition :: CoreModule
+stringCondition = coreModule [unitFunction mainName [CoreIf stringValue [] []]]
+
+unresolvedExpression :: CoreModule
+unresolvedExpression =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreLiteral (CoreInteger 1) ErrorType)]]
+
+zeroVariableSymbol :: CoreModule
+zeroVariableSymbol =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreVariable (resolved 0 "value") intType)]]
+
+undefinedVariable :: CoreModule
+undefinedVariable = coreModule [unitFunction mainName [CoreEvaluate (CoreVariable valueName intType)]]
+
+variableTypeMismatch :: CoreModule
+variableTypeMismatch =
+    coreModule
+        [ function
+            mainName
+            [(valueName, intType)]
+            unitType
+            [CoreEvaluate (CoreVariable valueName boolType), CoreReturn unitValue]
+        ]
+
+helperFunction :: CoreFunction
+helperFunction = function helperName [(valueName, intType)] intType [CoreReturn (CoreVariable valueName intType)]
+
+helperReference :: CoreExpression
+helperReference = CoreVariable helperName (FunctionType [intType] intType)
+
+callArityMismatch :: CoreModule
+callArityMismatch =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreApply helperReference [] intType)], helperFunction]
+
+callArgumentMismatch :: CoreModule
+callArgumentMismatch =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreApply helperReference [boolValue] intType)], helperFunction]
+
+callResultMismatch :: CoreModule
+callResultMismatch =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreApply helperReference [intValue] boolType)], helperFunction]
+
+nonCallableTarget :: CoreModule
+nonCallableTarget =
+    coreModule
+        [ function
+            mainName
+            [(valueName, intType)]
+            unitType
+            [CoreEvaluate (CoreApply (CoreVariable valueName intType) [] intType), CoreReturn unitValue]
+        ]
+
+primitiveArityMismatch :: CoreModule
+primitiveArityMismatch =
+    coreModule [unitFunction mainName [CoreEvaluate (CorePrimitive CoreAdd [intValue] intType)]]
+
+stringArithmetic :: CoreModule
+stringArithmetic =
+    coreModule [unitFunction mainName [CoreEvaluate (CorePrimitive CoreAdd [stringValue, stringValue] stringType)]]
+
+mixedArithmetic :: CoreModule
+mixedArithmetic =
+    let longValue = CoreLiteral (CoreInteger 1) (namedType "long")
+     in coreModule [unitFunction mainName [CoreEvaluate (CorePrimitive CoreAdd [intValue, longValue] intType)]]
+
+primitiveResultMismatch :: CoreModule
+primitiveResultMismatch =
+    coreModule [unitFunction mainName [CoreEvaluate (CorePrimitive CoreAdd [intValue, intValue] boolType)]]
+
+overflowingByteLiteral :: CoreModule
+overflowingByteLiteral =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreLiteral (CoreInteger 128) (namedType "byte"))]]
+
+floatingPayloadOnInt :: CoreModule
+floatingPayloadOnInt =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreLiteral (CoreFloating "1.0") intType)]]
+
+malformedFloatingSpelling :: CoreModule
+malformedFloatingSpelling =
+    coreModule [unitFunction mainName [CoreEvaluate (CoreLiteral (CoreFloating "1e") (namedType "float"))]]
+
+closure :: [CoreCapture] -> [(ResolvedName, Type)] -> Type -> [CoreStatement] -> Type -> CoreExpression
+closure = CoreClosure
+
+validClosureValue :: CoreExpression
+validClosureValue =
+    closure
+        []
+        [(valueName, intType)]
+        intType
+        [CoreReturn (CoreVariable valueName intType)]
+        (FunctionType [intType] intType)
+
+duplicateCaptures :: CoreModule
+duplicateCaptures =
+    let capture = CoreCapture StrongCapture captureValueName intType intValue
+        value = closure [capture, capture] [] unitType [CoreReturn unitValue] (FunctionType [] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+duplicateClosureParameters :: CoreModule
+duplicateClosureParameters =
+    let value =
+            closure
+                []
+                [(valueName, intType), (valueName, intType)]
+                intType
+                [CoreReturn intValue]
+                (FunctionType [intType, intType] intType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+closureMissingReturn :: CoreModule
+closureMissingReturn =
+    let value = closure [] [] intType [] (FunctionType [] intType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+closureParameterMismatch :: CoreModule
+closureParameterMismatch =
+    let value = closure [] [(valueName, boolType)] unitType [CoreReturn unitValue] (FunctionType [intType] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+closureArityMismatch :: CoreModule
+closureArityMismatch =
+    let value = closure [] [(valueName, intType)] unitType [CoreReturn unitValue] (FunctionType [] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+closureResultMismatch :: CoreModule
+closureResultMismatch =
+    let value = closure [] [] unitType [CoreReturn unitValue] (FunctionType [] intType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+closureNotCallable :: CoreModule
+closureNotCallable =
+    let value = closure [] [] unitType [CoreReturn unitValue] intType
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+zeroCaptureSymbol :: CoreModule
+zeroCaptureSymbol =
+    let capture = CoreCapture StrongCapture (resolved 0 "captured") intType intValue
+        value = closure [capture] [] unitType [CoreReturn unitValue] (FunctionType [] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+unresolvedCaptureType :: CoreModule
+unresolvedCaptureType =
+    let capture = CoreCapture StrongCapture captureValueName ErrorType intValue
+        value = closure [capture] [] unitType [CoreReturn unitValue] (FunctionType [] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+captureInitializerMismatch :: CoreModule
+captureInitializerMismatch =
+    let capture = CoreCapture StrongCapture captureValueName boolType intValue
+        value = closure [capture] [] unitType [CoreReturn unitValue] (FunctionType [] unitType)
+     in coreModule [unitFunction mainName [CoreEvaluate value]]
+
+numericCondition :: CoreModule
+numericCondition = coreModule [unitFunction mainName [CoreIf intValue [] []]]
+
+booleanCondition :: CoreModule
+booleanCondition = coreModule [unitFunction mainName [CoreIf boolValue [] []]]
+
+allReturningBranch :: CoreModule
+allReturningBranch =
+    coreModule
+        [ function
+            mainName
+            [(valueName, boolType)]
+            intType
+            [ CoreIf
+                (CoreVariable valueName boolType)
+                [CoreReturn (CoreLiteral (CoreInteger 1) intType)]
+                [CoreReturn (CoreLiteral (CoreInteger 2) intType)]
+            ]
+        ]
+
+validDirectCall :: CoreModule
+validDirectCall =
+    coreModule
+        [ function mainName [] intType [CoreReturn (CoreApply helperReference [intValue] intType)]
+        , helperFunction
+        ]
+
+validClosure :: CoreModule
+validClosure = coreModule [unitFunction mainName [CoreEvaluate validClosureValue]]
+
+loopModule :: [CoreStatement] -> CoreModule
+loopModule body = coreModule [unitFunction mainName body]
+
+forModule :: [CoreStatement] -> [CoreStatement] -> CoreModule
+forModule body update = loopModule [CoreFor boolValue body update]
+ test/DiagnosticProtocolTests.hs view
@@ -0,0 +1,269 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module DiagnosticProtocolTests (diagnosticProtocolTests) where
+
+import Data.ByteString qualified as ByteString
+import Data.Word (Word8)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Diagnostic.Protocol
+
+diagnosticProtocolTests :: [(String, Bool)]
+diagnosticProtocolTests =
+    [ ("diagnostic wire has a stable empty-document header", emptyGolden)
+    , ("diagnostic wire round-trips a compiler diagnostic", simpleRoundTrip)
+    , ("diagnostic conversion changes source positions to zero-based coordinates", convertsCoordinates)
+    , ("diagnostic wire round-trips every stage", everyStageRoundTrips)
+    , ("diagnostic wire round-trips both frontend severities", everySeverityRoundTrips)
+    , ("diagnostic wire preserves supplementary Unicode scalars", unicodeRoundTrip)
+    , ("diagnostic wire preserves arguments, related locations, and fixes", richRoundTrip)
+    , ("diagnostic wire rejects bad magic", rejectsMagic)
+    , ("diagnostic wire rejects unsupported versions", rejectsVersion)
+    , ("diagnostic wire rejects reserved flags", rejectsFlags)
+    , ("diagnostic wire rejects trailing bytes", rejectsTrailingBytes)
+    , ("diagnostic wire rejects every truncated prefix", rejectsTruncation)
+    , ("diagnostic wire rejects unknown stage tags", rejectsUnknownStage)
+    , ("diagnostic wire rejects unknown severity tags", rejectsUnknownSeverity)
+    , ("diagnostic wire rejects invalid Boolean tags", rejectsInvalidBoolean)
+    , ("diagnostic wire rejects malformed codes", rejectsMalformedCode)
+    , ("diagnostic wire rejects duplicate argument names", rejectsDuplicateArguments)
+    , ("diagnostic wire rejects reversed source ranges", rejectsReversedRange)
+    , ("diagnostic wire rejects empty fixes", rejectsEmptyFix)
+    , ("diagnostic wire enforces record limits before encoding", encodeRecordLimit)
+    , ("diagnostic wire enforces record limits before decoding", decodeRecordLimit)
+    , ("diagnostic wire enforces text scalar limits", textLimit)
+    , ("diagnostic wire enforces total byte limits", byteLimit)
+    , ("diagnostic conversion rejects non-positive source coordinates", rejectsInvalidCoordinate)
+    ]
+
+emptyGolden :: Bool
+emptyGolden =
+    encode emptyDocument
+        == Right (ByteString.pack [0x54, 0x41, 0x44, 0x47, 1, 0, 0, 0, 0, 0, 0, 0])
+
+simpleRoundTrip :: Bool
+simpleRoundTrip = case diagnosticDocument [sampleDiagnostic] of
+    Left _ -> False
+    Right document -> decodeEncoded document == Right document
+
+convertsCoordinates :: Bool
+convertsCoordinates = case diagnosticDocument [sampleDiagnostic] of
+    Right (DiagnosticDocument [record]) ->
+        recordPrimary record
+            == Just (DiagnosticLocation "Main.tala" 7 12 7 18)
+    _ -> False
+
+everyStageRoundTrips :: Bool
+everyStageRoundTrips =
+    all
+        (\stage -> roundTrips (simpleRecord {recordStage = stage}))
+        [minBound .. maxBound]
+
+everySeverityRoundTrips :: Bool
+everySeverityRoundTrips =
+    all
+        (\severity -> roundTrips (simpleRecord {recordSeverity = severity}))
+        [minBound .. maxBound]
+
+unicodeRoundTrip :: Bool
+unicodeRoundTrip =
+    roundTrips
+        (simpleRecord {recordMessage = "Unicode scalar: \x1f9ea; path: C:/\x03b4/Main.tala"})
+
+richRoundTrip :: Bool
+richRoundTrip = roundTrips richRecord
+
+rejectsMagic :: Bool
+rejectsMagic = rejected (replace 0 0x42 emptyBytes)
+
+rejectsVersion :: Bool
+rejectsVersion = rejected (replace 4 2 emptyBytes)
+
+rejectsFlags :: Bool
+rejectsFlags = rejected (replace 6 1 emptyBytes)
+
+rejectsTrailingBytes :: Bool
+rejectsTrailingBytes = rejected (ByteString.snoc emptyBytes 0)
+
+rejectsTruncation :: Bool
+rejectsTruncation = case encode (DiagnosticDocument [richRecord]) of
+    Left _ -> False
+    Right bytes ->
+        all (rejected . (`ByteString.take` bytes)) [0 .. ByteString.length bytes - 1]
+            && decode bytes == Right (DiagnosticDocument [richRecord])
+
+rejectsUnknownStage :: Bool
+rejectsUnknownStage = rejected (replace 12 0xff simpleBytes)
+
+rejectsUnknownSeverity :: Bool
+rejectsUnknownSeverity = rejected (replace 13 0xff simpleBytes)
+
+rejectsInvalidBoolean :: Bool
+rejectsInvalidBoolean = case primaryPresenceOffset of
+    Nothing -> False
+    Just offset -> rejected (replace offset 2 simpleBytes)
+
+rejectsMalformedCode :: Bool
+rejectsMalformedCode =
+    left (encode (DiagnosticDocument [simpleRecord {recordCode = "lowercase"}]))
+        && left (encode (DiagnosticDocument [simpleRecord {recordCode = ""}]))
+
+rejectsDuplicateArguments :: Bool
+rejectsDuplicateArguments =
+    left
+        ( encode
+            ( DiagnosticDocument
+                [ simpleRecord
+                    { recordArguments =
+                        [DiagnosticArgument "name" "first", DiagnosticArgument "name" "second"]
+                    }
+                ]
+            )
+        )
+
+rejectsReversedRange :: Bool
+rejectsReversedRange =
+    left
+        ( encode
+            ( DiagnosticDocument
+                [ simpleRecord
+                    { recordPrimary = Just (DiagnosticLocation "Main.tala" 4 1 3 9)
+                    }
+                ]
+            )
+        )
+
+rejectsEmptyFix :: Bool
+rejectsEmptyFix =
+    left
+        ( encode
+            ( DiagnosticDocument
+                [simpleRecord {recordFixes = [DiagnosticFix "Apply correction" []]}]
+            )
+        )
+
+encodeRecordLimit :: Bool
+encodeRecordLimit =
+    let limits = defaultDiagnosticProtocolLimits {maximumRecords = 0}
+     in left (encodeDiagnosticDocument limits (DiagnosticDocument [simpleRecord]))
+
+decodeRecordLimit :: Bool
+decodeRecordLimit =
+    let limits = defaultDiagnosticProtocolLimits {maximumRecords = 0}
+     in left (decodeDiagnosticDocument limits simpleBytes)
+
+textLimit :: Bool
+textLimit =
+    let limits = defaultDiagnosticProtocolLimits {maximumTextScalars = 2}
+     in left (encodeDiagnosticDocument limits (DiagnosticDocument [simpleRecord]))
+
+byteLimit :: Bool
+byteLimit =
+    let limits = defaultDiagnosticProtocolLimits {maximumWireBytes = 11}
+     in left (decodeDiagnosticDocument limits emptyBytes)
+            && left (encodeDiagnosticDocument limits emptyDocument)
+
+rejectsInvalidCoordinate :: Bool
+rejectsInvalidCoordinate =
+    left
+        ( diagnosticDocument
+            [ sampleDiagnostic
+                { diagnosticSpan =
+                    Just (SourceSpan "Main.tala" (SourcePosition 0 1) (SourcePosition 1 1))
+                }
+            ]
+        )
+
+sampleDiagnostic :: Diagnostic
+sampleDiagnostic =
+    Diagnostic
+        TypeCheckerStage
+        Error
+        "TAT204"
+        (Just (SourceSpan "Main.tala" (SourcePosition 8 13) (SourcePosition 8 19)))
+        "argument cannot be converted"
+
+simpleRecord :: DiagnosticRecord
+simpleRecord =
+    DiagnosticRecord
+        { recordStage = ParserStage
+        , recordSeverity = ProtocolError
+        , recordCode = "TAP100"
+        , recordMessage = "expected declaration"
+        , recordArguments = []
+        , recordPrimary = Nothing
+        , recordRelated = []
+        , recordFixes = []
+        }
+
+richRecord :: DiagnosticRecord
+richRecord =
+    DiagnosticRecord
+        { recordStage = TypeCheckerStage
+        , recordSeverity = ProtocolWarning
+        , recordCode = "TAT204"
+        , recordMessage = "argument {actual} cannot be converted to {expected}"
+        , recordArguments =
+            [ DiagnosticArgument "actual" "String"
+            , DiagnosticArgument "expected" "int"
+            ]
+        , recordPrimary = Just mainLocation
+        , recordRelated =
+            [ DiagnosticRelatedLocation
+                (DiagnosticLocation "Library.tala" 2 4 2 12)
+                "parameter is declared here"
+            ]
+        , recordFixes =
+            [ DiagnosticFix
+                "Convert the argument"
+                [DiagnosticTextEdit mainLocation "value.ToInt()"]
+            ]
+        }
+
+mainLocation :: DiagnosticLocation
+mainLocation = DiagnosticLocation "Main.tala" 7 12 7 18
+
+emptyDocument :: DiagnosticDocument
+emptyDocument = DiagnosticDocument []
+
+emptyBytes :: ByteString.ByteString
+emptyBytes = either (const ByteString.empty) id (encode emptyDocument)
+
+simpleBytes :: ByteString.ByteString
+simpleBytes = either (const ByteString.empty) id (encode (DiagnosticDocument [simpleRecord]))
+
+-- The primary-presence byte follows the fixed header, tags, two counted scalar
+-- strings, and the zero argument count. Computing it keeps this test readable
+-- while still pinning the exact wire layout.
+primaryPresenceOffset :: Maybe Int
+primaryPresenceOffset =
+    let scalarBytes value = 4 + 4 * length value
+        offset = 12 + 2 + scalarBytes (recordCode simpleRecord) + scalarBytes (recordMessage simpleRecord) + 4
+     in if offset < ByteString.length simpleBytes then Just offset else Nothing
+
+encode :: DiagnosticDocument -> Either DiagnosticProtocolError ByteString.ByteString
+encode = encodeDiagnosticDocument defaultDiagnosticProtocolLimits
+
+decode :: ByteString.ByteString -> Either DiagnosticProtocolError DiagnosticDocument
+decode = decodeDiagnosticDocument defaultDiagnosticProtocolLimits
+
+decodeEncoded :: DiagnosticDocument -> Either DiagnosticProtocolError DiagnosticDocument
+decodeEncoded document = encode document >>= decode
+
+roundTrips :: DiagnosticRecord -> Bool
+roundTrips record = decodeEncoded (DiagnosticDocument [record]) == Right (DiagnosticDocument [record])
+
+rejected :: ByteString.ByteString -> Bool
+rejected = left . decode
+
+left :: Either leftValue rightValue -> Bool
+left value = case value of
+    Left _ -> True
+    Right _ -> False
+
+replace :: Int -> Word8 -> ByteString.ByteString -> ByteString.ByteString
+replace index value bytes =
+    ByteString.take index bytes
+        <> ByteString.singleton value
+        <> ByteString.drop (index + 1) bytes
+ test/DiagnosticSideChannelTests.hs view
@@ -0,0 +1,104 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module DiagnosticSideChannelTests (diagnosticSideChannelTests) where
+
+import Control.Exception (finally)
+import Data.ByteString qualified as ByteString
+import System.Directory (doesFileExist, getTemporaryDirectory, removeFile)
+import System.FilePath ((</>))
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Diagnostic.Protocol
+import Progmasoft.Tala.Diagnostic.SideChannel
+
+diagnosticSideChannelTests :: [(String, IO Bool)]
+diagnosticSideChannelTests =
+    [ ("diagnostic side channel writes the stable empty success document", writesEmptyDocument)
+    , ("diagnostic side channel writes an error document", writesErrorDocument)
+    , ("diagnostic side channel overwrites stale bytes", overwritesStaleBytes)
+    , ("diagnostic side channel rejects an empty path", rejectsEmptyPath)
+    , ("diagnostic side channel reports unavailable parent directories", reportsWriteFailure)
+    , ("diagnostic side channel rejects invalid source coordinates before I/O", rejectsInvalidModel)
+    ]
+
+writesEmptyDocument :: IO Bool
+writesEmptyDocument = withDiagnosticPath "empty" $ \path -> do
+    written <- writeDiagnosticFile path []
+    bytes <- ByteString.readFile path
+    pure
+        ( written == Right ()
+            && decodeDiagnosticDocument defaultDiagnosticProtocolLimits bytes == Right (DiagnosticDocument [])
+        )
+
+writesErrorDocument :: IO Bool
+writesErrorDocument = withDiagnosticPath "error" $ \path -> do
+    written <- writeDiagnosticFile path [sampleDiagnostic]
+    bytes <- ByteString.readFile path
+    pure $ case (written, decodeDiagnosticDocument defaultDiagnosticProtocolLimits bytes) of
+        (Right (), Right (DiagnosticDocument [record])) ->
+            recordStage record == ParserStage
+                && recordSeverity record == ProtocolError
+                && recordCode record == "TAP100"
+                && recordMessage record == "expected declaration"
+                && recordPrimary record == Just (DiagnosticLocation "Main.tala" 3 8 3 13)
+        _ -> False
+
+overwritesStaleBytes :: IO Bool
+overwritesStaleBytes = withDiagnosticPath "stale" $ \path -> do
+    ByteString.writeFile path (ByteString.replicate 256 0xff)
+    written <- writeDiagnosticFile path []
+    bytes <- ByteString.readFile path
+    pure
+        ( written == Right ()
+            && ByteString.length bytes == 12
+            && decodeDiagnosticDocument defaultDiagnosticProtocolLimits bytes == Right (DiagnosticDocument [])
+        )
+
+rejectsEmptyPath :: IO Bool
+rejectsEmptyPath = do
+    result <- writeDiagnosticFile "" []
+    pure $ case result of
+        Left (SideChannelWriteError "" _) -> True
+        _ -> False
+
+reportsWriteFailure :: IO Bool
+reportsWriteFailure = do
+    temporary <- getTemporaryDirectory
+    let path = temporary </> "xide-side-channel-missing-parent" </> "diagnostics.vxdg"
+    result <- writeDiagnosticFile path []
+    pure $ case result of
+        Left (SideChannelWriteError failedPath _) -> failedPath == path
+        _ -> False
+
+rejectsInvalidModel :: IO Bool
+rejectsInvalidModel = withDiagnosticPath "invalid" $ \path -> do
+    let invalid =
+            sampleDiagnostic
+                { diagnosticSpan =
+                    Just (SourceSpan "Main.tala" (SourcePosition 0 1) (SourcePosition 1 1))
+                }
+    result <- writeDiagnosticFile path [invalid]
+    exists <- doesFileExist path
+    pure $ case result of
+        Left (SideChannelModelError _) -> not exists
+        _ -> False
+
+sampleDiagnostic :: Diagnostic
+sampleDiagnostic =
+    Diagnostic
+        ParserStage
+        Error
+        "TAP100"
+        (Just (SourceSpan "Main.tala" (SourcePosition 4 9) (SourcePosition 4 14)))
+        "expected declaration"
+
+withDiagnosticPath :: String -> (FilePath -> IO Bool) -> IO Bool
+withDiagnosticPath suffix action = do
+    temporary <- getTemporaryDirectory
+    let path = temporary </> ("tala-diagnostics-" ++ suffix ++ ".vxdg")
+        cleanup = do
+            exists <- doesFileExist path
+            if exists then removeFile path else pure ()
+    cleanup
+    action path `finally` cleanup
+ test/EffectTests.hs view
@@ -0,0 +1,214 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Which expressions the compiler takes to have an effect.
+
+Evaluation is by need and effects are not: an expression that writes to the
+console is evaluated where it stands. The source does not say which
+expressions write, so the frontend infers it from the bodies of the methods
+a program has; see "Progmasoft.Tala.Desugarer.Effects". These cases hold that
+inference from both sides:
+
+* it must find every write, however far down the calls it is, or a program
+  would write later than it says, or not at all;
+* it must not find writes that are not there, or a program without output
+  would lose the evaluation by need it had.
+
+Each program is checked twice: for what it writes, in the reference
+evaluator before and after optimization, and for the shape of its Core,
+where a flag named @$known@ marks a binding that was deferred and
+@CoreMemoize@ an argument that was suspended.
+
+Where the inference errs on purpose, the case says so and pins it, so that
+making it more exact shows as a change here.
+-}
+module EffectTests (effectTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf)
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+
+effectTests :: [(String, Bool)]
+effectTests =
+    concat
+        [ [ ("unoptimized: " ++ label, written artifactCore members body == Just expected)
+          , ("optimized: " ++ label, written artifactOptimizedCore members body == Just expected)
+          ]
+        | (label, members, body, expected) <- orders
+        ]
+        ++ [ (label, deferred members body == expected) | (label, members, body, expected) <- deferrals
+           ]
+        ++ [ (label, suspended members body == expected) | (label, members, body, expected) <- suspensions
+           ]
+
+-- | A method that writes, and methods that reach it through one and two calls.
+chain :: String
+chain =
+    unlines
+        [ "    public static int Log(_ int v) { Console.Println(v); return v; }"
+        , "    public static int Once(_ int v) { return Log(v) + 1; }"
+        , "    public static int Twice(_ int v) { return Once(v) + 1; }"
+        , "    public static int Quiet(_ int v) { return v + 1; }"
+        , "    public static int Deep(_ int v) { return Quiet(Quiet(v)); }"
+        , "    public static int Zero() { return 0; }"
+        ]
+
+-- | Two methods that call each other, one of which writes.
+mutual :: String
+mutual =
+    unlines
+        [ "    public static int Ping(_ int n) { if (n <= 0) { return 0; } return Pong(n - 1) + 1; }"
+        , "    public static int Pong(_ int n) { Console.Print(n); if (n <= 0) { return 0; } return Ping(n - 1) + 1; }"
+        , "    public static int Even(_ int n) { if (n <= 0) { return 1; } return Odd(n - 1); }"
+        , "    public static int Odd(_ int n) { if (n <= 0) { return 0; } return Even(n - 1); }"
+        , "    public static int Zero() { return 0; }"
+        ]
+
+-- | Methods with parameters that are read first, after a write, and never.
+parameters :: String
+parameters =
+    unlines
+        [ "    public static int Log(_ int v) { Console.Println(v); return v; }"
+        , "    public static int Zero() { return 0; }"
+        , "    public static int Pick(_ int flag, _ int value) { if (flag > 0) { return value; } return 7; }"
+        , "    public static int Announce(_ int flag, _ int value) { Console.Print(\"a\"); if (flag > 0) { return value; } return 7; }"
+        , "    public static int Through(_ int flag, _ int value) { return Announce(flag, value); }"
+        ]
+
+-- | Callables that write and callables that do not.
+callables :: String
+callables =
+    unlines
+        [ "    public static int Log(_ int v) { Console.Println(v); return v; }"
+        , "    public static int Zero() { return 0; }"
+        , "    public static int Apply(_ int v) { auto step = \\(int w) -> w + 1; return step(v); }"
+        , "    public static int Run(_ (int) -> int f) { return f(3); }"
+        , "    public static int Quiet(_ int v) { return v + 1; }"
+        ]
+
+{- | What a program writes: the label, the members beside @Main@, the body of
+@Main@ and the text. An expression that writes is carried out where it
+stands, in order, whether or not anything needs its value.
+-}
+orders :: [(String, String, String, String)]
+orders =
+    [ ("a write one call down happens at the binding", chain, "int x = Once(1); Console.Println(9);", "1\n9\n")
+    , ("a write two calls down happens at the binding", chain, "int x = Twice(1); Console.Println(9);", "1\n9\n")
+    , ("bindings that write keep their order", chain, "int a = Twice(1); int b = Once(2); int c = Log(3); Console.Println(a + b + c);", "1\n2\n3\n9\n")
+    , ("a binding that only computes does not move a write", chain, "int a = Deep(1); int b = Log(2); Console.Println(a);", "2\n3\n")
+    , ("a value that is never needed is not computed beside writes", chain, "int a = Deep(1) / Zero(); int b = Log(2);", "2\n")
+    , ("a write in a method that calls itself through another", mutual, "int x = Ping(4); Console.Println(\"!\");", "31!\n")
+    , ("methods that call each other and never write stay quiet", mutual, "int x = Even(5); Console.Println(x);", "0\n")
+    , ("a recursion that is never needed does not run", mutual, "int x = Even(4) / Zero(); Console.Println(\"ok\");", "ok\n")
+    , ("an argument that writes is carried out at the call", parameters, "Console.Println(Pick(0, Log(5)));", "5\n7\n")
+    , ("an argument that writes is carried out before the method writes", parameters, "Console.Println(Announce(0, Log(5)));", "5\na7\n")
+    , ("a method that writes may still leave an argument unused", parameters, "Console.Println(Announce(0, 8 / Zero()));", "a7\n")
+    , ("an argument handed through a method that writes may stay unused", parameters, "Console.Println(Through(0, 8 / Zero()));", "a7\n")
+    , ("an argument handed through is computed when it is needed", parameters, "Console.Println(Through(1, 8 / 2));", "a4\n")
+    , ("arguments that write keep the order they are written in", parameters, "Console.Println(Pick(Log(1), Log(2)));", "1\n2\n2\n")
+    , ("a callable that writes is called where its call stands", callables, "auto say = \\(int v) -> Log(v); int x = say(1); Console.Println(9);", "1\n9\n")
+    , ("a callable that writes directly is called where its call stands", callables, "auto say = \\(int v) -> { Console.Println(v); return v; }; int x = say(1); Console.Println(9);", "1\n9\n")
+    , ("creating a callable that writes writes nothing", callables, "auto say = \\(int v) -> Log(v); Console.Println(9);", "9\n")
+    , ("a callable is called as often as its call is reached", callables, "auto say = \\(int v) -> Log(v); int t = 0; for (int i = 0; i < 3; i += 1) { t += say(i); } Console.Println(t);", "0\n1\n2\n3\n")
+    , -- A method is a callable value too, and no callable expression is
+      -- written anywhere in these programs.
+      ("a method that writes held as a value is called where its call stands", callables, "auto f = Log; int x = f(1); Console.Println(9);", "1\n9\n")
+    , ("a method that writes handed to a method is called where that call stands", callables, "int x = Run(Log); Console.Println(9);", "3\n9\n")
+    , ("a quiet method handed to a method is called when its value is needed", callables, "int x = Run(Quiet); Console.Println(x);", "4\n")
+    , ("a quiet method handed to a method is not called for a value nothing needs", callables, "int x = Run(Quiet) / Zero(); Console.Println(9);", "9\n")
+    , ("a method that writes called through its class writes at the binding", callables, "int x = Program.Log(1); Console.Println(9);", "1\n9\n")
+    , ("a method with a quiet callable computes", callables, "Console.Println(Apply(4));", "5\n")
+    , ("a write in a condition happens when the condition is evaluated", chain, "if (Log(1) > 0) { Console.Println(\"yes\"); }", "1\nyes\n")
+    , ("a write behind a short circuit happens only when it is reached", chain, "bool b = Zero() > 0 && Log(1) > 0; Console.Println(\"end\");", "end\n")
+    , ("a write in the taken arm of a conditional happens", chain, "int v = Zero() > 0 ? Log(1) : Log(2); Console.Println(v);", "2\n2\n")
+    , ("a write in a loop condition happens on every pass", chain, "int i = 0; while (Log(i) < 2) { i += 1; }", "0\n1\n2\n")
+    , ("a discarded value that writes is carried out", chain, "_ = Twice(4);", "4\n")
+    , ("a statement that writes is carried out", chain, "Twice(4);", "4\n")
+    ]
+
+{- | Whether the Core of @Main@ holds a deferred binding. A binding whose
+initializer may write is never deferred; one that only computes still is.
+-}
+deferrals :: [(String, String, String, Bool)]
+deferrals =
+    [ ("a binding of a method that writes is not deferred", chain, "int x = Log(1); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding one call above a write is not deferred", chain, "int x = Once(1); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding two calls above a write is not deferred", chain, "int x = Twice(1); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a method that only computes is deferred", chain, "int x = Quiet(1); if (Zero() > 0) { Console.Println(x); }", True)
+    , ("a binding two calls above nothing is deferred", chain, "int x = Deep(1); if (Zero() > 0) { Console.Println(x); }", True)
+    , ("a binding with a write in one operand is not deferred", chain, "int x = Quiet(1) + Log(2); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding with a write in a conditional arm is not deferred", chain, "int x = Zero() > 0 ? Log(1) : 2; if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a recursion that writes is not deferred", mutual, "int x = Ping(2); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a recursion that only computes is deferred", mutual, "int x = Even(2); if (Zero() > 0) { Console.Println(x); }", True)
+    , -- No callable of this program writes, so a call through one computes.
+      ("a binding of a callable is deferred when no callable writes", callables, "auto step = \\(int w) -> w + 1; int x = step(1); if (Zero() > 0) { Console.Println(x); }", True)
+    , ("a binding of a callable is not deferred when a callable writes", callables, "auto say = \\(int v) -> Log(v); int x = say(1); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a method value that writes is not deferred", callables, "auto f = Log; int x = f(1); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a call handing on a method that writes is not deferred", callables, "int x = Run(Log); if (Zero() > 0) { Console.Println(x); }", False)
+    , ("a binding of a call handing on a quiet method is deferred", callables, "int x = Run(Quiet); if (Zero() > 0) { Console.Println(x); }", True)
+    , -- Calling a method by name does not make it a value.
+      ("a method that writes and is only called by name leaves callables quiet", callables, "Log(0); auto step = \\(int w) -> w + 1; int x = step(1); if (Zero() > 0) { Console.Println(x); }", True)
+    , -- The inference does not follow which callable a value holds: once
+      -- any callable of the program writes, a call through any callable
+      -- is taken to write. Pinned, so that a more exact answer shows.
+      ("a quiet callable is taken to write beside one that does", callables, "auto say = \\(int v) -> Log(v); auto step = \\(int w) -> w + 1; int x = step(1); if (Zero() > 0) { Console.Println(x); }", False)
+    ]
+
+{- | Whether the Core of @Main@ suspends an argument. An argument that may
+write is computed at the call; one that only computes, and may fail, is
+suspended when the method may leave it unused.
+-}
+suspensions :: [(String, String, String, Bool)]
+suspensions =
+    [ ("an argument that only computes is suspended", parameters, "Console.Println(Pick(0, 8 / Zero()));", True)
+    , ("an argument that writes is not suspended", parameters, "Console.Println(Pick(0, Log(5)));", False)
+    , ("an argument with a write in one operand is not suspended", parameters, "Console.Println(Pick(0, 8 / Zero() + Log(5)));", False)
+    , -- A method that writes before it reads a parameter is not certain to
+      -- need the parameter first, so the argument may stay unused.
+      ("an argument of a method that writes first is suspended", parameters, "Console.Println(Announce(0, 8 / Zero()));", True)
+    , ("an argument handed through a method is suspended", parameters, "Console.Println(Through(0, 8 / Zero()));", True)
+    , ("an argument that cannot fail is passed as a value", parameters, "Console.Println(Pick(0, 8 + 1));", False)
+    ]
+
+program :: String -> String -> String
+program members body =
+    unlines
+        [ "namespace Demo;"
+        , "public class Program {"
+        , members
+        , "    public static void Main() {"
+        , "        " ++ body
+        , "    }"
+        , "}"
+        ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "effects.tala" text)
+
+-- | What @Main@ writes to standard output.
+written :: (FrontendArtifacts -> CoreModule) -> String -> String -> Maybe String
+written select members body = case compileSource (program members body) of
+    Right artifacts -> case runFunctionWriting 200000 (select artifacts) "Main" [] of
+        Just (_, Written output "") -> Just output
+        _ -> Nothing
+    Left _ -> Nothing
+
+-- | Whether the unoptimized Core of @Main@ mentions the given text.
+mainMentions :: String -> String -> String -> Bool
+mainMentions needle members body = case compileSource (program members body) of
+    Right artifacts ->
+        any
+            ((needle `isInfixOf`) . show . coreFunctionBody)
+            [ function
+            | function <- coreModuleFunctions (artifactCore artifacts)
+            , "Main" `isInfixOf` show (coreFunctionName function)
+            ]
+    Left _ -> False
+
+deferred :: String -> String -> Bool
+deferred = mainMentions "$known"
+
+suspended :: String -> String -> Bool
+suspended = mainMentions "CoreMemoize"
+ test/EnumTests.hs view
@@ -0,0 +1,283 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for classic enums.
+
+A classic enum is a value type whose members are named integers. The tests
+pin its declaration, the numbering of its members, the one comparison its
+values have, the absence of any conversion to or from an integer, and
+@match@ over it: a case pattern names a member, two members with one value
+are one case, and a match that names every value needs no catch-all arm.
+
+The expected values are written by hand. Every accepted program runs in the
+reference evaluator on the unoptimized and on the optimized Core and is
+verified as Core and as CorePrep; an enum must reach Core as its underlying
+integer type and never as a named type.
+-}
+module EnumTests (enumTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+
+enumTests :: [(String, Bool)]
+enumTests =
+    parserTests
+        ++ concat
+            [ [ ("unoptimized Core computes " ++ label, runs artifactCore statements arguments expected)
+              , ("optimized Core computes " ++ label, runs artifactOptimizedCore statements arguments expected)
+              ]
+            | (statements, runsOfCase) <- evaluationCases
+            , (arguments, expected) <- runsOfCase
+            , let label = show expected ++ " for " ++ show arguments ++ ": " ++ statements
+            ]
+        ++ [ ("every enum program verifies as Core and as CorePrep", all (verifies . fst) evaluationCases)
+           , ("no enum reaches Core as a named type", all (erased . fst) evaluationCases)
+           ]
+        ++ [(label, rejectedWith code (program statements)) | (label, code, statements) <- rejectedBodies]
+        ++ [(label, rejectedWith code (declarations ++ emptyProgram)) | (label, code, declarations) <- rejectedDeclarations]
+        ++ [ ("an enum without members is a declaration", accepted ("enum Empty { }\n" ++ emptyProgram))
+           , ("an enum may be declared after the class that follows it in use", accepted (emptyProgram ++ "enum Later { ONE }\n"))
+           , ("a negative member value is accepted in a signed type", accepted ("enum Signed { LOW = -5, NEXT }\n" ++ emptyProgram))
+           ]
+
+-- ---------------------------------------------------------------- sources
+
+enums :: String
+enums =
+    unlines
+        [ "enum Status { NONE, UNKNOWN = 0, READY }"
+        , "enum Level = byte { LOW = 1, MID, HIGH = 10, TOP }"
+        , -- Values computed from earlier members: 1, 2, 4, 7, 8, 2, 250, 5.
+          "enum Flag = ubyte { READ = 1, WRITE = READ << 1, EXECUTE = WRITE * 2, ALL = READ | WRITE | EXECUTE, NEXT,"
+            ++ " HALF = EXECUTE / 2, REST = !(EXECUTE + 1), ROUNDED = 9 // 2 }"
+        , "enum Offset = int { BASE = -3, ABOVE = BASE + 5, POWER = 2 ** 10, MODULO = -7 % 4, MIX = (ABOVE ^ 7) & 6 }"
+        ]
+
+-- | A program around the given body of @Evaluate@.
+program :: String -> String
+program statements =
+    "namespace Test;\n"
+        ++ enums
+        ++ unlines
+            [ "class Program {"
+            , "    public static Status Pick(_ int v) { if (v > 0) { return Status.READY; } return Status.NONE; }"
+            , "    public static int Rank(_ Level l) { return match (l) { .LOW -> 1, .MID -> 2, .HIGH -> 10, .TOP -> 11 }; }"
+            , "    public static Level Raise(_ Level l) { return match (l) { .LOW -> Level.MID, .MID -> Level.HIGH, _ -> Level.TOP }; }"
+            , "    public static Flag Bit(_ int v) { return match (v) { 1 -> .READ, 2 -> .WRITE, 4 -> .EXECUTE, 7 -> .ALL, 8 -> .NEXT, 250 -> .REST, 5 -> .ROUNDED, _ -> .HALF }; }"
+            , "    public static Offset At(_ int v) { return match (v) { -3 -> .BASE, 2 -> .ABOVE, 1024 -> .POWER, _ -> .MODULO }; }"
+            , "    public static int Evaluate(_ int left, _ int right) {"
+            , "        " ++ statements
+            , "    }"
+            , "}"
+            ]
+
+emptyProgram :: String
+emptyProgram = "class Program { public static int Evaluate(_ int left, _ int right) { return 0; } }\n"
+
+-- ----------------------------------------------------------------- parser
+
+parserTests :: [(String, Bool)]
+parserTests =
+    [ ("an enum declaration keeps its members in order", memberNames "enum Direction { NORTH, EAST, SOUTH, WEST, }" == Just ["NORTH", "EAST", "SOUTH", "WEST"])
+    , ("a member keeps the value written for it", memberValues "enum Value = int { FIRST = 5, SECOND, THIRD = 10, FOURTH }" == Just [Just "5", Nothing, Just "10", Nothing])
+    , ("a member value may be negative", memberValues "enum Signed { LOW = -5 }" == Just [Just "-5"])
+    , -- A value is an expression; a comma ends it and the next member begins.
+      ("a member value is an expression", memberValues "enum Bits { A = 1, B = A << 1, C = (A | B) + 2 * 3, D }" == Just [Just "1", Just "A<<1", Just "A|B+2*3", Nothing])
+    , ("a member value is not an assignment", not (parses "enum Bad { A = 1, B = A = 2 }"))
+    , ("the underlying type is kept", underlying "enum Status = byte { PENDING = 0 }" == Just (Just (ExplicitType (Identifier "byte"))))
+    , ("the underlying type may be absent", underlying "enum Status { PENDING }" == Just Nothing)
+    , ("enum is a reserved word", not (parses "class enum { }"))
+    ]
+    where
+        -- The underlying type and the members of the first declaration.
+        declaration text = case parseSource text of
+            Right (ParsedAST (SyntaxTree _ (EnumDeclaration _ _ _ written members : _))) -> Just (written, members)
+            _ -> Nothing
+        memberNames text = map (identifierText . enumCaseName) . snd <$> declaration text
+        memberValues text = map (fmap spelled . enumCaseValue) . snd <$> declaration text
+        -- The operators and operands of a value in the order they are written.
+        spelled :: Expression Identifier () -> String
+        spelled value = case value of
+            LiteralExpression _ (IntegerLiteral number) _ -> show number
+            NameExpression _ name _ -> identifierText name
+            UnaryExpression _ UnaryNegate operand _ -> "-" ++ spelled operand
+            BinaryExpression _ operator left right _ -> spelled left ++ sign operator ++ spelled right
+            _ -> "?"
+        sign operator = case operator of
+            Add -> "+"
+            Multiply -> "*"
+            ShiftLeft -> "<<"
+            BitwiseOr -> "|"
+            _ -> "?"
+        underlying text = fst <$> declaration text
+
+-- -------------------------------------------------------------- evaluation
+
+-- | Bodies of @Evaluate@, the arguments to run them on, and the value each run must return.
+evaluationCases :: [(String, [((Integer, Integer), Integer)])]
+evaluationCases =
+    [ -- Values of an enum are compared with == and \=.
+      ("Status s = Pick(left); return s == Status.READY ? 1 : 2;", [((3, 0), 1), ((0, 0), 2)])
+    , ("Status s = Pick(left); return s \\= Status.NONE ? 1 : 2;", [((3, 0), 1), ((0, 0), 2)])
+    , -- Two members with one value are equal.
+      ("return Status.NONE == Status.UNKNOWN ? 1 : 2;", [((0, 0), 1)])
+    , -- Members are numbered from zero; one without a value follows the one before it.
+      ("return Rank(Level.LOW) + Rank(Level.MID) * 10 + Rank(Level.HIGH) * 100 + Rank(Level.TOP) * 1000;", [((0, 0), 12021)])
+    , -- A match that names every value needs no catch-all arm.
+      ("return match (Pick(left)) { .NONE -> 10, .READY -> 20 };", [((3, 0), 20), ((0, 0), 10)])
+    , -- A member is named through either of its names.
+      ("return match (Pick(left)) { .UNKNOWN -> 10, .READY -> 20 };", [((0, 0), 10)])
+    , ("Level l = Level.MID; return match (l) { .LOW -> 1, _ -> 9 };", [((0, 0), 9)])
+    , -- A guard does not count towards completeness, and a catch-all is then needed.
+      ("return match (Pick(left)) { .READY if (right > 0) -> 1, .READY -> 2, .NONE -> 3 };", [((1, 1), 1), ((1, 0), 2), ((0, 5), 3)])
+    , -- An enum is assigned, passed, returned and yielded by a match.
+      ("Status s = Status.NONE; s = Status.READY; return s == Status.READY ? 7 : 8;", [((0, 0), 7)])
+    , ("return Rank(Raise(Raise(Level.LOW))) * 100 + Rank(Raise(Level.HIGH));", [((0, 0), 1011)])
+    , ("Status s = match (left) { 1 -> Status.READY, _ -> Status.NONE }; return s == Status.READY ? 1 : 0;", [((1, 0), 1), ((2, 0), 0)])
+    , -- The target-typed spelling names a member of the enum the place
+      -- expects: a declared type, a parameter, a return type, the other
+      -- operand of a comparison, the variable assigned to.
+      ("Status s = .READY; return s == Status.READY ? 1 : 2;", [((0, 0), 1)])
+    , ("Status s = Pick(left); return s == .READY ? 1 : 2;", [((3, 0), 1), ((0, 0), 2)])
+    , ("return Rank(.HIGH) + Rank(.LOW);", [((0, 0), 11)])
+    , ("Level l = .LOW; l = .TOP; return Rank(l);", [((0, 0), 11)])
+    , ("Status s = match (left) { 1 -> .READY, _ -> .NONE }; return s == .READY ? 1 : 0;", [((1, 0), 1), ((2, 0), 0)])
+    , -- A conditional, an if expression and a loop expression yield an enum.
+      ("Status s = left > 0 ? .READY : .NONE; return s == .READY ? 1 : 0;", [((1, 0), 1), ((0, 0), 0)])
+    , ("Level l = if (left > 0) { Level.HIGH } else { Level.LOW }; return Rank(l);", [((1, 0), 10), ((0, 0), 1)])
+    , ("Level l = while (true) { if (left > 0) { break Level.TOP; } break Level.MID; }; return Rank(l);", [((1, 0), 11), ((0, 0), 2)])
+    , -- Two subjects with closed sets of values are complete together.
+        ( "return match (Pick(left)), (right > 0) { (.NONE), (true) -> 1, (.NONE), (false) -> 2, (.READY), (true) -> 3, (.READY), (false) -> 4 };"
+        , [((0, 1), 1), ((0, 0), 2), ((1, 1), 3), ((1, 0), 4)]
+        )
+    , -- A member has the value its expression computes from the members
+      -- before it. Two members are equal exactly when their values are, so
+      -- comparing a member with the one a number selects reads its value.
+      ("return Bit(left) == Flag.WRITE ? 1 : 0;", [((2, 0), 1), ((1, 0), 0), ((4, 0), 0)])
+    , ("return Bit(left) == Flag.EXECUTE ? 1 : 0;", [((4, 0), 1), ((2, 0), 0)])
+    , ("return Bit(left) == Flag.ALL ? 1 : 0;", [((7, 0), 1), ((4, 0), 0)])
+    , -- A member without a value follows a computed one: ALL is 7, NEXT is 8.
+      ("return Bit(left) == Flag.NEXT ? 1 : 0;", [((8, 0), 1), ((7, 0), 0)])
+    , -- HALF is 4 / 2, the value of WRITE: the two names are one value.
+      ("return Flag.HALF == Flag.WRITE ? 1 : 0;", [((0, 0), 1)])
+    , -- The complement is that of the underlying type: !5 in ubyte is 250.
+      ("return Bit(left) == Flag.REST ? 1 : 0;", [((250, 0), 1), ((5, 0), 0)])
+    , -- 9 // 2 rounds to the nearest integer, away from zero at a half.
+      ("return Bit(left) == Flag.ROUNDED ? 1 : 0;", [((5, 0), 1), ((4, 0), 0)])
+    , ("return At(left) == Offset.ABOVE ? 1 : 0;", [((2, 0), 1), ((-3, 0), 0)])
+    , ("return At(left) == Offset.POWER ? 1 : 0;", [((1024, 0), 1), ((2, 0), 0)])
+    , -- -7 % 4 is -3, the value of BASE; (2 ^ 7) & 6 is 4.
+      ("return Offset.MODULO == Offset.BASE ? 1 : 0;", [((0, 0), 1)])
+    , ("return Offset.MIX == Offset.ABOVE ? 1 : 0;", [((0, 0), 0)])
+    , -- A match over an enum with computed values is complete when every
+      -- value is named, through whichever of its names.
+      ("return match (Bit(left)) { .READ -> 1, .HALF -> 2, .EXECUTE -> 3, .ALL -> 4, .NEXT -> 5, .REST -> 6, .ROUNDED -> 7 };", [((1, 0), 1), ((2, 0), 2), ((4, 0), 3), ((7, 0), 4), ((8, 0), 5), ((250, 0), 6), ((5, 0), 7)])
+    , -- An enum in a callable and in a loop.
+      ("auto f = \\(Status s) -> s == Status.READY ? 5 : 6; return f(Pick(left)) * 10 + f(Status.NONE);", [((2, 0), 56)])
+    , ("Level l = Level.LOW; int n = 0; while (l \\= Level.TOP) { l = Raise(l); n += 1; } return n;", [((0, 0), 3)])
+    ]
+
+-- ----------------------------------------------------------------- rejects
+
+rejectedBodies :: [(String, String, String)]
+rejectedBodies =
+    [ ("a match over an enum must name every value", "TAT0052", "return match (Pick(left)) { .NONE -> 10 };")
+    , ("a guarded arm does not complete a match over an enum", "TAT0052", "return match (Pick(left)) { .NONE -> 1, .READY if (right > 0) -> 2 };")
+    , ("a second arm for the same value can never be selected", "TAT0053", "return match (Pick(left)) { .NONE -> 10, .UNKNOWN -> 11, .READY -> 20 };")
+    , ("an enum has only the members it declares", "TAT0064", "Status s = Status.MISSING; return 0;")
+    , ("a case pattern names a member of the subject's enum", "TAT0064", "return match (Pick(left)) { .LOW -> 1, _ -> 2 };")
+    , ("a case pattern needs a subject of an enum type", "TAT0056", "return match (left) { .NONE -> 1, _ -> 2 };")
+    , ("values of an enum are not ordered", "TAT0065", "return Status.NONE < Status.READY ? 1 : 2;")
+    , ("values of an enum are not added", "TAT0065", "Status s = Status.NONE + Status.READY; return 0;")
+    , ("values of different enums are not compared", "TAT0065", "return Status.NONE == Level.LOW ? 1 : 2;")
+    , ("a value of an enum is not compared with an integer", "TAT0065", "return Status.NONE == 0 ? 1 : 2;")
+    , ("a value of an enum is not an integer", "TAT0002", "int x = Status.READY; return x;")
+    , ("an integer is not a value of an enum", "TAT0002", "Status s = 1; return 0;")
+    , ("a value of an enum is not a condition", "TAT0006", "if (Status.READY) { return 1; } return 2;")
+    , ("the target-typed spelling needs a known target type", "TAT0069", "auto s = .READY; return 0;")
+    , ("the target-typed spelling needs an enum as its target", "TAT0069", "int x = .READY; return x;")
+    , ("the left operand of a comparison has no target type", "TAT0069", "Status s = Pick(left); return .NONE == s ? 1 : 2;")
+    , ("the target-typed spelling names a member of the expected enum", "TAT0064", "Status s = .LOW; return 0;")
+    , ("a call through the target-typed spelling has no type to call on", "TAT0032", ".Pick(left); return 0;")
+    , ("a value of one enum is not a value of another", "TAT0002", "Level l = Status.NONE; return 0;")
+    ]
+
+rejectedDeclarations :: [(String, String, String)]
+rejectedDeclarations =
+    [ ("the underlying type of an enum is an integer type", "TAT0066", "enum A = float { X }\n")
+    , ("a member is named once", "TAT0067", "enum A { X, X }\n")
+    , ("a member value fits the underlying type", "TAT0068", "enum A = byte { X = 300 }\n")
+    , ("a numbered member fits the underlying type", "TAT0068", "enum A = byte { X = 127, Y }\n")
+    , ("an unsigned underlying type has no negative member", "TAT0068", "enum A = ubyte { X = -1 }\n")
+    , ("a computed member value fits the underlying type", "TAT0068", "enum A = byte { X = 100, Y = X + X }\n")
+    , ("a member value is an integer", "TAT0070", "enum A { X = 1.5 }\n")
+    , ("a member value names earlier members only", "TAT0070", "enum A { X = Y, Y = 1 }\n")
+    , ("a member value does not name the member itself", "TAT0070", "enum A { X = X + 1 }\n")
+    , ("a member value does not name a member of another enum", "TAT0070", "enum A { X = 1 }\nenum B { Y = A.X }\n")
+    , ("a member value is not a call", "TAT0070", "enum A { X = Program.Evaluate(1, 2) }\n")
+    , ("a member value is not a comparison", "TAT0070", "enum A { X = 1, Y = X < 2 }\n")
+    , ("a member value does not divide by zero", "TAT0070", "enum A { X = 0, Y = 4 / X }\n")
+    , ("a member value has no negative exponent", "TAT0070", "enum A { X = 2 ** -1 }\n")
+    , -- The value of a member is an expression like any other for the
+      -- nesting limit: 1100 negations in one another are deeper than 1024
+      -- levels.
+        ( "a member value is within the expression nesting limit"
+        , "TAP0040"
+        , "enum A { X = " ++ concat (replicate 1100 "-(") ++ "1" ++ replicate 1100 ')' ++ " }\n"
+        )
+    , ("the match of a member with a computed value is unreachable after its other name", "TAT0053", "enum A { X = 1, Y = X }\nclass P { public static int F(_ A a) { return match (a) { .X -> 1, .Y -> 2 }; } }\n")
+    , ("an enum is declared once", "TAR0001", "enum A { X }\nenum A { Y }\n")
+    , ("an enum and a class do not share a name", "TAR0001", "enum A { X }\nclass A { }\n")
+    ]
+
+-- ----------------------------------------------------------------- helpers
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource text = do
+    tokens <- runLexer defaultLexer (LexerInput "enum.tala" text)
+    runParser defaultParser (ParserInput "enum.tala" tokens)
+
+parses :: String -> Bool
+parses = either (const False) (const True) . parseSource
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "enum.tala" text)
+
+accepted :: String -> Bool
+accepted = either (const False) (const True) . compileSource
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> (Integer, Integer) -> Integer -> Bool
+runs select statements (left, right) expected = case compileSource (program statements) of
+    Right artifacts ->
+        runFunction (select artifacts) "Evaluate" [IntegerValue left, IntegerValue right] == Just (IntegerValue expected)
+    Left _ -> False
+
+verifies :: String -> Bool
+verifies statements = case compileSource (program statements) of
+    Right artifacts ->
+        verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+            && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+            && verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+
+{- | Whether the Core of a program mentions no enum. The reserved root of an
+enum type cannot be spelled in source, so its absence from the printed Core
+shows that every enum was lowered to its underlying type.
+-}
+erased :: String -> Bool
+erased statements = case compileSource (program statements) of
+    Right artifacts -> not ("Identifier \"enum\"" `isInfixOf` show (artifactCore artifacts))
+    Left _ -> False
+ test/FallThroughTests.hs view
@@ -0,0 +1,151 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | How a function ends when its body ends.
+
+A function without a result may end without a @return@: reaching the end of
+its body returns. A function with a result returns on every path, so the end
+of its body is never reached, and the block that stands there is marked as
+unreachable rather than given a value nobody wrote.
+
+The two must not be confused. A function without a result whose last block
+was marked unreachable had no meaning once it was called, and a native
+program whose @Main@ ended that way stopped instead of ending.
+-}
+module FallThroughTests (fallThroughTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
+
+fallThroughTests :: [(String, Bool)]
+fallThroughTests =
+    [ ("an empty body without a result returns", terminators (prepared unitType []) == [returnsNothing])
+    , ("a body without a result that ends in a binding returns", terminators (prepared unitType [bind 2 1]) == [returnsNothing])
+    , ("a body without a result that ends in a call returns", terminators (prepared unitType [CoreEvaluate callee]) == [returnsNothing])
+    , ("a body without a result never ends in an unreachable block", all (noneUnreachable . prepared unitType) resultless)
+    , ("every body without a result passes verification", all (verified . prepared unitType) resultless)
+    , -- One return was written; the other is the end of the body.
+      ("the block after a conditional return returns", returns (prepared unitType [returnWhen]) == 2)
+    , ("the block after a loop returns", returns (prepared unitType [CoreWhile condition [bind 3 1]]) == 1)
+    , ("the block after a loop that is left early returns", returns (prepared unitType [CoreWhile condition [CoreBreak]]) == 1)
+    , ("the block after nested conditionals returns", returns (prepared unitType [nested]) == 2)
+    , ("an explicit return keeps its own value", terminators (prepared unitType [CoreReturn nothing]) == [returnsNothing])
+    , ("a loop body still continues with its loop", loopBodyJumps)
+    , ("a body with a result is not given a value at its end", notElem returnsNothing (terminators (prepared intType [bothReturn])))
+    , ("the end of a body with a result stays unreachable", elem CorePrepUnreachable (terminators (prepared intType [bothReturn])))
+    , ("a body with a result that returns directly has one block", terminators (prepared intType [CoreReturn (integer 1)]) == [CorePrepReturn (CorePrepLiteral (CoreInteger 1) intType)])
+    , ("a closure without a result returns at the end of its body", closureReturns)
+    , ("a closure with a result is not given a value at its end", closureWithResultIsNotCompleted)
+    ]
+    where
+        resultless =
+            [ []
+            , [bind 2 1]
+            , [CoreEvaluate callee]
+            , [returnWhen]
+            , [returnWhen, bind 2 1]
+            , [CoreWhile condition [bind 3 1]]
+            , [CoreWhile condition [CoreBreak]]
+            , [CoreWhile condition [CoreContinue]]
+            , [CoreDoWhile [bind 3 1] condition]
+            , [CoreFor condition [bind 3 1] [bind 4 1]]
+            , [nested]
+            , [nested, returnWhen, CoreWhile condition []]
+            , [CoreIf condition [CoreReturn nothing] [CoreReturn nothing]]
+            ]
+
+-- | What reaching the end of a body without a result does.
+returnsNothing :: CorePrepTerminator
+returnsNothing = CorePrepReturn (CorePrepLiteral CoreUnit unitType)
+
+nothing :: CoreExpression
+nothing = CoreLiteral CoreUnit unitType
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+condition :: CoreExpression
+condition = CoreVariable (name 20 "flag") boolType
+
+callee :: CoreExpression
+callee = CoreApply (CoreVariable (name 21 "Act") (FunctionType [] unitType)) [] unitType
+
+bind :: Int -> Integer -> CoreStatement
+bind symbol value = CoreBind (CoreBinding (name symbol "local") intType False (integer value))
+
+-- | @if (flag) { return; }@
+returnWhen :: CoreStatement
+returnWhen = CoreIf condition [CoreReturn nothing] []
+
+-- | @if (flag) { if (flag) { return; } } else { local = 1; }@
+nested :: CoreStatement
+nested = CoreIf condition [returnWhen] [bind 5 1]
+
+-- | @if (flag) { return 1; } else { return 2; }@
+bothReturn :: CoreStatement
+bothReturn = CoreIf condition [CoreReturn (integer 1)] [CoreReturn (integer 2)]
+
+prepared :: Type -> [CoreStatement] -> CorePrepModule
+prepared returnType statements = case prepareCore moduleValue of
+    Right moduleResult -> moduleResult
+    Left _ -> error "prepareCore cannot fail on these bodies"
+    where
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "FallThrough"]) [function] [] []
+        function =
+            CoreFunction
+                (name 1 "Evaluate")
+                [(name 20 "flag", boolType), (name 21 "Act", FunctionType [] unitType)]
+                returnType
+                statements
+
+terminators :: CorePrepModule -> [CorePrepTerminator]
+terminators moduleValue =
+    [ corePrepBlockTerminator block
+    | function <- corePrepModuleFunctions moduleValue
+    , block <- corePrepFunctionBlocks function
+    ]
+
+-- | How many blocks return without a value.
+returns :: CorePrepModule -> Int
+returns = length . filter (== returnsNothing) . terminators
+
+noneUnreachable :: CorePrepModule -> Bool
+noneUnreachable = notElem CorePrepUnreachable . terminators
+
+verified :: CorePrepModule -> Bool
+verified = either (const False) (const True) . verifyCorePrep
+
+{- | The end of a loop body goes back to the loop, whatever the function
+returns: only the end of the function's own body returns.
+-}
+loopBodyJumps :: Bool
+loopBodyJumps =
+    length [() | CorePrepJump _ <- found] >= 2
+        && length (filter (== returnsNothing) found) == 1
+    where
+        found = terminators (prepared unitType [CoreWhile condition [bind 3 1]])
+
+closureReturns :: Bool
+closureReturns = case corePrepModuleFunctions moduleValue of
+    [_, lifted] -> map corePrepBlockTerminator (corePrepFunctionBlocks lifted) == [returnsNothing]
+    _ -> False
+    where
+        moduleValue = prepared unitType [CoreBind (CoreBinding (name 6 "act") callable False closure)]
+        callable = FunctionType [] unitType
+        closure = CoreClosure [] [] unitType [bind 7 1] callable
+
+closureWithResultIsNotCompleted :: Bool
+closureWithResultIsNotCompleted = case corePrepModuleFunctions moduleValue of
+    [_, lifted] ->
+        let found = map corePrepBlockTerminator (corePrepFunctionBlocks lifted)
+         in notElem returnsNothing found && elem CorePrepUnreachable found
+    _ -> False
+    where
+        moduleValue = prepared unitType [CoreBind (CoreBinding (name 6 "pick") callable False closure)]
+        callable = FunctionType [] intType
+        closure = CoreClosure [] [] intType [bothReturn] callable
+
+name :: Int -> String -> ResolvedName
+name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+ test/FloatingOptimizerTests.hs view
@@ -0,0 +1,304 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module FloatingOptimizerTests (floatingOptimizerTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Verifier (verifyCore)
+
+-- These are end-to-end Core tests, not tests of a private arithmetic helper.
+-- Every case first passes the Core verifier, executes the real fixed-point
+-- optimizer, and observes the resulting verified Core expression.
+floatingOptimizerTests :: [(String, Bool)]
+floatingOptimizerTests =
+    [
+        ( "binary64 addition rounds 0.1 plus 0.2 to the IEEE result"
+        , resultIs CoreAdd ["0.1", "0.2"] floatType (CoreFloating "3.0000000000000004e-1")
+        )
+    , ("binary16 addition folds exact finite values", resultIs CoreAdd ["1.5", "2.25"] sfloatType (CoreFloating "3.75e0"))
+    ,
+        ( "binary32 uses its own precision rather than binary64"
+        , resultIs CoreAdd ["0.1", "0.2"] lfloatType (CoreFloating "3.0e-1")
+        )
+    , ("binary128 preserves precision beyond a host Double", binary128RetainsSmallIncrement)
+    ,
+        ( "binary16 halfway rounds to the even lower significand"
+        , resultIs CoreAdd ["1.0", "4.8828125e-4"] sfloatType (CoreFloating "1.0e0")
+        )
+    ,
+        ( "binary16 halfway rounds to the even upper significand"
+        , resultIs CoreAdd ["1.0", "1.46484375e-3"] sfloatType (CoreFloating "1.002e0")
+        )
+    , ("binary32 halfway rounds to the even lower significand", halfwayRoundsDown lfloatType 24)
+    , ("binary64 halfway rounds to the even lower significand", halfwayRoundsDown floatType 53)
+    , ("binary128 halfway rounds to the even lower significand", halfwayRoundsDown doubleType 113)
+    ,
+        ( "binary16 minimum subnormal survives exact addition"
+        , resultIs CoreAdd ["5.9604644775390625e-8", "5.9604644775390625e-8"] sfloatType (CoreFloating "1.0e-7")
+        )
+    ,
+        ( "binary16 half-minimum subnormal tie rounds to signed zero"
+        , resultIs CoreMultiply ["5.9604644775390625e-8", "0.5"] sfloatType (CoreFloating "0.0")
+        )
+    ,
+        ( "finite arithmetic overflow produces infinity"
+        , resultIs CoreMultiply ["65504.0", "2.0"] sfloatType (CoreFloating "inf")
+        )
+    ,
+        ( "negative finite overflow retains its sign"
+        , resultIs CoreMultiply ["-65504.0", "2.0"] sfloatType (CoreFloating "-inf")
+        )
+    , ("addition preserves two negative zero operands", resultIs CoreAdd ["-0.0", "-0.0"] floatType (CoreFloating "-0.0"))
+    ,
+        ( "opposite signed zero addition produces positive zero"
+        , resultIs CoreAdd ["-0.0", "0.0"] floatType (CoreFloating "0.0")
+        )
+    ,
+        ( "exact finite cancellation produces positive zero"
+        , resultIs CoreSubtract ["1.0", "1.0"] doubleType (CoreFloating "0.0")
+        )
+    ,
+        ( "negative zero multiplication tracks the sign xor"
+        , resultIs CoreMultiply ["-0.0", "3.0"] floatType (CoreFloating "-0.0")
+        )
+    , ("negative zero division tracks the sign xor", resultIs CoreDivide ["-0.0", "2.0"] floatType (CoreFloating "-0.0"))
+    ,
+        ( "division by positive zero produces positive infinity"
+        , resultIs CoreDivide ["1.0", "0.0"] floatType (CoreFloating "inf")
+        )
+    ,
+        ( "division by negative zero produces negative infinity"
+        , resultIs CoreDivide ["1.0", "-0.0"] floatType (CoreFloating "-inf")
+        )
+    , ("zero divided by zero produces NaN", resultIs CoreDivide ["0.0", "0.0"] floatType (CoreFloating "nan"))
+    , ("infinity divided by infinity produces NaN", resultIs CoreDivide ["inf", "inf"] floatType (CoreFloating "nan"))
+    ,
+        ( "positive infinity plus finite remains positive infinity"
+        , resultIs CoreAdd ["inf", "-12.5"] floatType (CoreFloating "inf")
+        )
+    , ("opposite infinities add to NaN", resultIs CoreAdd ["inf", "-inf"] floatType (CoreFloating "nan"))
+    , ("infinity times zero produces NaN", resultIs CoreMultiply ["inf", "0.0"] floatType (CoreFloating "nan"))
+    ,
+        ( "infinity times a negative finite value flips sign"
+        , resultIs CoreMultiply ["inf", "-2.0"] floatType (CoreFloating "-inf")
+        )
+    ,
+        ( "finite value divided by infinity produces signed zero"
+        , resultIs CoreDivide ["-1.0", "inf"] floatType (CoreFloating "-0.0")
+        )
+    , ("infinity divided by zero remains infinity", resultIs CoreDivide ["-inf", "-0.0"] floatType (CoreFloating "inf"))
+    ,
+        ( "remainder keeps the sign of a negative dividend"
+        , resultIs CoreRemainder ["-7.5", "2.0"] floatType (CoreFloating "-1.5e0")
+        )
+    , ("remainder ignores a negative divisor sign", resultIs CoreRemainder ["7.5", "-2.0"] floatType (CoreFloating "1.5e0"))
+    ,
+        ( "negative-zero remainder preserves the dividend sign"
+        , resultIs CoreRemainder ["-0.0", "3.0"] floatType (CoreFloating "-0.0")
+        )
+    ,
+        ( "finite remainder by infinity preserves the dividend"
+        , resultIs CoreRemainder ["-7.5", "inf"] floatType (CoreFloating "-7.5e0")
+        )
+    , ("infinite remainder remains NaN", resultIs CoreRemainder ["inf", "2.0"] floatType (CoreFloating "nan"))
+    ,
+        ( "rounded division of floating operands returns int"
+        , resultIs CoreFloorDivide ["7.8", "2.0"] integerResultType (CoreInteger 4)
+        )
+    , ("floating rounded division by one is not an invalid identity", floatingRoundedDivisionByOne)
+    ,
+        ( "floating rounded division breaks exact halves away from zero"
+        , resultIs CoreFloorDivide ["-7.0", "2.0"] integerResultType (CoreInteger (-4))
+        )
+    , ("out-of-range rounded floating quotient is not narrowed", roundedQuotientRetained)
+    , ("NaN equality is false", resultIs CoreEqual ["nan", "nan"] boolType (CoreBoolean False))
+    , ("NaN inequality is true under unordered comparison", resultIs CoreNotEqual ["nan", "1.0"] boolType (CoreBoolean True))
+    , ("NaN less-than is false", resultIs CoreLessThan ["nan", "1.0"] boolType (CoreBoolean False))
+    , ("NaN less-equal is false", resultIs CoreLessEqual ["nan", "1.0"] boolType (CoreBoolean False))
+    , ("NaN greater-than is false", resultIs CoreGreaterThan ["nan", "1.0"] boolType (CoreBoolean False))
+    , ("NaN greater-equal is false", resultIs CoreGreaterEqual ["nan", "1.0"] boolType (CoreBoolean False))
+    , ("positive and negative zero compare equal", resultIs CoreEqual ["-0.0", "0.0"] boolType (CoreBoolean True))
+    ,
+        ( "positive infinity compares above every finite binary64 value"
+        , resultIs CoreGreaterThan ["inf", "1e300"] boolType (CoreBoolean True)
+        )
+    ,
+        ( "negative infinity compares below every finite binary128 value"
+        , resultIsFor CoreLessThan ["-inf", "1e4000"] doubleType boolType (CoreBoolean True)
+        )
+    ,
+        ( "finite binary128 value compares above negative infinity"
+        , resultIsFor CoreGreaterThan ["-1e4000", "-inf"] doubleType boolType (CoreBoolean True)
+        )
+    ,
+        ( "positive infinity compares above negative infinity"
+        , resultIs CoreGreaterThan ["inf", "-inf"] boolType (CoreBoolean True)
+        )
+    ,
+        ( "finite division retains binary16 target precision"
+        , resultIs CoreDivide ["1.0", "3.0"] sfloatType (CoreFloating "3.333e-1")
+        )
+    ,
+        ( "finite division retains binary32 target precision"
+        , resultIs CoreDivide ["1.0", "3.0"] lfloatType (CoreFloating "3.3333334e-1")
+        )
+    ,
+        ( "finite division retains binary64 target precision"
+        , resultIs CoreDivide ["1.0", "3.0"] floatType (CoreFloating "3.333333333333333e-1")
+        )
+    ,
+        ( "finite division retains binary128 target precision"
+        , resultIs CoreDivide ["1.0", "3.0"] doubleType (CoreFloating "3.333333333333333333333333333333333e-1")
+        )
+    , ("logical truthiness treats floating positive zero as false", logicalNot "0.0" (CoreBoolean True))
+    , ("logical truthiness treats negative zero as false", logicalNot "-0.0" (CoreBoolean True))
+    , ("logical truthiness treats NaN as nonzero", logicalNot "nan" (CoreBoolean False))
+    , ("logical truthiness treats infinity as nonzero", logicalNot "inf" (CoreBoolean False))
+    , ("unsupported floating power stays explicit", powerRemainsExplicit)
+    , ("long runs of insignificant zeroes fold through normalized decimal digits", longZeroRunsFold)
+    , ("the significant-digit fold limit is inclusive", maximumSupportedDecimalFolds)
+    , ("oversized significant decimal input remains a valid explicit primitive", oversizedDecimalRemainsExplicit)
+    , ("oversized exponent text remains a valid explicit primitive", oversizedExponentRemainsExplicit)
+    , ("short decimal rendering round-trips through the Core verifier", renderedLiteralVerifies)
+    ]
+
+sfloatType, lfloatType, floatType, doubleType, integerResultType :: Type
+sfloatType = namedType "sfloat"
+lfloatType = namedType "lfloat"
+floatType = namedType "float"
+doubleType = namedType "double"
+integerResultType = namedType "int"
+
+functionName :: ResolvedName
+functionName = ResolvedName (SymbolId 1) (Identifier "Evaluate")
+
+testModule :: Type -> CoreExpression -> CoreModule
+testModule resultType expression =
+    CoreModule
+        (QualifiedName [Identifier "Floating", Identifier "Optimizer", Identifier "Tests"])
+        [CoreFunction functionName [] resultType [CoreReturn expression]]
+
+expressionResult :: Type -> CoreExpression -> Maybe CoreExpression
+expressionResult resultType expression = do
+    optimized <- either (const Nothing) Just (runCoreOptimizer defaultCoreOptimizer (testModule resultType expression))
+    function <- case coreModuleFunctions optimized of
+        [single] -> Just single
+        _ -> Nothing
+    case coreFunctionBody function of
+        [CoreReturn value] -> Just value
+        _ -> Nothing
+
+resultIs :: CorePrimitive -> [String] -> Type -> CoreLiteral -> Bool
+resultIs primitive spellings resultType expected =
+    resultIsFor primitive spellings (operandType spellings resultType) resultType expected
+
+resultIsFor :: CorePrimitive -> [String] -> Type -> Type -> CoreLiteral -> Bool
+resultIsFor primitive spellings sourceType resultType expected =
+    let operands = map (\spelling -> CoreLiteral (CoreFloating spelling) sourceType) spellings
+        expression = CorePrimitive primitive operands resultType
+     in expressionResult resultType expression == Just (CoreLiteral expected resultType)
+
+operandType :: [String] -> Type -> Type
+operandType _ resultType
+    | resultType == boolType = floatType
+    | resultType == integerResultType = floatType
+    | otherwise = resultType
+
+logicalNot :: String -> CoreLiteral -> Bool
+logicalNot spelling expected =
+    let expression = CorePrimitive CoreLogicalNot [CoreLiteral (CoreFloating spelling) floatType] boolType
+     in expressionResult boolType expression == Just (CoreLiteral expected boolType)
+
+binary128RetainsSmallIncrement :: Bool
+binary128RetainsSmallIncrement =
+    case resultOf CoreAdd ["1.0", "1e-34"] doubleType doubleType of
+        Just (CoreLiteral (CoreFloating spelling) _) -> spelling /= "1.0e0"
+        _ -> False
+
+halfwayRoundsDown :: Type -> Int -> Bool
+halfwayRoundsDown valueType precision =
+    resultIs CoreAdd ["1.0", exactHalfUlp precision] valueType (CoreFloating "1.0e0")
+
+exactHalfUlp :: Int -> String
+exactHalfUlp precision = "0." ++ replicate (precision - length digits) '0' ++ digits
+    where
+        digits = show (5 ^ precision :: Integer)
+
+roundedQuotientRetained :: Bool
+roundedQuotientRetained =
+    case resultOf CoreFloorDivide ["1e100", "1.0"] floatType integerResultType of
+        Just CorePrimitive {} -> True
+        _ -> False
+
+floatingRoundedDivisionByOne :: Bool
+floatingRoundedDivisionByOne =
+    case runCoreOptimizer defaultCoreOptimizer inputModule of
+        Right optimized -> case coreModuleFunctions optimized of
+            [function] -> case coreFunctionBody function of
+                [CoreReturn (CorePrimitive CoreFloorDivide _ resultType)] -> resultType == integerResultType
+                _ -> False
+            _ -> False
+        Left _ -> False
+    where
+        inputName = ResolvedName (SymbolId 2) (Identifier "value")
+        inputModule =
+            CoreModule
+                (QualifiedName [Identifier "Floating", Identifier "Optimizer", Identifier "Tests"])
+                [ CoreFunction
+                    functionName
+                    [(inputName, floatType)]
+                    integerResultType
+                    [ CoreReturn
+                        ( CorePrimitive
+                            CoreFloorDivide
+                            [ CoreVariable inputName floatType
+                            , CoreLiteral (CoreFloating "1.0") floatType
+                            ]
+                            integerResultType
+                        )
+                    ]
+                ]
+
+powerRemainsExplicit :: Bool
+powerRemainsExplicit =
+    case resultOf CorePower ["2.0", "3.0"] floatType floatType of
+        Just CorePrimitive {} -> True
+        _ -> False
+
+renderedLiteralVerifies :: Bool
+renderedLiteralVerifies = case resultOf CoreAdd ["0.1", "0.2"] floatType floatType of
+    Just literal@CoreLiteral {} -> either (const False) (const True) (verifyCore (testModule floatType literal))
+    _ -> False
+
+longZeroRunsFold :: Bool
+longZeroRunsFold =
+    case resultOf CoreAdd [spelling, "0.0"] doubleType doubleType of
+        Just literal@(CoreLiteral (CoreFloating "1.0e-1001") _) -> either (const False) (const True) (verifyCore (testModule doubleType literal))
+        _ -> False
+    where
+        spelling = "0." ++ replicate 1000 '0' ++ "1" ++ replicate 1000 '0'
+
+maximumSupportedDecimalFolds :: Bool
+maximumSupportedDecimalFolds =
+    case resultOf CoreAdd ["1" ++ replicate 511 '0', "0.0"] doubleType doubleType of
+        Just CoreLiteral {} -> True
+        _ -> False
+
+oversizedDecimalRemainsExplicit :: Bool
+oversizedDecimalRemainsExplicit =
+    case resultOf CoreAdd [replicate 513 '1', "0.0"] doubleType doubleType of
+        Just CorePrimitive {} -> True
+        _ -> False
+
+oversizedExponentRemainsExplicit :: Bool
+oversizedExponentRemainsExplicit =
+    case resultOf CoreAdd ["1e" ++ replicate 20 '9', "0.0"] doubleType doubleType of
+        Just CorePrimitive {} -> True
+        _ -> False
+
+resultOf :: CorePrimitive -> [String] -> Type -> Type -> Maybe CoreExpression
+resultOf primitive spellings operand value =
+    let operands = map (\spelling -> CoreLiteral (CoreFloating spelling) operand) spellings
+     in expressionResult value (CorePrimitive primitive operands value)
+ test/FormatSweepTests.hs view
@@ -0,0 +1,246 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Every conversion that can be written, against the rules stated a second time.
+
+"RuntimeCallTests" holds formats chosen by hand. This module writes all of
+them: every set of flags, with and without a width and a precision, each
+written as a number and as a star, for every conversion letter. What the
+reader of formats makes of each is compared with a prediction that is
+written here from the rules alone and shares nothing with the reader:
+
+* a conversion takes the flags of its row and no other;
+* @-@ excludes @0@, and @+@ excludes the space;
+* a precision is taken by @%f@ and @%s@;
+* @%n@ and @%%@ take nothing at all.
+
+The same sweep is then run through the reference text functions, where the
+properties of a field hold whatever the conversion: it is never shorter than
+its width, padding is all it adds, and a value at the left is followed by
+spaces only.
+-}
+module FormatSweepTests (formatSweepTests) where
+
+import Data.Char (isDigit)
+import Data.List (isPrefixOf, isSuffixOf, subsequences)
+import RuntimeText qualified as Reference
+import Progmasoft.Tala.RuntimeCall
+import Progmasoft.Tala.TypeChecker.Format
+
+formatSweepTests :: [(String, Bool)]
+formatSweepTests =
+    [ ("every way to write %" ++ [letter] ++ " is read as the rules say", all agrees (written letter))
+    | letter <- letters
+    ]
+        ++ [ ("the sweep writes every combination", length (concatMap written letters) == 9 * 64 * 3 * 3)
+           , ("the sweep holds formats that are accepted", any ((== Accepted) . predicted) (concatMap written letters))
+           , ("the sweep holds formats that are rejected", any ((== Rejected) . predicted) (concatMap written letters))
+           , ("a letter outside the grammar is never a conversion", all (unknown . (: [])) (filter (`notElem` letters ++ "AO") ['a' .. 'z'] ++ ['B' .. 'N']))
+           , ("an integer field is never shorter than its width", and [length (integerField value) >= maybe 0 id width | value@(_, width, _) <- integerCases])
+           , ("an integer field without a width is the number itself", and [integerField (flags, Nothing, value) == unpadded flags value | (flags, _, value) <- integerCases])
+           , ("padding is all a width adds to an integer", all paddingOnly integerCases)
+           , ("a value at the left is followed by spaces only", all leftAligned integerCases)
+           , ("zeros stand after the sign", all zerosAfterSign integerCases)
+           , ("grouping adds apostrophes and nothing else", all groupingOnly [value | (_, _, value) <- integerCases])
+           , ("groups have three digits, the first at most three", all groupsOfThree [value | (_, _, value) <- integerCases])
+           , ("hexadecimal digits are those of the magnitude", all hexadecimalDigits [value | (_, _, value) <- integerCases])
+           , ("a text field is never shorter than its width", and [length (textField width Nothing value) >= width | width <- [0 .. 9], value <- texts])
+           , ("a precision is the most characters a text field keeps", and [textField 0 (Just precision) value == take precision value | precision <- [0 .. 9], value <- texts])
+           , ("a fixed-point field has its precision's digits after the point", all fractionDigits fixedCases)
+           , ("a fixed-point number with no digits after the point has no point", and ['.' `notElem` fixed 0 value | value <- rationals])
+           , ("more digits never change the digits before them by more than rounding", all prefixStable rationals)
+           ]
+    where
+        letters = "duxfscbn%"
+
+-- ------------------------------------------------------------ the reader
+
+-- | What the rules say of a format.
+data Verdict = Accepted | Rejected
+    deriving (Eq, Show)
+
+-- | One conversion as it is written: flags, width, precision and letter.
+type Written = (String, String, String, Char)
+
+-- | Every way to write a conversion with the given letter.
+written :: Char -> [Written]
+written letter =
+    [ (flags, width, precision, letter)
+    | flags <- subsequences "-0+ #'"
+    , width <- ["", "7", "*"]
+    , precision <- ["", ".3", ".*"]
+    ]
+
+spelled :: Written -> String
+spelled (flags, width, precision, letter) = "%" ++ flags ++ width ++ precision ++ [letter]
+
+-- | The rules, from the specification, without the reader.
+predicted :: Written -> Verdict
+predicted (flags, width, precision, letter)
+    | letter `elem` "n%" = if null flags && null width && null precision then Accepted else Rejected
+    | any (`notElem` taken) flags = Rejected
+    | '-' `elem` flags && '0' `elem` flags = Rejected
+    | '+' `elem` flags && ' ' `elem` flags = Rejected
+    | not (null precision) && letter `notElem` "fs" = Rejected
+    | otherwise = Accepted
+    where
+        taken = case letter of
+            'd' -> "-0+ '"
+            'u' -> "-0'"
+            'x' -> "-0#"
+            'f' -> "-0+ '"
+            _ -> "-"
+
+{- | Whether the reader agrees with the rules, and, for an accepted
+conversion, reads what was written: the flags as their bits, and one
+argument more for each star.
+-}
+agrees :: Written -> Bool
+agrees value@(flags, width, precision, letter) = case (predicted value, parseFormat (spelled value)) of
+    (Rejected, Left problem) -> formatProblemCode problem == "TAT0076"
+    (Accepted, Right [ConversionPiece conversion]) ->
+        letter `notElem` "n%"
+            && conversionLetter (conversionKind conversion) == letter
+            && conversionFlagBits conversion == sum (map bit flags) + (if letter == 'x' then flagHexadecimal else 0)
+            && conversionArgumentCount conversion == 1 + length (filter (== "*") [width, drop 1 precision])
+            && conversionWidth conversion == size width
+            && conversionPrecision conversion == size (drop 1 precision)
+    (Accepted, Right [NewlinePiece]) -> letter == 'n'
+    (Accepted, Right [LiteralPiece "%"]) -> letter == '%'
+    _ -> False
+    where
+        bit flag = case flag of
+            '-' -> flagLeft
+            '0' -> flagZero
+            '+' -> flagPlus
+            ' ' -> flagSpace
+            '#' -> flagAlternate
+            _ -> flagGroup
+        size text
+            | null text = NoSize
+            | text == "*" = ArgumentSize
+            | otherwise = FixedSize (read text)
+
+unknown :: String -> Bool
+unknown letter = case parseFormat ('%' : letter) of
+    Left problem -> formatProblemCode problem == "TAT0075"
+    Right _ -> False
+
+-- --------------------------------------------------- the reference fields
+
+-- | Flags, a width when there is one, and a value.
+type IntegerCase = (Integer, Maybe Int, Integer)
+
+integerCases :: [IntegerCase]
+integerCases =
+    [ (flags, width, value)
+    | flags <- [0, flagLeft, flagZero, flagPlus, flagSpace, flagGroup, flagPlus + flagZero, flagGroup + flagLeft, flagHexadecimal, flagHexadecimal + flagAlternate, flagHexadecimal + flagZero]
+    , width <- [Nothing, Just 0, Just 1, Just 5, Just 12, Just 30]
+    , value <- values
+    ]
+    where
+        values =
+            [0, 1, -1, 9, 10, -10, 99, 100, 999, 1000, -1000, 12345, 123456, 1234567, -1234567]
+                ++ [2 ^ (63 :: Int) - 1, negate (2 ^ (63 :: Int)), 2 ^ (64 :: Int) - 1]
+
+fieldOf :: Integer -> Maybe Int -> Reference.Conversion
+fieldOf flags width = Reference.Conversion flags (maybe absent toInteger width) absent
+
+integerField :: IntegerCase -> String
+integerField (flags, width, value) = Reference.formatInteger (fieldOf flags width) value
+
+unpadded :: Integer -> Integer -> String
+unpadded flags = Reference.formatInteger (fieldOf flags Nothing)
+
+has :: Integer -> Integer -> Bool
+has flags flag = (flags `div` flag) `mod` 2 == 1
+
+-- | A field is the number without a width, with padding and nothing else.
+paddingOnly :: IntegerCase -> Bool
+paddingOnly value@(flags, width, number) =
+    length field == max (maybe 0 id width) (length plain)
+        && filter (`notElem` " 0") plain `isSubsequence` field
+        && length field >= maybe 0 id width
+    where
+        field = integerField value
+        plain = unpadded flags number
+        isSubsequence [] _ = True
+        isSubsequence _ [] = False
+        isSubsequence (x : xs) (y : ys)
+            | x == y = isSubsequence xs ys
+            | otherwise = isSubsequence (x : xs) ys
+
+leftAligned :: IntegerCase -> Bool
+leftAligned value@(flags, _, number)
+    | has flags flagLeft = plain `isPrefixOf` field && all (== ' ') (drop (length plain) field)
+    | otherwise = True
+    where
+        field = integerField value
+        plain = unpadded flags number
+
+-- | With the zero flag the field ends in the digits and begins with the sign.
+zerosAfterSign :: IntegerCase -> Bool
+zerosAfterSign value@(flags, _, number)
+    | has flags flagZero && not (has flags flagLeft) =
+        ' ' `notElem` dropWhile (== ' ') (drop (length sign) field)
+            && sign `isPrefixOf` field
+            && digits `isSuffixOf` field
+    | otherwise = True
+    where
+        field = integerField value
+        plain = unpadded flags number
+        (sign, digits) = span (`elem` "+- ") plain
+
+groupingOnly :: Integer -> Bool
+groupingOnly value = filter (/= '\'') (unpadded flagGroup value) == unpadded 0 value
+
+groupsOfThree :: Integer -> Bool
+groupsOfThree value = case groups (dropWhile (== '-') (unpadded flagGroup value)) of
+    first : rest -> not (null first) && length first <= 3 && all ((== 3) . length) rest && all (all isDigit) (first : rest)
+    [] -> False
+    where
+        groups text = case break (== '\'') text of
+            (group, _ : more) -> group : groups more
+            (group, []) -> [group]
+
+hexadecimalDigits :: Integer -> Bool
+hexadecimalDigits value =
+    all (`elem` "0123456789abcdef") digits
+        && foldl (\total digit -> total * 16 + toInteger (position digit)) 0 digits == abs value
+        && (value < 0) == ("-" `isPrefixOf` field)
+    where
+        field = unpadded flagHexadecimal value
+        digits = dropWhile (== '-') field
+        position digit = length (takeWhile (/= digit) "0123456789abcdef")
+
+texts :: [String]
+texts = ["", "a", "abc", "abcdefghij", "\233\8364"]
+
+textField :: Int -> Maybe Int -> String -> String
+textField width precision = Reference.formatText (Reference.Conversion 0 (toInteger width) (maybe absent toInteger precision))
+
+rationals :: [Rational]
+rationals = [0, 1, 1 / 2, 1 / 4, 1 / 8, 3 / 8, 5 / 2, 7 / 2, 12345 / 8, 1 / 1024, 999999 / 1000, 1234567 / 2]
+
+fixed :: Integer -> Rational -> String
+fixed precision = Reference.formatFloating (Reference.Conversion 0 absent precision) False
+
+fixedCases :: [(Integer, Rational)]
+fixedCases = [(precision, value) | precision <- [1 .. 12], value <- rationals]
+
+fractionDigits :: (Integer, Rational) -> Bool
+fractionDigits (precision, value) = case break (== '.') (fixed precision value) of
+    (whole, _ : fraction) -> not (null whole) && all isDigit whole && length fraction == fromInteger precision && all isDigit fraction
+    _ -> False
+
+{- | With a precision large enough to hold every digit of the value, a
+larger precision only appends zeros. These values are sums of powers of two
+with at most ten binary places, or thousandths, so twelve digits hold the
+binary ones exactly.
+-}
+prefixStable :: Rational -> Bool
+prefixStable value
+    | exact = fixed 20 value == fixed 12 value ++ replicate 8 '0'
+    | otherwise = True
+    where
+        exact = fromRational (value * 1024) == (fromInteger (round (value * 1024)) :: Rational)
+ test/InferredReturnTests.hs view
@@ -0,0 +1,221 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for return types that are inferred, and for callables that are
+called.
+
+A method declared with @auto@ takes its return type from its returns, and a
+caller must see that type wherever the method is declared: later in the
+class, in another class, or behind a chain of other such methods. A method
+all of whose results depend on itself has none. A callable takes its return
+type the same way, and may be created inside another callable, whose
+captures it reads.
+
+The expected values are written by hand. Every program runs in the reference
+evaluator of "CoreInterpreter" on the unoptimized and on the optimized Core,
+and is verified as Core and as CorePrep.
+-}
+module InferredReturnTests (inferredReturnTests) where
+
+import CoreInterpreter
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Diagnostic
+
+inferredReturnTests :: [(String, Bool)]
+inferredReturnTests =
+    concat
+        [ [ ("unoptimized Core computes " ++ label, runs artifactCore source left right expected)
+          , ("optimized Core computes " ++ label, runs artifactOptimizedCore source left right expected)
+          ]
+        | (source, runsOfCase) <- map inMethods methodCases ++ map inMethods closureCases ++ classCases
+        , ((left, right), expected) <- runsOfCase
+        , let label = show expected ++ " for " ++ show (left, right) ++ ": " ++ lastLine source
+        ]
+        ++ [ ("every program verifies as Core and as CorePrep", all verifies (map (fst . inMethods) (methodCases ++ closureCases) ++ map fst classCases))
+           , ("a method whose only result is a call of itself has no return type", rejectedWith "TAT0063" (methods "return 0;" ++ loops))
+           , ("two methods that only return each other have no return type", rejectedWith "TAT0063" (methods "return 0;" ++ cycle2))
+           , ("returns of different types in an inferred method are reported", rejectedWith "TAT0062" (methods "return 0;" ++ mixed))
+           , ("an inferred return type is checked at the call", rejected (methods "int x = Even(2); return x;"))
+           , ("a declared return type is still checked against its returns", rejectedWith "TAT0001" (methods "return 0;" ++ declared))
+           , ("a method without a result is inferred as void", accepted (methods "Nothing(); return 0;"))
+           ]
+    where
+        inMethods (statements, runsOfCase) = (methods statements, runsOfCase)
+        lastLine = last . lines
+
+-- | A class around the given body of @Evaluate@, closed by the caller.
+methods :: String -> String
+methods statements =
+    unlines
+        [ "namespace Test;"
+        , "class Helper {"
+        , "    public static auto Five() { return Program.Base() + 4; }"
+        , "}"
+        , "class Program {"
+        , "    public static auto Twice(_ int value) { return value + value; }"
+        , "    public static auto Factorial(_ int value) { if (value <= 1) { return 1; } return value * Factorial(value - 1); }"
+        , "    public static auto First(_ int v) { return Second(v) + 1; }"
+        , "    public static auto Second(_ int v) { return Third(v) + 10; }"
+        , "    public static auto Third(_ int v) { return v * 2; }"
+        , "    public static auto Even(_ int v) { if (v == 0) { return true; } return Odd(v - 1); }"
+        , "    public static auto Odd(_ int v) { if (v == 0) { return false; } return Even(v - 1); }"
+        , "    public static auto Pick(_ int v) { int q = if (v > 0) { return v * 3; } else { 5 }; return q + 1; }"
+        , "    public static auto Scan(_ int v) { int q = while (true) { if (v > 3) { return 7; } break 2; }; return q + v; }"
+        , "    public static auto Base() { return 1; }"
+        , "    public static auto Nothing() { }"
+        , "    public static int Apply(_ (int) -> int f, _ int v) { return f(v); }"
+        , "    public static (int) -> int Adder(_ int n) { return \\(int w) -> w + n; }"
+        , "    public static (int) -> int Pass(_ (int) -> int f) { return f; }"
+        , "    public static (int) -> int Choose(_ bool c, _ (int) -> int a, _ (int) -> int b) { if (c) { return a; } return b; }"
+        , "    public static (int) -> int Compose2(_ (int) -> int f) { return \\(int w) -> f(f(w)); }"
+        , "    public static int Plain(_ int value) { return value + value; }"
+        , "    public static int Evaluate(_ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "}"
+        ]
+
+-- Declarations that are appended as further classes.
+loops, cycle2, mixed, declared :: String
+loops = "class Loops { public static auto Loop() { return Loop(); } }\n"
+cycle2 = "class Cycle { public static auto A() { return B(); } public static auto B() { return A(); } }\n"
+mixed = "class Mixed { public static auto Pick(_ bool f) { if (f) { return 1; } return true; } }\n"
+declared = "class Declared { public static int Wrong() { return true; } }\n"
+
+-- | Bodies of @Evaluate@ that call methods with inferred return types.
+methodCases :: [(String, [((Integer, Integer), Integer)])]
+methodCases =
+    [ ("return Twice(left) + Factorial(right);", [((3, 4), 30), ((0, 1), 1)])
+    , -- A chain is inferred from its end, against the order of declaration.
+      ("return First(left);", [((4, 0), 19)])
+    , -- Mutual recursion is inferred from the base cases.
+      ("return (Even(left) ? 10 : 20) + (Odd(left) ? 1 : 2);", [((4, 0), 12), ((3, 0), 21)])
+    , -- Returns that stand in a value block and in a loop expression.
+      ("return Pick(left) * 10 + Pick(0 - left);", [((2, 0), 66)])
+    , ("return Scan(left) * 100 + Scan(left + 3);", [((1, 0), 307)])
+    , -- The inferred type is the type of the call.
+      ("int x = Twice(left); bool b = Even(x); return b ? x : 0 - x;", [((3, 0), 6)])
+    , -- A method of another class, which calls back into this one.
+      ("return Helper.Five() * left;", [((3, 0), 15)])
+    ]
+
+-- | Bodies of @Evaluate@ that create callables and call them.
+closureCases :: [(String, [((Integer, Integer), Integer)])]
+closureCases =
+    [ -- A callable created inside a callable.
+      ("auto outer = \\(int v) -> { auto inner = \\(int w) -> w + 1; return inner(v) * 2; }; return outer(left);", [((3, 0), 8)])
+    , -- The inner callable reads a local of the method through the outer one.
+        ( "int k = left; auto outer = \\(int v) -> { auto inner = \\(int w) -> w + k + v; return inner(v) * 2; }; return outer(right);"
+        , [((3, 3), 18), ((5, 1), 14)]
+        )
+    , -- The same with the captures written out.
+        ( "int k = left; auto outer = [k] \\(int v) -> { auto inner = [k, v] \\(int w) -> w + k + v; return inner(v); }; return outer(right);"
+        , [((3, 3), 9)]
+        )
+    , -- Three levels; every level reads a name of each level around it.
+        ( "int k = left; auto a = \\(int v) -> { auto b = \\(int w) -> { auto c = \\(int x) -> x + w * 10 + v * 100 + k * 1000; return c(1); }; return b(2); }; return a(3);"
+        , [((4, 0), 4321)]
+        )
+    , -- A callable outlives the call that created it and keeps its capture.
+        ( "auto make = \\(int v) -> { auto inner = \\(int w) -> w + v; return inner; }; auto f = make(left); auto g = make(right); return f(2) * 100 + g(2);"
+        , [((5, 7), 709)]
+        )
+    , -- A capture initializer is evaluated once, where the callable is created.
+        ( "int k = left; auto held = [kept = k] \\ -> kept; k += 10; return held() * 100 + k;"
+        , [((1, 0), 111)]
+        )
+    , -- The return type of a callable is inferred through its expressions.
+        ( "auto pick = \\(int v) -> { int q = if (v > 0) { return 1; } else { 2 }; return q; }; return pick(left) * 10 + pick(0 - left);"
+        , [((3, 0), 12)]
+        )
+    ,
+        ( "auto scan = \\(int v) -> { int q = while (true) { if (v > 3) { return 7; } break 2; }; return q + v; }; return scan(left) * 100 + scan(left + 3);"
+        , [((1, 0), 307)]
+        )
+    , -- A callable passed to a method, made by one and returned by one.
+      ("return Apply(\\(int w) -> w * 3, left);", [((4, 0), 12)])
+    , ("auto add = Adder(left); auto ten = Adder(10); return add(right) * 100 + ten(1);", [((5, 2), 711)])
+    , ("auto f = \\(int w) -> w + 1; auto g = Pass(f); return g(left) + f(left);", [((3, 0), 8)])
+    ,
+        ( "auto a = \\(int w) -> w + 1; auto b = \\(int w) -> w * 2; auto c = Choose(left > right, a, b); return c(left);"
+        , [((5, 1), 6), ((5, 9), 10)]
+        )
+    , -- A method named where a value is expected.
+      ("auto f = Plain; return Apply(f, left) + Apply(Plain, right);", [((3, 4), 14)])
+    , -- A callable variable assigned again, in a loop and from itself.
+        ( "auto f = Adder(0); for (int i = 1; i <= left; i += 1) { f = Adder(i); } return f(100);"
+        , [((3, 0), 103), ((0, 0), 100)]
+        )
+    , ("auto f = Adder(1); f = Compose2(f); return f(left);", [((5, 0), 7)])
+    , -- Callables that exist on one branch only.
+        ( "int r = 0; if (left > right) { auto f = Adder(left); r = f(1); } else { auto g = Adder(right); auto h = Compose2(g); r = h(1); } return r;"
+        , [((4, 1), 5), ((1, 3), 7)]
+        )
+    , -- A callable result that nothing receives.
+      ("_ = Adder(left); auto f = Adder(2); _ = Adder(3); return f(left);", [((5, 0), 7)])
+    , -- A callable kept alive only by the callable that captured it.
+      ("auto inner = Adder(left); auto outer = \\(int w) -> inner(w) * 2; return outer(right);", [((5, 1), 12)])
+    , -- A callable made in every pass of a loop that returns from its middle.
+        ( "for (int i = 0; i < 5; i += 1) { auto f = Adder(i); if (f(left) > 6) { return f(100); } } return 0;"
+        , [((4, 0), 103), ((0, 0), 0)]
+        )
+    , -- Callables alive across the transfers of a loop header.
+        ( "int n = 0; int t = 0; while (if (n >= left) { break; } else { true }) { auto step = [by = n] \\ -> by + 1; n = step(); t += n; } return t;"
+        , [((3, 0), 6)]
+        )
+    ,
+        ( "int t = 0; for (int i = 0; i < 10; i += if (i == left) { break; } else { 1 }) { auto add = [by = i] \\(int w) -> w + by; t = add(t); } return t;"
+        , [((3, 0), 6)]
+        )
+    ]
+
+-- | Whole sources: an inferred method that only another class declares.
+classCases :: [(String, [((Integer, Integer), Integer)])]
+classCases =
+    [
+        ( unlines
+            [ "namespace Test;"
+            , "class Program {"
+            , "    public static int Evaluate(_ int left, _ int right) { return Later.Sum(left, right) + Later.Flag(left); }"
+            , "}"
+            , "class Later {"
+            , "    public static auto Sum(_ int a, _ int b) { return Double(a) + b; }"
+            , "    public static auto Flag(_ int a) { return Positive(a) ? 100 : 200; }"
+            , "    private static auto Double(_ int a) { return a + a; }"
+            , "    private static auto Positive(_ int a) { return a > 0; }"
+            , "}"
+            ]
+        , [((3, 4), 110), ((0, 1), 201)]
+        )
+    ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "inferred.tala" text)
+
+accepted :: String -> Bool
+accepted = either (const False) (const True) . compileSource
+
+rejected :: String -> Bool
+rejected = not . accepted
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> Integer -> Integer -> Integer -> Bool
+runs select source left right expected = case compileSource source of
+    Right artifacts ->
+        runFunction (select artifacts) "Evaluate" [IntegerValue left, IntegerValue right] == Just (IntegerValue expected)
+    Left _ -> False
+
+verifies :: String -> Bool
+verifies source = case compileSource source of
+    Right artifacts ->
+        verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+            && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+            && verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+ test/IntegerEvaluationTests.hs view
@@ -0,0 +1,243 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module IntegerEvaluationTests (integerEvaluationTests) where
+
+import Data.Bits (shiftL, shiftR)
+import Progmasoft.Tala.IntegerEvaluation
+
+integerEvaluationTests :: [(String, Bool)]
+integerEvaluationTests =
+    [ ("bounded integer arithmetic agrees with direct small-value evaluation", smallArithmeticMatrix)
+    , ("bounded exponentiation agrees with direct nonnegative powers", smallPowerMatrix)
+    , ("bounded signed shifts agree with direction-reversing semantics", smallShiftMatrix)
+    , ("multiplication preflight preserves exact results around limb boundaries", multiplicationBoundaryMatrix)
+    , ("multiplication distinguishes both exact magnitude edges", multiplicationExactEdges)
+    , ("multiplication checks inconclusive bit-length bounds exactly", multiplicationConservativeBoundary)
+    , ("power checks non-power-of-two magnitudes at the exact ceiling", nonPowerOfTwoPowerBoundaries)
+    , ("power boundary cases reject before exponent-sized work", powerBoundaryMatrix)
+    , ("left and right shifts honor magnitude and sign boundaries", shiftBoundaryMatrix)
+    , ("left shift accepts the last value below the ceiling and rejects the next", leftShiftExactEdges)
+    , ("arbitrarily wide shift counts stay bounded by their result", unboundedShiftCountsRemainSafe)
+    , ("very wide exponents use identities or fail at the first unsafe square", veryWideExponentMatrix)
+    , ("very wide shift distances do not induce distance-sized work", veryWideShiftMatrix)
+    , ("bounded integer predicate agrees with the half-open interval at varied bit lengths", integerMagnitudeMatrix)
+    , ("an out-of-limit base is rejected even for zero exponent", invalidPowerBase)
+    , ("zero multiplication remains valid at both signed magnitude edges", zeroProductBoundary)
+    ]
+
+-- The dense but small matrix acts as an independent oracle: these operations
+-- are cheap enough to compute directly, without sharing the production
+-- exponentiation or preflight algorithms under test.
+-- Boundary inputs are fixed rather than pseudo-random, so any failure is
+-- reproducible on every supported development host.
+smallArithmeticMatrix :: Bool
+smallArithmeticMatrix =
+    and
+        [ add left right == reference (left + right)
+            && subtractInteger left right == reference (left - right)
+            && multiplyCompileTimeIntegers left right == reference (left * right)
+        | left <- [-64 .. 64]
+        , right <- [-64 .. 64]
+        ]
+
+smallPowerMatrix :: Bool
+smallPowerMatrix =
+    and
+        [ evaluateCompileTimePower base exponentValue == reference (base ^ exponentValue)
+        | base <- [-8 .. 8]
+        , exponentValue <- [0 .. 32]
+        ]
+        && all
+            (\exponentValue -> evaluateCompileTimePower 2 (negate exponentValue) == Left CompileTimeNegativeExponent)
+            [1 .. 32]
+
+smallShiftMatrix :: Bool
+smallShiftMatrix =
+    and
+        [ evaluateCompileTimeShiftLeft value amount == reference (referenceShiftLeft value amount)
+            && evaluateCompileTimeShiftRight value amount == reference (referenceShiftRight value amount)
+        | value <- [-32 .. 32]
+        , amount <- [-16 .. 16]
+        ]
+
+referenceShiftLeft :: Integer -> Integer -> Integer
+referenceShiftLeft value amount
+    | amount < 0 = shiftR value (fromInteger (negate amount))
+    | otherwise = shiftL value (fromInteger amount)
+
+referenceShiftRight :: Integer -> Integer -> Integer
+referenceShiftRight value amount
+    | amount < 0 = shiftL value (fromInteger (negate amount))
+    | otherwise = shiftR value (fromInteger amount)
+
+multiplicationBoundaryMatrix :: Bool
+multiplicationBoundaryMatrix = all checkCase cases
+    where
+        bitPositions = [1, 2, 7, 8, 9, 31, 32, 33, 63, 64, 65, 127, 128, 129, 1023, 32767, 32768, 32769, 65534, 65535, 65536]
+        powers = [1 `shiftL` (position - 1) | position <- bitPositions]
+        values = concatMap (\power -> [power, negate power]) powers
+        factors = [-3 .. 3]
+        cases = [(value, factor) | value <- values, factor <- factors]
+        checkCase (value, factor) =
+            multiplyCompileTimeIntegers value factor == reference (value * factor)
+
+multiplicationExactEdges :: Bool
+multiplicationExactEdges =
+    and
+        [ multiplyCompileTimeIntegers (compileTimeLimit `quot` 2 - 1) 2 == Right (compileTimeLimit - 2)
+        , multiplyCompileTimeIntegers (compileTimeLimit `quot` 2) 2 == Left CompileTimeIntegerLimitExceeded
+        , multiplyCompileTimeIntegers (negate (compileTimeLimit `quot` 2) + 1) 2 == Right (negate compileTimeLimit + 2)
+        , multiplyCompileTimeIntegers (negate (compileTimeLimit `quot` 2)) 2 == Left CompileTimeIntegerLimitExceeded
+        , multiplyCompileTimeIntegers (compileTimeLimit - 1) (-1) == Right (1 - compileTimeLimit)
+        , multiplyCompileTimeIntegers (1 - compileTimeLimit) (-1) == Right (compileTimeLimit - 1)
+        ]
+
+multiplicationConservativeBoundary :: Bool
+multiplicationConservativeBoundary =
+    let power = 1 `shiftL` 32768
+        belowPower = power - 1
+        validProduct = power * belowPower
+        invalidProduct = power * (power + 1)
+     in multiplyCompileTimeIntegers power belowPower == Right validProduct
+            && multiplyCompileTimeIntegers (negate power) belowPower == Right (negate validProduct)
+            && multiplyCompileTimeIntegers power (power + 1) == Left CompileTimeIntegerLimitExceeded
+            && validProduct < compileTimeLimit
+            && invalidProduct >= compileTimeLimit
+
+nonPowerOfTwoPowerBoundaries :: Bool
+nonPowerOfTwoPowerBoundaries =
+    evaluateCompileTimePower 3 40000 == reference (3 ^ (40000 :: Int))
+        && evaluateCompileTimePower 3 42000 == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimePower (-3) 40001 == reference (negate (3 ^ (40001 :: Int)))
+        && evaluateCompileTimePower (-3) 42001 == Left CompileTimeIntegerLimitExceeded
+
+powerBoundaryMatrix :: Bool
+powerBoundaryMatrix =
+    and
+        [ evaluateCompileTimePower 2 exponentValue == expectedPower exponentValue
+            && evaluateCompileTimePower (-2) exponentValue == expectedNegativePower exponentValue
+        | exponentValue <- [0, 1, 2, 7, 8, 31, 32, 63, 64, 127, 128, 1023, 32767, 32768, 32769, 65534, 65535, 65536]
+        ]
+
+expectedPower :: Integer -> Either CompileTimeIntegerError Integer
+expectedPower exponentValue
+    | exponentValue < 65536 = reference (2 ^ exponentValue)
+    | otherwise = Left CompileTimeIntegerLimitExceeded
+
+expectedNegativePower :: Integer -> Either CompileTimeIntegerError Integer
+expectedNegativePower exponentValue
+    | exponentValue < 65536 = reference ((-2) ^ exponentValue)
+    | otherwise = Left CompileTimeIntegerLimitExceeded
+
+shiftBoundaryMatrix :: Bool
+shiftBoundaryMatrix = all checkCase cases
+    where
+        limit = 1 `shiftL` 65536
+        cases =
+            [ (1, 65534, Just (1 `shiftL` 65534))
+            , (1, 65535, Just (limit `quot` 2))
+            , (1, 65536, Nothing)
+            , (-1, 65535, Just (negate (limit `quot` 2)))
+            , (-1, 65536, Nothing)
+            , (limit `quot` 2, 1, Nothing)
+            , (negate (limit `quot` 2), 0, Just (negate (limit `quot` 2)))
+            , (limit - 1, -65535, Just 1)
+            , (negate (limit - 1), -65535, Just (-2))
+            ]
+        checkCase (value, amount, expected) =
+            evaluateCompileTimeShiftLeft value amount == maybe (Left CompileTimeIntegerLimitExceeded) Right expected
+                && evaluateCompileTimeShiftRight value amount == expectedRight value amount
+        expectedRight value amount
+            | amount < 0 = expectedShiftLeft value (negate amount)
+            | amount >= 65536 = Right (if value < 0 then -1 else 0)
+            | otherwise = reference (shiftR value (fromInteger amount))
+        expectedShiftLeft value amount
+            | amount >= 65536 = if value == 0 then Right 0 else Left CompileTimeIntegerLimitExceeded
+            | otherwise = reference (shiftL value (fromInteger amount))
+
+leftShiftExactEdges :: Bool
+leftShiftExactEdges =
+    evaluateCompileTimeShiftLeft 3 65534 == Right (3 `shiftL` 65534)
+        && evaluateCompileTimeShiftLeft 3 65535 == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimeShiftLeft (-3) 65534 == Right (negate (3 `shiftL` 65534))
+        && evaluateCompileTimeShiftLeft (-3) 65535 == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimeShiftRight (compileTimeLimit - 1) 65535 == Right 1
+        && evaluateCompileTimeShiftRight (negate (compileTimeLimit - 1)) 65535 == Right (-2)
+
+integerMagnitudeMatrix :: Bool
+integerMagnitudeMatrix =
+    -- Sign-symmetric neighbors ensure neither exclusive endpoint drifts.
+    -- Values immediately below each edge must remain accepted.
+    and
+        [ checkCompileTimeInteger value == reference value
+        | bitPosition <- [0, 1, 2, 7, 8, 9, 31, 32, 33, 63, 64, 65, 127, 128, 129, 1024, 32767, 32768, 32769, 65534, 65535, 65536]
+        , signValue <- [-1, 1] :: [Integer]
+        , delta <- [-1, 0, 1] :: [Integer]
+        , let value = signValue * (1 `shiftL` bitPosition + delta)
+        ]
+        && checkCompileTimeInteger (compileTimeLimit - 1) == Right (compileTimeLimit - 1)
+        && checkCompileTimeInteger (1 - compileTimeLimit) == Right (1 - compileTimeLimit)
+        && checkCompileTimeInteger compileTimeLimit == Left CompileTimeIntegerLimitExceeded
+        && checkCompileTimeInteger (negate compileTimeLimit) == Left CompileTimeIntegerLimitExceeded
+
+unboundedShiftCountsRemainSafe :: Bool
+unboundedShiftCountsRemainSafe =
+    evaluateCompileTimeShiftRight 0 compileTimeLimit == Right 0
+        && evaluateCompileTimeShiftRight (-5) compileTimeLimit == Right (-1)
+        && evaluateCompileTimeShiftRight 0 (negate compileTimeLimit) == Right 0
+        && evaluateCompileTimeShiftLeft 1 compileTimeLimit == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimeShiftLeft 1 (negate compileTimeLimit) == Right 0
+        && evaluateCompileTimeShiftRight 1 (negate compileTimeLimit) == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimeShiftRight 5 (compileTimeLimit - 1) == Right 0
+        && evaluateCompileTimeShiftRight (-5) (compileTimeLimit - 1) == Right (-1)
+        && evaluateCompileTimeShiftLeft 0 (compileTimeLimit - 1) == Right 0
+
+veryWideExponentMatrix :: Bool
+veryWideExponentMatrix =
+    let exponentValue = 1 `shiftL` 65535
+     in evaluateCompileTimePower 0 exponentValue == Right 0
+            && evaluateCompileTimePower 1 exponentValue == Right 1
+            && evaluateCompileTimePower (-1) exponentValue == Right 1
+            && evaluateCompileTimePower (-1) (exponentValue + 1) == Right (-1)
+            && evaluateCompileTimePower 2 exponentValue == Left CompileTimeIntegerLimitExceeded
+            && evaluateCompileTimePower 2 (negate exponentValue) == Left CompileTimeNegativeExponent
+
+veryWideShiftMatrix :: Bool
+veryWideShiftMatrix =
+    let distance = 1 `shiftL` 65535
+     in evaluateCompileTimeShiftLeft 0 distance == Right 0
+            && evaluateCompileTimeShiftLeft 1 distance == Left CompileTimeIntegerLimitExceeded
+            && evaluateCompileTimeShiftLeft 1 (negate distance) == Right 0
+            && evaluateCompileTimeShiftRight 3 distance == Right 0
+            && evaluateCompileTimeShiftRight (-3) distance == Right (-1)
+            && evaluateCompileTimeShiftRight 1 (negate distance) == Left CompileTimeIntegerLimitExceeded
+
+invalidPowerBase :: Bool
+invalidPowerBase =
+    evaluateCompileTimePower compileTimeLimit 0 == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimePower (negate compileTimeLimit) 1 == Left CompileTimeIntegerLimitExceeded
+        && evaluateCompileTimePower 0 (compileTimeLimit - 1) == Right 0
+        && evaluateCompileTimePower 1 (compileTimeLimit - 1) == Right 1
+        && evaluateCompileTimePower (-1) (compileTimeLimit - 1) == Right (-1)
+
+zeroProductBoundary :: Bool
+zeroProductBoundary =
+    multiplyCompileTimeIntegers 0 (compileTimeLimit - 1) == Right 0
+        && multiplyCompileTimeIntegers (negate (compileTimeLimit - 1)) 0 == Right 0
+        && multiplyCompileTimeIntegers 0 compileTimeLimit == Left CompileTimeIntegerLimitExceeded
+        && multiplyCompileTimeIntegers compileTimeLimit 0 == Left CompileTimeIntegerLimitExceeded
+
+reference :: Integer -> Either CompileTimeIntegerError Integer
+reference value
+    | value <= negate compileTimeLimit || value >= compileTimeLimit = Left CompileTimeIntegerLimitExceeded
+    | otherwise = Right value
+
+compileTimeLimit :: Integer
+compileTimeLimit = 1 `shiftL` 65536
+
+add :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+add left right = reference (left + right)
+
+subtractInteger :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+subtractInteger left right = reference (left - right)
+ test/IntegerFlowTests.hs view
@@ -0,0 +1,770 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module IntegerFlowTests (integerFlowTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Scalar (coreIntegerBitWidth, coreIntegerIsSigned, coreIntegerTypeNames)
+import Prelude hiding (compare)
+
+integerFlowTests :: [(String, Bool)]
+integerFlowTests =
+    [ ("unguarded integer division remains failure-capable", effectIs FailureEffect unguardedDivision)
+    , ("unguarded floor division remains failure-capable", effectIs FailureEffect unguardedFloorDivision)
+    , ("unguarded remainder remains failure-capable", effectIs FailureEffect unguardedRemainder)
+    , ("x != 0 true edge proves a safe divisor", effectIs PureEffect guardedNotEqual)
+    , ("x == 0 false edge proves a safe divisor", effectIs PureEffect guardedFalseEqual)
+    , ("zero on the left is normalized", effectIs PureEffect guardedReversedComparison)
+    ,
+        ( "positive and negative ranges prove nonzero"
+        , effectIs PureEffect guardedPositive && effectIs PureEffect guardedNegative
+        )
+    ,
+        ( "false <= and >= comparisons prove nonzero"
+        , effectIs PureEffect guardedFalseLessEqual && effectIs PureEffect guardedFalseGreaterEqual
+        )
+    , ("the zero-valued path remains failure-capable", effectIs FailureEffect guardedZeroEdge)
+    , ("assignment to zero invalidates a nonzero proof", effectIs FailureEffect assignedZero)
+    , ("assignment to nonzero establishes a fresh proof", effectIs PureEffect assignedNonzero)
+    , ("calls invalidate facts established by a guard", effectIs CallEffect callAfterGuard)
+    , ("calls inside conditions do not leak facts", effectIs CallEffect callInCondition)
+    , ("a guard after a call can establish a fresh divisor fact", callBeforeGuardRemovesDeadDivision)
+    , ("a call after a guard invalidates the divisor fact", callAfterGuardKeepsDeadDivision)
+    , ("false logical-and skips a failure-capable right operand", falseAndSkipsFailure)
+    , ("true logical-or skips a failure-capable right operand", trueOrSkipsFailure)
+    , ("short-circuit right operand uses its path facts", guardedLogicalRightIsDiscardable)
+    , ("conjunction true edges combine operand facts", effectIs PureEffect guardedConjunction)
+    , ("disjunction true edges preserve common facts", effectIs PureEffect guardedDisjunction)
+    , ("false conjunction joins retain shared facts", effectIs PureEffect falseConjunction)
+    , ("false disjunction proves both operands false", effectIs PureEffect falseDisjunction)
+    , ("nested contradictory branches are folded", nestedContradictionIsRemoved)
+    , ("conflicting branch assignments drop facts", effectIs FailureEffect conflictingJoin)
+    , ("equivalent branch assignments retain facts", effectIs PureEffect commonJoin)
+    , ("a guarded closure body retains its proof", closureBodyRetainsGuard)
+    , ("all failure-capable integer primitives use the same proof", guardedOperatorMatrix)
+    , ("comparison direction and operand order are conservative", comparisonMatrix)
+    , ("branch-edge mutation updates the abstract state", branchMutationMatrix)
+    , ("integer comparison bounds fold contradictory paths", contradictoryBoundsAreRemoved)
+    , ("integer comparison bounds fold redundant tests", redundantBoundTestIsRemoved)
+    , ("type limits seed range facts", unsignedComparisonUsesTypeRange)
+    , ("proved facts survive a join when both paths exclude zero", nonzeroJoinSurvives)
+    , ("arithmetic range facts compose through mutable locals", arithmeticFactSurvivesBinding)
+    , ("arithmetic overflow discards an interval proof", overflowingArithmeticIsConservative)
+    , ("equality transfers a nonzero fact between symbols", equalityTransfersNonzero)
+    , ("equality intersects both operand ranges", equalityIntersectsRanges)
+    , ("false equality transfers zero exclusion", falseEqualityTransfersNonzero)
+    , ("strict ordering transfers a positive lower bound", orderingTransfersPositiveBound)
+    , ("strict ordering transfers a negative upper bound", orderingTransfersNegativeBound)
+    , ("ordering false edges transfer inverse bounds", orderingFalseEdgeTransfersBound)
+    , ("same-symbol comparisons select their only feasible edge", sameSymbolComparisonIsFolded)
+    , ("disjoint integer intervals fold equality", disjointIntervalsFoldEquality)
+    , ("disjoint integer intervals fold ordering", disjointIntervalsFoldOrdering)
+    , ("bounded arithmetic agrees with the concrete interval oracle", arithmeticRangeMatrix)
+    , ("every integer type seeds its exact signed boundary", integerTypeBoundaryMatrix)
+    ]
+        ++ boundedComparisonCases
+        ++ relationalComparisonCases
+        ++ [("bounded comparisons match the integer oracle", boundedComparisonMatrix)]
+
+guardedOperatorMatrix :: Bool
+guardedOperatorMatrix = all (effectIs PureEffect . guardedPrimitive) [CoreDivide, CoreFloorDivide, CoreRemainder]
+
+comparisonMatrix :: Bool
+comparisonMatrix = all check comparisons
+    where
+        comparisons =
+            [ (CoreNotEqual, True, True)
+            , (CoreNotEqual, False, False)
+            , (CoreEqual, True, False)
+            , (CoreEqual, False, True)
+            , (CoreGreaterThan, True, True)
+            , (CoreGreaterThan, False, False)
+            , (CoreLessThan, True, True)
+            , (CoreLessThan, False, False)
+            , (CoreGreaterEqual, True, False)
+            , (CoreGreaterEqual, False, True)
+            , (CoreLessEqual, True, False)
+            , (CoreLessEqual, False, True)
+            ]
+        check (operator, truth, proof) =
+            let expected = if proof then PureEffect else FailureEffect
+             in effectIs expected (comparisonGuard operator truth False)
+                    && effectIs expected (comparisonGuard operator truth True)
+
+branchMutationMatrix :: Bool
+branchMutationMatrix =
+    all check [(True, 0), (True, 8), (False, 0), (False, 8)]
+    where
+        check (trueEdge, assigned) =
+            effectIs (if assigned == 0 then FailureEffect else PureEffect) (mutateGuardedEdge trueEdge assigned)
+
+contradictoryBoundsAreRemoved :: Bool
+contradictoryBoundsAreRemoved =
+    let condition =
+            operationBool
+                CoreLogicalAnd
+                [compare CoreGreaterThan x (integer 4), compare CoreLessThan x (integer 5)]
+        function = intFunction [CoreIf condition [CoreReturn (divide x)] [CoreReturn zero]]
+     in effectIs PureEffect function
+            && case optimize function of
+                Just [CoreReturn (CoreLiteral (CoreInteger 0) _)] -> True
+                _ -> False
+
+redundantBoundTestIsRemoved :: Bool
+redundantBoundTestIsRemoved =
+    let nested =
+            CoreIf
+                (compare CoreGreaterThan x (integer 8))
+                [ CoreIf
+                    (compare CoreGreaterThan x (integer 2))
+                    [CoreReturn (divide x)]
+                    [CoreReturn (integer 99)]
+                ]
+                [CoreReturn zero]
+     in case optimize (intFunction [nested]) of
+            Just [CoreIf _ [CoreReturn (CorePrimitive CoreDivide _ _)] [CoreReturn _]] -> True
+            _ -> False
+
+unsignedComparisonUsesTypeRange :: Bool
+unsignedComparisonUsesTypeRange =
+    let parameter = resolved 30 "unsignedValue"
+        function =
+            CoreFunction
+                mainName
+                [(parameter, ubyteType)]
+                boolType
+                [ CoreIf
+                    (CorePrimitive CoreLessThan [variable parameter ubyteType, CoreLiteral (CoreInteger 0) ubyteType] boolType)
+                    [CoreReturn (CoreLiteral (CoreBoolean True) boolType)]
+                    [CoreReturn (CoreLiteral (CoreBoolean False) boolType)]
+                ]
+     in case optimize function of
+            Just [CoreReturn (CoreLiteral (CoreBoolean False) _)] -> True
+            _ -> False
+
+nonzeroJoinSurvives :: Bool
+nonzeroJoinSurvives =
+    let initialize = CoreBind (CoreBinding mutableName intType True x)
+        body =
+            [ initialize
+            , CoreIf
+                (compare CoreNotEqual x zero)
+                [ CoreIf
+                    (variable flagName boolType)
+                    [CoreAssign mutableName x]
+                    [CoreAssign mutableName (integer 2)]
+                , CoreReturn (divide mutableValue)
+                ]
+                [CoreReturn zero]
+            ]
+     in effectIs PureEffect (intFunction body)
+
+arithmeticFactSurvivesBinding :: Bool
+arithmeticFactSurvivesBinding =
+    let local = resolved 31 "local"
+        binding = CoreBind (CoreBinding local intType True (integer 0))
+        assign = CoreAssign local (CorePrimitive CoreAdd [x, integer 2] intType)
+        divideLocal = CoreReturn (divide (variable local intType))
+        condition = compare CoreEqual x (integer 3)
+     in effectIs PureEffect (intFunction [CoreIf condition [binding, assign, divideLocal] [CoreReturn zero]])
+
+overflowingArithmeticIsConservative :: Bool
+overflowingArithmeticIsConservative =
+    let maximumInt = CoreLiteral (CoreInteger (2 ^ (63 :: Int) - 1)) intType
+        expression = CorePrimitive CoreAdd [x, integer 2] intType
+        function =
+            intFunction
+                [ CoreBind (CoreBinding mutableName intType True maximumInt)
+                , CoreAssign mutableName expression
+                , CoreReturn (divide mutableValue)
+                ]
+     in effectIs FailureEffect function
+
+arithmeticRangeMatrix :: Bool
+arithmeticRangeMatrix = all check generatedCases
+    where
+        operators = [CoreAdd, CoreSubtract, CoreMultiply]
+        generatedCases =
+            [ (operator, lower, upper, constant)
+            | operator <- operators
+            , lower <- [-2 .. 2]
+            , upper <- [lower .. 2]
+            , constant <- [-2 .. 2]
+            ]
+        check (operator, lower, upper, constant) =
+            let concreteValues = map (\value -> applyInteger operator value constant) [lower .. upper]
+                allNonzero = all (/= 0) concreteValues
+                rangeGuard =
+                    operationBool
+                        CoreLogicalAnd
+                        [ compare CoreGreaterEqual x (integer lower)
+                        , compare CoreLessEqual x (integer upper)
+                        ]
+                derivedValue = CorePrimitive operator [x, integer constant] intType
+                binding = CoreBind (CoreBinding derivedName intType True derivedValue)
+                useDerived = CoreReturn (divide (variable derivedName intType))
+                function = intFunction [CoreIf rangeGuard [binding, useDerived] [CoreReturn zero]]
+             in effectIs (if allNonzero then PureEffect else FailureEffect) function
+
+applyInteger :: CorePrimitive -> Integer -> Integer -> Integer
+applyInteger primitive left right = case primitive of
+    CoreAdd -> left + right
+    CoreSubtract -> left - right
+    CoreMultiply -> left * right
+    _ -> left
+
+integerTypeBoundaryMatrix :: Bool
+integerTypeBoundaryMatrix = all checkType coreIntegerTypeNames
+    where
+        checkType typeName =
+            case (coreIntegerBitWidth valueType, coreIntegerIsSigned valueType) of
+                (Just width, Just signed) ->
+                    let magnitude = 2 ^ (width - if signed then 1 else 0)
+                        minimumValue = if signed then negate magnitude else 0
+                        maximumValue = magnitude - 1
+                        belowMinimum = checkComparison CoreLessThan minimumValue False
+                        atOrAboveMinimum = checkComparison CoreGreaterEqual minimumValue True
+                        aboveMaximum = checkComparison CoreGreaterThan maximumValue False
+                        atOrBelowMaximum = checkComparison CoreLessEqual maximumValue True
+                     in and [belowMinimum, atOrAboveMinimum, aboveMaximum, atOrBelowMaximum]
+                _ -> False
+            where
+                valueType = namedType typeName
+                checkComparison operator boundary expected =
+                    let parameter = resolved 50 typeName
+                        expression = compare operator (variable parameter valueType) (CoreLiteral (CoreInteger boundary) valueType)
+                        function =
+                            CoreFunction
+                                mainName
+                                [(parameter, valueType)]
+                                boolType
+                                [CoreIf expression [CoreReturn trueValue] [CoreReturn falseValue]]
+                     in case optimize function of
+                            Just [CoreReturn (CoreLiteral (CoreBoolean actual) _)] -> actual == expected
+                            _ -> False
+                trueValue = CoreLiteral (CoreBoolean True) boolType
+                falseValue = CoreLiteral (CoreBoolean False) boolType
+
+equalityTransfersNonzero :: Bool
+equalityTransfersNonzero =
+    let condition = operationBool CoreLogicalAnd [compare CoreNotEqual y zero, compare CoreEqual x y]
+     in effectIs PureEffect (guarded condition True)
+
+equalityIntersectsRanges :: Bool
+equalityIntersectsRanges =
+    let condition =
+            operationBool
+                CoreLogicalAnd
+                [ compare CoreGreaterThan x (integer 4)
+                , operationBool
+                    CoreLogicalAnd
+                    [compare CoreLessThan x (integer 9), compare CoreEqual x y]
+                ]
+     in effectIs PureEffect (guarded condition True)
+
+falseEqualityTransfersNonzero :: Bool
+falseEqualityTransfersNonzero =
+    let condition = operationBool CoreLogicalAnd [compare CoreEqual y zero, compare CoreNotEqual x y]
+     in effectIs PureEffect (guarded condition True)
+
+orderingTransfersPositiveBound :: Bool
+orderingTransfersPositiveBound =
+    let condition = operationBool CoreLogicalAnd [compare CoreGreaterEqual y zero, compare CoreGreaterThan x y]
+     in effectIs PureEffect (guarded condition True)
+
+orderingTransfersNegativeBound :: Bool
+orderingTransfersNegativeBound =
+    let condition = operationBool CoreLogicalAnd [compare CoreLessEqual y zero, compare CoreLessThan x y]
+     in effectIs PureEffect (guarded condition True)
+
+orderingFalseEdgeTransfersBound :: Bool
+orderingFalseEdgeTransfersBound =
+    let condition = compare CoreLessThan x y
+        guardedFunction =
+            intFunction
+                [ CoreIf
+                    (compare CoreGreaterThan y zero)
+                    [CoreIf condition [CoreReturn zero] [CoreReturn (divide x)]]
+                    [CoreReturn zero]
+                ]
+     in effectIs PureEffect guardedFunction
+
+sameSymbolComparisonIsFolded :: Bool
+sameSymbolComparisonIsFolded =
+    all
+        check
+        [ (CoreEqual, True)
+        , (CoreNotEqual, False)
+        , (CoreLessThan, False)
+        , (CoreLessEqual, True)
+        , (CoreGreaterThan, False)
+        , (CoreGreaterEqual, True)
+        ]
+    where
+        check (operator, result) =
+            let condition = compare operator x x
+                whenTrue = if result then [CoreReturn (divide x)] else [CoreReturn zero]
+                whenFalse = if result then [CoreReturn zero] else [CoreReturn (divide x)]
+                guardedFunction =
+                    intFunction
+                        [ CoreIf
+                            (compare CoreNotEqual x zero)
+                            [CoreIf condition whenTrue whenFalse]
+                            [CoreReturn zero]
+                        ]
+             in effectIs PureEffect guardedFunction
+
+disjointIntervalsFoldEquality :: Bool
+disjointIntervalsFoldEquality =
+    let outer =
+            CoreIf
+                (compare CoreGreaterThan x (integer 4))
+                [ CoreIf
+                    (compare CoreLessThan y (integer 0))
+                    [ CoreIf
+                        (compare CoreEqual x y)
+                        [CoreReturn (integer 1)]
+                        [CoreReturn (integer 2)]
+                    ]
+                    [CoreReturn zero]
+                ]
+                [CoreReturn zero]
+     in case optimize (intFunction [outer]) of
+            Just [CoreIf _ [CoreIf _ [CoreReturn (CoreLiteral (CoreInteger 2) _)] [CoreReturn _]] [CoreReturn _]] -> True
+            _ -> False
+
+disjointIntervalsFoldOrdering :: Bool
+disjointIntervalsFoldOrdering =
+    let outer =
+            CoreIf
+                (compare CoreGreaterThan x (integer 4))
+                [ CoreIf
+                    (compare CoreLessThan y zero)
+                    [ CoreIf
+                        (compare CoreLessThan x y)
+                        [CoreReturn (integer 1)]
+                        [CoreReturn (integer 2)]
+                    ]
+                    [CoreReturn zero]
+                ]
+                [CoreReturn zero]
+     in case optimize (intFunction [outer]) of
+            Just [CoreIf _ [CoreIf _ [CoreReturn (CoreLiteral (CoreInteger 2) _)] [CoreReturn _]] [CoreReturn _]] -> True
+            _ -> False
+
+boundedComparisonMatrix :: Bool
+boundedComparisonMatrix = all snd boundedComparisonCases
+
+boundedComparisonCases :: [(String, Bool)]
+boundedComparisonCases = map makeCase generatedCases
+    where
+        operators = [CoreEqual, CoreNotEqual, CoreLessThan, CoreLessEqual, CoreGreaterThan, CoreGreaterEqual]
+        generatedCases =
+            [ (operator, boundary, truth, reversed)
+            | operator <- operators
+            , boundary <- [-3 .. 3]
+            , truth <- [False, True]
+            , reversed <- [False, True]
+            ]
+        makeCase (operator, boundary, truth, reversed) =
+            ( label operator boundary truth reversed
+            , effectIs
+                (if comparisonProvesNonzero operator boundary truth reversed then PureEffect else FailureEffect)
+                (comparisonAgainstLiteral operator boundary truth reversed)
+            )
+        label operator boundary truth reversed =
+            "oracle " ++ show operator ++ " " ++ show boundary ++ " " ++ show truth ++ " reversed=" ++ show reversed
+
+relationalComparisonCases :: [(String, Bool)]
+relationalComparisonCases = map makeCase generatedCases
+    where
+        operators = [CoreEqual, CoreNotEqual, CoreLessThan, CoreLessEqual, CoreGreaterThan, CoreGreaterEqual]
+        generatedCases =
+            [ (operator, boundary, truth, reversed)
+            | operator <- operators
+            , boundary <- [-3 .. 3]
+            , truth <- [False, True]
+            , reversed <- [False, True]
+            ]
+        makeCase (operator, boundary, truth, reversed) =
+            let normalized = if reversed then reverseComparison operator else operator
+             in ( "relational oracle " ++ show operator ++ " " ++ show boundary ++ " true=" ++ show truth ++ " reversed=" ++ show reversed
+                , effectIs
+                    (if comparisonProvesNonzero normalized boundary truth False then PureEffect else FailureEffect)
+                    (comparisonAgainstKnownVariable operator boundary truth reversed)
+                )
+
+comparisonAgainstKnownVariable :: CorePrimitive -> Integer -> Bool -> Bool -> CoreFunction
+comparisonAgainstKnownVariable operator boundary truth reversed =
+    let equality = compare CoreEqual y (integer boundary)
+        relation = if reversed then compare operator y x else compare operator x y
+        divideArm = [CoreReturn (divide x)]
+        safeArm = [CoreReturn zero]
+        whenTrue = if truth then divideArm else safeArm
+        whenFalse = if truth then safeArm else divideArm
+     in intFunction
+            [ CoreIf
+                equality
+                [CoreIf relation whenTrue whenFalse]
+                [CoreReturn zero]
+            ]
+
+comparisonProvesNonzero :: CorePrimitive -> Integer -> Bool -> Bool -> Bool
+comparisonProvesNonzero operator boundary truth reversed = case normalized of
+    CoreEqual -> (truth && boundary /= 0) || (not truth && boundary == 0)
+    CoreNotEqual -> (truth && boundary == 0) || (not truth && boundary /= 0)
+    CoreLessThan -> (truth && boundary <= 0) || (not truth && boundary > 0)
+    CoreLessEqual -> (truth && boundary < 0) || (not truth && boundary >= 0)
+    CoreGreaterThan -> (truth && boundary >= 0) || (not truth && boundary < 0)
+    CoreGreaterEqual -> (truth && boundary > 0) || (not truth && boundary <= 0)
+    _ -> False
+    where
+        normalized = if reversed then reverseComparison operator else operator
+
+reverseComparison :: CorePrimitive -> CorePrimitive
+reverseComparison operator = case operator of
+    CoreLessThan -> CoreGreaterThan
+    CoreLessEqual -> CoreGreaterEqual
+    CoreGreaterThan -> CoreLessThan
+    CoreGreaterEqual -> CoreLessEqual
+    _ -> operator
+
+comparisonAgainstLiteral :: CorePrimitive -> Integer -> Bool -> Bool -> CoreFunction
+comparisonAgainstLiteral operator boundary truth reversed =
+    let literal = integer boundary
+        condition = if reversed then compareReversed operator literal x else compare operator x literal
+        divideArm = [CoreReturn (divide x)]
+        zeroArm = [CoreReturn zero]
+        whenTrue = if truth then divideArm else zeroArm
+        whenFalse = if truth then zeroArm else divideArm
+     in intFunction [CoreIf condition whenTrue whenFalse]
+
+unguardedDivision, unguardedFloorDivision, unguardedRemainder :: CoreFunction
+unguardedDivision = intFunction [CoreReturn (divide x)]
+unguardedFloorDivision = intFunction [CoreReturn (operation CoreFloorDivide x)]
+unguardedRemainder = intFunction [CoreReturn (operation CoreRemainder x)]
+
+guardedNotEqual, guardedFalseEqual, guardedReversedComparison :: CoreFunction
+guardedNotEqual = guarded (compare CoreNotEqual x zero) True
+guardedFalseEqual = guarded (compare CoreEqual x zero) False
+guardedReversedComparison = guarded (compareReversed CoreNotEqual zero x) True
+
+guardedPositive, guardedNegative, guardedFalseLessEqual, guardedFalseGreaterEqual :: CoreFunction
+guardedPositive = guarded (compare CoreGreaterThan x zero) True
+guardedNegative = guarded (compare CoreLessThan x zero) True
+guardedFalseLessEqual = guarded (compare CoreLessEqual x zero) False
+guardedFalseGreaterEqual = guarded (compare CoreGreaterEqual x zero) False
+guardedZeroEdge, assignedZero, assignedNonzero :: CoreFunction
+guardedZeroEdge = guarded (compare CoreEqual x zero) True
+assignedZero = intFunction [mutableBinding, CoreAssign mutableName zero, CoreReturn (divide mutableValue)]
+assignedNonzero = intFunction [mutableBinding, CoreAssign mutableName (integer 4), CoreReturn (divide mutableValue)]
+
+callAfterGuard, callInCondition, guardedConjunction, guardedDisjunction :: CoreFunction
+callAfterGuard =
+    guardedBody (compare CoreNotEqual x zero) [CoreEvaluate unitCallback, CoreReturn (divide x)]
+callInCondition =
+    guardedBody (operationBool CoreLogicalAnd [compare CoreNotEqual x zero, boolCallback]) [CoreReturn (divide x)]
+guardedConjunction =
+    guarded
+        (operationBool CoreLogicalAnd [compare CoreNotEqual x zero, compare CoreGreaterThan y zero])
+        True
+guardedDisjunction =
+    guarded
+        ( operationBool
+            CoreLogicalAnd
+            [ compare CoreNotEqual x zero
+            , operationBool CoreLogicalOr [compare CoreNotEqual y zero, compare CoreEqual y zero]
+            ]
+        )
+        True
+
+callBeforeGuardRemovesDeadDivision :: Bool
+callBeforeGuardRemovesDeadDivision =
+    optimizedDivisionShape
+        (operationBool CoreLogicalAnd [boolCallback, compare CoreNotEqual x zero])
+        False
+
+callAfterGuardKeepsDeadDivision :: Bool
+callAfterGuardKeepsDeadDivision =
+    optimizedDivisionShape
+        (operationBool CoreLogicalAnd [compare CoreNotEqual x zero, boolCallback])
+        True
+
+optimizedDivisionShape :: CoreExpression -> Bool -> Bool
+optimizedDivisionShape condition expectedDivision =
+    case optimize function of
+        Just body -> containsDivision body == expectedDivision && containsCall body
+        Nothing -> False
+    where
+        function =
+            intFunction
+                [ CoreIf condition [CoreEvaluate (divide x)] []
+                , CoreReturn zero
+                ]
+
+falseAndSkipsFailure :: Bool
+falseAndSkipsFailure =
+    discardedCondition
+        (operationBool CoreLogicalAnd [falseLiteral, divide zero])
+
+trueOrSkipsFailure :: Bool
+trueOrSkipsFailure =
+    discardedCondition
+        (operationBool CoreLogicalOr [trueLiteral, divide zero])
+
+guardedLogicalRightIsDiscardable :: Bool
+guardedLogicalRightIsDiscardable =
+    discardedCondition
+        (operationBool CoreLogicalAnd [compare CoreNotEqual x zero, divide x])
+
+discardedCondition :: CoreExpression -> Bool
+discardedCondition expression =
+    case optimize (intFunction [CoreEvaluate expression, CoreReturn zero]) of
+        Just [CoreReturn (CoreLiteral (CoreInteger 0) _)] -> True
+        _ -> False
+
+containsDivision :: [CoreStatement] -> Bool
+containsDivision = any (statementContainsPrimitive CoreDivide)
+
+containsCall :: [CoreStatement] -> Bool
+containsCall = any statementContainsCall
+
+statementContainsPrimitive :: CorePrimitive -> CoreStatement -> Bool
+statementContainsPrimitive primitive statement = case statement of
+    CoreBind binding -> expressionContainsPrimitive primitive (coreBindingValue binding)
+    CoreAssign _ value -> expressionContainsPrimitive primitive value
+    CoreReturn value -> expressionContainsPrimitive primitive value
+    CoreEvaluate value -> expressionContainsPrimitive primitive value
+    CoreIf condition whenTrue whenFalse ->
+        expressionContainsPrimitive primitive condition
+            || containsPrimitiveInStatements primitive whenTrue
+            || containsPrimitiveInStatements primitive whenFalse
+    CoreWhile condition body ->
+        expressionContainsPrimitive primitive condition || containsPrimitiveInStatements primitive body
+    CoreDoWhile body condition ->
+        containsPrimitiveInStatements primitive body || expressionContainsPrimitive primitive condition
+    CoreFor condition updates body ->
+        expressionContainsPrimitive primitive condition
+            || containsPrimitiveInStatements primitive updates
+            || containsPrimitiveInStatements primitive body
+    CoreBreak -> False
+    CoreContinue -> False
+
+containsPrimitiveInStatements :: CorePrimitive -> [CoreStatement] -> Bool
+containsPrimitiveInStatements primitive = any (statementContainsPrimitive primitive)
+
+expressionContainsPrimitive :: CorePrimitive -> CoreExpression -> Bool
+expressionContainsPrimitive primitive expression = case expression of
+    CoreVariable _ _ -> False
+    CoreLiteral _ _ -> False
+    CoreApply callee arguments _ ->
+        any (expressionContainsPrimitive primitive) (callee : arguments)
+    CorePrimitive current arguments _ ->
+        current == primitive || any (expressionContainsPrimitive primitive) arguments
+    CoreLet _ _ value body _ ->
+        expressionContainsPrimitive primitive value || expressionContainsPrimitive primitive body
+    CoreConditional condition whenTrue whenFalse _ -> any (expressionContainsPrimitive primitive) [condition, whenTrue, whenFalse]
+    CoreClosure captures _ _ body _ ->
+        any (expressionContainsPrimitive primitive . coreCaptureValue) captures
+            || containsPrimitiveInStatements primitive body
+
+statementContainsCall :: CoreStatement -> Bool
+statementContainsCall statement = case statement of
+    CoreBind binding -> expressionContainsCall (coreBindingValue binding)
+    CoreAssign _ value -> expressionContainsCall value
+    CoreReturn value -> expressionContainsCall value
+    CoreEvaluate value -> expressionContainsCall value
+    CoreIf condition whenTrue whenFalse ->
+        expressionContainsCall condition || any statementContainsCall (whenTrue ++ whenFalse)
+    CoreWhile condition body -> expressionContainsCall condition || any statementContainsCall body
+    CoreDoWhile body condition -> any statementContainsCall body || expressionContainsCall condition
+    CoreFor condition updates body ->
+        expressionContainsCall condition || any statementContainsCall updates || any statementContainsCall body
+    CoreBreak -> False
+    CoreContinue -> False
+
+expressionContainsCall :: CoreExpression -> Bool
+expressionContainsCall expression = case expression of
+    CoreVariable _ _ -> False
+    CoreLiteral _ _ -> False
+    CoreApply {} -> True
+    CorePrimitive _ arguments _ -> any expressionContainsCall arguments
+    CoreLet _ _ value body _ -> expressionContainsCall value || expressionContainsCall body
+    CoreConditional condition whenTrue whenFalse _ -> any expressionContainsCall [condition, whenTrue, whenFalse]
+    CoreClosure captures _ _ body _ ->
+        any (expressionContainsCall . coreCaptureValue) captures
+            || any statementContainsCall body
+
+falseConjunction, falseDisjunction :: CoreFunction
+falseConjunction =
+    let condition = operationBool CoreLogicalAnd [compare CoreNotEqual y zero, variable flagName boolType]
+     in intFunction
+            [ CoreIf
+                (compare CoreNotEqual x zero)
+                [CoreIf condition [CoreReturn zero] [CoreReturn (divide x)]]
+                [CoreReturn zero]
+            ]
+falseDisjunction =
+    let condition = operationBool CoreLogicalOr [compare CoreEqual x zero, compare CoreEqual y zero]
+     in intFunction [CoreIf condition [CoreReturn zero] [CoreReturn (divide x)]]
+
+conflictingJoin, commonJoin :: CoreFunction
+conflictingJoin =
+    intFunction
+        [ mutableBinding
+        , CoreIf (variable flagName boolType) [CoreAssign mutableName zero] [CoreAssign mutableName (integer 2)]
+        , CoreReturn (divide mutableValue)
+        ]
+commonJoin =
+    intFunction
+        [ mutableBinding
+        , CoreIf (variable flagName boolType) [CoreAssign mutableName (integer 2)] [CoreAssign mutableName (integer 9)]
+        , CoreReturn (divide mutableValue)
+        ]
+
+comparisonGuard :: CorePrimitive -> Bool -> Bool -> CoreFunction
+comparisonGuard operator truth reverseOperands =
+    let condition = if reverseOperands then compareReversed operator zero x else compare operator x zero
+        yes = if truth then [CoreReturn (divide x)] else [CoreReturn zero]
+        no = if truth then [CoreReturn zero] else [CoreReturn (divide x)]
+     in intFunction [CoreIf condition yes no]
+
+guardedPrimitive :: CorePrimitive -> CoreFunction
+guardedPrimitive operator =
+    intFunction
+        [ CoreIf
+            (compare CoreNotEqual x zero)
+            [CoreReturn (operation operator x)]
+            [CoreReturn zero]
+        ]
+
+mutateGuardedEdge :: Bool -> Integer -> CoreFunction
+mutateGuardedEdge trueEdge value =
+    let condition = compare CoreNotEqual mutableValue zero
+        mutated = [CoreAssign mutableName (integer value), CoreReturn (divide mutableValue)]
+        retained = [CoreReturn zero]
+     in intFunction
+            [ mutableBinding
+            , CoreIf (if trueEdge then condition else operationBool CoreLogicalNot [condition]) mutated retained
+            ]
+
+nestedContradictionIsRemoved :: Bool
+nestedContradictionIsRemoved =
+    let function =
+            intFunction
+                [ CoreIf
+                    (compare CoreNotEqual x zero)
+                    [CoreIf (compare CoreEqual x zero) [CoreReturn (integer 99)] [CoreReturn (divide x)]]
+                    [CoreReturn zero]
+                ]
+     in case optimize function of
+            Just [CoreIf _ [CoreReturn (CorePrimitive CoreDivide _ _)] [CoreReturn _]] -> True
+            _ -> False
+
+closureBodyRetainsGuard :: Bool
+closureBodyRetainsGuard =
+    let closure =
+            CoreClosure
+                []
+                [(xName, intType)]
+                intType
+                [CoreIf (compare CoreNotEqual x zero) [CoreReturn (divide x)] [CoreReturn zero]]
+                (FunctionType [intType] intType)
+        function = CoreFunction mainName [] (FunctionType [intType] intType) [CoreReturn closure]
+     in case optimize function of
+            Just [CoreReturn (CoreClosure _ _ _ [CoreIf _ [CoreReturn (CorePrimitive CoreDivide _ _)] [CoreReturn _]] _)] -> True
+            _ -> False
+
+guarded :: CoreExpression -> Bool -> CoreFunction
+guarded condition trueEdge =
+    let yes = if trueEdge then [CoreReturn (divide x)] else [CoreReturn zero]
+        no = if trueEdge then [CoreReturn zero] else [CoreReturn (divide x)]
+     in intFunction [CoreIf condition yes no]
+
+guardedBody :: CoreExpression -> [CoreStatement] -> CoreFunction
+guardedBody condition unsafeArm = intFunction [CoreIf condition unsafeArm [CoreReturn zero]]
+
+intFunction :: [CoreStatement] -> CoreFunction
+intFunction = CoreFunction mainName parameters intType
+
+parameters :: [(ResolvedName, Type)]
+parameters =
+    [ (xName, intType)
+    , (yName, intType)
+    , (flagName, boolType)
+    , (unitCallbackName, FunctionType [] unitType)
+    , (boolCallbackName, FunctionType [] boolType)
+    ]
+
+mainName, xName, yName, flagName, unitCallbackName, boolCallbackName, mutableName, derivedName :: ResolvedName
+mainName = resolved 1 "Main"
+xName = resolved 10 "x"
+yName = resolved 11 "y"
+flagName = resolved 12 "flag"
+unitCallbackName = resolved 13 "unitCallback"
+boolCallbackName = resolved 14 "boolCallback"
+mutableName = resolved 15 "mutableValue"
+derivedName = resolved 32 "derived"
+
+ubyteType :: Type
+ubyteType = namedType "ubyte"
+
+x, y, zero, unitCallback, boolCallback, mutableValue :: CoreExpression
+x = variable xName intType
+y = variable yName intType
+zero = integer 0
+unitCallback = CoreApply (variable unitCallbackName (FunctionType [] unitType)) [] unitType
+boolCallback = CoreApply (variable boolCallbackName (FunctionType [] boolType)) [] boolType
+trueLiteral, falseLiteral :: CoreExpression
+trueLiteral = CoreLiteral (CoreBoolean True) boolType
+falseLiteral = CoreLiteral (CoreBoolean False) boolType
+mutableValue = variable mutableName intType
+
+mutableBinding :: CoreStatement
+mutableBinding = CoreBind (CoreBinding mutableName intType True x)
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+variable :: ResolvedName -> Type -> CoreExpression
+variable = CoreVariable
+
+compare :: CorePrimitive -> CoreExpression -> CoreExpression -> CoreExpression
+compare operator left right = CorePrimitive operator [left, right] boolType
+
+compareReversed :: CorePrimitive -> CoreExpression -> CoreExpression -> CoreExpression
+compareReversed operator left right = CorePrimitive operator [left, right] boolType
+
+operation :: CorePrimitive -> CoreExpression -> CoreExpression
+operation operator divisor = CorePrimitive operator [integer 12, divisor] intType
+
+divide :: CoreExpression -> CoreExpression
+divide = operation CoreDivide
+
+operationBool :: CorePrimitive -> [CoreExpression] -> CoreExpression
+operationBool operator operands = CorePrimitive operator operands boolType
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+effectIs :: Effect -> CoreFunction -> Bool
+effectIs expected function =
+    case optimizeCoreWith defaultOptimizerOptions (CoreModule moduleName [function]) of
+        Right result -> case optimizationEffectReports result of
+            [report] -> effectClassification report == expected
+            _ -> False
+        Left _ -> False
+    where
+        moduleName = QualifiedName [Identifier "Optimizer", Identifier "IntegerFlow"]
+
+optimize :: CoreFunction -> Maybe [CoreStatement]
+optimize function =
+    case optimizeCoreWith defaultOptimizerOptions (CoreModule moduleName [function]) of
+        Right result -> case coreModuleFunctions (optimizedCore result) of
+            [optimizedFunction] -> Just (coreFunctionBody optimizedFunction)
+            _ -> Nothing
+        Left _ -> Nothing
+    where
+        moduleName = QualifiedName [Identifier "Optimizer", Identifier "IntegerFlow"]
+ test/IterationTests.hs view
@@ -0,0 +1,328 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module IterationTests (iterationTests) where
+
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser (Token (..), TokenKind (..))
+
+-- These are vertical tests: each accepted loop must survive the source parser,
+-- name resolution, type checking, Core verification, Core wire, and CorePrep.
+iterationTests :: [(String, Bool)]
+iterationTests =
+    [ ("loop-control words are reserved lexer tokens", loopControlWordsAreReserved)
+    , ("a double dash after a value starts a line comment", doubleDashAfterValueIsComment)
+    , ("while lowers to a verified CorePrep back-edge", whileLowersToControlFlow)
+    , ("do/while executes its body before testing its condition", doWhileEntersBodyFirst)
+    , ("classic for lowers its update as a separate CFG region", forUpdateHasItsOwnBlock)
+    , ("continue in a for body targets the update region", forContinueRunsUpdate)
+    , ("break in a while body targets that loop's exit", breakTargetsLoopExit)
+    , ("nested break targets the innermost loop exit", nestedBreakTargetsInnerLoop)
+    , ("prefix and postfix increments are accepted in statement position", bothIncrementFormsCompile)
+    , ("for initializer bindings remain scoped to the loop", forInitializerDoesNotEscape)
+    , ("increment rejects immutable bindings", immutableIncrementIsRejected)
+    , ("increment rejects non-numeric bindings", nonNumericIncrementIsRejected)
+    , ("loop conditions use the ordinary boolean-or-numeric rule", stringLoopConditionIsRejected)
+    , ("break outside a loop is rejected", breakOutsideLoopIsRejected)
+    , ("continue outside a loop is rejected", continueOutsideLoopIsRejected)
+    , ("value-carrying break remains an explicit unsupported feature", valuedBreakIsRejected)
+    , ("enumerable for remains guarded by its missing generator ABI", forEachIsRejected)
+    , ("Core v10 round-trips all structured loop statement tags", loopCoreRoundTrips)
+    , ("loop CorePrep survives its verifier", loopCorePrepVerifies)
+    ]
+
+source :: String -> String
+source body =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(int limit) {"
+        , body
+        , "    }"
+        , "}"
+        ]
+
+whileSource :: String
+whileSource =
+    source $
+        unlines
+            [ "        int index = 0;"
+            , "        int total = 0;"
+            , "        while (index < limit) {"
+            , "            total = total + index;"
+            , "            index++;"
+            , "        }"
+            , "        return total;"
+            ]
+
+doWhileSource :: String
+doWhileSource =
+    source $
+        unlines
+            [ "        int value = limit;"
+            , "        do {"
+            , "            value -= 1;"
+            , "        } while (value > 0);"
+            , "        return value;"
+            ]
+
+forSource :: String
+forSource =
+    source $
+        unlines
+            [ "        int total = 0;"
+            , "        for (int index = 0; index < limit; index++) {"
+            , "            if (index == 2) {"
+            , "                continue;"
+            , "            }"
+            , "            total = total + index;"
+            , "        }"
+            , "        return total;"
+            ]
+
+allLoopsSource :: String
+allLoopsSource =
+    unlines
+        [ "class Program {"
+        , "    public static int WhileLoop(int limit) {"
+        , "        int index = 0;"
+        , "        while (index < limit) { index++; }"
+        , "        return index;"
+        , "    }"
+        , "    public static int DoLoop(int limit) {"
+        , "        int index = limit;"
+        , "        do { index -= 1; } while (index > 0);"
+        , "        return index;"
+        , "    }"
+        , "    public static int ForLoop(int limit) {"
+        , "        int total = 0;"
+        , "        for (int index = 0; index < limit; index++) {"
+        , "            if (index == 2) { continue; }"
+        , "            total = total + index;"
+        , "        }"
+        , "        return total;"
+        , "    }"
+        , "}"
+        ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "iteration.tala" text)
+
+compiled :: String -> Maybe FrontendArtifacts
+compiled text = either (const Nothing) Just (compileSource text)
+
+singleCoreFunction :: FrontendArtifacts -> Maybe CoreFunction
+singleCoreFunction artifacts = case coreModuleFunctions (artifactOptimizedCore artifacts) of
+    [function] -> Just function
+    _ -> Nothing
+
+singlePreparedFunction :: FrontendArtifacts -> Maybe CorePrepFunction
+singlePreparedFunction artifacts = case corePrepModuleFunctions (artifactCorePrep artifacts) of
+    [function] -> Just function
+    _ -> Nothing
+
+loopControlWordsAreReserved :: Bool
+loopControlWordsAreReserved = case runLexer defaultLexer (LexerInput "iteration.tala" "while do for break continue") of
+    Right tokens ->
+        [(tokenText token, tokenKind token) | token <- tokens, tokenKind token /= EndOfFileToken]
+            == [(word, KeywordToken) | word <- ["while", "do", "for", "break", "continue"]]
+    Left _ -> False
+
+-- The language has no decrement operator: `--` starts a comment wherever it
+-- stands outside a string, also directly after a value.
+doubleDashAfterValueIsComment :: Bool
+doubleDashAfterValueIsComment = case runLexer defaultLexer (LexerInput "iteration.tala" "value--; -- explanation\nnext") of
+    Right tokens ->
+        [(tokenText token, tokenKind token) | token <- tokens, tokenKind token /= EndOfFileToken]
+            == [("value", IdentifierToken), ("next", IdentifierToken)]
+    Left _ -> False
+
+whileLowersToControlFlow :: Bool
+whileLowersToControlFlow = case compiled whileSource of
+    Just artifacts -> any hasLoopShape (corePrepModuleFunctions (artifactCorePrep artifacts))
+    Nothing -> False
+    where
+        hasLoopShape function =
+            let blocks = corePrepFunctionBlocks function
+             in any isBranch blocks && any jumpsBackward blocks
+        jumpsBackward block = case corePrepBlockTerminator block of
+            CorePrepJump target -> target <= corePrepBlockId block
+            _ -> False
+
+doWhileEntersBodyFirst :: Bool
+doWhileEntersBodyFirst = case compiled doWhileSource >>= singlePreparedFunction of
+    Just function -> case corePrepFunctionBlocks function of
+        entry : body : condition : _ ->
+            corePrepBlockTerminator entry == CorePrepJump (corePrepBlockId body)
+                && corePrepBlockTerminator body == CorePrepJump (corePrepBlockId condition)
+                && case corePrepBlockTerminator condition of
+                    CorePrepBranch _ bodyId _ -> bodyId == corePrepBlockId body
+                    _ -> False
+        _ -> False
+    Nothing -> False
+
+forUpdateHasItsOwnBlock :: Bool
+forUpdateHasItsOwnBlock = case compiled forSource >>= singlePreparedFunction of
+    Just function -> case corePrepFunctionBlocks function of
+        entry : condition : _ -> case corePrepBlockTerminator condition of
+            CorePrepBranch _ bodyId _ ->
+                let updateId = bodyId + 1
+                 in corePrepBlockTerminator entry == CorePrepJump (corePrepBlockId condition)
+                        && maybe
+                            False
+                            ((== CorePrepJump (corePrepBlockId condition)) . corePrepBlockTerminator)
+                            (findBlock updateId (corePrepFunctionBlocks function))
+                        && maybe False (not . null . corePrepBlockInstructions) (findBlock updateId (corePrepFunctionBlocks function))
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+forContinueRunsUpdate :: Bool
+forContinueRunsUpdate = case compiled forSource >>= singlePreparedFunction of
+    Just function -> case corePrepFunctionBlocks function of
+        entry : condition : _ -> case corePrepBlockTerminator condition of
+            CorePrepBranch _ bodyId _ ->
+                let updateId = bodyId + 1
+                    blocks = corePrepFunctionBlocks function
+                    loopBodyBlocks = takeWhile ((/= updateId) . corePrepBlockId) (drop 2 blocks)
+                 in corePrepBlockTerminator entry == CorePrepJump (corePrepBlockId condition)
+                        && maybe False ((== CorePrepJump (corePrepBlockId condition)) . corePrepBlockTerminator) (findBlock updateId blocks)
+                        && any (jumpsTo updateId) loopBodyBlocks
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+breakTargetsLoopExit :: Bool
+breakTargetsLoopExit =
+    let text = source "int value = 0; while (value < limit) { break; } return value;"
+     in case compiled text >>= singlePreparedFunction of
+            Just function -> case corePrepFunctionBlocks function of
+                _entry : condition : _ -> case corePrepBlockTerminator condition of
+                    CorePrepBranch _ bodyId exitId ->
+                        bodyId /= corePrepBlockId condition
+                            && maybe False ((== CorePrepJump exitId) . corePrepBlockTerminator) (findBlock bodyId (corePrepFunctionBlocks function))
+                    _ -> False
+                _ -> False
+            Nothing -> False
+
+nestedBreakTargetsInnerLoop :: Bool
+nestedBreakTargetsInnerLoop =
+    let text =
+            source $
+                unlines
+                    [ "int outer = 0;"
+                    , "while (outer < limit) {"
+                    , "    int inner = 0;"
+                    , "    while (inner < limit) { break; }"
+                    , "    outer++;"
+                    , "}"
+                    , "return outer;"
+                    ]
+     in case compiled text >>= singlePreparedFunction of
+            Just function -> case filter isBranch (corePrepFunctionBlocks function) of
+                outerCondition : innerCondition : _ ->
+                    case (corePrepBlockTerminator outerCondition, corePrepBlockTerminator innerCondition) of
+                        (CorePrepBranch _ _ outerExit, CorePrepBranch _ innerBody innerExit) ->
+                            let blocks = corePrepFunctionBlocks function
+                             in innerBody /= corePrepBlockId outerCondition
+                                    && maybe False ((== CorePrepJump innerExit) . corePrepBlockTerminator) (findBlock innerBody blocks)
+                                    && innerExit /= outerExit
+                        _ -> False
+                _ -> False
+            Nothing -> False
+
+bothIncrementFormsCompile :: Bool
+bothIncrementFormsCompile =
+    let text = source "int value = 0; ++value; value++; return value;"
+     in case compiled text >>= singleCoreFunction of
+            Just function -> countIncrements (coreFunctionBody function) == 2
+            Nothing -> False
+
+forInitializerDoesNotEscape :: Bool
+forInitializerDoesNotEscape =
+    let text = source "for (int index = 0; index < limit; index++) {} return index;"
+     in case compileSource text of
+            Left _ -> True
+            Right _ -> False
+
+immutableIncrementIsRejected :: Bool
+immutableIncrementIsRejected = rejectedWith "TAT0022" (source "final int value = 0; value++; return value;")
+
+nonNumericIncrementIsRejected :: Bool
+nonNumericIncrementIsRejected = rejectedWith "TAT0024" (source "bool value = false; value++; return 0;")
+
+stringLoopConditionIsRejected :: Bool
+stringLoopConditionIsRejected = rejectedWith "TAT0020" (source "while (\"not a condition\") {} return 0;")
+
+breakOutsideLoopIsRejected :: Bool
+breakOutsideLoopIsRejected = rejectedWith "TAT0025" (source "break; return 0;")
+
+continueOutsideLoopIsRejected :: Bool
+continueOutsideLoopIsRejected = rejectedWith "TAT0027" (source "continue; return 0;")
+
+valuedBreakIsRejected :: Bool
+valuedBreakIsRejected = rejectedWith "TAT0026" (source "while (true) { break 3; } return 0;")
+
+forEachIsRejected :: Bool
+forEachIsRejected =
+    let text =
+            unlines
+                [ "class Program {"
+                , "    public static int Evaluate(System.Array<int> values) {"
+                , "        for (int value : values) {}"
+                , "        return 0;"
+                , "    }"
+                , "}"
+                ]
+     in rejectedWith "TAT0021" text
+
+loopCoreRoundTrips :: Bool
+loopCoreRoundTrips = case compiled allLoopsSource of
+    Just artifacts ->
+        let core = artifactOptimizedCore artifacts
+         in case encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits of
+                Right decoded -> decoded == core
+                Left _ -> False
+    Nothing -> False
+
+loopCorePrepVerifies :: Bool
+loopCorePrepVerifies = case compiled allLoopsSource of
+    Just artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Nothing -> False
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+countIncrements :: [CoreStatement] -> Int
+countIncrements = sum . map count
+    where
+        count statement = case statement of
+            CoreAssign _ (CorePrimitive primitive _ _) | primitive `elem` [CoreAdd, CoreSubtract] -> 1
+            CoreIf _ yes no -> countIncrements yes + countIncrements no
+            CoreWhile _ body -> countIncrements body
+            CoreDoWhile body _ -> countIncrements body
+            CoreFor _ body update -> countIncrements body + countIncrements update
+            _ -> 0
+
+isBranch :: CorePrepBlock -> Bool
+isBranch block = case corePrepBlockTerminator block of
+    CorePrepBranch {} -> True
+    _ -> False
+
+jumpsTo :: Int -> CorePrepBlock -> Bool
+jumpsTo target block = corePrepBlockTerminator block == CorePrepJump target
+
+findBlock :: Int -> [CorePrepBlock] -> Maybe CorePrepBlock
+findBlock identifier = find ((== identifier) . corePrepBlockId)
+    where
+        find _ [] = Nothing
+        find predicate (value : rest)
+            | predicate value = Just value
+            | otherwise = find predicate rest
+ test/LazyEvaluationTests.hs view
@@ -0,0 +1,301 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for evaluation by need.
+
+Tala computes a value when it is first needed and at most once, and
+never computes a value that is not needed. The tests observe that through
+the only things that tell a computed value from one that was not: a division
+by zero, a call that never returns, and the number of steps a program takes.
+The reference evaluator of "CoreInterpreter" gives no result for a division
+by zero and for a program that exceeds its step budget, so a value computed
+without need shows as a missing result.
+
+The effects of a program are not lazy. A store into a variable happens where
+it is written, whether or not the value around it is ever read, and a value
+means what its variables held where it was bound, however late it is
+computed. The tests pin both.
+
+An argument is a value like any other. A method receives it by need: it is
+computed when the method first needs it, at most once however often the
+method reads it and however many methods it is handed through, and not at
+all when no one needs it. The caller and the method share the one
+computation when both need the value.
+
+The expected values are written by hand.
+-}
+module LazyEvaluationTests (lazyEvaluationTests) where
+
+import CoreInterpreter
+import Data.List (intercalate, isInfixOf)
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Diagnostic
+
+lazyEvaluationTests :: [(String, Bool)]
+lazyEvaluationTests =
+    concat
+        [ [ ("unoptimized Core computes " ++ label, runs artifactCore statements arguments == Just expected)
+          , ("optimized Core computes " ++ label, runs artifactOptimizedCore statements arguments == Just expected)
+          ]
+        | (statements, runsOfCase) <- evaluationCases
+        , (arguments, expected) <- runsOfCase
+        , let label = show expected ++ " for " ++ show arguments ++ ": " ++ statements
+        ]
+        ++ [ ("every lazy program verifies as Core and as CorePrep", all (verifies . fst) evaluationCases)
+           , -- A value that is needed and cannot be computed is still a failure.
+             ("a needed division by zero has no result", runs artifactCore "int x = left / right; return x;" (1, 0) == Nothing)
+           ,
+               ( "a needed division by zero has no result on one path"
+               , runs artifactCore neededOnOnePath (6, 0) == Just 7 && runs artifactCore neededOnOnePath (6, 3) == Just 2
+               )
+           , ("a discarded value is evaluated", runs artifactCore "_ = left / right; return 5;" (1, 0) == Nothing)
+           , -- A value that is never needed leaves nothing behind in Core.
+             ("a value that is never read is not in the lowered Core", not (mentions "CoreDivide" artifactCore "int x = left / right; return 5;"))
+           , -- A value that the next statement is certain to need is
+             -- computed where it is bound, without a flag.
+             ("a value needed at once is computed in place", not (mentions "$known" artifactCore "int x = Half(left); return x + x;"))
+           , ("a value needed on one path only keeps its flag", mentions "$known" artifactCore neededOnOnePath)
+           , -- A value needed fifty times is computed once: the budget that
+             -- suffices for one computation does not suffice for fifty.
+             ("a value is computed at most once", runsWithin 2000 neededOften (100, 1) == Just 5000)
+           , ("the same work fifty times exceeds that budget", runsWithin 2000 computedOften (100, 1) == Nothing)
+           , -- An argument the method reads fifty times is computed once.
+             ("an argument is computed at most once", runsWithin 2000 "return Often(right, Step(left));" (100, 1) == Just 5000)
+           , -- The caller and the method it calls need the same value:
+             -- whoever needs it first computes it for both. The budget that
+             -- suffices for one computation does not suffice for two.
+             ("a value the caller and the method both need is computed once", runsWithin 800 sharedWithCallee (300, 1) == Just 600)
+           , ("the same work in the caller and in the method exceeds that budget", runsWithin 800 computedTwice (300, 1) == Nothing)
+           , -- An argument that may fail is handed on as a suspended
+             -- computation; one that cannot is passed as the value it is.
+             ("an argument that may fail is suspended", mentions "CoreMemoize" artifactCore "return Pick(right, left / right);")
+           , ("an argument that cannot fail is passed as a value", not (mentions "CoreMemoize" artifactCore "return Pick(right, left + 1);"))
+           , -- The method itself is unchanged: it still takes values, for
+             -- every caller that has them.
+             ("a method with a parameter by need still takes values", calls "Pick" [1, 5] == Just 5 && calls "Pick" [0, 5] == Just 7)
+           , -- Pending, and pinned so that a change shows: a callable
+             -- receives its arguments as values, and an argument that a
+             -- closure of the method captures is computed when the method
+             -- is entered.
+             ("an argument of a callable is still computed at the call", runs artifactCore throughCallable (6, 0) == Nothing)
+           , ("a captured argument is still computed on entry", runs artifactCore "return Kept(0, left / right);" (1, 0) == Nothing)
+           , -- A value that reads another value by need twice does not
+             -- carry the computation of that value twice: the Core of a
+             -- chain grows with its length, not with a power of it.
+             ("a chain of values read twice each grows linearly", coreSize (sharedChain 12) < 3 * coreSize (sharedChain 6))
+           ]
+    where
+        neededOnOnePath = "int x = left / right; if (right > 0) { return x; } return 7;"
+        neededOften = "int x = Step(left); int t = 0; for (int i = 0; i < 50; i += 1) { if (right > 0) { t += x; } } return t;"
+        computedOften = "int t = 0; for (int i = 0; i < 50; i += 1) { if (right > 0) { t += Step(left); } } return t;"
+        sharedWithCallee = "int x = Step(left); int y = Pick(right, x); return right > 0 ? x + y : y;"
+        computedTwice = "return Step(left) + Pick(right, Step(left));"
+        throughCallable = "auto f = \\(int a, int b) -> a > 0 ? b : 7; return f(right, left / right);"
+        calls name arguments = case compileSource (program "return 0;") of
+            Right artifacts -> case runFunction (artifactCore artifacts) name (map IntegerValue arguments) of
+                Just (IntegerValue value) -> Just value
+                _ -> Nothing
+            Left _ -> Nothing
+
+-- | A program around the given body of @Evaluate@.
+program :: String -> String
+program statements =
+    unlines
+        [ "namespace Test;"
+        , "class Program {"
+        , "    public static int Never(_ int v) { return Never(v + 1); }"
+        , "    public static int Half(_ int v) { return v / 2; }"
+        , "    public static int Step(_ int n) { return n > 0 ? 1 + Step(n - 1) : 0; }"
+        , "    public static int Pick(_ int flag, _ int value) { if (flag > 0) { return value; } return 7; }"
+        , "    public static int Pass(_ int flag, _ int value) { return Pick(flag, value); }"
+        , "    public static int Both(_ int flag, _ int first, _ int second) { if (flag > 0) { return first; } return second; }"
+        , "    public static int Kept(_ int flag, _ int value) { auto read = \\(int more) -> value + more; if (flag > 0) { return read(1); } return 3; }"
+        , "    public static int Often(_ int flag, _ int value) { int total = 0; for (int i = 0; i < 50; i += 1) { if (flag > 0) { total += value; } } return total; }"
+        , "    public static int Down(_ int n, _ int spare) { if (n <= 0) { return 0; } return 1 + Down(n - 1, spare / 0); }"
+        , "    public static int Evaluate(_ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "}"
+        ]
+
+-- | Bodies of @Evaluate@, the arguments to run them on, and the value each run must return.
+evaluationCases :: [(String, [((Integer, Integer), Integer)])]
+evaluationCases =
+    [ -- A value that is never needed is never computed: not a division by
+      -- zero, and not a call that never returns.
+      ("int x = left / right; return 5;", [((1, 0), 5)])
+    , ("int x = Never(left); return 5;", [((1, 0), 5)])
+    , -- A value is computed on the path that needs it and not on the other.
+      ("int x = left / right; if (right > 0) { return x; } return 7;", [((6, 0), 7), ((6, 3), 2)])
+    , ("int x = left / right; return match (right) { 0 -> 1, _ -> x };", [((6, 0), 1), ((6, 2), 3)])
+    , ("int x = left / right; return right > 0 && x > 1 ? 1 : 2;", [((6, 0), 2), ((6, 3), 1), ((1, 3), 2)])
+    , ("int x = left / right; int r = if (right > 0) { x + 1 } else { 0 }; return r;", [((6, 0), 0), ((6, 2), 4)])
+    , -- A value computed from a value that is not needed is not needed either.
+      ("int x = left / right; int y = x + 1; return right > 0 ? y : 9;", [((6, 0), 9), ((6, 3), 3)])
+    , ("int x = Never(left); int y = x + 1; int z = y * 2; return right > 0 ? 1 : 2;", [((0, 1), 1), ((0, 0), 2)])
+    , -- In a loop, each pass has its own value, computed if that pass needs it.
+        ( "int t = 0; for (int i = 0; i <= 3; i += 1) { int x = left / i; if (i > 1) { t += x; } } return t;"
+        , [((12, 0), 10)]
+        )
+    , ("int limit = left / right; int n = 0; while (right > 0 && n < limit) { n += 1; } return n;", [((6, 0), 0), ((6, 2), 3)])
+    , -- A value means what its variables held where it was bound.
+      ("int a = left; int x = Half(a) + a; a = 100; return x * 1000 + a;", [((8, 0), 12100)])
+    ,
+        ( "int a = left; int x = Half(a); a = a + 100; if (right > 0) { return x; } return a;"
+        , [((8, 1), 4), ((8, 0), 108)]
+        )
+    , ("int t = 0; for (int i = 1; i <= 3; i += 1) { int x = Half(i * 10); if (i == 2) { t = x; } } return t;", [((0, 0), 10)])
+    , -- A store happens where it is written, whether or not the value
+      -- around it is ever read.
+      ("int n = 0; int x = (n += 1) + left; return n * 10;", [((1, 0), 10)])
+    , ("int n = left; int x = n++ + Half(n); return n;", [((4, 0), 5)])
+    , -- The body of a callable evaluates by need as the body of a method
+      -- does, also when the callable stands inside another.
+        ( "auto f = \\(int v, int d) -> { int q = v / d; if (d > 0) { return q; } return 7; }; return f(left, right);"
+        , [((6, 0), 7), ((6, 3), 2)]
+        )
+    , ("auto f = \\(int v) -> { int q = Never(v); return 5; }; return f(left);", [((1, 0), 5)])
+    ,
+        ( "auto f = \\(int v, int d) -> { auto g = \\(int w) -> { int q = w / d; return d > 0 ? q : 9; }; return g(v); }; return f(left, right);"
+        , [((6, 0), 9), ((6, 2), 3)]
+        )
+    , ("auto f = \\(int v) -> { int a = v; int q = Half(a + 2); a = 100; return q * 1000 + a; }; return f(left);", [((8, 0), 5100)])
+    , -- A value that needs another value twice computes it once, also
+      -- through a long chain of such values.
+      (sharedChain 40, [((6, 0), 7), ((6, 3), 1)])
+    , -- The operands of an operator are evaluated whenever it is, so a value
+      -- by need that one of them reads twice is computed once, ahead of the
+      -- whole expression, however deep in the operands the reads are.
+      ("int x = left / right; int y = (x + 1) * (x + 2) - x; if (right > 0) { return y; } return 7;", [((6, 0), 7), ((6, 3), 10)])
+    , ("int x = left / right; int y = -(x - -x) + left; if (right > 0) { return y; } return 7;", [((6, 0), 7), ((6, 3), 2)])
+    , -- Reads that an operand may skip are still reads of that operand alone.
+      ("int x = left / right; int y = 1 + (right > 0 ? x + x : 0) + left; return right > 1 ? y : 9;", [((6, 0), 9), ((6, 1), 9), ((6, 3), 11)])
+    , ("int x = left / right; int y = (right > 0 && x + x > 3 ? 1 : 0) + left; return y;", [((6, 0), 6), ((6, 2), 7), ((6, 6), 6)])
+    , -- A value by need whose expression is a very long chain of operators
+      -- is lowered once for each operator, not once for each pair of them.
+      (operatorChain 1500, [((6, 0), 7), ((6, 3), 18002)])
+    , ("int x = left / right; int y = right > 0 ? x / 1 + x / 2 : 0; return right > 1 ? y : 9;", [((6, 0), 9), ((6, 1), 9), ((6, 3), 3)])
+    , -- A value by need is computed where a loop header or a guard reads it.
+        ( "int x = left / right; int t = 0; for (int i = 0; right > 0 && i < 3; i += x) { t += 1; } return t;"
+        , [((6, 0), 0), ((6, 3), 2)]
+        )
+    , ("int x = left / right; int n = 0; do { n += 1; } while (right > 0 && n < x); return n;", [((6, 0), 1), ((6, 2), 3)])
+    , ("int x = left / right; return match (right) { 0 -> 1, _ if x > 1 -> 2, _ -> 3 };", [((6, 0), 1), ((6, 3), 2), ((6, 6), 3)])
+    , -- An argument that the method never needs is never computed: not a
+      -- division by zero, and not a call that never returns.
+      ("return Pick(right, left / right);", [((6, 0), 7), ((6, 3), 2)])
+    , ("return Pick(right, Never(left));", [((1, 0), 7)])
+    , ("return Pick(0, Never(left)) + Pick(1, Half(left));", [((8, 0), 11)])
+    , ("return Both(right, Never(left), left + 1);", [((5, 0), 6)])
+    , ("return Both(right, left / right, Never(left));", [((6, 3), 2)])
+    , -- A value handed through one method to another is computed by the
+      -- one that needs it, if any does.
+      ("return Pass(right, left / right);", [((6, 0), 7), ((6, 3), 2)])
+    , ("return Pick(right, Pick(right, left / right) + 1);", [((6, 0), 7), ((6, 2), 4)])
+    , -- A method that calls itself hands a value on at every level; none
+      -- of them is ever needed.
+      ("return Down(left, right);", [((5, 0), 5), ((0, 0), 0)])
+    , -- A local by need is handed on by need, alone and inside a larger
+      -- argument, and to several methods.
+      ("int x = Never(left); int y = Pick(right, x); return y;", [((1, 0), 7)])
+    , ("int x = left / right; return Pick(right, x) + Pick(right, x + 1);", [((6, 0), 14), ((6, 3), 5)])
+    , ("int x = Step(left); int y = Pick(right, x); return right > 0 ? x + y : y;", [((30, 1), 60), ((30, 0), 7)])
+    , ("int x = left / right; int y = x + 1; return Pass(right, y * 2);", [((6, 0), 7), ((6, 3), 6)])
+    , -- An argument means what its variables held at the call.
+      ("int a = left; int r = Pick(right, Half(a) + a); a = 100; return r * 1000 + a;", [((8, 1), 12100), ((8, 0), 7100)])
+    , -- A store in an argument happens at the call, whether or not the
+      -- method ever reads the argument.
+      ("int n = 0; int r = Pick(0, (n += 1) + left); return n * 10 + r;", [((1, 0), 17)])
+    , -- A method reads an argument often and it is one value.
+      ("return Often(right, Half(left));", [((8, 1), 200), ((8, 0), 0)])
+    , -- An argument that a closure of the method captures is a value of
+      -- the closure.
+      ("return Kept(1, left / right);", [((6, 3), 3)])
+    , ("return Kept(0, left / right);", [((6, 3), 3)])
+    , -- In a loop every pass hands on a value of its own.
+        ( "int t = 0; for (int i = 0; i <= 3; i += 1) { t += Pick(i, left / i); } return t;"
+        , [((12, 0), 29)]
+        )
+    , -- An expression written as a statement is evaluated: that is all a
+      -- statement can be for.
+      ("int n = 0; _ = (n += 1) + left; return n;", [((1, 0), 1)])
+    , -- Needed values give what they always gave.
+      ("int x = Half(left); return x + x + x;", [((8, 0), 12)])
+    , ("int x = Step(left); int y = Step(right); return x * 10 + y;", [((3, 4), 34)])
+    ]
+
+{- | A chain of values of which each reads the one before it twice, and of
+which only the last is read, on one path.
+-}
+sharedChain :: Int -> String
+sharedChain links =
+    "int z0 = left / right; "
+        ++ concat
+            [ "int z" ++ show link ++ " = z" ++ show (link - 1) ++ " / 1 - z" ++ show (link - 1) ++ " / 2; "
+            | link <- [1 .. links]
+            ]
+        ++ "if (right > 0) { return z"
+        ++ show links
+        ++ "; } return 7;"
+
+{- | A value by need that adds the given number of terms, a division and
+the same number of terms again, and is read on one path only.
+-}
+operatorChain :: Int -> String
+operatorChain terms =
+    "int x = "
+        ++ intercalate " + " (replicate terms "left" ++ ["left / right"] ++ replicate terms "left")
+        ++ "; return match (right) { 0 -> 7, _ -> x };"
+
+-- | The size of the unoptimized Core of @Evaluate@ for the given body.
+coreSize :: String -> Int
+coreSize statements = case compileSource (program statements) of
+    Right artifacts ->
+        sum
+            [ length (show (coreFunctionBody function))
+            | function <- coreModuleFunctions (artifactCore artifacts)
+            , "Evaluate" `isInfixOf` show (coreFunctionName function)
+            ]
+    Left _ -> 0
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "lazy.tala" text)
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> (Integer, Integer) -> Maybe Integer
+runs select statements (left, right) = case compileSource (program statements) of
+    Right artifacts -> case runFunction (select artifacts) "Evaluate" [IntegerValue left, IntegerValue right] of
+        Just (IntegerValue value) -> Just value
+        _ -> Nothing
+    Left _ -> Nothing
+
+-- | The result of the unoptimized program within the given number of steps.
+runsWithin :: Int -> String -> (Integer, Integer) -> Maybe Integer
+runsWithin budget statements (left, right) = case compileSource (program statements) of
+    Right artifacts ->
+        case runFunctionWithBudget budget (artifactCore artifacts) "Evaluate" [IntegerValue left, IntegerValue right] of
+            Just (IntegerValue value) -> Just value
+            _ -> Nothing
+    Left _ -> Nothing
+
+-- | Whether the Core of @Evaluate@ mentions the given constructor or generated name.
+mentions :: String -> (FrontendArtifacts -> CoreModule) -> String -> Bool
+mentions needle select statements = case compileSource (program statements) of
+    Right artifacts ->
+        any
+            ((needle `isInfixOf`) . show . coreFunctionBody)
+            [ function
+            | function <- coreModuleFunctions (select artifacts)
+            , "Evaluate" `isInfixOf` show (coreFunctionName function)
+            ]
+    Left _ -> False
+
+verifies :: String -> Bool
+verifies statements = case compileSource (program statements) of
+    Right artifacts ->
+        verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+            && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts)
+            && verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+ test/LoopExpressionTests.hs view
@@ -0,0 +1,523 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Vertical tests for loops used as expressions and the value-carrying
+@break@ that supplies their value.
+
+A loop expression must not be able to end without a value, so the type
+tests pin every way it could: a condition that can become false, a bare
+@break@, no value-carrying @break@ at all. The evaluation tests state the
+value each program must compute and run the reference evaluator of
+"CoreInterpreter" on the unoptimized and on the optimized Core.
+-}
+module LoopExpressionTests (loopExpressionTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Specialization
+
+loopExpressionTests :: [(String, Bool)]
+loopExpressionTests = parserTests ++ typeTests ++ loweringTests ++ evaluationTests ++ pipelineTests
+
+-- ---------------------------------------------------------------- sources
+
+body :: String -> String
+body statements =
+    unlines
+        [ "class Program {"
+        , "    public static int Evaluate(_ bool flag, _ bool other, _ int left, _ int right) {"
+        , "        " ++ statements
+        , "    }"
+        , "    public static int Twice(_ int value) { return value + value; }"
+        , "}"
+        ]
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource text = do
+    tokens <- runLexer defaultLexer (LexerInput "loop-expression.tala" text)
+    runParser defaultParser (ParserInput "loop-expression.tala" tokens)
+
+parseRejected :: String -> Bool
+parseRejected text = either (const True) (const False) (parseSource text)
+
+firstStatements :: String -> Maybe [Statement Identifier ()]
+firstStatements text = case parseSource text of
+    Right (ParsedAST (SyntaxTree _ (TypeDeclaration {typeMembers = member : _} : _))) -> case member of
+        FunctionDeclaration {declarationBody = Block statements} -> Just statements
+        _ -> Nothing
+    _ -> Nothing
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "loop-expression.tala" text)
+
+accepted :: String -> Bool
+accepted text = either (const False) (const True) (compileSource text)
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+-- | The codes a source is rejected with, or none when it is accepted.
+codesOf :: String -> [String]
+codesOf text = either (map diagnosticCode) (const []) (compileSource text)
+
+-- ---------------------------------------------------------------- parser
+
+parserTests :: [(String, Bool)]
+parserTests =
+    [ ("a while loop in a binding initializer is a loop expression", whileExpressionParses)
+    , ("a for loop in a binding initializer is a loop expression", forExpressionParses)
+    , ("a loop expression is an operand of a binary operator", loopIsOperand)
+    , ("a loop expression is a call argument", loopIsArgument)
+    , ("a loop at the start of a statement stays a loop statement", loopStatementKeepsItsNode)
+    , ("a break may carry an assignment expression", breakCarriesAssignment)
+    , ("a do/while loop is not an expression", parseRejected (body "int r = do { break 1; } while (true); return r;"))
+    ,
+        ( "a loop expression in a binding needs its terminator"
+        , parseRejected (body "int r = while (true) { break 1; } return r;")
+        )
+    ]
+
+whileExpressionParses :: Bool
+whileExpressionParses = case firstStatements (body "int r = while (true) { break 10; }; return r;") of
+    Just
+        [ BindingStatement
+                _
+                _
+                _
+                (Identifier "r")
+                ()
+                (LoopExpression _ (WhileStatement _ _ (Block [BreakStatement _ (Just _)])) ())
+            , _
+            ] ->
+        True
+    _ -> False
+
+forExpressionParses :: Bool
+forExpressionParses = case firstStatements (body "int r = for (int i = 0; ; i++) { break i; }; return r;") of
+    Just [BindingStatement _ _ _ _ () (LoopExpression _ (ForStatement _ (Just _) Nothing [_] _) ()), _] -> True
+    _ -> False
+
+loopIsOperand :: Bool
+loopIsOperand = case firstStatements (body "return 1 + while (true) { break 2; } + 3;") of
+    Just [ReturnStatement _ (Just (BinaryExpression _ Add (BinaryExpression _ Add _ (LoopExpression {}) ()) _ ()))] -> True
+    _ -> False
+
+loopIsArgument :: Bool
+loopIsArgument = case firstStatements (body "return Twice(while (true) { break 2; });") of
+    Just [ReturnStatement _ (Just (CallExpression _ _ [LoopExpression {}] ()))] -> True
+    _ -> False
+
+loopStatementKeepsItsNode :: Bool
+loopStatementKeepsItsNode = case firstStatements (body "while (flag) { break; } for (int i = 0; i < 2; i++) { } return 0;") of
+    Just [WhileStatement {}, ForStatement {}, _] -> True
+    _ -> False
+
+breakCarriesAssignment :: Bool
+breakCarriesAssignment = case firstStatements (body "int a = 0; int r = while (true) { break a = 5; }; return r;") of
+    Just
+        [ _
+            , BindingStatement
+                _
+                _
+                _
+                _
+                ()
+                (LoopExpression _ (WhileStatement _ _ (Block [BreakStatement _ (Just (AssignmentExpression {}))])) ())
+            , _
+            ] ->
+        True
+    _ -> False
+
+-- ---------------------------------------------------------------- type checker
+
+typeTests :: [(String, Bool)]
+typeTests =
+    [ ("a while loop expression is accepted", accepted (body "int r = while (true) { break 10; }; return r;"))
+    ,
+        ( "a for loop expression without a condition is accepted"
+        , accepted (body "int r = for (int i = 0; ; i++) { if (i > left) { break i; } }; return r;")
+        )
+    ,
+        ( "a for loop expression with a constant true condition is accepted"
+        , accepted (body "int r = for (int i = 0; true; i++) { if (i > left) { break i; } }; return r;")
+        )
+    ,
+        ( "a break value takes its type from the receiver of the loop value"
+        , accepted (body "long wide = while (true) { break 10; }; return wide > 5 ? 1 : 0;")
+        )
+    ,
+        ( "a Boolean loop expression is accepted"
+        , accepted (body "bool found = while (true) { break flag; }; return found ? 1 : 0;")
+        )
+    ,
+        ( "continue is allowed in a loop expression"
+        , accepted (body "int n = 0; int r = while (true) { n++; if (n < 3) { continue; } break n; }; return r;")
+        )
+    ,
+        ( "a nested loop statement may use a bare break"
+        , accepted (body "int r = while (true) { while (flag) { break; } break 1; }; return r;")
+        )
+    ,
+        ( "a loop expression may nest in a loop expression"
+        , accepted (body "int r = while (true) { int inner = while (true) { break 2; }; break inner; }; return r;")
+        )
+    ,
+        ( "a value-carrying break in a loop statement is rejected"
+        , rejectedWith "TAT0026" (body "while (true) { break 3; } return 0;")
+        )
+    ,
+        ( "a value-carrying break in a nested loop statement is rejected"
+        , rejectedWith "TAT0026" (body "int r = while (true) { while (flag) { break 2; } break 1; }; return r;")
+        )
+    ,
+        ( "a value-carrying break in a do/while statement is rejected"
+        , rejectedWith "TAT0026" (body "do { break 3; } while (flag); return 0;")
+        )
+    , ("a value-carrying break outside any loop is rejected", codesOf (body "break 3; return 0;") == ["TAT0025"])
+    ,
+        ( "a bare break in a loop expression is rejected"
+        , rejectedWith "TAT0040" (body "int r = while (true) { if (flag) { break; } break 1; }; return r;")
+        )
+    ,
+        ( "a while loop expression with a variable condition is rejected"
+        , rejectedWith "TAT0041" (body "int r = while (flag) { break 1; }; return r;")
+        )
+    ,
+        ( "a while loop expression with a constant false condition is rejected"
+        , rejectedWith "TAT0041" (body "int r = while (false) { break 1; }; return r;")
+        )
+    ,
+        ( "a for loop expression with a condition that can fail is rejected"
+        , rejectedWith "TAT0041" (body "int r = for (int i = 0; i < left; i++) { break i; }; return r;")
+        )
+    ,
+        ( "a loop expression without a value-carrying break is rejected"
+        , rejectedWith "TAT0042" (body "int n = 0; int r = while (true) { n++; }; return r;")
+        )
+    ,
+        ( "break values of different types are rejected"
+        , rejectedWith "TAT0043" (body "long wide = 3; int r = while (true) { if (flag) { break wide; } break left; }; return r;")
+        )
+    ,
+        ( "a loop yields a string"
+        , accepted (body "int n = 0; auto text = while (true) { break \"done\"; }; return n;")
+        )
+    ,
+        ( "a callable loop value is not lowered yet"
+        , rejectedWith "TAT0044" (body "int n = 0; auto pick = while (true) { break \\(int v) -> v + 1; }; return n;")
+        )
+    ,
+        ( "a string and a number are not values of one loop"
+        , rejectedWith "TAT0043" (body "int n = 0; auto text = while (true) { if (n > 0) { break \"done\"; } break 1; }; return n;")
+        )
+    ,
+        ( "a return inside a loop expression leaves the method"
+        , accepted (body "int r = while (true) { if (flag) { return 1; } break 2; }; return r;")
+        )
+    ,
+        ( "a return inside a loop expression carries the return type of the method"
+        , rejectedWith "TAT0005" (body "int r = while (true) { if (flag) { return true; } break 2; }; return r;")
+        )
+    ,
+        ( "a loop expression that only returns never yields a value"
+        , accepted (body "int r = while (true) { return left; }; return r;")
+        )
+    ,
+        ( "a loop value of the wrong type is rejected by its receiver"
+        , not (accepted (body "long wide = 3; int r = while (true) { break wide; }; return r;"))
+        )
+    ,
+        ( "a problem in a break value is still reported"
+        , not (accepted (body "int r = while (true) { break missing; }; return r;"))
+        )
+    , ("an accepted loop expression reports nothing", codesOf (body "int r = while (true) { break left; }; return r;") == [])
+    ]
+
+-- ---------------------------------------------------------------- lowering
+
+loweringTests :: [(String, Bool)]
+loweringTests =
+    [ ("a loop expression lowers to a result slot, the loop and a read of the slot", loopLowers)
+    , ("a break value is stored before the loop is left", breakStoresThenLeaves)
+    , ("a for loop expression keeps its initializer and update", forLoopLowers)
+    , ("a loop expression in a conditional result runs only in that branch", loopInConditionalIsLazy)
+    , ("an operand read before a loop expression is held across it", operandIsHeldAcrossLoop)
+    , ("a loop statement lowers without a result slot", statementLoopHasNoSlot)
+    ]
+
+loweredBody :: String -> Maybe [CoreStatement]
+loweredBody text = case compileSource text of
+    Right artifacts -> functionBody (artifactCore artifacts)
+    Left _ -> Nothing
+
+functionBody :: CoreModule -> Maybe [CoreStatement]
+functionBody moduleValue =
+    case [ coreFunctionBody function
+         | function <- coreModuleFunctions moduleValue
+         , spelling (coreFunctionName function) == "Evaluate"
+         ] of
+        statements : _ -> Just statements
+        [] -> Nothing
+
+spelling :: ResolvedName -> String
+spelling = identifierText . resolvedSpelling
+
+isSlot :: ResolvedName -> Bool
+isSlot name = "$loop" `isPrefixOf` spelling name
+
+alwaysTrue :: CoreExpression
+alwaysTrue = CoreLiteral (CoreBoolean True) boolType
+
+loopLowers :: Bool
+loopLowers = case loweredBody (body "int r = while (true) { break left; }; return r;") of
+    Just
+        [ CoreBind (CoreBinding slot _ True initial)
+            , CoreWhile header [CoreAssign stored value, CoreBreak]
+            , CoreBind (CoreBinding _ _ _ (CoreVariable result _))
+            , _
+            ] ->
+        isSlot slot
+            && initial == CoreLiteral (CoreInteger 0) intType
+            && header == alwaysTrue
+            && stored == slot
+            && result == slot
+            && case value of
+                CoreVariable name _ -> spelling name == "left"
+                _ -> False
+    _ -> False
+
+breakStoresThenLeaves :: Bool
+breakStoresThenLeaves = case loweredBody (body "int n = 0; int r = while (true) { n += 1; if (n > left) { break n; } }; return r;") of
+    Just [_, CoreBind (CoreBinding slot _ True _), CoreWhile _ [_, CoreIf _ [CoreAssign stored _, CoreBreak] []], _, _] ->
+        isSlot slot && stored == slot
+    _ -> False
+
+forLoopLowers :: Bool
+forLoopLowers = case loweredBody (body "int r = for (int i = 0; ; i++) { if (i > left) { break i; } }; return r;") of
+    Just
+        [ CoreBind (CoreBinding slot _ True _)
+            , CoreBind (CoreBinding index _ True _)
+            , CoreFor header [CoreIf _ [CoreAssign stored _, CoreBreak] []] [CoreAssign updated _]
+            , _
+            , _
+            ] ->
+        isSlot slot && spelling index == "i" && header == alwaysTrue && stored == slot && updated == index
+    _ -> False
+
+loopInConditionalIsLazy :: Bool
+loopInConditionalIsLazy = case loweredBody (body "int r = flag ? while (true) { break left; } : right; return r;") of
+    Just
+        [ CoreBind (CoreBinding selected _ True _)
+            , CoreIf _ [CoreBind (CoreBinding slot _ True _), CoreWhile {}, CoreAssign first _] [CoreAssign second _]
+            , _
+            , _
+            ] ->
+        "$selected" `isPrefixOf` spelling selected && isSlot slot && first == selected && second == selected
+    _ -> False
+
+operandIsHeldAcrossLoop :: Bool
+operandIsHeldAcrossLoop = case loweredBody (body "int a = left; return a + while (true) { a += 1; break a; };") of
+    Just
+        [ _
+            , CoreBind (CoreBinding held _ False (CoreVariable source _))
+            , CoreBind (CoreBinding slot _ True _)
+            , CoreWhile {}
+            , CoreReturn (CorePrimitive CoreAdd [CoreVariable first _, CoreVariable second _] _)
+            ] ->
+        "$operand" `isPrefixOf` spelling held && spelling source == "a" && isSlot slot && first == held && second == slot
+    _ -> False
+
+statementLoopHasNoSlot :: Bool
+statementLoopHasNoSlot = case loweredBody (body "int n = 0; while (true) { n += 1; if (n > left) { break; } } return n;") of
+    Just [_, CoreWhile _ [_, CoreIf _ [CoreBreak] []], _] -> True
+    _ -> False
+
+-- ---------------------------------------------------------------- evaluation
+
+-- | Source bodies, the arguments to run them on, and the value each run must return.
+evaluationCases :: [(String, [((Bool, Bool, Integer, Integer), Integer)])]
+evaluationCases =
+    [ ("int r = while (true) { break left; }; return r;", [(plain 7 0, 7)])
+    ,
+        ( "int n = 0; int r = while (true) { n += 1; if (n * n > left) { break n; } }; return r;"
+        , [(plain 10 0, 4), (plain 0 0, 1)]
+        )
+    , ("int r = for (int i = 0; ; i++) { if (i * 3 >= left) { break i; } }; return r;", [(plain 10 0, 4), (plain 0 0, 0)])
+    , ("int r = for (int i = 0; true; i += 2) { if (i > left) { break i * 10; } }; return r;", [(plain 5 0, 60)])
+    ,
+        ( "int n = left; int r = while (true) { if (n > 100) { break 1; } if (n < 0) { break 0 - 1; } n = n * 2 - 3; }; return r * 1000 + n;"
+        , [(plain 5 0, 1131), (plain 2 0, -1001)]
+        )
+    ,
+        ( "int r = while (true) { int inner = for (int j = 0; ; j++) { if (j == right) { break j * 2; } }; break inner + 1; }; return r;"
+        , [(plain 0 3, 7)]
+        )
+    , ("int a = left; return a + while (true) { a += 1; if (a > 5) { break a; } } + a;", [(plain 3 0, 15), (plain 9 0, 29)])
+    ,
+        ( "int n = 0; int sum = 0; int r = while (true) { n++; if (n == 2) { continue; } sum += n; if (n >= left) { break sum; } }; return r;"
+        , [(plain 4 0, 8)]
+        )
+    ,
+        ( "int total = 0; int r = while (true) { int k = 0; while (true) { k++; if (k == 3) { break; } } total += k; if (total > left) { break total; } }; return r;"
+        , [(plain 7 0, 9)]
+        )
+    ,
+        ( "bool found = while (true) { break flag; }; return found ? 1 : 0;"
+        , [(flags True False 0 0, 1), (flags False False 0 0, 0)]
+        )
+    ,
+        ( "int a = 0; int r = while (true) { if (flag) { break a = left; } break (a += right); }; return r * 10 + a;"
+        , [(flags True False 3 4, 33), (flags False False 3 4, 44)]
+        )
+    ,
+        ( "int a = 0; int r = flag ? while (true) { a += 1; if (a == 3) { break a; } } : 7; return r * 10 + a;"
+        , [(flags True False 0 0, 33), (flags False False 0 0, 70)]
+        )
+    ,
+        ( "int n = 0; int sum = 0; while (for (int i = n; ; i++) { if (i >= n) { break i; } } < left) { sum += n; n++; } return sum;"
+        , [(plain 4 0, 6)]
+        )
+    , ("int n = 0; int r = Twice(while (true) { n += 3; if (n > left) { break n; } }) + n; return r;", [(plain 7 0, 27)])
+    ,
+        ( "int n = left; bool big = flag && while (true) { n += 1; break n > 5; }; return n * 10 + (big ? 1 : 0);"
+        , [(flags True False 5 0, 61), (flags True False 1 0, 20), (flags False False 5 0, 50)]
+        )
+    ,
+        ( "int n = 0; int r = (n = left) ?: while (true) { n += 7; break n; }; return r * 100 + n;"
+        , [(plain 4 0, 404), (plain 0 0, 707)]
+        )
+    , ("long wide = while (true) { break 10; }; return wide > 9 ? 1 : 0;", [(plain 0 0, 1)])
+    ]
+
+plain :: Integer -> Integer -> (Bool, Bool, Integer, Integer)
+plain = flags False False
+
+flags :: Bool -> Bool -> Integer -> Integer -> (Bool, Bool, Integer, Integer)
+flags flag other left right = (flag, other, left, right)
+
+evaluationTests :: [(String, Bool)]
+evaluationTests =
+    concat
+        [ [ ("unoptimized Core computes " ++ label, runs artifactCore statements arguments expected)
+          , ("optimized Core computes " ++ label, runs artifactOptimizedCore statements arguments expected)
+          ]
+        | (statements, runsOfCase) <- evaluationCases
+        , (arguments, expected) <- runsOfCase
+        , let label = show expected ++ " for " ++ show arguments ++ ": " ++ statements
+        ]
+
+runs :: (FrontendArtifacts -> CoreModule) -> String -> (Bool, Bool, Integer, Integer) -> Integer -> Bool
+runs select statements (flag, other, left, right) expected = case compileSource (body statements) of
+    Right artifacts ->
+        runFunction
+            (select artifacts)
+            "Evaluate"
+            [BooleanValue flag, BooleanValue other, IntegerValue left, IntegerValue right]
+            == Just (IntegerValue expected)
+    Left _ -> False
+
+-- ---------------------------------------------------------------- pipeline
+
+pipelineTests :: [(String, Bool)]
+pipelineTests =
+    [ ("every lowered case verifies as Core", all (verifies artifactCore) (map fst evaluationCases))
+    , ("every optimized case verifies as Core", all (verifies artifactOptimizedCore) (map fst evaluationCases))
+    , ("every case verifies as CorePrep", all corePrepVerifies (map fst evaluationCases))
+    , ("Core wire round-trips every lowered case", all wireRoundTrips (map fst evaluationCases))
+    , ("a template member keeps its loop expression when instantiated", templateInstantiates)
+    , ("a call inside a loop expression is reachable from the template member", templateReachesLoopCalls)
+    , ("a closure body may contain a loop expression", accepted closureSource)
+    ]
+
+verifies :: (FrontendArtifacts -> CoreModule) -> String -> Bool
+verifies select statements = case compileSource (body statements) of
+    Right artifacts -> verifyCore (select artifacts) == Right (select artifacts)
+    Left _ -> False
+
+corePrepVerifies :: String -> Bool
+corePrepVerifies statements = case compileSource (body statements) of
+    Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts)
+    Left _ -> False
+
+wireRoundTrips :: String -> Bool
+wireRoundTrips statements = case compileSource (body statements) of
+    Right artifacts ->
+        let core = artifactCore artifacts
+         in (encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits) == Right core
+    Left _ -> False
+
+templateSource :: String
+templateSource =
+    unwords
+        [ "template<typename T> class Box {"
+        , "int Entry(_ int seed) {"
+        , "int n = seed;"
+        , "int found = while (true) { n = Step(n); if (n > 9) { break Scale(n); } };"
+        , "return found;"
+        , "}"
+        , "int Step(_ int value) { return value + 4; }"
+        , "int Scale(_ int value) { return value * 2; }"
+        , "int Unused() { return 9; }"
+        , "}"
+        ]
+
+templatePlan :: Maybe TemplateSpecializationPlan
+templatePlan = case analyzeSemantics (CompilerInput "loop-template.tala" templateSource) of
+    Right artifacts ->
+        either
+            (const Nothing)
+            Just
+            ( planTemplateSpecializations
+                defaultTemplateSpecializationLimits
+                (semanticTypedAST artifacts)
+                [ TemplateSpecializationDemand
+                    (TemplateApplication (QualifiedName [Identifier "Box"]) [TypeTemplateArgument stringType])
+                    (TemplateMemberDemand [Identifier "Entry"])
+                    "loop-template"
+                ]
+            )
+    Left _ -> Nothing
+
+specializedMembers :: [Declaration ResolvedName Type]
+specializedMembers = case templatePlan of
+    Just plan ->
+        concat
+            [ members
+            | TypeDeclaration {typeMembers = members} <-
+                map templateSpecializationDeclaration (plannedTemplateSpecializations plan)
+            ]
+    Nothing -> []
+
+specializedNames :: [String]
+specializedNames = map (identifierText . resolvedSpelling . declarationName) specializedMembers
+
+templateInstantiates :: Bool
+templateInstantiates = "LoopExpression" `isInfixOf` show specializedMembers
+
+templateReachesLoopCalls :: Bool
+templateReachesLoopCalls =
+    all (`elem` specializedNames) ["Entry", "Step", "Scale"] && "Unused" `notElem` specializedNames
+
+closureSource :: String
+closureSource =
+    unlines
+        [ "class Program {"
+        , "    public static int Run(_ int seed) {"
+        , "        auto search = \\(int limit) -> { int n = 0; return while (true) { n += 1; if (n * n > limit) { break n; } }; };"
+        , "        return search(seed);"
+        , "    }"
+        , "}"
+        ]
+ test/LoopFlowOracleTests.hs view
@@ -0,0 +1,423 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Small concrete Core executor used only as an independent loop-fact oracle.
+
+The executor intentionally supports a strict, side-effect-free integer subset.
+Generated fixtures stay inside that subset; unsupported Core is reported as a
+test failure rather than silently assigned invented runtime behavior. For each
+finite execution, the abstract loop summary must contain its concrete normal
+exit value.
+-}
+module LoopFlowOracleTests (loopFlowOracleTests) where
+
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer.IntegerFacts
+
+loopFlowOracleTests :: [(String, Bool)]
+loopFlowOracleTests =
+    [ ("small while executions are contained by their abstract exit", all whileScenarioIsContained whileScenarios)
+    , ("small do-while executions preserve mandatory-body exits", all doScenarioIsContained doScenarios)
+    , ("small for executions route continue through update", all forScenarioIsContained forScenarios)
+    , ("nested transfer execution agrees with nearest-loop ownership", nestedScenarioIsContained)
+    , ("a concrete break exit is represented even beside a false edge", flaggedBreakIsContained)
+    , ("concrete while returns are contained by the return summary", all whileReturnIsContained smallInputs)
+    , ("concrete for-update returns are contained by the return summary", all forReturnIsContained smallInputs)
+    , ("a do-loop return preserves its exact pre-return state", doReturnIsContained)
+    , ("break skips the remaining body statements", breakSkipsBodyTail)
+    , ("continue-only cycles have no fabricated normal exit", continueCycleHasNoExit)
+    ]
+
+type Scenario = (Integer, Integer, Integer)
+
+smallInputs :: [Integer]
+smallInputs = [-2 .. 2]
+
+whileScenarios, doScenarios, forScenarios :: [Scenario]
+whileScenarios = [(initial, limit, stopAt) | initial <- smallInputs, limit <- smallInputs, stopAt <- smallInputs]
+doScenarios = whileScenarios
+forScenarios = whileScenarios
+
+whileScenarioIsContained :: Scenario -> Bool
+whileScenarioIsContained (initial, limit, stopAt) =
+    let index = indexVariable
+        advance = assignIndex (addInteger index 1)
+        transfer =
+            CoreIf
+                (compareInteger CoreEqual index (integerLiteral stopAt))
+                [CoreBreak]
+                [ CoreIf
+                    (compareInteger CoreEqual index (integerLiteral 1))
+                    [advance, CoreContinue]
+                    [advance]
+                ]
+        loop = CoreWhile (compareInteger CoreLessThan index (integerLiteral limit)) [transfer]
+     in scenarioExitIsContained initial [] loop
+
+doScenarioIsContained :: Scenario -> Bool
+doScenarioIsContained (initial, limit, stopAt) =
+    let index = indexVariable
+        advance = assignIndex (addInteger index 1)
+        body =
+            [ CoreIf
+                (compareInteger CoreEqual index (integerLiteral stopAt))
+                [CoreBreak]
+                [ CoreIf
+                    (compareInteger CoreEqual index (integerLiteral 0))
+                    [advance, CoreContinue]
+                    [advance]
+                ]
+            ]
+        loop = CoreDoWhile body (compareInteger CoreLessThan index (integerLiteral limit))
+     in scenarioExitIsContained initial [] loop
+
+forScenarioIsContained :: Scenario -> Bool
+forScenarioIsContained (initial, limit, stopAt) =
+    let index = indexVariable
+        advance = assignIndex (addInteger index 1)
+        body =
+            [ CoreIf
+                (compareInteger CoreEqual index (integerLiteral stopAt))
+                [CoreBreak]
+                [ CoreIf
+                    (compareInteger CoreEqual index (integerLiteral 1))
+                    [CoreContinue]
+                    [advance]
+                ]
+            ]
+        update = [advance]
+        loop = CoreFor (compareInteger CoreLessThan index (integerLiteral limit)) body update
+     in scenarioExitIsContained initial [] loop
+
+nestedScenarioIsContained :: Bool
+nestedScenarioIsContained =
+    let outerName = resolved 20 "outer"
+        innerName = resolved 21 "inner"
+        outer = CoreVariable outerName intType
+        inner = CoreVariable innerName intType
+        innerLoop = CoreDoWhile [CoreBreak] trueLiteral
+        forLoop = CoreFor (compareInteger CoreLessThan inner (integerLiteral 1)) [CoreContinue] [CoreBreak]
+        outerLoop =
+            CoreWhile
+                (compareInteger CoreLessThan outer (integerLiteral 3))
+                [innerLoop, forLoop, CoreAssign outerName (addInteger outer 1)]
+        facts =
+            bindFact
+                outerName
+                0
+                (bindFact innerName 0 emptyIntegerFacts)
+        environment = Map.fromList [(resolvedSymbol outerName, IntegerValue 0), (resolvedSymbol innerName, IntegerValue 0)]
+     in case loopFactSummary facts outerLoop of
+            Nothing -> False
+            Just summary -> case executeStatement 64 environment outerLoop of
+                Completed final -> runtimeInteger outerName final == Just 3 && stateContains summary outerName 3
+                _ -> False
+
+flaggedBreakIsContained :: Bool
+flaggedBreakIsContained =
+    let index = indexVariable
+        flag = CoreVariable flagName boolType
+        loop =
+            CoreWhile
+                trueLiteral
+                [CoreIf flag [CoreBreak] [assignIndex (addInteger index 1)]]
+        initial = initialFacts 2
+        environment =
+            Map.fromList
+                [ (resolvedSymbol indexName, IntegerValue 2)
+                , (resolvedSymbol flagName, BooleanValue True)
+                ]
+     in case loopFactSummary initial loop of
+            Nothing -> False
+            Just summary -> case executeStatement 64 environment loop of
+                Completed final -> runtimeInteger indexName final == Just 2 && stateContains summary indexName 2
+                _ -> False
+
+whileReturnIsContained :: Integer -> Bool
+whileReturnIsContained initial =
+    let index = indexVariable
+        loop =
+            CoreWhile
+                (compareInteger CoreLessThan index (integerLiteral 4))
+                [ CoreIf
+                    (compareInteger CoreEqual index (integerLiteral 2))
+                    [CoreReturn index]
+                    [assignIndex (addInteger index 1)]
+                ]
+     in returnScenarioIsContained initial loop
+
+forReturnIsContained :: Integer -> Bool
+forReturnIsContained initial =
+    let index = indexVariable
+        update =
+            [ CoreIf
+                (compareInteger CoreEqual index (integerLiteral 3))
+                [CoreReturn index]
+                [assignIndex (addInteger index 1)]
+            ]
+        loop = CoreFor (compareInteger CoreLessThan index (integerLiteral 5)) [CoreContinue] update
+     in returnScenarioIsContained initial loop
+
+doReturnIsContained :: Bool
+doReturnIsContained = returnScenarioIsContained 7 (CoreDoWhile [CoreReturn indexVariable] falseLiteral)
+
+returnScenarioIsContained :: Integer -> CoreStatement -> Bool
+returnScenarioIsContained initial loop =
+    let facts = initialFacts initial
+        environment = Map.singleton (resolvedSymbol indexName) (IntegerValue initial)
+     in case loopFactSummary facts loop of
+            Nothing -> False
+            Just summary -> case executeStatement 64 environment loop of
+                Returned final ->
+                    maybe False (stateHasValue (loopReturnFacts summary) indexName) (runtimeInteger indexName final)
+                Completed _ -> True
+                FuelExhausted -> False
+                UnsupportedRuntime -> False
+                Broke {} -> False
+                Continued {} -> False
+
+stateHasValue :: IntegerFacts -> ResolvedName -> Integer -> Bool
+stateHasValue facts name value = case lookupIntegerFact facts (resolvedSymbol name) of
+    Nothing -> not (isUnreachableFacts facts)
+    Just fact -> factContains fact value
+
+breakSkipsBodyTail :: Bool
+breakSkipsBodyTail =
+    let loop =
+            CoreWhile
+                trueLiteral
+                [CoreBreak, assignIndex (integerLiteral 999)]
+        facts = initialFacts 1
+        environment = Map.singleton (resolvedSymbol indexName) (IntegerValue 1)
+     in case loopFactSummary facts loop of
+            Nothing -> False
+            Just summary -> case executeStatement 64 environment loop of
+                Completed final ->
+                    runtimeInteger indexName final == Just 1
+                        && stateContains summary indexName 1
+                _ -> False
+
+continueCycleHasNoExit :: Bool
+continueCycleHasNoExit =
+    let loop =
+            CoreWhile
+                (compareInteger CoreLessThan indexVariable (integerLiteral 2))
+                [CoreContinue, assignIndex (integerLiteral 999)]
+        facts = initialFacts 0
+        environment = Map.singleton (resolvedSymbol indexName) (IntegerValue 0)
+     in case loopFactSummary facts loop of
+            Nothing -> False
+            Just summary ->
+                isUnreachableFacts (loopExitFacts summary)
+                    && case executeStatement 8 environment loop of
+                        FuelExhausted -> True
+                        _ -> False
+
+scenarioExitIsContained :: Integer -> [(ResolvedName, Integer)] -> CoreStatement -> Bool
+scenarioExitIsContained initial additionalValues loop =
+    let facts = foldr (uncurry bindFact) (initialFacts initial) additionalValues
+        environment =
+            Map.fromList
+                ((resolvedSymbol indexName, IntegerValue initial) : map toEntry additionalValues)
+     in case loopFactSummary facts loop of
+            Nothing -> False
+            Just summary -> case executeStatement 64 environment loop of
+                Completed final ->
+                    maybe False (stateContains summary indexName) (runtimeInteger indexName final)
+                FuelExhausted -> False
+                UnsupportedRuntime -> False
+                Returned {} -> False
+                Broke {} -> False
+                Continued {} -> False
+    where
+        toEntry (name, value) = (resolvedSymbol name, IntegerValue value)
+
+initialFacts :: Integer -> IntegerFacts
+initialFacts value = bindFact indexName value emptyIntegerFacts
+
+bindFact :: ResolvedName -> Integer -> IntegerFacts -> IntegerFacts
+bindFact name value facts =
+    transferStatementFacts
+        facts
+        (CoreBind (CoreBinding name intType True (integerLiteral value)))
+
+stateContains :: LoopFactSummary -> ResolvedName -> Integer -> Bool
+stateContains summary name value = case lookupIntegerFact (loopExitFacts summary) (resolvedSymbol name) of
+    Nothing -> not (isUnreachableFacts (loopExitFacts summary))
+    Just fact -> factContains fact value
+
+factContains :: IntegerFact -> Integer -> Bool
+factContains fact value =
+    maybe True (<= value) (integerMinimum fact)
+        && maybe True (>= value) (integerMaximum fact)
+        && not (value == 0 && integerExcludesZero fact)
+
+data RuntimeValue
+    = IntegerValue Integer
+    | BooleanValue Bool
+    deriving (Eq, Ord, Read, Show)
+
+type Runtime = Map SymbolId RuntimeValue
+
+data RuntimeResult
+    = Completed Runtime
+    | Returned Runtime
+    | Broke Runtime
+    | Continued Runtime
+    | FuelExhausted
+    | UnsupportedRuntime
+    deriving (Eq, Ord, Read, Show)
+
+executeStatement :: Int -> Runtime -> CoreStatement -> RuntimeResult
+executeStatement fuel environment statement = case statement of
+    CoreBind binding ->
+        case evaluateExpression environment (coreBindingValue binding) of
+            Just value -> Completed (Map.insert (resolvedSymbol (coreBindingName binding)) value environment)
+            Nothing -> UnsupportedRuntime
+    CoreAssign name expression ->
+        case evaluateExpression environment expression of
+            Just value -> Completed (Map.insert (resolvedSymbol name) value environment)
+            Nothing -> UnsupportedRuntime
+    CoreReturn expression ->
+        maybe UnsupportedRuntime (const (Returned environment)) (evaluateExpression environment expression)
+    CoreIf condition whenTrue whenFalse -> doBranch fuel environment condition whenTrue whenFalse
+    CoreEvaluate expression -> maybe UnsupportedRuntime (const (Completed environment)) (evaluateExpression environment expression)
+    CoreWhile condition body -> runWhile fuel environment condition body
+    CoreDoWhile body condition -> runDoWhile fuel environment body condition
+    CoreFor condition body update -> runFor fuel environment condition body update
+    CoreBreak -> Broke environment
+    CoreContinue -> Continued environment
+
+executeStatements :: Int -> Runtime -> [CoreStatement] -> RuntimeResult
+executeStatements _ environment [] = Completed environment
+executeStatements fuel environment (statement : remaining) =
+    case executeStatement fuel environment statement of
+        Completed next -> executeStatements fuel next remaining
+        abrupt -> abrupt
+
+doBranch :: Int -> Runtime -> CoreExpression -> [CoreStatement] -> [CoreStatement] -> RuntimeResult
+doBranch fuel environment condition whenTrue whenFalse =
+    case evaluateExpression environment condition of
+        Just (BooleanValue True) -> executeStatements fuel environment whenTrue
+        Just (BooleanValue False) -> executeStatements fuel environment whenFalse
+        _ -> UnsupportedRuntime
+
+runWhile :: Int -> Runtime -> CoreExpression -> [CoreStatement] -> RuntimeResult
+runWhile fuel environment condition body
+    | fuel <= 0 = FuelExhausted
+    | otherwise = case evaluateExpression environment condition of
+        Just (BooleanValue False) -> Completed environment
+        Just (BooleanValue True) ->
+            case executeStatements (fuel - 1) environment body of
+                Completed next -> runWhile (fuel - 1) next condition body
+                Continued next -> runWhile (fuel - 1) next condition body
+                Broke next -> Completed next
+                abrupt -> abrupt
+        _ -> UnsupportedRuntime
+
+runDoWhile :: Int -> Runtime -> [CoreStatement] -> CoreExpression -> RuntimeResult
+runDoWhile fuel environment body condition
+    | fuel <= 0 = FuelExhausted
+    | otherwise = case executeStatements (fuel - 1) environment body of
+        Broke next -> Completed next
+        Returned next -> Returned next
+        UnsupportedRuntime -> UnsupportedRuntime
+        FuelExhausted -> FuelExhausted
+        Continued next -> testAgain (fuel - 1) next
+        Completed next -> testAgain (fuel - 1) next
+    where
+        testAgain remaining next = case evaluateExpression next condition of
+            Just (BooleanValue False) -> Completed next
+            Just (BooleanValue True) -> runDoWhile remaining next body condition
+            _ -> UnsupportedRuntime
+
+runFor :: Int -> Runtime -> CoreExpression -> [CoreStatement] -> [CoreStatement] -> RuntimeResult
+runFor fuel environment condition body update
+    | fuel <= 0 = FuelExhausted
+    | otherwise = case evaluateExpression environment condition of
+        Just (BooleanValue False) -> Completed environment
+        Just (BooleanValue True) ->
+            case executeStatements (fuel - 1) environment body of
+                Broke next -> Completed next
+                Returned next -> Returned next
+                UnsupportedRuntime -> UnsupportedRuntime
+                FuelExhausted -> FuelExhausted
+                Completed next -> updateThenRepeat (fuel - 1) next
+                Continued next -> updateThenRepeat (fuel - 1) next
+        _ -> UnsupportedRuntime
+    where
+        updateThenRepeat remaining next = case executeStatements remaining next update of
+            Broke afterUpdate -> Completed afterUpdate
+            Returned afterUpdate -> Returned afterUpdate
+            UnsupportedRuntime -> UnsupportedRuntime
+            FuelExhausted -> FuelExhausted
+            Completed afterUpdate -> runFor remaining afterUpdate condition body update
+            Continued afterUpdate -> runFor remaining afterUpdate condition body update
+
+evaluateExpression :: Runtime -> CoreExpression -> Maybe RuntimeValue
+evaluateExpression environment expression = case expression of
+    CoreVariable name _ -> Map.lookup (resolvedSymbol name) environment
+    CoreLiteral (CoreInteger value) _ -> Just (IntegerValue value)
+    CoreLiteral (CoreBoolean value) _ -> Just (BooleanValue value)
+    CorePrimitive CoreLogicalAnd [left, right] _ -> do
+        BooleanValue leftValue <- evaluateExpression environment left
+        if leftValue then evaluateExpression environment right else pure (BooleanValue False)
+    CorePrimitive CoreLogicalOr [left, right] _ -> do
+        BooleanValue leftValue <- evaluateExpression environment left
+        if leftValue then pure (BooleanValue True) else evaluateExpression environment right
+    CorePrimitive CoreLogicalNot [value] _ -> do
+        BooleanValue boolean <- evaluateExpression environment value
+        pure (BooleanValue (not boolean))
+    CorePrimitive primitive [left, right] _ -> do
+        leftValue <- evaluateExpression environment left
+        rightValue <- evaluateExpression environment right
+        evaluateBinary primitive leftValue rightValue
+    _ -> Nothing
+
+evaluateBinary :: CorePrimitive -> RuntimeValue -> RuntimeValue -> Maybe RuntimeValue
+evaluateBinary primitive left right = case (primitive, left, right) of
+    (CoreAdd, IntegerValue first, IntegerValue second) -> Just (IntegerValue (first + second))
+    (CoreSubtract, IntegerValue first, IntegerValue second) -> Just (IntegerValue (first - second))
+    (CoreEqual, first, second) -> Just (BooleanValue (first == second))
+    (CoreNotEqual, first, second) -> Just (BooleanValue (first /= second))
+    (CoreLessThan, IntegerValue first, IntegerValue second) -> Just (BooleanValue (first < second))
+    (CoreLessEqual, IntegerValue first, IntegerValue second) -> Just (BooleanValue (first <= second))
+    (CoreGreaterThan, IntegerValue first, IntegerValue second) -> Just (BooleanValue (first > second))
+    (CoreGreaterEqual, IntegerValue first, IntegerValue second) -> Just (BooleanValue (first >= second))
+    _ -> Nothing
+
+runtimeInteger :: ResolvedName -> Runtime -> Maybe Integer
+runtimeInteger name environment = case Map.lookup (resolvedSymbol name) environment of
+    Just (IntegerValue value) -> Just value
+    _ -> Nothing
+
+assignIndex :: CoreExpression -> CoreStatement
+assignIndex = CoreAssign indexName
+
+addInteger :: CoreExpression -> Integer -> CoreExpression
+addInteger left right = CorePrimitive CoreAdd [left, integerLiteral right] intType
+
+compareInteger :: CorePrimitive -> CoreExpression -> CoreExpression -> CoreExpression
+compareInteger primitive left right = CorePrimitive primitive [left, right] boolType
+
+integerLiteral :: Integer -> CoreExpression
+integerLiteral value = CoreLiteral (CoreInteger value) intType
+
+trueLiteral :: CoreExpression
+trueLiteral = CoreLiteral (CoreBoolean True) boolType
+
+falseLiteral :: CoreExpression
+falseLiteral = CoreLiteral (CoreBoolean False) boolType
+
+indexVariable :: CoreExpression
+indexVariable = CoreVariable indexName intType
+
+indexName, flagName :: ResolvedName
+indexName = resolved 10 "index"
+flagName = resolved 11 "flag"
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+ test/LoopFlowTests.hs view
@@ -0,0 +1,514 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Regression tests for loop-carried Core facts.
+
+These tests observe the public optimizer result rather than importing the
+private abstract-domain implementation. That keeps the contract behavioral:
+the optimizer may change its widening strategy without changing which source
+paths are provable or which potentially failing operations must remain.
+-}
+module LoopFlowTests (loopFlowTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Optimizer.IntegerFacts
+
+loopFlowTests :: [(String, Bool)]
+loopFlowTests =
+    [ ("while false-edge facts reach the following branch", whileCounterExit)
+    , ("a pre-test loop that starts false preserves its exact input", falseFirstIteration)
+    , ("do-while transfers the mandatory first body execution", doWhileExecutesOnce)
+    , ("do-while continue reaches its condition", doWhileContinueReachesCondition)
+    , ("for continue still executes the update sequence", forContinueExecutesUpdate)
+    , ("break contributes an exit without becoming a back-edge", breakIsAnExit)
+    , ("break exits join with the condition-false edge", breakAndConditionExitsJoin)
+    , ("a zero-valued break path is not lost at the loop join", breakCanCarryZero)
+    , ("a return inside a do-loop does not contaminate normal exit facts", returnDoesNotJoinExit)
+    , ("nested-loop break is consumed by its nearest loop", nestedBreakIsLocal)
+    , ("nested-loop continue is consumed by its nearest loop", nestedContinueIsLocal)
+    , ("calls in loop bodies invalidate loop-carried facts", callInBodyInvalidates)
+    , ("calls in loop conditions invalidate loop-carried facts", callInConditionInvalidates)
+    , ("overflowing induction arithmetic drops the interval", overflowingInductionIsUnknown)
+    , ("a loop assignment can replace nonzero with exact zero", assignmentCanEstablishZero)
+    , ("a do-loop join keeps zero exclusion common to both arms", doLoopNonzeroJoin)
+    , ("a false while condition prevents body assignments from leaking", falseWhileDoesNotRun)
+    , ("a false for condition prevents body assignments from leaking", falseForDoesNotRun)
+    , ("an unconditional loop break retains its exact exit value", unconditionalBreakRetainsValue)
+    , ("a continue-only infinite path is not treated as a break", continueDoesNotExit)
+    , ("loop headers retain stable facts that do not expand", stableHeaderFact)
+    , ("condition calls are not folded from stale pre-loop constants", callConditionStaysUnknown)
+    , ("widened false-edge summaries include every concrete counter exit", boundedCounterExitOracle)
+    , ("the fixed-point solver completes ordinary counters within its cap", boundedCounterSolverBudget)
+    , ("do-loop summaries keep first-entry and condition facts distinct", doWhileSummarySeparatesPhases)
+    , ("for continue facts enter the update before an update break", forContinueSummary)
+    , ("a body break skips the for update", forBreakSkipsUpdate)
+    , ("loop return facts remain separate from the normal exit", loopReturnSummary)
+    , ("a called body produces unconstrained loop exits", calledBodySummaryIsTop)
+    , ("a called for-update drops stale exit facts", callInUpdateInvalidates)
+    , ("an update break carries its post-update assignment", updateBreakCarriesUpdatedValue)
+    ]
+        ++ boundedCounterCases
+
+-- A counted while loop exercises interval widening: the header must stop
+-- growing even though the concrete loop may execute many more than eight
+-- iterations. Its false condition then proves the complementary lower bound.
+whileCounterExit :: Bool
+whileCounterExit = postGuardIsRemoved (counterLoop 0 17 WhileCounter)
+
+falseFirstIteration :: Bool
+falseFirstIteration = postGuardIsRemoved (counterLoop 12 5 WhileCounter)
+
+doWhileExecutesOnce :: Bool
+doWhileExecutesOnce =
+    let index = loopVariable
+        loop = CoreDoWhile [assign indexName 1] falseLiteral
+        program = localLoopProgram 0 loop (notEqual index 1) []
+     in postGuardIsRemoved program
+
+doWhileContinueReachesCondition :: Bool
+doWhileContinueReachesCondition =
+    let index = loopVariable
+        loop =
+            CoreDoWhile
+                [ assign indexName 1
+                , CoreContinue
+                , assign indexName 0
+                ]
+                falseLiteral
+        program = localLoopProgram 0 loop (notEqual index 1) []
+     in postGuardIsRemoved program
+
+forContinueExecutesUpdate :: Bool
+forContinueExecutesUpdate =
+    let index = loopVariable
+        increment = add index 1
+        body =
+            [ CoreIf
+                (equal index 2)
+                [CoreContinue]
+                [CoreAssign indexName increment]
+            ]
+        update = [CoreAssign indexName increment]
+        loop = CoreFor (lessThan index 4) body update
+        program = localLoopProgram 0 loop (lessThan index 4) []
+     in postGuardIsRemoved program
+
+breakIsAnExit :: Bool
+breakIsAnExit =
+    let index = loopVariable
+        loop = CoreWhile trueLiteral [CoreIf (equal index 0) [CoreBreak] []]
+        program = localLoopProgram 0 loop (equal index 0) []
+     in optimizedBody program `containsReturnedDivision` indexName
+
+breakAndConditionExitsJoin :: Bool
+breakAndConditionExitsJoin =
+    let index = loopVariable
+        loop =
+            CoreWhile
+                (lessThan index 12)
+                [ CoreIf
+                    (equal index 3)
+                    [CoreBreak]
+                    [CoreAssign indexName (add index 1)]
+                ]
+        program = localLoopProgram 0 loop (equal index 3) []
+     in topLevelGuardRemains program
+
+breakCanCarryZero :: Bool
+breakCanCarryZero =
+    let index = loopVariable
+        loop = CoreWhile trueLiteral [CoreIf (equal index 0) [CoreBreak] [assign indexName 1]]
+        program = localLoopProgram 0 loop (equal index 0) []
+     in optimizedBody program `containsReturnedDivision` indexName
+
+returnDoesNotJoinExit :: Bool
+returnDoesNotJoinExit =
+    let index = loopVariable
+        flag = CoreVariable flagName boolType
+        loop = CoreDoWhile [CoreIf flag [CoreReturn (literal 99)] [], assign indexName 1] falseLiteral
+        program = localLoopProgram 0 loop (notEqual index 1) [(flagName, boolType)]
+     in postGuardIsRemoved program
+
+nestedBreakIsLocal :: Bool
+nestedBreakIsLocal =
+    let index = loopVariable
+        inner = CoreWhile trueLiteral [CoreBreak]
+        outer =
+            CoreWhile
+                (lessThan index 1)
+                [inner, assign indexName 1]
+        program = localLoopProgram 0 outer (lessThan index 1) []
+     in postGuardIsRemoved program
+
+nestedContinueIsLocal :: Bool
+nestedContinueIsLocal =
+    let index = loopVariable
+        inner = CoreDoWhile [CoreContinue] falseLiteral
+        outer = CoreWhile (lessThan index 1) [inner, assign indexName 1]
+        program = localLoopProgram 0 outer (lessThan index 1) []
+     in postGuardIsRemoved program
+
+callInBodyInvalidates :: Bool
+callInBodyInvalidates =
+    let index = loopVariable
+        callback = CoreVariable callbackName (FunctionType [] unitType)
+        invoke = CoreEvaluate (CoreApply callback [] unitType)
+        loop = CoreWhile trueLiteral [invoke, CoreBreak]
+        program = localLoopProgram 2 loop (notEqual index 0) [(callbackName, FunctionType [] unitType)]
+     in topLevelGuardRemains program
+
+callInConditionInvalidates :: Bool
+callInConditionInvalidates =
+    let index = loopVariable
+        predicate = CoreVariable predicateName (FunctionType [] boolType)
+        loop = CoreWhile (CoreApply predicate [] boolType) [CoreBreak]
+        program = localLoopProgram 2 loop (notEqual index 0) [(predicateName, FunctionType [] boolType)]
+     in topLevelGuardRemains program
+
+overflowingInductionIsUnknown :: Bool
+overflowingInductionIsUnknown =
+    let index = loopVariable
+        flag = CoreVariable flagName boolType
+        loop = CoreWhile flag [CoreAssign indexName (add index 1)]
+        program = localLoopProgram 0 loop (equal index 0) [(flagName, boolType)]
+     in topLevelGuardRemains program
+
+assignmentCanEstablishZero :: Bool
+assignmentCanEstablishZero =
+    let index = loopVariable
+        loop = CoreDoWhile [assign indexName 0] (notEqual index 0)
+        program = localLoopProgram 7 loop (notEqual index 0) []
+     in postGuardIsRemoved program
+
+doLoopNonzeroJoin :: Bool
+doLoopNonzeroJoin =
+    let index = loopVariable
+        flag = CoreVariable flagName boolType
+        loop =
+            CoreDoWhile
+                [CoreIf flag [assign indexName 2] [assign indexName 3]]
+                falseLiteral
+        program = localLoopProgram 0 loop (equal index 0) [(flagName, boolType)]
+     in postGuardIsRemoved program
+
+falseWhileDoesNotRun :: Bool
+falseWhileDoesNotRun =
+    let index = loopVariable
+        loop = CoreWhile falseLiteral [assign indexName 4]
+        program = localLoopProgram 0 loop (notEqual index 0) []
+     in postGuardIsRemoved program
+
+falseForDoesNotRun :: Bool
+falseForDoesNotRun =
+    let index = loopVariable
+        loop = CoreFor falseLiteral [assign indexName 4] []
+        program = localLoopProgram 0 loop (notEqual index 0) []
+     in postGuardIsRemoved program
+
+unconditionalBreakRetainsValue :: Bool
+unconditionalBreakRetainsValue =
+    let index = loopVariable
+        loop = CoreWhile trueLiteral [CoreBreak]
+        program = localLoopProgram 6 loop (notEqual index 6) []
+     in postGuardIsRemoved program
+
+continueDoesNotExit :: Bool
+continueDoesNotExit =
+    let index = loopVariable
+        loop =
+            CoreWhile
+                trueLiteral
+                [CoreIf (equal index 0) [CoreContinue] [CoreBreak]]
+        program = localLoopProgram 0 loop (equal index 0) []
+     in topLevelGuardRemains program
+
+stableHeaderFact :: Bool
+stableHeaderFact =
+    let index = loopVariable
+        loop = CoreWhile (lessThan index 4) [CoreEvaluate (equal index 3)]
+        program = localLoopProgram 10 loop (notEqual index 10) []
+     in postGuardIsRemoved program
+
+callConditionStaysUnknown :: Bool
+callConditionStaysUnknown =
+    let index = loopVariable
+        flag = CoreVariable flagName boolType
+        predicate = CoreVariable predicateName (FunctionType [] boolType)
+        shortCircuitCondition =
+            CorePrimitive
+                CoreLogicalAnd
+                [flag, CoreApply predicate [] boolType]
+                boolType
+        loop = CoreWhile shortCircuitCondition [CoreBreak]
+        program =
+            localLoopProgram
+                5
+                loop
+                (equal index 0)
+                [(flagName, boolType), (predicateName, FunctionType [] boolType)]
+     in topLevelGuardRemains program
+
+{- | Small bounded counter programs act as an executable oracle for the
+false-edge equation. Each loop either starts beyond its limit or advances by
+one from a range where the increment is representable. In both cases the
+post-loop @index < limit@ edge is infeasible, regardless of the concrete
+number of runtime iterations.
+-}
+boundedCounterCases :: [(String, Bool)]
+boundedCounterCases =
+    [ ( "bounded counter " ++ show initial ++ " -> " ++ show limit ++ " prunes the false exit guard"
+      , postGuardIsRemoved (counterLoop initial limit WhileCounter)
+      )
+    | initial <- [-4 .. 4]
+    , limit <- [-4 .. 4]
+    ]
+
+boundedCounterExitOracle :: Bool
+boundedCounterExitOracle = all counterSummaryContainsConcreteExit counterPairs
+
+boundedCounterSolverBudget :: Bool
+boundedCounterSolverBudget = all counterSummaryIsBounded counterPairs
+
+counterPairs :: [(Integer, Integer)]
+counterPairs = [(initial, limit) | initial <- [-4 .. 4], limit <- [-4 .. 4]]
+
+counterSummaryContainsConcreteExit :: (Integer, Integer) -> Bool
+counterSummaryContainsConcreteExit (initial, limit) =
+    case counterSummary initial limit of
+        Nothing -> False
+        Just summary -> abstractStateContains (loopExitFacts summary) concreteExit
+    where
+        concreteExit = if initial < limit then limit else initial
+
+counterSummaryIsBounded :: (Integer, Integer) -> Bool
+counterSummaryIsBounded (initial, limit) =
+    case counterSummary initial limit of
+        Nothing -> False
+        Just summary ->
+            loopFixedPointIterations summary > 0
+                && loopFixedPointIterations summary <= 8
+                && not (loopAnalysisWidened summary)
+
+counterSummary :: Integer -> Integer -> Maybe LoopFactSummary
+counterSummary initial limit =
+    let input = initialFacts initial
+        index = loopVariable
+        loop = CoreWhile (lessThan index limit) [CoreAssign indexName (add index 1)]
+     in loopFactSummary input loop
+
+doWhileSummarySeparatesPhases :: Bool
+doWhileSummarySeparatesPhases =
+    case loopFactSummary (initialFacts 0) (CoreDoWhile [assign indexName 1] falseLiteral) of
+        Nothing -> False
+        Just summary ->
+            abstractStateIs (loopBodyFacts summary) 0
+                && abstractStateIs (loopConditionFacts summary) 1
+                && abstractStateIs (loopExitFacts summary) 1
+
+forContinueSummary :: Bool
+forContinueSummary =
+    let loop = CoreFor (lessThan loopVariable 4) [CoreContinue] [CoreBreak]
+     in case loopFactSummary (initialFacts 2) loop of
+            Nothing -> False
+            Just summary ->
+                abstractStateIs (loopUpdateFacts summary) 2
+                    && abstractStateIs (loopExitFacts summary) 2
+
+forBreakSkipsUpdate :: Bool
+forBreakSkipsUpdate =
+    let loop = CoreFor trueLiteral [CoreBreak] [assign indexName 9]
+     in case loopFactSummary (initialFacts 6) loop of
+            Nothing -> False
+            Just summary ->
+                isUnreachableFacts (loopUpdateFacts summary)
+                    && abstractStateIs (loopExitFacts summary) 6
+
+loopReturnSummary :: Bool
+loopReturnSummary =
+    let loop = CoreDoWhile [CoreReturn (literal 42)] falseLiteral
+     in case loopFactSummary (initialFacts 6) loop of
+            Nothing -> False
+            Just summary ->
+                abstractStateIs (loopReturnFacts summary) 6
+                    && isUnreachableFacts (loopExitFacts summary)
+
+calledBodySummaryIsTop :: Bool
+calledBodySummaryIsTop =
+    let callback = CoreVariable callbackName (FunctionType [] unitType)
+        loop = CoreWhile trueLiteral [CoreEvaluate (CoreApply callback [] unitType), CoreBreak]
+     in case loopFactSummary (initialFacts 6) loop of
+            Nothing -> False
+            Just summary ->
+                lookupIntegerFact (loopExitFacts summary) (resolvedSymbol indexName) == Nothing
+
+callInUpdateInvalidates :: Bool
+callInUpdateInvalidates =
+    let callback = CoreVariable callbackName (FunctionType [] unitType)
+        loop = CoreFor trueLiteral [CoreContinue] [CoreEvaluate (CoreApply callback [] unitType), CoreBreak]
+        program = localLoopProgram 2 loop (equal loopVariable 0) [(callbackName, FunctionType [] unitType)]
+     in topLevelGuardRemains program
+
+updateBreakCarriesUpdatedValue :: Bool
+updateBreakCarriesUpdatedValue =
+    let loop = CoreFor trueLiteral [CoreContinue] [assign indexName 7, CoreBreak]
+     in case loopFactSummary (initialFacts 3) loop of
+            Nothing -> False
+            Just summary ->
+                abstractStateIs (loopUpdateFacts summary) 3
+                    && abstractStateIs (loopExitFacts summary) 7
+
+initialFacts :: Integer -> IntegerFacts
+initialFacts value =
+    transferStatementFacts
+        emptyIntegerFacts
+        (CoreBind (CoreBinding indexName intType True (literal value)))
+
+abstractStateContains :: IntegerFacts -> Integer -> Bool
+abstractStateContains state value = case lookupIntegerFact state (resolvedSymbol indexName) of
+    Nothing -> not (isUnreachableFacts state)
+    Just fact ->
+        maybe True (<= value) (integerMinimum fact)
+            && maybe True (>= value) (integerMaximum fact)
+            && not (value == 0 && integerExcludesZero fact)
+
+abstractStateIs :: IntegerFacts -> Integer -> Bool
+abstractStateIs state value =
+    not (isUnreachableFacts state)
+        && abstractStateContains state value
+        && case lookupIntegerFact state (resolvedSymbol indexName) of
+            Just fact -> integerMinimum fact == Just value && integerMaximum fact == Just value
+            Nothing -> False
+
+data CounterLoop = WhileCounter
+
+counterLoop :: Integer -> Integer -> CounterLoop -> CoreFunction
+counterLoop initial limit WhileCounter =
+    let index = loopVariable
+        loop = CoreWhile (lessThan index limit) [CoreAssign indexName (add index 1)]
+     in localLoopProgram initial loop (lessThan index limit) []
+
+localLoopProgram :: Integer -> CoreStatement -> CoreExpression -> [(ResolvedName, Type)] -> CoreFunction
+localLoopProgram initial loop postCondition extraParameters =
+    CoreFunction
+        functionName
+        extraParameters
+        intType
+        [ CoreBind (CoreBinding indexName intType True (literal initial))
+        , loop
+        , CoreIf
+            postCondition
+            [CoreReturn (divide (CoreVariable indexName intType))]
+            [CoreReturn (literal 0)]
+        ]
+
+postGuardIsRemoved :: CoreFunction -> Bool
+postGuardIsRemoved function = case optimizedBody function of
+    Just statements -> case reverse statements of
+        CoreReturn (CoreLiteral (CoreInteger 0) valueType) : _ -> valueType == intType
+        _ -> False
+    Nothing -> False
+
+topLevelGuardRemains :: CoreFunction -> Bool
+topLevelGuardRemains function = case optimizedBody function of
+    Just statements -> case reverse statements of
+        CoreIf {} : _ -> True
+        _ -> False
+    Nothing -> False
+
+containsReturnedDivision :: Maybe [CoreStatement] -> ResolvedName -> Bool
+containsReturnedDivision maybeStatements name =
+    maybe False (any (statementContainsDivision name)) maybeStatements
+
+statementContainsDivision :: ResolvedName -> CoreStatement -> Bool
+statementContainsDivision name statement = case statement of
+    CoreBind binding -> expressionContainsDivision name (coreBindingValue binding)
+    CoreAssign _ expression -> expressionContainsDivision name expression
+    CoreReturn expression -> expressionContainsDivision name expression
+    CoreIf condition whenTrue whenFalse ->
+        expressionContainsDivision name condition
+            || any (statementContainsDivision name) whenTrue
+            || any (statementContainsDivision name) whenFalse
+    CoreEvaluate expression -> expressionContainsDivision name expression
+    CoreWhile condition body ->
+        expressionContainsDivision name condition || any (statementContainsDivision name) body
+    CoreDoWhile body condition ->
+        any (statementContainsDivision name) body || expressionContainsDivision name condition
+    CoreFor condition body update ->
+        expressionContainsDivision name condition
+            || any (statementContainsDivision name) body
+            || any (statementContainsDivision name) update
+    CoreBreak -> False
+    CoreContinue -> False
+
+expressionContainsDivision :: ResolvedName -> CoreExpression -> Bool
+expressionContainsDivision name expression = case expression of
+    CorePrimitive CoreDivide [_numerator, CoreVariable divisor _] _ -> resolvedSymbol divisor == resolvedSymbol name
+    CoreVariable {} -> False
+    CoreLiteral {} -> False
+    CoreApply callee arguments _ ->
+        expressionContainsDivision name callee || any (expressionContainsDivision name) arguments
+    CorePrimitive _ arguments _ -> any (expressionContainsDivision name) arguments
+    CoreLet _ _ value body _ ->
+        expressionContainsDivision name value || expressionContainsDivision name body
+    CoreConditional condition whenTrue whenFalse _ -> any (expressionContainsDivision name) [condition, whenTrue, whenFalse]
+    CoreClosure captures _ _ body _ ->
+        any (expressionContainsDivision name . coreCaptureValue) captures
+            || any (statementContainsDivision name) body
+
+optimizedBody :: CoreFunction -> Maybe [CoreStatement]
+optimizedBody function = do
+    result <- either (const Nothing) Just (optimizeCoreWith loopTestOptions (CoreModule moduleName [function]))
+    case coreModuleFunctions (optimizedCore result) of
+        [optimized] -> Just (coreFunctionBody optimized)
+        _ -> Nothing
+
+loopTestOptions :: OptimizerOptions
+loopTestOptions =
+    defaultOptimizerOptions
+        { optimizerInterproceduralEffects = False
+        , optimizerInlining = False
+        , optimizerDeadCodeElimination = False
+        }
+
+assign :: ResolvedName -> Integer -> CoreStatement
+assign name value = CoreAssign name (literal value)
+
+add :: CoreExpression -> Integer -> CoreExpression
+add left right = CorePrimitive CoreAdd [left, literal right] intType
+
+lessThan :: CoreExpression -> Integer -> CoreExpression
+lessThan left right = CorePrimitive CoreLessThan [left, literal right] boolType
+
+equal :: CoreExpression -> Integer -> CoreExpression
+equal left right = CorePrimitive CoreEqual [left, literal right] boolType
+
+notEqual :: CoreExpression -> Integer -> CoreExpression
+notEqual left right = CorePrimitive CoreNotEqual [left, literal right] boolType
+
+divide :: CoreExpression -> CoreExpression
+divide value = CorePrimitive CoreDivide [literal 24, value] intType
+
+literal :: Integer -> CoreExpression
+literal value = CoreLiteral (CoreInteger value) intType
+
+trueLiteral, falseLiteral :: CoreExpression
+trueLiteral = CoreLiteral (CoreBoolean True) boolType
+falseLiteral = CoreLiteral (CoreBoolean False) boolType
+
+indexName, flagName, callbackName, predicateName, functionName :: ResolvedName
+indexName = resolved 10 "index"
+flagName = resolved 11 "flag"
+callbackName = resolved 12 "callback"
+predicateName = resolved 13 "predicate"
+functionName = resolved 1 "LoopFacts"
+
+loopVariable :: CoreExpression
+loopVariable = CoreVariable indexName intType
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+moduleName :: QualifiedName
+moduleName = QualifiedName [Identifier "Optimizer", Identifier "LoopFlow"]
+ test/Main.hs view
@@ -0,0 +1,898 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+module Main (main) where
+
+import AssignmentExpressionTests (assignmentExpressionTests)
+import BranchingDiagnosticTests (branchingDiagnosticTests)
+import BranchingOracleTests (branchingOracleTests)
+import BranchingTests (branchingTests)
+import ClosureTests (closureTests)
+import CompileTimeParityTests (compileTimeParityTests)
+import ConditionalExpressionTests (conditionalExpressionTests)
+import Control.Exception (finally)
+import CoreInliningTests (coreInliningTests)
+import CoreLinearInliningTests (coreLinearInliningTests)
+import CoreOptimizerSourceTests (coreOptimizerSourceTests)
+import CoreOptimizerTests (coreOptimizerTests)
+import CoreVerifierTests (coreVerifierTests)
+import Data.List (isInfixOf)
+import Data.Word (Word8)
+import DiagnosticProtocolTests (diagnosticProtocolTests)
+import DiagnosticSideChannelTests (diagnosticSideChannelTests)
+import EnumTests (enumTests)
+import FloatingOptimizerTests (floatingOptimizerTests)
+import IntegerEvaluationTests (integerEvaluationTests)
+import InferredReturnTests (inferredReturnTests)
+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 MonomorphizationTests (monomorphizationTests)
+import NestingLimitTests (nestingLimitTests)
+import Numeric (readHex)
+import NumericTests (numericTests)
+import ParserContractTests (parserContractTests)
+import PatternTests (patternTests)
+import QualifiedNameTests (qualifiedNameTests)
+import ScalarWireTests (scalarWireTests)
+import ShortCircuitTests (shortCircuitTests)
+import SourceSetTests (sourceSetTests)
+import SourceTextTests (sourceTextTests)
+import SourceWarningTests (sourceWarningTests)
+import SpecializationTests (specializationTests)
+import StaticMemberCoreTests (staticMemberCoreTests)
+import StaticMemberOverloadTests (staticMemberOverloadTests)
+import StaticMemberParserTests (staticMemberParserTests)
+import StaticMemberProjectTests (staticMemberProjectTests)
+import StaticMemberSemanticTests (staticMemberSemanticTests)
+import System.Directory (doesFileExist, getTemporaryDirectory, removeFile)
+import System.Exit (exitFailure)
+import System.FilePath ((</>))
+import TemplateApplicationTests (templateApplicationTests)
+import TemplateDeclarationTests (templateDeclarationTests)
+import TemplateDiscoveryTests (templateDiscoveryTests)
+import TemplateInstantiationTests (templateInstantiationTests)
+import TemplateManglingTests (templateManglingTests)
+import TemplateMemberReachabilityTests (templateMemberReachabilityTests)
+import TemplatePlanVerifierTests (templatePlanVerifierTests)
+import TemplateSpecializationPlannerTests (templateSpecializationPlannerTests)
+import TemplateTests (templateTests)
+import TemplateVerifierTests (templateVerifierTests)
+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 ()
+main = do
+    mapM_ (uncurry check) coreLinearInliningTests
+    check "full frontend compiles renewed declarations through CorePrep" fullPipeline
+    check "operator precedence is preserved and constants fold" precedence
+    check "unterminated final expression supplies the function result" finalExpression
+    check "semicolon-terminated pure expression is rejected" pureExpressionStatement
+    check "binding statements require semicolons" bindingRequiresSemicolon
+    check "assignment statements require semicolons" assignmentRequiresSemicolon
+    check "return statements require semicolons" returnRequiresSemicolon
+    check "call expression statements require semicolons" callRequiresSemicolon
+    check "void functions cannot use a final result expression" voidFinalExpression
+    check "nested blocks cannot use a final result expression" nestedFinalExpression
+    check "Core optimizer removes a provably pure dead call before CorePrep" preparedControlFlow
+    check "CorePrep creates explicit control flow for a live branch" preparedBranchControlFlow
+    check "project entry resolves namespace, class, and public static void Main" entryContract
+    check "int Main is rejected for the selected project entry" wrongEntryReturn
+    check "top-level runtime functions are rejected" topLevelFunction
+    check "unknown names are diagnosed by name resolution" unknownName
+    check "Renamer starts SymbolId allocation above the reserved zero sentinel" positiveSymbolSeed
+    check "name resolution rejects the reserved zero SymbolId" zeroSymbolRejected
+    check "immutable assignment is rejected" immutableAssignment
+    check "string conditions are rejected while numeric conditions remain valid" wrongCondition
+    check "wrong call arity is rejected" wrongArity
+    check "lexer rejects unsupported input" badCharacter
+    check "if requires a parenthesized condition" ifRequiresParentheses
+    check "logical NOT has only the not spelling" logicalNotHasOneSpelling
+    check "String comment forms obey long-bracket levels" stringCommentForms
+    check "raw strings preserve content and trim boundary newlines" rawStringBoundaries
+    check "higher raw-string levels admit lower closing delimiters" rawStringLevels
+    check "raw strings do not decode escapes or interpolation" rawStringIsLiteral
+    check "normal string line breaks become spaces and escapes decode" normalStringSemantics
+    check "comments and raw strings pass through the frontend pipeline" stringPipeline
+    check "legacy C comments are not recognized as comments" legacyCommentsRemoved
+    check "unterminated raw strings report a lexer diagnostic" unterminatedRawString
+    check
+        "renewed artifact extensions remain stable"
+        (map artifactExtension [Core, CorePrep, Xpp, Xmm] == [Just ".core", Nothing, Just ".xpp", Just ".xmm"])
+    check "Haskell CorePrep verifier rejects duplicate blocks" invalidCorePrep
+    check "Core verifier rejects an undefined variable" invalidCoreReference
+    check "Core verifier rejects a zero function SymbolId" invalidCoreFunctionSymbol
+    check "Core verifier rejects assignment to an immutable binding" invalidCoreMutation
+    check "Core verifier requires all non-void paths to return" invalidCoreReturnPath
+    check "Core wire codec round-trips optimized Core" coreWireRoundTrip
+    check "Core wire rejects CorePrep magic" coreWireRejectsCorePrep
+    check "Core wire rejects truncated input" coreWireRejectsTruncation
+    check "Core wire rejects trailing bytes" coreWireRejectsTrailingInput
+    check "Core wire rejects unresolved types" coreWireRejectsUnresolvedType
+    check "Core wire preserves Unicode scalar values" coreWirePreservesUnicode
+    check "Core wire v10 provenance fields remain stable" coreWireGoldenDocument
+    check "Core wire v10 preserves non-empty source-owner field order" coreWireProjectSourceGolden
+    check "CorePrep wire codec round-trips the frontend result" wireRoundTrip
+    check "CorePrep wire codec rejects truncated input" wireRejectsTruncation
+    check "CorePrep wire codec rejects trailing input" wireRejectsTrailingInput
+    check "CorePrep wire codec rejects unsupported types" wireRejectsUnsupportedType
+    check "CorePrep wire codec preserves Unicode scalar values" wirePreservesUnicode
+    check "CorePrep wire v7 provenance fields remain stable" wireGoldenDocument
+    checkIO "real Core artifact round-trips through .core I/O" coreArtifactRoundTrip
+    checkIO "Core artifact rejects an invalid Core module" coreArtifactRejectsInvalidModule
+    checkIO "Core artifact rejects a non-.core path" coreArtifactRejectsExtension
+    mapM_ (uncurry checkIO) sourceSetTests
+    mapM_ (uncurry check) sourceTextTests
+    mapM_ (uncurry check) sourceWarningTests
+    mapM_ (uncurry check) staticMemberParserTests
+    mapM_ (uncurry check) staticMemberSemanticTests
+    mapM_ (uncurry check) staticMemberCoreTests
+    mapM_ (uncurry check) staticMemberOverloadTests
+    mapM_ (uncurry checkIO) staticMemberProjectTests
+    mapM_ (uncurry check) templateTests
+    mapM_ (uncurry check) templateDeclarationTests
+    mapM_ (uncurry check) templateDiscoveryTests
+    mapM_ (uncurry check) templateApplicationTests
+    mapM_ (uncurry check) templateInstantiationTests
+    mapM_ (uncurry check) templateManglingTests
+    mapM_ (uncurry check) templateMemberReachabilityTests
+    mapM_ (uncurry check) templatePlanVerifierTests
+    mapM_ (uncurry check) templateSpecializationPlannerTests
+    mapM_ (uncurry check) templateVerifierTests
+    mapM_ (uncurry check) typeClassificationTests
+    mapM_ (uncurry check) closureTests
+    mapM_ (uncurry check) coreOptimizerTests
+    mapM_ (uncurry check) integerFlowTests
+    mapM_ (uncurry check) loopFlowTests
+    mapM_ (uncurry check) loopFlowOracleTests
+    mapM_ (uncurry check) iterationTests
+    mapM_ (uncurry check) floatingOptimizerTests
+    mapM_ (uncurry check) coreInliningTests
+    mapM_ (uncurry check) coreOptimizerSourceTests
+    mapM_ (uncurry check) coreVerifierTests
+    mapM_ (uncurry check) diagnosticProtocolTests
+    mapM_ (uncurry checkIO) diagnosticSideChannelTests
+    mapM_ (uncurry check) numericTests
+    mapM_ (uncurry check) integerEvaluationTests
+    mapM_ (uncurry check) compileTimeParityTests
+    mapM_ (uncurry check) patternTests
+    mapM_ (uncurry check) monomorphizationTests
+    mapM_ (uncurry check) parserContractTests
+    mapM_ (uncurry check) scalarWireTests
+    mapM_ (uncurry check) shortCircuitTests
+    mapM_ (uncurry check) conditionalExpressionTests
+    mapM_ (uncurry check) assignmentExpressionTests
+    mapM_ (uncurry check) loopExpressionTests
+    mapM_ (uncurry check) branchingTests
+    mapM_ (uncurry check) inferredReturnTests
+    mapM_ (uncurry check) enumTests
+    mapM_ (uncurry check) lazyEvaluationTests
+    mapM_ (uncurry check) memoizeTests
+    mapM_ (uncurry check) fallThroughTests
+    mapM_ (uncurry check) runtimeCallTests
+    mapM_ (uncurry check) consoleTests
+    mapM_ (uncurry check) effectTests
+    mapM_ (uncurry check) qualifiedNameTests
+    mapM_ (uncurry check) formatSweepTests
+    mapM_ (uncurry check) branchingOracleTests
+    mapM_ (uncurry check) branchingDiagnosticTests
+    mapM_ (uncurry check) nestingLimitTests
+    mapM_ (uncurry check) specializationTests
+    mapM_ (uncurry check) voidTests
+
+check :: String -> Bool -> IO ()
+check label passed = if passed then putStrLn ("PASS: " ++ label) else putStrLn ("FAIL: " ++ label) >> exitFailure
+
+checkIO :: String -> IO Bool -> IO ()
+checkIO label action = action >>= check label
+
+compile :: String -> Either [Diagnostic] FrontendArtifacts
+compile = compileToCorePrep . CompilerInput "test.tala"
+
+sample :: String
+sample =
+    unlines
+        [ "namespace Name;"
+        , "class Main {"
+        , "  public static int Sum(_ int left, _ int right) {"
+        , "    return left + right;"
+        , "  }"
+        , "  public static void Main() {"
+        , "    int value = Sum(20, 22);"
+        , "    if (value >= 40) {"
+        , "        value = value + 1;"
+        , "    } else {"
+        , "        value = 0;"
+        , "    }"
+        , "    return;"
+        , "  }"
+        , "}"
+        ]
+
+fullPipeline :: Bool
+fullPipeline = case compileEntryToCorePrep (QualifiedName [Identifier "Name", Identifier "Main"]) (CompilerInput "test.tala" sample) of
+    Right artifacts ->
+        length (syntaxDeclarations (typedSyntaxTree (artifactTypedAST artifacts))) == 1
+            && length (coreModuleFunctions (artifactOptimizedCore artifacts)) == 2
+            && length (corePrepModuleFunctions (artifactCorePrep artifacts)) == 2
+    Left _ -> False
+
+precedence :: Bool
+precedence = case compile "class Math { public static int Calculate() { return 2 + 3 * 4; } }" of
+    Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
+        function : _ -> case coreFunctionBody function of
+            [CoreReturn (CoreLiteral (CoreInteger 14) _)] -> True
+            _ -> False
+        [] -> False
+    Left _ -> False
+
+finalExpression :: Bool
+finalExpression = case compile "class Math { auto Calculate() { 2 + 3 * 4 } }" of
+    Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
+        function : _ -> case coreFunctionBody function of [CoreReturn (CoreLiteral (CoreInteger 14) _)] -> True; _ -> False
+        [] -> False
+    Left _ -> False
+
+pureExpressionStatement :: Bool
+pureExpressionStatement = hasCode "TAT0013" (compile "class App { void Run() { 42; } }")
+
+bindingRequiresSemicolon :: Bool
+bindingRequiresSemicolon = rejected (compile "class App { int Read() { int value = 1 return value; } }")
+
+assignmentRequiresSemicolon :: Bool
+assignmentRequiresSemicolon = rejected (compile "class App { int Read() { int value = 1; value = 2 return value; } }")
+
+returnRequiresSemicolon :: Bool
+returnRequiresSemicolon = rejected (compile "class App { int Read() { return 1 } }")
+
+callRequiresSemicolon :: Bool
+callRequiresSemicolon =
+    rejected (compile "class App { void Save() { return; } void Run() { Save() return; } }")
+
+voidFinalExpression :: Bool
+voidFinalExpression = rejected (compile "class App { void Run() { 1 } }")
+
+nestedFinalExpression :: Bool
+nestedFinalExpression = rejected (compile "class App { int Read() { if (true) { 1 } 2 } }")
+
+preparedControlFlow :: Bool
+preparedControlFlow = case compileEntryToCorePrep (QualifiedName [Identifier "Name", Identifier "Main"]) (CompilerInput "test.tala" sample) of
+    Right artifacts ->
+        let functions = corePrepModuleFunctions (artifactCorePrep artifacts)
+            mainFunction = functions !! 1
+            blocks = corePrepFunctionBlocks mainFunction
+         in not (null blocks) && not (any hasCall blocks) && not (any isBranch blocks)
+    Left _ -> False
+    where
+        isBranch block = case corePrepBlockTerminator block of CorePrepBranch _ _ _ -> True; _ -> False
+        hasCall block = any callInstruction (corePrepBlockInstructions block)
+        callInstruction instruction = case instruction of
+            CorePrepBind _ _ _ (CorePrepCall _ _) -> True
+            CorePrepEvaluate (CorePrepCall _ _) -> True
+            _ -> False
+
+preparedBranchControlFlow :: Bool
+preparedBranchControlFlow = case compile source of
+    Right artifacts -> case corePrepModuleFunctions (artifactCorePrep artifacts) of
+        [function] ->
+            let blocks = corePrepFunctionBlocks function
+             in length blocks >= 3 && any isBranch blocks
+        _ -> False
+    Left _ -> False
+    where
+        source =
+            "class Flow { int Choose(_ int value) { if (value) { return 1; } else { return 2; } } }"
+        isBranch block = case corePrepBlockTerminator block of CorePrepBranch _ _ _ -> True; _ -> False
+
+entryContract :: Bool
+entryContract = case compile sample of
+    Right artifacts -> validateEntryPoint (QualifiedName [Identifier "Name", Identifier "Main"]) (artifactTypedAST artifacts) == Right ()
+    Left _ -> False
+wrongEntryReturn :: Bool
+wrongEntryReturn =
+    hasCode
+        "TAE0008"
+        ( compileEntryToCorePrep
+            (QualifiedName [Identifier "Name", Identifier "Main"])
+            (CompilerInput "entry.tala" "namespace Name; class Main { public static int Main() { return 0; } }")
+        )
+topLevelFunction :: Bool
+topLevelFunction = hasCode "TAP0006" (compile "int Main() { return 0; }")
+
+hasCode :: String -> Either [Diagnostic] a -> Bool
+hasCode code result = case result of Left problems -> any ((== code) . diagnosticCode) problems; Right _ -> False
+
+rejected :: Either [Diagnostic] a -> Bool
+rejected result = case result of Left _ -> True; Right _ -> False
+unknownName :: Bool
+unknownName = hasCode "TAN0001" (compile "class App { int Read() { return missing; } }")
+
+positiveSymbolSeed :: Bool
+positiveSymbolSeed = case compile "class FirstType { void Run() { return; } }" of
+    Right artifacts -> case syntaxDeclarations (resolvedSyntaxTree (artifactResolvedAST artifacts)) of
+        TypeDeclaration {declarationName = name} : _ -> symbolIdValue (resolvedSymbol name) == 1
+        _ -> False
+    Left _ -> False
+
+zeroSymbolRejected :: Bool
+zeroSymbolRejected =
+    let position = SourcePosition 1 1
+        spanValue = SourceSpan "zero-symbol.tala" position position
+        declaration = TypeDeclaration spanValue (RenamedName (Identifier "ZeroSymbol") 0) () []
+        renamed = RenamedAST (SyntaxTree Nothing [declaration])
+     in hasCode "TAN0002" (runNameResolution defaultNameResolution renamed)
+immutableAssignment :: Bool
+immutableAssignment = hasCode "TAT0003" (compile "class App { int Read() { final int value = 1; value = 2; return value; } }")
+wrongCondition :: Bool
+wrongCondition = hasCode "TAT0006" (compile "class App { void Run() { if (\"yes\") { return; } return; } }")
+
+ifRequiresParentheses :: Bool
+ifRequiresParentheses =
+    rejected (compile "class App { void Run() { if true { return; } return; } }")
+        && not (rejected (compile "class App { void Run() { if (true) { return; } return; } }"))
+
+logicalNotHasOneSpelling :: Bool
+logicalNotHasOneSpelling =
+    not (rejected (compile "class App { void Run() { if (not false) { return; } return; } }"))
+        && rejected (compile "class App { void Run() { if (!false) { return; } return; } }")
+wrongArity :: Bool
+wrongArity = hasCode "TAT0008" (compile "class App { int Sum(_ int a, _ int b) { return a+b; } void Run() { Sum(1); } }")
+badCharacter :: Bool
+badCharacter = case compile "class App { void Run() { @; } }" of
+    Left problems -> any (isInfixOf "unsupported character" . diagnosticMessage) problems
+    Right _ -> False
+
+lexTokens :: String -> Either [Diagnostic] [Token]
+lexTokens = runLexer defaultLexer . LexerInput "string-test.tala"
+
+lexicalText :: String -> Either [Diagnostic] [String]
+lexicalText source =
+    map tokenText . filter ((/= EndOfFileToken) . tokenKind) <$> lexTokens source
+
+singleStringValue :: String -> Either [Diagnostic] String
+singleStringValue source = do
+    tokens <- lexTokens source
+    case [tokenText value | value <- tokens, tokenKind value == StringToken] of
+        [value] -> Right value
+        _ -> Left []
+
+stringCommentForms :: Bool
+stringCommentForms =
+    lexicalText
+        ( "int first; -- ordinary\r\n"
+            ++ "--| declaration documentation\n"
+            ++ "--! namespace documentation\n"
+            ++ "--[=[ long comment containing ]] and --[[ text ]=]\n"
+            ++ "--[==[! namespace long comment containing ]=] ]==]\n"
+            ++ "int second;"
+        )
+        == Right ["int", "first", ";", "int", "second", ";"]
+
+rawStringBoundaries :: Bool
+rawStringBoundaries =
+    singleStringValue "[[\r\nHello\nWorld\r\n]]" == Right "Hello\nWorld"
+        && singleStringValue "[[Hello\nWorld]]" == Right "Hello\nWorld"
+        && singleStringValue "[[\nHello\n\n]]" == Right "Hello\n"
+
+rawStringLevels :: Bool
+rawStringLevels =
+    singleStringValue "[==[\nThis ]] and this ]=] remain content.\n]==]"
+        == Right "This ]] and this ]=] remain content."
+
+rawStringIsLiteral :: Bool
+rawStringIsLiteral =
+    singleStringValue "[=[\\n\\t${value}]=]" == Right "\\n\\t${value}"
+
+normalStringSemantics :: Bool
+normalStringSemantics =
+    singleStringValue "\"first\r\nsecond\\nthird\\u0021\\x000000000041\"" == Right "first second\nthird!A"
+
+stringPipeline :: Bool
+stringPipeline = case compile source of
+    Right artifacts ->
+        sourceCorePreservesLiteral (artifactCore artifacts)
+            && optimizedCoreRemovesDeadLiteral (artifactOptimizedCore artifacts)
+    Left _ -> False
+    where
+        sourceCorePreservesLiteral moduleValue = case coreModuleFunctions moduleValue of
+            [function] -> case coreFunctionBody function of
+                [CoreBind (CoreBinding _ _ _ (CoreLiteral (CoreString text) _)), CoreReturn (CoreLiteral (CoreInteger 1) _)] ->
+                    text == "-- remains raw\n[=[ remains raw"
+                _ -> False
+            _ -> False
+        optimizedCoreRemovesDeadLiteral moduleValue = case coreModuleFunctions moduleValue of
+            [function] -> coreFunctionBody function == [CoreReturn (CoreLiteral (CoreInteger 1) intType)]
+            _ -> False
+        source =
+            unlines
+                [ "--! Pipeline namespace documentation"
+                , "namespace Text;"
+                , "class App {"
+                , "  --[=[ ordinary long comment containing ]] ]=]"
+                , "  --| Returns a value after exercising a raw literal."
+                , "  int Value() {"
+                , "    auto text = [["
+                , "-- remains raw"
+                , "[=[ remains raw"
+                , "]] ;"
+                , "    return 1;"
+                , "  }"
+                , "}"
+                ]
+
+legacyCommentsRemoved :: Bool
+legacyCommentsRemoved =
+    lexicalText "int quotient = left // right; /* legacy */"
+        == Right ["int", "quotient", "=", "left", "//", "right", ";", "/", "*", "legacy", "*", "/"]
+
+unterminatedRawString :: Bool
+unterminatedRawString = hasCode "TAL0005" (lexTokens "[=[never closed")
+
+invalidCorePrep :: Bool
+invalidCorePrep = case compile sample of
+    Right artifacts -> case corePrepModuleFunctions (artifactCorePrep artifacts) of
+        function : rest ->
+            hasCode
+                "TAC0003"
+                ( verifyCorePrep
+                    (artifactCorePrep artifacts)
+                        { corePrepModuleFunctions =
+                            function {corePrepFunctionBlocks = corePrepFunctionBlocks function ++ take 1 (corePrepFunctionBlocks function)} : rest
+                        }
+                )
+        [] -> False
+    Left _ -> False
+
+invalidCoreReference :: Bool
+invalidCoreReference =
+    let name = ResolvedName (SymbolId 90) (Identifier "missing")
+        function =
+            CoreFunction
+                (ResolvedName (SymbolId 1) (Identifier "Read"))
+                []
+                intType
+                [CoreReturn (CoreVariable name intType)]
+     in hasCode "TAC1020" (verifyCore (CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []))
+
+invalidCoreFunctionSymbol :: Bool
+invalidCoreFunctionSymbol =
+    let name = ResolvedName (SymbolId 0) (Identifier "Invalid")
+        function = CoreFunction name [] unitType [CoreReturn (CoreLiteral CoreUnit unitType)]
+     in hasCode "TAC1006" (verifyCore (CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []))
+
+invalidCoreMutation :: Bool
+invalidCoreMutation =
+    let name = ResolvedName (SymbolId 2) (Identifier "value")
+        literal value = CoreLiteral (CoreInteger value) intType
+        binding = CoreBinding name intType False (literal 1)
+        function =
+            CoreFunction
+                (ResolvedName (SymbolId 1) (Identifier "Read"))
+                []
+                intType
+                [CoreBind binding, CoreAssign name (literal 2), CoreReturn (CoreVariable name intType)]
+     in hasCode "TAC1013" (verifyCore (CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []))
+
+invalidCoreReturnPath :: Bool
+invalidCoreReturnPath =
+    let condition = CoreLiteral (CoreBoolean True) boolType
+        result = CoreLiteral (CoreInteger 1) intType
+        function =
+            CoreFunction
+                (ResolvedName (SymbolId 1) (Identifier "Read"))
+                []
+                intType
+                [CoreIf condition [CoreReturn result] []]
+     in hasCode "TAC1005" (verifyCore (CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []))
+
+coreWireRoundTrip :: Bool
+coreWireRoundTrip = case compile sample of
+    Right artifacts ->
+        let value = artifactOptimizedCore artifacts
+         in (encodeCore defaultCoreWireLimits value >>= decodeCore defaultCoreWireLimits) == Right value
+    Left _ -> False
+
+coreWireRejectsCorePrep :: Bool
+coreWireRejectsCorePrep = case encodeCorePrep goldenModule of
+    Right bytes -> case decodeCore defaultCoreWireLimits bytes of
+        Left issue -> coreWireErrorKind issue == CoreInvalidMagic
+        Right _ -> False
+    Left _ -> False
+
+coreWireRejectsTruncation :: Bool
+coreWireRejectsTruncation = case compile sample of
+    Right artifacts -> case encodeCore defaultCoreWireLimits (artifactOptimizedCore artifacts) of
+        Right bytes -> case decodeCore defaultCoreWireLimits (init bytes) of
+            Left issue -> coreWireErrorKind issue == CoreTruncatedInput
+            Right _ -> False
+        Left _ -> False
+    Left _ -> False
+
+coreWireRejectsTrailingInput :: Bool
+coreWireRejectsTrailingInput = case compile sample of
+    Right artifacts -> case encodeCore defaultCoreWireLimits (artifactOptimizedCore artifacts) of
+        Right bytes -> case decodeCore defaultCoreWireLimits (bytes ++ [0]) of
+            Left issue -> coreWireErrorKind issue == CoreTrailingInput
+            Right _ -> False
+        Left _ -> False
+    Left _ -> False
+
+coreWireRejectsUnresolvedType :: Bool
+coreWireRejectsUnresolvedType =
+    let function = CoreFunction (ResolvedName (SymbolId 1) (Identifier "Broken")) [] ErrorType []
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []
+     in case encodeCore defaultCoreWireLimits moduleValue of
+            Left issue -> coreWireErrorKind issue == CoreUnsupportedType
+            Right _ -> False
+
+coreWirePreservesUnicode :: Bool
+coreWirePreservesUnicode =
+    let functionName = ResolvedName (SymbolId 1) (Identifier "Metinλ")
+        value = CoreLiteral (CoreString "Tala λ 😀") stringType
+        moduleValue =
+            CoreModuleWithSources
+                (QualifiedName [Identifier "Unicode"])
+                [CoreFunction functionName [] stringType [CoreReturn value]]
+                []
+                []
+     in (encodeCore defaultCoreWireLimits moduleValue >>= decodeCore defaultCoreWireLimits) == Right moduleValue
+
+coreWireGoldenDocument :: Bool
+coreWireGoldenDocument =
+    let mainName = ResolvedName (SymbolId 1) (Identifier "Main")
+        mainFunction = CoreFunction mainName [] unitType [CoreReturn (CoreLiteral CoreUnit unitType)]
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "Demo"]) [mainFunction] [] []
+        bytes =
+            upgradeSimpleV5
+                0x0a
+                [ 0x54
+                , 0x41
+                , 0x43
+                , 0x52
+                , 0x05
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x01
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x04
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x44
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x65
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x6d
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x6f
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x01
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x01
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x04
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x4d
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x61
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x69
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x6e
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x01
+                , 0x00
+                , 0x00
+                , 0x00
+                , 0x02
+                , 0x01
+                , 0x00
+                , 0x00
+                ]
+     in encodeCore defaultCoreWireLimits moduleValue == Right bytes
+            && decodeCore defaultCoreWireLimits bytes == Right moduleValue
+
+coreWireProjectSourceGolden :: Bool
+coreWireProjectSourceGolden =
+    let source = "Sources/Main.tala"
+        mainName = ResolvedName (SymbolId 1) (Identifier "Main")
+        mainFunction = CoreFunction mainName [] unitType [CoreReturn (CoreLiteral CoreUnit unitType)]
+        moduleValue =
+            CoreModuleWithSources
+                (QualifiedName [Identifier "Demo"])
+                [mainFunction]
+                [source]
+                [(symbolIdValue (resolvedSymbol mainName), source)]
+        goldenHex =
+            unwords
+                [ "54 41 43 52 0A 00 00 00 01 00 00 00"
+                , "04 00 00 00 44 00 00 00 65 00 00 00"
+                , "6d 00 00 00 6f 00 00 00 01 00 00 00"
+                , "11 00 00 00 53 00 00 00 6f 00 00 00"
+                , "75 00 00 00 72 00 00 00 63 00 00 00"
+                , "65 00 00 00 73 00 00 00 2f 00 00 00"
+                , "4d 00 00 00 61 00 00 00 69 00 00 00"
+                , "6e 00 00 00 2e 00 00 00 74 00 00 00"
+                , "61 00 00 00 6c 00 00 00 61 00 00 00"
+                , "01 00 00 00"
+                , "01 00 00 00 00 00 00 00 04 00 00 00"
+                , "4d 00 00 00 61 00 00 00 69 00 00 00"
+                , "6e 00 00 00 11 00 00 00 53 00 00 00"
+                , "6f 00 00 00 75 00 00 00 72 00 00 00"
+                , "63 00 00 00 65 00 00 00 73 00 00 00"
+                , "2f 00 00 00 4d 00 00 00 61 00 00 00"
+                , "69 00 00 00 6e 00 00 00 2e 00 00 00"
+                , "74 00 00 00 61 00 00 00 6c 00 00 00"
+                , "61 00 00 00"
+                , "00 00 00 00 00 01 00 00 00 02 01 00 00"
+                ]
+        parsedExpectedBytes = traverse parseHexByte (words goldenHex)
+     in case parsedExpectedBytes of
+            Just expected ->
+                encodeCore defaultCoreWireLimits moduleValue == Right expected
+                    && decodeCore defaultCoreWireLimits expected == Right moduleValue
+            Nothing -> False
+    where
+        parseHexByte token = case readHex token of
+            [(value, "")] | value >= (0 :: Int) && value <= 255 -> Just (fromIntegral value)
+            _ -> Nothing
+
+-- Adding the empty source catalog and function owner to the compact v5 golden
+-- shape gives an independent byte-level v10 expectation. The owner follows the
+-- function symbol, before its parameter vector, matching the wire contract.
+upgradeSimpleV5 :: Word8 -> [Word8] -> [Word8]
+upgradeSimpleV5 currentVersion v5Bytes =
+    replaceVersion
+        (insertAt 68 emptyText (insertAt 32 emptyVector v5Bytes))
+    where
+        emptyVector = [0, 0, 0, 0]
+        emptyText = [0, 0, 0, 0]
+        insertAt offset inserted bytes = take offset bytes ++ inserted ++ drop offset bytes
+        replaceVersion bytes = take 4 bytes ++ [currentVersion] ++ drop 5 bytes
+
+wireRoundTrip :: Bool
+wireRoundTrip = case compile sample of
+    Right artifacts -> case encodeCorePrep (artifactCorePrep artifacts) >>= decodeCorePrep of
+        Right decoded -> decoded == artifactCorePrep artifacts
+        Left _ -> False
+    Left _ -> False
+
+wireRejectsTruncation :: Bool
+wireRejectsTruncation = case compile sample of
+    Right artifacts -> case encodeCorePrep (artifactCorePrep artifacts) of
+        Right bytes -> case decodeCorePrep (take (length bytes - 1) bytes) of
+            Left problem -> wireErrorKind problem == TruncatedInput
+            Right _ -> False
+        Left _ -> False
+    Left _ -> False
+
+wireRejectsTrailingInput :: Bool
+wireRejectsTrailingInput = case compile sample of
+    Right artifacts -> case encodeCorePrep (artifactCorePrep artifacts) of
+        Right bytes -> case decodeCorePrep (bytes ++ [0]) of
+            Left problem -> wireErrorKind problem == TrailingInput
+            Right _ -> False
+        Left _ -> False
+    Left _ -> False
+
+wireRejectsUnsupportedType :: Bool
+wireRejectsUnsupportedType = case compile sample of
+    Right artifacts ->
+        let prepared = artifactCorePrep artifacts
+         in case corePrepModuleFunctions prepared of
+                function : remaining ->
+                    let changed = prepared {corePrepModuleFunctions = function {corePrepFunctionReturnType = ErrorType} : remaining}
+                     in case encodeCorePrep changed of
+                            Left problem -> wireErrorKind problem == UnsupportedType
+                            Right _ -> False
+                [] -> False
+    Left _ -> False
+
+wirePreservesUnicode :: Bool
+wirePreservesUnicode =
+    let name = ResolvedName (SymbolId 1) (Identifier "Text")
+        literal = CorePrepLiteral (CoreString "Tala λ 😀") stringType
+        function = CorePrepFunction name "" [] stringType 0 [CorePrepBlock 0 [] (CorePrepReturn literal)]
+        prepared = CorePrepModule (QualifiedName [Identifier "Unicode"]) [function] []
+     in case encodeCorePrep prepared >>= decodeCorePrep of
+            Right decoded -> decoded == prepared
+            Left _ -> False
+
+goldenModule :: CorePrepModule
+goldenModule =
+    let mainName = ResolvedName (SymbolId 1) (Identifier "Main")
+        mainBlock = CorePrepBlock 0 [] (CorePrepReturn (CorePrepLiteral CoreUnit unitType))
+        mainFunction = CorePrepFunction mainName "" [] unitType 0 [mainBlock]
+     in CorePrepModule (QualifiedName [Identifier "Demo"]) [mainFunction] []
+
+goldenBytes :: [Word8]
+goldenBytes =
+    upgradeSimpleV5
+        0x08
+        [ 0x54
+        , 0x41
+        , 0x43
+        , 0x50
+        , 0x05
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x01
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x04
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x44
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x65
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x6d
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x6f
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x01
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x01
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x04
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x4d
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x61
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x69
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x6e
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x01
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x00
+        , 0x01
+        , 0x00
+        , 0x00
+        ]
+
+wireGoldenDocument :: Bool
+wireGoldenDocument =
+    encodeCorePrep goldenModule == Right goldenBytes
+        && decodeCorePrep goldenBytes == Right goldenModule
+
+coreArtifactRoundTrip :: IO Bool
+coreArtifactRoundTrip = case compile sample of
+    Left _ -> pure False
+    Right artifacts -> do
+        temporary <- getTemporaryDirectory
+        let path = temporary </> "tala-core-wire-v10.core"
+            cleanup = doesFileExist path >>= \exists -> if exists then removeFile path else pure ()
+            value = artifactOptimizedCore artifacts
+        ( do
+                written <- writeCoreArtifact path value
+                loaded <- readCoreArtifact path
+                pure (written == Right () && loaded == Right value)
+            )
+            `finally` cleanup
+
+coreArtifactRejectsInvalidModule :: IO Bool
+coreArtifactRejectsInvalidModule = do
+    let function = CoreFunction (ResolvedName (SymbolId 1) (Identifier "Broken")) [] intType []
+        invalid = CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []
+    result <- writeCoreArtifact "invalid-core.core" invalid
+    pure $ case result of
+        Left (CoreArtifactVerificationError messages) -> any (isInfixOf "TAC1005") messages
+        _ -> False
+
+coreArtifactRejectsExtension :: IO Bool
+coreArtifactRejectsExtension = do
+    let function =
+            CoreFunction
+                (ResolvedName (SymbolId 1) (Identifier "Main"))
+                []
+                unitType
+                [CoreReturn (CoreLiteral CoreUnit unitType)]
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "CoreTest"]) [function] [] []
+    result <- writeCoreArtifact "invalid-core.xpp" moduleValue
+    pure (result == Left (InvalidCoreArtifactPath "invalid-core.xpp"))
+ test/MemoizeTests.hs view
@@ -0,0 +1,365 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for the callable that remembers its result, 'CoreMemoize'.
+
+It is the suspended computation of evaluation by need: a callable without
+parameters that calls its operand the first time it is called, keeps what
+the operand returned, and returns that again from then on, to every holder
+of it. The tests here build Core by hand, so that they state what the
+primitive is without the lowering that produces it: what the verifiers
+accept, that both wire formats carry it, that the optimizer neither removes
+a needed computation nor repeats one, and what the reference evaluator
+computes and how often.
+
+"LazyEvaluationTests" checks the lowering that uses the primitive.
+-}
+module MemoizeTests (memoizeTests) where
+
+import CoreInterpreter
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.CorePrep.Wire
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+
+memoizeTests :: [(String, Bool)]
+memoizeTests =
+    [ -- What the Core verifier accepts.
+      ("a remembered callable without parameters verifies", accepted (returning (callOf remembered)))
+    , ("a remembered Boolean result verifies", accepted booleanModule)
+    , ("remembering a callable with a parameter is refused", rejectedWith "TAC1073" (holding (memoize parameterized)))
+    , ("remembering a callable that returns nothing is refused", rejectedWith "TAC1073" (holding (memoize unitCallable)))
+    , ("remembering a callable that returns a String is refused", rejectedWith "TAC1073" (holding (memoize stringCallable)))
+    , ("remembering a number is refused", rejectedWith "TAC1073" (holding (CorePrimitive CoreMemoize [integer 1] intType)))
+    , ("remembering takes one operand", rejectedWith "TAC1026" (holding (CorePrimitive CoreMemoize [stepClosure, stepClosure] intCallable)))
+    , ("a remembered callable has the type of its operand", rejectedWith "TAC1028" (holding (CorePrimitive CoreMemoize [stepClosure] boolCallable)))
+    , -- Both wire formats carry it, and CorePrep accepts what Core does.
+      ("the Core wire format carries a remembered callable", coreRoundTrip sharedModule)
+    , ("CorePrep accepts a remembered callable", preparedAndVerified sharedModule)
+    , ("the CorePrep wire format carries a remembered callable", corePrepRoundTrip sharedModule)
+    , ("CorePrep refuses to remember a number", corePrepRejects "TAC0025" (malformedCorePrep intType))
+    , ("CorePrep refuses to remember a callable with a parameter", corePrepRejects "TAC0025" (malformedCorePrep (FunctionType [intType] intType)))
+    , -- What it computes, and how often. The budgets are those of one
+      -- computation of Step(100): a second one does not fit.
+      ("a remembered callable returns what its operand returns", run sharedModule 5 == Just 15)
+    , ("three calls of one remembered callable compute once", runWithin 500 sharedModule 100 == Just 300)
+    , ("three calls of the callable itself compute three times", runWithin 500 repeatedModule 100 == Nothing)
+    , ("the callable itself, called three times, gives the same value", run repeatedModule 5 == Just 15)
+    , ("a copy of a remembered callable shares its result", runWithin 500 copiedModule 100 == Just 200)
+    , ("two remembered callables each compute their own result", runWithin 500 separateModule 100 == Nothing)
+    , ("two remembered callables give two results", run separateModule 5 == Just 10)
+    , ("a remembered callable handed to a function is computed once for both", runWithin 500 handedModule 100 == Just 200)
+    , ("a remembered callable that is never called computes nothing", run neverCalledModule 5 == Just 7)
+    , ("a remembered callable keeps the values it was created with", run capturedModule 5 == Just 506)
+    , -- The optimizer keeps the meaning and the count.
+      ("the optimizer keeps a remembered callable verified", all optimizedVerifies allModules)
+    , ("the optimizer does not repeat a remembered computation", runOptimizedWithin 500 sharedModule 100 == Just 300)
+    , ("the optimizer does not compute what is never called", runOptimized neverCalledModule 5 == Just 7)
+    , ("the optimizer keeps sharing through a copy", runOptimizedWithin 500 copiedModule 100 == Just 200)
+    , ("the optimizer keeps sharing across a call", runOptimizedWithin 500 handedModule 100 == Just 200)
+    , ("the optimizer does not turn one remembered callable into two", all (not . memoizesMore) allModules)
+    ]
+    where
+        allModules = [sharedModule, repeatedModule, copiedModule, separateModule, handedModule, neverCalledModule, capturedModule, booleanModule]
+
+-- ---------------------------------------------------------------- fixtures
+
+intCallable :: Type
+intCallable = FunctionType [] intType
+
+boolCallable :: Type
+boolCallable = FunctionType [] boolType
+
+named :: Int -> String -> ResolvedName
+named identifier spelling = ResolvedName (SymbolId identifier) (Identifier spelling)
+
+runName, stepName, takeName, inputName, heldName, otherName, countName, callableParameter :: ResolvedName
+runName = named 1 "Run"
+stepName = named 2 "Step"
+takeName = named 3 "Take"
+inputName = named 10 "input"
+heldName = named 11 "held"
+otherName = named 12 "other"
+countName = named 13 "count"
+callableParameter = named 14 "computation"
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+variable :: ResolvedName -> Type -> CoreExpression
+variable = CoreVariable
+
+input :: CoreExpression
+input = variable inputName intType
+
+-- | @Step(n)@ counts down to zero and returns how far it counted: n steps of work.
+stepFunction :: CoreFunction
+stepFunction =
+    CoreFunction
+        stepName
+        [(countName, intType)]
+        intType
+        [ CoreIf
+            (CorePrimitive CoreGreaterThan [count, integer 0] boolType)
+            [ CoreReturn
+                ( CorePrimitive
+                    CoreAdd
+                    [integer 1, CoreApply stepReference [CorePrimitive CoreSubtract [count, integer 1] intType] intType]
+                    intType
+                )
+            ]
+            []
+        , CoreReturn (integer 0)
+        ]
+    where
+        count = variable countName intType
+
+stepReference :: CoreExpression
+stepReference = variable stepName (FunctionType [intType] intType)
+
+-- | A callable that computes @Step(input)@ from the input it was created with.
+stepClosure :: CoreExpression
+stepClosure =
+    CoreClosure
+        [CoreCapture StrongCapture inputName intType input]
+        []
+        intType
+        [CoreReturn (CoreApply stepReference [input] intType)]
+        intCallable
+
+memoize :: CoreExpression -> CoreExpression
+memoize computation = CorePrimitive CoreMemoize [computation] (expressionType computation)
+
+remembered :: CoreExpression
+remembered = memoize stepClosure
+
+callOf :: CoreExpression -> CoreExpression
+callOf callable = CoreApply callable [] intType
+
+held, other :: CoreExpression
+held = variable heldName intCallable
+other = variable otherName intCallable
+
+sumOf :: [CoreExpression] -> CoreExpression
+sumOf = foldr1 (\left right -> CorePrimitive CoreAdd [left, right] intType)
+
+-- | A module whose @Run(input)@ has the given body, beside @Step@.
+moduleOf :: [CoreFunction] -> [CoreStatement] -> CoreModule
+moduleOf others body =
+    CoreModule
+        (QualifiedName [Identifier "Memo"])
+        (CoreFunction runName [(inputName, intType)] intType body : stepFunction : others)
+
+returning :: CoreExpression -> CoreModule
+returning value = moduleOf [] [CoreReturn value]
+
+-- | @Run@ binds the value and returns zero; for the cases the verifier refuses.
+holding :: CoreExpression -> CoreModule
+holding value =
+    moduleOf [] [CoreBind (CoreBinding heldName (expressionType value) False value), CoreReturn (integer 0)]
+
+bindHeld :: CoreExpression -> CoreStatement
+bindHeld value = CoreBind (CoreBinding heldName intCallable False value)
+
+-- | One remembered callable, called three times.
+sharedModule :: CoreModule
+sharedModule = moduleOf [] [bindHeld remembered, CoreReturn (sumOf [callOf held, callOf held, callOf held])]
+
+-- | The same callable without remembering, called three times.
+repeatedModule :: CoreModule
+repeatedModule = moduleOf [] [bindHeld stepClosure, CoreReturn (sumOf [callOf held, callOf held, callOf held])]
+
+-- | A second name for one remembered callable: both names call the same one.
+copiedModule :: CoreModule
+copiedModule =
+    moduleOf
+        []
+        [ bindHeld remembered
+        , CoreBind (CoreBinding otherName intCallable False held)
+        , CoreReturn (sumOf [callOf held, callOf other])
+        ]
+
+-- | Two remembered callables of the same computation.
+separateModule :: CoreModule
+separateModule =
+    moduleOf
+        []
+        [ bindHeld remembered
+        , CoreBind (CoreBinding otherName intCallable False remembered)
+        , CoreReturn (sumOf [callOf held, callOf other])
+        ]
+
+-- | @Take(computation)@ calls the callable it is given.
+takeFunction :: CoreFunction
+takeFunction =
+    CoreFunction
+        takeName
+        [(callableParameter, intCallable)]
+        intType
+        [CoreReturn (callOf (variable callableParameter intCallable))]
+
+-- | A remembered callable that the function it is handed to and the caller both call.
+handedModule :: CoreModule
+handedModule =
+    moduleOf
+        [takeFunction]
+        [ bindHeld remembered
+        , CoreReturn
+            (sumOf [CoreApply (variable takeName (FunctionType [intCallable] intType)) [held] intType, callOf held])
+        ]
+
+-- | A remembered callable of a computation that never returns, never called.
+neverCalledModule :: CoreModule
+neverCalledModule =
+    moduleOf
+        []
+        [ bindHeld
+            ( memoize
+                ( CoreClosure
+                    [CoreCapture StrongCapture inputName intType input]
+                    []
+                    intType
+                    [CoreWhile (CoreLiteral (CoreBoolean True) boolType) [], CoreReturn input]
+                    intCallable
+                )
+            )
+        , CoreReturn (integer 7)
+        ]
+
+-- | The callable keeps the input it was created with; a later store does not reach it.
+capturedModule :: CoreModule
+capturedModule =
+    moduleOf
+        []
+        [ CoreBind (CoreBinding countName intType True input)
+        , bindHeld
+            ( memoize
+                ( CoreClosure
+                    [CoreCapture StrongCapture countName intType (variable countName intType)]
+                    []
+                    intType
+                    [CoreReturn (CorePrimitive CoreAdd [variable countName intType, integer 1] intType)]
+                    intCallable
+                )
+            )
+        , CoreAssign countName (integer 500)
+        , CoreReturn (sumOf [callOf held, variable countName intType])
+        ]
+
+-- | A remembered Boolean.
+booleanModule :: CoreModule
+booleanModule =
+    moduleOf
+        []
+        [ CoreBind
+            ( CoreBinding
+                heldName
+                boolCallable
+                False
+                ( memoize
+                    ( CoreClosure
+                        [CoreCapture StrongCapture inputName intType input]
+                        []
+                        boolType
+                        [CoreReturn (CorePrimitive CoreGreaterThan [input, integer 0] boolType)]
+                        boolCallable
+                    )
+                )
+            )
+        , CoreIf (CoreApply (variable heldName boolCallable) [] boolType) [CoreReturn (integer 1)] []
+        , CoreReturn (integer 0)
+        ]
+
+parameterized, unitCallable, stringCallable :: CoreExpression
+parameterized =
+    CoreClosure [] [(countName, intType)] intType [CoreReturn (variable countName intType)] (FunctionType [intType] intType)
+unitCallable = CoreClosure [] [] unitType [CoreReturn (CoreLiteral CoreUnit unitType)] (FunctionType [] unitType)
+stringCallable =
+    CoreClosure [] [] stringType [CoreReturn (CoreLiteral (CoreString "text") stringType)] (FunctionType [] stringType)
+
+-- | A CorePrep function that remembers an atom of the given type.
+malformedCorePrep :: Type -> CorePrepModule
+malformedCorePrep operandType =
+    CorePrepModule
+        (QualifiedName [Identifier "Memo"])
+        [ CorePrepFunction
+            runName
+            ""
+            [(inputName, operandType)]
+            intType
+            0
+            [ CorePrepBlock
+                0
+                [CorePrepBind heldName operandType False (CorePrepPrimitive CoreMemoize [CorePrepVariable inputName operandType])]
+                (CorePrepReturn (CorePrepLiteral (CoreInteger 0) intType))
+            ]
+        ]
+        []
+
+-- ----------------------------------------------------------------- checks
+
+accepted :: CoreModule -> Bool
+accepted moduleValue = verifyCore moduleValue == Right moduleValue
+
+rejectedWith :: String -> CoreModule -> Bool
+rejectedWith code moduleValue = case verifyCore moduleValue of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+coreRoundTrip :: CoreModule -> Bool
+coreRoundTrip moduleValue =
+    (encodeCore defaultCoreWireLimits moduleValue >>= decodeCore defaultCoreWireLimits) == Right moduleValue
+
+preparedAndVerified :: CoreModule -> Bool
+preparedAndVerified moduleValue = case prepareCore moduleValue of
+    Right prepared -> verifyCorePrep prepared == Right prepared && remembers prepared
+    Left _ -> False
+    where
+        remembers prepared = "CoreMemoize" `isInfixOf` show prepared
+
+corePrepRoundTrip :: CoreModule -> Bool
+corePrepRoundTrip moduleValue = case prepareCore moduleValue of
+    Right prepared -> (encodeCorePrep prepared >>= decodeCorePrep) == Right prepared
+    Left _ -> False
+
+corePrepRejects :: String -> CorePrepModule -> Bool
+corePrepRejects code moduleValue = case verifyCorePrep moduleValue of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+run :: CoreModule -> Integer -> Maybe Integer
+run = runWithin 200000
+
+runWithin :: Int -> CoreModule -> Integer -> Maybe Integer
+runWithin budget moduleValue argument = case runFunctionWithBudget budget moduleValue "Run" [IntegerValue argument] of
+    Just (IntegerValue value) -> Just value
+    _ -> Nothing
+
+optimized :: CoreModule -> Maybe CoreModule
+optimized moduleValue = either (const Nothing) Just (runCoreOptimizer defaultCoreOptimizer moduleValue)
+
+optimizedVerifies :: CoreModule -> Bool
+optimizedVerifies moduleValue = case optimized moduleValue of
+    Just result -> accepted result
+    Nothing -> False
+
+runOptimized :: CoreModule -> Integer -> Maybe Integer
+runOptimized = runOptimizedWithin 200000
+
+runOptimizedWithin :: Int -> CoreModule -> Integer -> Maybe Integer
+runOptimizedWithin budget moduleValue argument = optimized moduleValue >>= \result -> runWithin budget result argument
+
+-- | Whether the optimized module holds more remembering than the module had.
+memoizesMore :: CoreModule -> Bool
+memoizesMore moduleValue = case optimized moduleValue of
+    Just result -> occurrences (show result) > occurrences (show moduleValue)
+    Nothing -> True
+    where
+        occurrences text = length [() | index <- [0 .. length text - 1], "CoreMemoize" `isPrefixOfAt` (index, text)]
+        isPrefixOfAt needle (index, text) = take (length needle) (drop index text) == needle
+ test/MonomorphizationTests.hs view
@@ -0,0 +1,1020 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+module MonomorphizationTests (monomorphizationTests) where
+
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.Monomorphization
+import Progmasoft.Tala.Core.Specialization
+
+monomorphizationTests :: [(String, Bool)]
+monomorphizationTests =
+    [ ("plain scalar Core produces an empty specialization plan", plainModuleIsEmpty)
+    , ("parameterized function parameters become root demands", parameterRootIsDiscovered)
+    , ("parameterized function results become root demands", returnRootIsDiscovered)
+    , ("binding declarations become root demands", bindingTypeIsDiscovered)
+    , ("binding initializers retain a separate origin", bindingValueIsDiscovered)
+    , ("assignment values become root demands", assignmentValueIsDiscovered)
+    , ("returned expressions become root demands", returnValueIsDiscovered)
+    , ("if conditions preserve their type location", conditionLocationIsDiscovered)
+    , ("both if branches are traversed", branchLocationsAreDiscovered)
+    , ("evaluated expressions become root demands", evaluatedValueIsDiscovered)
+    , ("call result types become demands", callResultIsDiscovered)
+    , ("call target types are traversed", callTargetIsDiscovered)
+    , ("call arguments are traversed", callArgumentsAreDiscovered)
+    , ("primitive result types become demands", primitiveResultIsDiscovered)
+    , ("primitive operands are traversed", primitiveOperandsAreDiscovered)
+    , ("closure callable types are traversed", closureCallableIsDiscovered)
+    , ("closure capture types are traversed", closureCaptureTypeIsDiscovered)
+    , ("closure capture values are traversed", closureCaptureValueIsDiscovered)
+    , ("closure parameter types are traversed", closureParameterIsDiscovered)
+    , ("closure return types are traversed", closureReturnIsDiscovered)
+    , ("closure body types are traversed", closureBodyIsDiscovered)
+    , ("function type containers expose nested specializations", callableChildrenAreDiscovered)
+    , ("nested template types create dependency demands", nestedDependencyIsPlanned)
+    , ("deep template trees reach a fixed point", deepFixedPointIsPlanned)
+    , ("duplicate root types share one demand", duplicateRootsCoalesce)
+    , ("duplicate roots retain distinct origins", duplicateOriginsAreRetained)
+    , ("an identical root origin is not duplicated", identicalOriginsCoalesce)
+    , ("dependency and root origins may coexist", rootAndDependencyOriginsCoexist)
+    , ("dependency edges are unique", dependencyEdgesAreUnique)
+    , ("demand identifiers follow insertion order", demandIdsAreOrdered)
+    , ("demand lookup by id succeeds", demandLookupByIdWorks)
+    , ("demand lookup by structural type succeeds", demandLookupByTypeWorks)
+    , ("missing demand id returns Nothing", missingDemandIdIsAbsent)
+    , ("missing demand type returns Nothing", missingDemandTypeIsAbsent)
+    , ("all completed plan entries are complete", everyDemandCompletes)
+    , ("root occurrence statistics count sites", rootOccurrenceStatisticsWork)
+    , ("unique demand statistics count canonical types", uniqueDemandStatisticsWork)
+    , ("dependency edge statistics count graph edges", edgeStatisticsWork)
+    , ("cache hit statistics count repeated requests", cacheHitStatisticsWork)
+    , ("depth statistics report the deepest dependency", depthStatisticsWork)
+    , ("zero maximum demand count is invalid", zeroDemandLimitIsRejected)
+    , ("negative maximum depth is invalid", negativeDepthLimitIsRejected)
+    , ("zero maximum origin count is invalid", zeroOriginLimitIsRejected)
+    , ("unique demand limits fail deterministically", demandLimitIsEnforced)
+    , ("dependency depth limits fail deterministically", depthLimitIsEnforced)
+    , ("origin limits fail deterministically", originLimitIsEnforced)
+    , ("open type parameters are rejected", openTypeIsRejected)
+    , ("open value parameters are rejected", openValueIsRejected)
+    , ("malformed fixed arrays are rejected", malformedArrayIsRejected)
+    , ("negative fixed array sizes are rejected", negativeArrayIsRejected)
+    , ("invalid Unicode template values are rejected", invalidCharacterIsRejected)
+    , ("empty qualified names are rejected", emptyNameIsRejected)
+    , ("built-in arrays are specialization candidates", builtinArrayIsCandidate)
+    , ("dynamic System.Array is a specialization candidate", dynamicArrayIsCandidate)
+    , ("fixed System.Array is a specialization candidate", fixedArrayIsCandidate)
+    , ("plain names are not specialization candidates", plainNameIsNotCandidate)
+    , ("callable types are not standalone specialization candidates", callableIsNotCandidate)
+    , ("type variables are not standalone specialization candidates", variableIsNotCandidate)
+    , ("direct dependencies preserve argument indexes", dependencyIndexesAreStable)
+    , ("value arguments do not manufacture dependencies", valueArgumentsDoNotDepend)
+    , ("nested callable arguments expose their concrete children", callableDependencyIsVisible)
+    , ("location rendering includes semantic function identity", locationRendersFunction)
+    , ("location rendering includes statement and argument indexes", locationRendersPath)
+    , ("limit diagnostics explain their boundary", limitDiagnosticIsReadable)
+    , ("open-type diagnostics explain the source location", openDiagnosticIsReadable)
+    , ("compiler artifacts carry an empty plan for scalar source", scalarPipelineCarriesPlan)
+    , ("compiler artifacts carry fixed-array specialization demands", fixedArrayPipelineCarriesPlan)
+    , ("compiler artifacts preserve dynamic and fixed array identities", arrayOverloadsStayDistinct)
+    , ("discovery order follows function order", functionOrderIsStable)
+    , ("discovery order follows statement order", statementOrderIsStable)
+    , ("discovery order follows argument order", argumentOrderIsStable)
+    , ("Boolean value arguments remain valid demands", booleanValueIsAccepted)
+    , ("character value arguments remain valid demands", characterValueIsAccepted)
+    , ("integer and character arguments remain distinct", valueKindsStayDistinct)
+    , ("fixed array zero length is accepted", zeroLengthArrayIsAccepted)
+    , ("large integer template values remain exact", largeIntegerStaysExact)
+    , ("plan derivations support deterministic equality", repeatedPlanningIsEqual)
+    , ("plan Show output contains concrete identities", planShowContainsIdentity)
+    , ("completed planner output passes graph validation", completedGraphIsValid)
+    , ("graph validation rejects duplicate demand ids", duplicateDemandIdsAreRejected)
+    , ("graph validation rejects non-positive demand ids", nonPositiveDemandIdsAreRejected)
+    , ("graph validation rejects missing dependencies", missingDependenciesAreRejected)
+    , ("graph validation rejects duplicate dependency edges", duplicateDependenciesAreRejected)
+    , ("graph validation rejects self dependencies", selfDependenciesAreRejected)
+    , ("graph validation rejects queued output entries", queuedDemandsAreRejected)
+    , ("graph validation checks demand statistics", demandStatisticsAreValidated)
+    , ("graph validation checks edge statistics", edgeStatisticsAreValidated)
+    , ("graph validation checks depth statistics", depthStatisticsAreValidated)
+    , ("graph validation detects cycles", cyclesAreRejected)
+    , ("graph roots retain directly observed demands", graphRootsAreObserved)
+    , ("graph roots include child types also observed directly", observedChildrenAreRoots)
+    , ("graph leaves contain dependency-free specializations", graphLeavesAreTerminal)
+    , ("reverse graph query returns direct dependents", reverseEdgesAreQueryable)
+    , ("reachable dependency query walks transitively", transitiveDependenciesAreQueryable)
+    , ("reachable dependency query excludes its root", transitiveDependenciesExcludeRoot)
+    , ("missing dependency query is empty", missingDependencyQueryIsEmpty)
+    , ("emission order places nested children before parents", childrenEmitBeforeParents)
+    , ("emission order remains stable across runs", emissionOrderIsStable)
+    , ("demand trace includes identity and depth", traceIncludesIdentityAndDepth)
+    , ("demand trace includes origins", traceIncludesOrigins)
+    , ("demand trace includes dependencies", traceIncludesDependencies)
+    , ("unknown demand trace is explicit", unknownTraceIsExplicit)
+    , ("completed graph failures have a readable diagnostic", graphDiagnosticIsReadable)
+    ]
+
+plainModuleIsEmpty :: Bool
+plainModuleIsEmpty = case planCoreMonomorphization (moduleWith [plainFunction]) of
+    Right plan -> null (monomorphizationDemands plan) && monomorphizationStatistics plan == emptyMonomorphizationStatistics
+    Left _ -> False
+
+parameterRootIsDiscovered :: Bool
+parameterRootIsDiscovered =
+    hasRootPath [FunctionParameterPath 0] dynamicArrayPlan
+
+returnRootIsDiscovered :: Bool
+returnRootIsDiscovered = case planCoreMonomorphization (moduleWith [functionWith [] dynamicIntArray []]) of
+    Right plan -> hasRootPath [FunctionReturnPath] plan
+    Left _ -> False
+
+bindingTypeIsDiscovered :: Bool
+bindingTypeIsDiscovered =
+    hasRootPath [StatementPath 0, BindingTypePath] bindingPlan
+
+bindingValueIsDiscovered :: Bool
+bindingValueIsDiscovered =
+    hasRootPath [StatementPath 0, BindingValuePath, ExpressionResultPath] bindingPlan
+
+assignmentValueIsDiscovered :: Bool
+assignmentValueIsDiscovered = case planCoreMonomorphization (moduleWith [functionWith [] unitType [CoreAssign localName dynamicLiteral]]) of
+    Right plan -> hasRootPath [StatementPath 0, AssignmentValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+returnValueIsDiscovered :: Bool
+returnValueIsDiscovered = case planCoreMonomorphization (moduleWith [functionWith [] dynamicIntArray [CoreReturn dynamicLiteral]]) of
+    Right plan -> hasRootPath [StatementPath 0, ReturnValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+conditionLocationIsDiscovered :: Bool
+conditionLocationIsDiscovered = case planCoreMonomorphization conditionModule of
+    Right plan -> hasRootPath [StatementPath 0, ConditionPath, ExpressionResultPath] plan
+    Left _ -> False
+
+branchLocationsAreDiscovered :: Bool
+branchLocationsAreDiscovered = case planCoreMonomorphization branchModule of
+    Right plan ->
+        hasRootPath [StatementPath 0, TrueBranchPath, StatementPath 0, EvaluatedValuePath, ExpressionResultPath] plan
+            && hasRootPath [StatementPath 0, FalseBranchPath, StatementPath 0, EvaluatedValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+evaluatedValueIsDiscovered :: Bool
+evaluatedValueIsDiscovered = case planCoreMonomorphization (moduleWith [functionWith [] unitType [CoreEvaluate dynamicLiteral]]) of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+callResultIsDiscovered :: Bool
+callResultIsDiscovered = case callPlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+callTargetIsDiscovered :: Bool
+callTargetIsDiscovered = case callPlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, CalleePath, ExpressionResultPath, ClosureReturnPath] plan
+    Left _ -> False
+
+callArgumentsAreDiscovered :: Bool
+callArgumentsAreDiscovered = case callPlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ArgumentPath 0, ExpressionResultPath] plan
+    Left _ -> False
+
+primitiveResultIsDiscovered :: Bool
+primitiveResultIsDiscovered = case primitivePlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ExpressionResultPath] plan
+    Left _ -> False
+
+primitiveOperandsAreDiscovered :: Bool
+primitiveOperandsAreDiscovered = case primitivePlan of
+    Right plan ->
+        hasRootPath [StatementPath 0, EvaluatedValuePath, PrimitiveOperandPath 0, ExpressionResultPath] plan
+            && hasRootPath [StatementPath 0, EvaluatedValuePath, PrimitiveOperandPath 1, ExpressionResultPath] plan
+    Left _ -> False
+
+closureCallableIsDiscovered :: Bool
+closureCallableIsDiscovered = case closurePlan of
+    Right plan ->
+        hasRootPath [StatementPath 0, EvaluatedValuePath, ExpressionResultPath, ClosureParameterPath 0] plan
+            && hasRootPath [StatementPath 0, EvaluatedValuePath, ExpressionResultPath, ClosureReturnPath] plan
+    Left _ -> False
+
+closureCaptureTypeIsDiscovered :: Bool
+closureCaptureTypeIsDiscovered = case closurePlan of
+    Right plan ->
+        hasRootPath
+            [StatementPath 0, EvaluatedValuePath, ClosureCapturePath 0, ClosureCaptureTypePath]
+            plan
+    Left _ -> False
+
+closureCaptureValueIsDiscovered :: Bool
+closureCaptureValueIsDiscovered = case closurePlan of
+    Right plan ->
+        hasRootPath
+            [StatementPath 0, EvaluatedValuePath, ClosureCapturePath 0, ClosureCaptureValuePath, ExpressionResultPath]
+            plan
+    Left _ -> False
+
+closureParameterIsDiscovered :: Bool
+closureParameterIsDiscovered = case closurePlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ClosureParameterPath 0] plan
+    Left _ -> False
+
+closureReturnIsDiscovered :: Bool
+closureReturnIsDiscovered = case closurePlan of
+    Right plan -> hasRootPath [StatementPath 0, EvaluatedValuePath, ClosureReturnPath] plan
+    Left _ -> False
+
+closureBodyIsDiscovered :: Bool
+closureBodyIsDiscovered = case closurePlan of
+    Right plan ->
+        hasRootPath
+            [StatementPath 0, EvaluatedValuePath, ClosureBodyPath, StatementPath 0, ReturnValuePath, ExpressionResultPath]
+            plan
+    Left _ -> False
+
+callableChildrenAreDiscovered :: Bool
+callableChildrenAreDiscovered = case planCoreMonomorphization callableModule of
+    Right plan -> demandTypes plan == [dynamicIntArray, fixedIntArray 4]
+    Left _ -> False
+
+nestedDependencyIsPlanned :: Bool
+nestedDependencyIsPlanned = case nestedPlan of
+    Right plan -> case monomorphizationDemands plan of
+        parent : child : _ -> monomorphizationDependencies parent == [monomorphizationDemandId child]
+        _ -> False
+    Left _ -> False
+
+deepFixedPointIsPlanned :: Bool
+deepFixedPointIsPlanned = case deepPlan of
+    Right plan ->
+        length (monomorphizationDemands plan) == 4
+            && deepestSpecializationDemand (monomorphizationStatistics plan) == 3
+    Left _ -> False
+
+duplicateRootsCoalesce :: Bool
+duplicateRootsCoalesce = case duplicatePlan of
+    Right plan -> length (monomorphizationDemands plan) == 1
+    Left _ -> False
+
+duplicateOriginsAreRetained :: Bool
+duplicateOriginsAreRetained = case duplicatePlan of
+    Right plan -> case monomorphizationDemands plan of
+        [demand] -> length (monomorphizationOrigins demand) == 2
+        _ -> False
+    Left _ -> False
+
+identicalOriginsCoalesce :: Bool
+identicalOriginsCoalesce = case planCoreMonomorphization (moduleWith [functionWith [(localName, dynamicIntArray)] unitType []]) of
+    Right plan -> case monomorphizationDemands plan of
+        [demand] -> length (monomorphizationOrigins demand) == 1
+        _ -> False
+    Left _ -> False
+
+rootAndDependencyOriginsCoexist :: Bool
+rootAndDependencyOriginsCoexist = case rootAndDependencyPlan of
+    Right plan -> case findDemandByType dynamicIntArray plan of
+        Just demand -> any isRoot (monomorphizationOrigins demand) && any isDependency (monomorphizationOrigins demand)
+        Nothing -> False
+    Left _ -> False
+
+dependencyEdgesAreUnique :: Bool
+dependencyEdgesAreUnique = case repeatedChildPlan of
+    Right plan -> case monomorphizationDemands plan of
+        parent : _ -> length (monomorphizationDependencies parent) == 1
+        [] -> False
+    Left _ -> False
+
+demandIdsAreOrdered :: Bool
+demandIdsAreOrdered = case multiPlan of
+    Right plan -> map (demandIdValue . monomorphizationDemandId) (monomorphizationDemands plan) == [1, 2, 3]
+    Left _ -> False
+
+demandLookupByIdWorks :: Bool
+demandLookupByIdWorks = case multiPlan of
+    Right plan -> case findDemand (DemandId 2) plan of
+        Just demand -> specializationType (monomorphizationSpecialization demand) == fixedIntArray 2
+        Nothing -> False
+    Left _ -> False
+
+demandLookupByTypeWorks :: Bool
+demandLookupByTypeWorks = case multiPlan of
+    Right plan -> case findDemandByType (fixedIntArray 3) plan of
+        Just demand -> monomorphizationDemandId demand == DemandId 3
+        Nothing -> False
+    Left _ -> False
+
+missingDemandIdIsAbsent :: Bool
+missingDemandIdIsAbsent = case multiPlan of
+    Right plan -> findDemand (DemandId 99) plan == Nothing
+    Left _ -> False
+
+missingDemandTypeIsAbsent :: Bool
+missingDemandTypeIsAbsent = case multiPlan of
+    Right plan -> findDemandByType (fixedIntArray 99) plan == Nothing
+    Left _ -> False
+
+everyDemandCompletes :: Bool
+everyDemandCompletes = case deepPlan of
+    Right plan -> all ((== DemandComplete) . monomorphizationState) (monomorphizationDemands plan)
+    Left _ -> False
+
+rootOccurrenceStatisticsWork :: Bool
+rootOccurrenceStatisticsWork = case duplicatePlan of
+    Right plan -> discoveredRootOccurrences (monomorphizationStatistics plan) == 2
+    Left _ -> False
+
+uniqueDemandStatisticsWork :: Bool
+uniqueDemandStatisticsWork = case multiPlan of
+    Right plan -> uniqueSpecializationDemands (monomorphizationStatistics plan) == 3
+    Left _ -> False
+
+edgeStatisticsWork :: Bool
+edgeStatisticsWork = case deepPlan of
+    Right plan -> specializationDependencyEdges (monomorphizationStatistics plan) == 3
+    Left _ -> False
+
+cacheHitStatisticsWork :: Bool
+cacheHitStatisticsWork = case duplicatePlan of
+    Right plan -> specializationCacheHits (monomorphizationStatistics plan) == 1
+    Left _ -> False
+
+depthStatisticsWork :: Bool
+depthStatisticsWork = case deepPlan of
+    Right plan -> deepestSpecializationDemand (monomorphizationStatistics plan) == 3
+    Left _ -> False
+
+zeroDemandLimitIsRejected :: Bool
+zeroDemandLimitIsRejected = case planCoreMonomorphizationWith (MonomorphizationLimits 0 1 1) (moduleWith []) of
+    Left (InvalidMonomorphizationLimits _) -> True
+    _ -> False
+
+negativeDepthLimitIsRejected :: Bool
+negativeDepthLimitIsRejected = case planCoreMonomorphizationWith (MonomorphizationLimits 1 (-1) 1) (moduleWith []) of
+    Left (InvalidMonomorphizationLimits _) -> True
+    _ -> False
+
+zeroOriginLimitIsRejected :: Bool
+zeroOriginLimitIsRejected = case planCoreMonomorphizationWith (MonomorphizationLimits 1 1 0) (moduleWith []) of
+    Left (InvalidMonomorphizationLimits _) -> True
+    _ -> False
+
+demandLimitIsEnforced :: Bool
+demandLimitIsEnforced = case planCoreMonomorphizationWith (MonomorphizationLimits 2 8 8) multiModule of
+    Left (SpecializationLimitExceeded 2 _ _) -> True
+    _ -> False
+
+depthLimitIsEnforced :: Bool
+depthLimitIsEnforced = case planCoreMonomorphizationWith (MonomorphizationLimits 8 1 8) deepModule of
+    Left (DemandDepthExceeded 1 _ _) -> True
+    _ -> False
+
+originLimitIsEnforced :: Bool
+originLimitIsEnforced = case planCoreMonomorphizationWith (MonomorphizationLimits 8 8 1) duplicateModule of
+    Left (DemandOriginLimitExceeded 1 _ _) -> True
+    _ -> False
+
+openTypeIsRejected :: Bool
+openTypeIsRejected = case planCoreMonomorphization (moduleWith [functionWith [(localName, openBox)] unitType []]) of
+    Left (InvalidDemandType _ (OpenSpecialization [SymbolId 80])) -> True
+    _ -> False
+
+openValueIsRejected :: Bool
+openValueIsRejected = case planCoreMonomorphization (moduleWith [functionWith [(localName, openArray)] unitType []]) of
+    Left (InvalidDemandType _ (OpenSpecialization [SymbolId 81])) -> True
+    _ -> False
+
+malformedArrayIsRejected :: Bool
+malformedArrayIsRejected = case planCoreMonomorphization (moduleWith [functionWith [(localName, malformedArray)] unitType []]) of
+    Left (InvalidDemandType _ (InvalidSpecialization _)) -> True
+    _ -> False
+
+negativeArrayIsRejected :: Bool
+negativeArrayIsRejected = case planCoreMonomorphization (moduleWith [functionWith [(localName, fixedIntArray (-1))] unitType []]) of
+    Left (InvalidDemandType _ (InvalidSpecialization _)) -> True
+    _ -> False
+
+invalidCharacterIsRejected :: Bool
+invalidCharacterIsRejected = case planCoreMonomorphization (moduleWith [functionWith [(localName, characterBox 0xd800)] unitType []]) of
+    Left (InvalidDemandType _ (InvalidSpecialization _)) -> True
+    _ -> False
+
+emptyNameIsRejected :: Bool
+emptyNameIsRejected = case planCoreMonomorphization
+    (moduleWith [functionWith [(localName, NamedType (QualifiedName []) [typeArgument intType])] unitType []]) of
+    Left (InvalidDemandType _ (InvalidSpecialization _)) -> True
+    _ -> False
+
+builtinArrayIsCandidate :: Bool
+builtinArrayIsCandidate = specializationCandidate builtinIntArray
+
+dynamicArrayIsCandidate :: Bool
+dynamicArrayIsCandidate = specializationCandidate dynamicIntArray
+
+fixedArrayIsCandidate :: Bool
+fixedArrayIsCandidate = specializationCandidate (fixedIntArray 8)
+
+plainNameIsNotCandidate :: Bool
+plainNameIsNotCandidate = not (specializationCandidate intType)
+
+callableIsNotCandidate :: Bool
+callableIsNotCandidate = not (specializationCandidate (FunctionType [dynamicIntArray] dynamicIntArray))
+
+variableIsNotCandidate :: Bool
+variableIsNotCandidate = not (specializationCandidate (TypeVariable typeParameter))
+
+dependencyIndexesAreStable :: Bool
+dependencyIndexesAreStable =
+    directTypeDependencies pairType
+        == [(0, dynamicIntArray), (1, fixedIntArray 4)]
+
+valueArgumentsDoNotDepend :: Bool
+valueArgumentsDoNotDepend = directTypeDependencies (fixedIntArray 4) == []
+
+callableDependencyIsVisible :: Bool
+callableDependencyIsVisible =
+    directTypeDependencies (FunctionType [dynamicIntArray] (fixedIntArray 4))
+        == [(0, dynamicIntArray), (1, fixedIntArray 4)]
+
+locationRendersFunction :: Bool
+locationRendersFunction = "Run#1" `isInfixOf` renderDemandLocation (DemandLocation functionName [])
+
+locationRendersPath :: Bool
+locationRendersPath =
+    ".statement[2].argument[3]"
+        `isInfixOf` renderDemandLocation (DemandLocation functionName [StatementPath 2, ArgumentPath 3])
+
+limitDiagnosticIsReadable :: Bool
+limitDiagnosticIsReadable = case planCoreMonomorphizationWith (MonomorphizationLimits 1 8 8) multiModule of
+    Left failure -> "specialization demand limit 1 exceeded" `isInfixOf` renderMonomorphizationError failure
+    Right _ -> False
+
+openDiagnosticIsReadable :: Bool
+openDiagnosticIsReadable = case planCoreMonomorphization (moduleWith [functionWith [(localName, openBox)] unitType []]) of
+    Left failure ->
+        "Run#1.parameter[0]" `isInfixOf` renderMonomorphizationError failure
+            && "unbound template parameters [80]" `isInfixOf` renderMonomorphizationError failure
+    Right _ -> False
+
+scalarPipelineCarriesPlan :: Bool
+scalarPipelineCarriesPlan = case compileToCorePrep (CompilerInput "scalar.tala" scalarSource) of
+    Right artifacts -> null (monomorphizationDemands (artifactMonomorphizationPlan artifacts))
+    Left _ -> False
+
+fixedArrayPipelineCarriesPlan :: Bool
+fixedArrayPipelineCarriesPlan = case compileToCorePrep (CompilerInput "array.tala" fixedArraySource) of
+    Right artifacts ->
+        any
+            ((== fixedIntArray 4) . specializationType . monomorphizationSpecialization)
+            (monomorphizationDemands (artifactMonomorphizationPlan artifacts))
+    Left _ -> False
+
+arrayOverloadsStayDistinct :: Bool
+arrayOverloadsStayDistinct = case compileToCorePrep (CompilerInput "arrays.tala" arrayOverloadSource) of
+    Right artifacts ->
+        let types = demandTypes (artifactMonomorphizationPlan artifacts)
+         in dynamicIntArray `elem` types && fixedIntArray 4 `elem` types
+    Left _ -> False
+
+functionOrderIsStable :: Bool
+functionOrderIsStable = case planCoreMonomorphization orderedFunctionsModule of
+    Right plan -> take 2 (demandTypes plan) == [fixedIntArray 1, fixedIntArray 2]
+    Left _ -> False
+
+statementOrderIsStable :: Bool
+statementOrderIsStable = case planCoreMonomorphization orderedStatementsModule of
+    Right plan -> take 2 (demandTypes plan) == [fixedIntArray 1, fixedIntArray 2]
+    Left _ -> False
+
+argumentOrderIsStable :: Bool
+argumentOrderIsStable = case planCoreMonomorphization orderedArgumentsModule of
+    Right plan -> take 2 (demandTypes plan) == [fixedIntArray 1, fixedIntArray 2]
+    Left _ -> False
+
+booleanValueIsAccepted :: Bool
+booleanValueIsAccepted = accepts (named "Flagged" [ValueTemplateArgument (BooleanTemplateValue True)])
+
+characterValueIsAccepted :: Bool
+characterValueIsAccepted = accepts (characterBox 65)
+
+valueKindsStayDistinct :: Bool
+valueKindsStayDistinct = case planCoreMonomorphization valueKindModule of
+    Right plan -> length (monomorphizationDemands plan) == 2
+    Left _ -> False
+
+zeroLengthArrayIsAccepted :: Bool
+zeroLengthArrayIsAccepted = accepts (fixedIntArray 0)
+
+largeIntegerStaysExact :: Bool
+largeIntegerStaysExact =
+    accepts (named "Wide" [ValueTemplateArgument (IntegerTemplateValue (2 ^ (200 :: Int)))])
+
+repeatedPlanningIsEqual :: Bool
+repeatedPlanningIsEqual = planCoreMonomorphization deepModule == planCoreMonomorphization deepModule
+
+planShowContainsIdentity :: Bool
+planShowContainsIdentity = case nestedPlan of
+    Right plan -> "System" `isInfixOf` show plan && "Array" `isInfixOf` show plan
+    Left _ -> False
+
+completedGraphIsValid :: Bool
+completedGraphIsValid = maybePlan deepPlan (null . validateDemandGraph)
+
+duplicateDemandIdsAreRejected :: Bool
+duplicateDemandIdsAreRejected = case planDemands deepPlan of
+    first : second : remaining ->
+        let duplicate = second {monomorphizationDemandId = monomorphizationDemandId first}
+            plan = replacePlanDemands deepPlan (first : duplicate : remaining)
+         in DuplicateDemandIdentifier (monomorphizationDemandId first) `elem` validateDemandGraph plan
+    _ -> False
+
+nonPositiveDemandIdsAreRejected :: Bool
+nonPositiveDemandIdsAreRejected = case planDemands nestedPlan of
+    first : remaining ->
+        let invalid = first {monomorphizationDemandId = DemandId 0}
+            plan = replacePlanDemands nestedPlan (invalid : remaining)
+         in NonPositiveDemandIdentifier (DemandId 0) `elem` validateDemandGraph plan
+    _ -> False
+
+missingDependenciesAreRejected :: Bool
+missingDependenciesAreRejected = case planDemands nestedPlan of
+    first : remaining ->
+        let invalid = first {monomorphizationDependencies = [DemandId 99]}
+            plan = replacePlanDemands nestedPlan (invalid : remaining)
+         in MissingDependency (monomorphizationDemandId first) (DemandId 99) `elem` validateDemandGraph plan
+    _ -> False
+
+duplicateDependenciesAreRejected :: Bool
+duplicateDependenciesAreRejected = case planDemands nestedPlan of
+    first : remaining -> case monomorphizationDependencies first of
+        dependency : _ ->
+            let invalid = first {monomorphizationDependencies = [dependency, dependency]}
+                plan = replacePlanDemands nestedPlan (invalid : remaining)
+             in DuplicateDependency (monomorphizationDemandId first) dependency `elem` validateDemandGraph plan
+        [] -> False
+    _ -> False
+
+selfDependenciesAreRejected :: Bool
+selfDependenciesAreRejected = case planDemands nestedPlan of
+    first : remaining ->
+        let owner = monomorphizationDemandId first
+            invalid = first {monomorphizationDependencies = [owner]}
+            plan = replacePlanDemands nestedPlan (invalid : remaining)
+         in SelfDependency owner `elem` validateDemandGraph plan
+    _ -> False
+
+queuedDemandsAreRejected :: Bool
+queuedDemandsAreRejected = case planDemands nestedPlan of
+    first : remaining ->
+        let invalid = first {monomorphizationState = DemandQueued}
+            plan = replacePlanDemands nestedPlan (invalid : remaining)
+         in IncompleteDemand (monomorphizationDemandId first) `elem` validateDemandGraph plan
+    _ -> False
+
+demandStatisticsAreValidated :: Bool
+demandStatisticsAreValidated = case nestedPlan of
+    Right plan ->
+        let statistics = (monomorphizationStatistics plan) {uniqueSpecializationDemands = 77}
+            invalid = plan {monomorphizationStatistics = statistics}
+         in StatisticsDemandCountMismatch 77 2 `elem` validateDemandGraph invalid
+    Left _ -> False
+
+edgeStatisticsAreValidated :: Bool
+edgeStatisticsAreValidated = case nestedPlan of
+    Right plan ->
+        let statistics = (monomorphizationStatistics plan) {specializationDependencyEdges = 77}
+            invalid = plan {monomorphizationStatistics = statistics}
+         in StatisticsEdgeCountMismatch 77 1 `elem` validateDemandGraph invalid
+    Left _ -> False
+
+depthStatisticsAreValidated :: Bool
+depthStatisticsAreValidated = case nestedPlan of
+    Right plan ->
+        let statistics = (monomorphizationStatistics plan) {deepestSpecializationDemand = 77}
+            invalid = plan {monomorphizationStatistics = statistics}
+         in StatisticsDepthMismatch 77 1 `elem` validateDemandGraph invalid
+    Left _ -> False
+
+cyclesAreRejected :: Bool
+cyclesAreRejected = case planDemands nestedPlan of
+    first : second : remaining ->
+        let firstId = monomorphizationDemandId first
+            secondId = monomorphizationDemandId second
+            left = first {monomorphizationDependencies = [secondId]}
+            right = second {monomorphizationDependencies = [firstId]}
+            plan = replacePlanDemands nestedPlan (left : right : remaining)
+         in any isCycle (validateDemandGraph plan)
+    _ -> False
+    where
+        isCycle (CyclicDemandPath _) = True
+        isCycle _ = False
+
+graphRootsAreObserved :: Bool
+graphRootsAreObserved = case nestedPlan of
+    Right plan -> map demandType (demandRoots plan) == [nestedArray]
+    Left _ -> False
+
+observedChildrenAreRoots :: Bool
+observedChildrenAreRoots = case rootAndDependencyPlan of
+    Right plan -> map demandType (demandRoots plan) == [nestedArray, dynamicIntArray]
+    Left _ -> False
+
+graphLeavesAreTerminal :: Bool
+graphLeavesAreTerminal = case deepPlan of
+    Right plan -> map demandType (demandLeaves plan) == [dynamicIntArray]
+    Left _ -> False
+
+reverseEdgesAreQueryable :: Bool
+reverseEdgesAreQueryable = case nestedPlan of
+    Right plan -> case findDemandByType dynamicIntArray plan of
+        Just child -> map demandType (dependentDemands (monomorphizationDemandId child) plan) == [nestedArray]
+        Nothing -> False
+    Left _ -> False
+
+transitiveDependenciesAreQueryable :: Bool
+transitiveDependenciesAreQueryable = case deepPlan of
+    Right plan -> case findDemandByType deepArray plan of
+        Just root -> length (reachableDependencies (monomorphizationDemandId root) plan) == 3
+        Nothing -> False
+    Left _ -> False
+
+transitiveDependenciesExcludeRoot :: Bool
+transitiveDependenciesExcludeRoot = case deepPlan of
+    Right plan -> case findDemandByType deepArray plan of
+        Just root -> root `notElem` reachableDependencies (monomorphizationDemandId root) plan
+        Nothing -> False
+    Left _ -> False
+
+missingDependencyQueryIsEmpty :: Bool
+missingDependencyQueryIsEmpty = maybePlan nestedPlan (null . reachableDependencies (DemandId 99))
+
+childrenEmitBeforeParents :: Bool
+childrenEmitBeforeParents = case deepPlan >>= mapGraphResult . specializationEmissionOrder of
+    Right demands -> map demandType demands == reverse (demandTypes (rightPlan deepPlan))
+    Left _ -> False
+
+emissionOrderIsStable :: Bool
+emissionOrderIsStable = case deepPlan of
+    Right plan -> specializationEmissionOrder plan == specializationEmissionOrder plan
+    Left _ -> False
+
+traceIncludesIdentityAndDepth :: Bool
+traceIncludesIdentityAndDepth = case nestedPlan of
+    Right plan ->
+        let trace = renderDemandTrace plan (DemandId 1)
+         in "System" `isInfixOf` trace && "depth=0" `isInfixOf` trace
+    Left _ -> False
+
+traceIncludesOrigins :: Bool
+traceIncludesOrigins = case nestedPlan of
+    Right plan -> "Run#1.parameter[0]" `isInfixOf` renderDemandTrace plan (DemandId 1)
+    Left _ -> False
+
+traceIncludesDependencies :: Bool
+traceIncludesDependencies = case nestedPlan of
+    Right plan -> "dependencies=[demand#2]" `isInfixOf` renderDemandTrace plan (DemandId 1)
+    Left _ -> False
+
+unknownTraceIsExplicit :: Bool
+unknownTraceIsExplicit = case nestedPlan of
+    Right plan -> renderDemandTrace plan (DemandId 99) == "unknown demand demand#99"
+    Left _ -> False
+
+graphDiagnosticIsReadable :: Bool
+graphDiagnosticIsReadable =
+    "completed specialization demand graph failed validation"
+        `isInfixOf` renderMonomorphizationError (InvalidCompletedDemandGraph ["SelfDependency (DemandId 1)"])
+
+demandType :: MonomorphizationDemand -> Type
+demandType = specializationType . monomorphizationSpecialization
+
+planDemands :: Either MonomorphizationError MonomorphizationPlan -> [MonomorphizationDemand]
+planDemands = maybe [] monomorphizationDemands . either (const Nothing) Just
+
+replacePlanDemands ::
+    Either MonomorphizationError MonomorphizationPlan -> [MonomorphizationDemand] -> MonomorphizationPlan
+replacePlanDemands result demands = case result of
+    Right plan -> plan {monomorphizationDemands = demands}
+    Left _ -> MonomorphizationPlan demands emptyMonomorphizationStatistics
+
+maybePlan :: Either MonomorphizationError MonomorphizationPlan -> (MonomorphizationPlan -> Bool) -> Bool
+maybePlan result predicate = case result of
+    Right plan -> predicate plan
+    Left _ -> False
+
+mapGraphResult :: Either [DemandId] value -> Either MonomorphizationError value
+mapGraphResult result = case result of
+    Right value -> Right value
+    Left _ -> Left (InternalMissingDemand "cycle")
+
+hasRootPath :: [DemandPathStep] -> MonomorphizationPlan -> Bool
+hasRootPath path plan =
+    any
+        (any matches . monomorphizationOrigins)
+        (monomorphizationDemands plan)
+    where
+        matches (RootDemand location) = demandLocationPath location == path
+        matches (DependencyDemand _ _) = False
+
+demandTypes :: MonomorphizationPlan -> [Type]
+demandTypes = map (specializationType . monomorphizationSpecialization) . monomorphizationDemands
+
+accepts :: Type -> Bool
+accepts valueType = case planCoreMonomorphization (moduleWith [functionWith [(localName, valueType)] unitType []]) of
+    Right _ -> True
+    Left _ -> False
+
+isRoot :: DemandOrigin -> Bool
+isRoot (RootDemand _) = True
+isRoot _ = False
+
+isDependency :: DemandOrigin -> Bool
+isDependency (DependencyDemand _ _) = True
+isDependency _ = False
+
+moduleWith :: [CoreFunction] -> CoreModule
+moduleWith functions = CoreModuleWithSources (QualifiedName [Identifier "Tests"]) functions [] []
+
+functionWith :: [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreFunction
+functionWith = CoreFunction functionName
+
+plainFunction :: CoreFunction
+plainFunction = functionWith [(localName, intType)] unitType [CoreReturn (CoreLiteral CoreUnit unitType)]
+
+dynamicArrayPlan :: MonomorphizationPlan
+dynamicArrayPlan = rightPlan (planCoreMonomorphization (moduleWith [functionWith [(localName, dynamicIntArray)] unitType []]))
+
+bindingPlan :: MonomorphizationPlan
+bindingPlan =
+    rightPlan
+        ( planCoreMonomorphization
+            (moduleWith [functionWith [] unitType [CoreBind (CoreBinding localName dynamicIntArray False dynamicLiteral)]])
+        )
+
+conditionModule :: CoreModule
+conditionModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [CoreIf (CoreLiteral (CoreString "condition") dynamicIntArray) [] []]
+        ]
+
+branchModule :: CoreModule
+branchModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [ CoreIf
+                (CoreLiteral (CoreBoolean True) boolType)
+                [CoreEvaluate dynamicLiteral]
+                [CoreEvaluate fixedLiteral]
+            ]
+        ]
+
+callPlan :: Either MonomorphizationError MonomorphizationPlan
+callPlan = planCoreMonomorphization callModule
+
+callModule :: CoreModule
+callModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [ CoreEvaluate
+                ( CoreApply
+                    (CoreVariable calleeName (FunctionType [dynamicIntArray] (fixedIntArray 4)))
+                    [dynamicLiteral]
+                    (fixedIntArray 4)
+                )
+            ]
+        ]
+
+primitivePlan :: Either MonomorphizationError MonomorphizationPlan
+primitivePlan =
+    planCoreMonomorphization
+        ( moduleWith
+            [ functionWith
+                []
+                unitType
+                [CoreEvaluate (CorePrimitive CoreAdd [dynamicLiteral, fixedLiteral] dynamicIntArray)]
+            ]
+        )
+
+closurePlan :: Either MonomorphizationError MonomorphizationPlan
+closurePlan = planCoreMonomorphization closureModule
+
+closureModule :: CoreModule
+closureModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [ CoreEvaluate
+                ( CoreClosure
+                    [CoreCapture StrongCapture capturedValueName dynamicIntArray dynamicLiteral]
+                    [(closureParameterName, fixedIntArray 4)]
+                    dynamicIntArray
+                    [CoreReturn dynamicLiteral]
+                    (FunctionType [fixedIntArray 4] dynamicIntArray)
+                )
+            ]
+        ]
+
+callableModule :: CoreModule
+callableModule =
+    moduleWith
+        [functionWith [(localName, FunctionType [dynamicIntArray] (fixedIntArray 4))] unitType []]
+
+nestedPlan :: Either MonomorphizationError MonomorphizationPlan
+nestedPlan = planCoreMonomorphization nestedModule
+
+nestedModule :: CoreModule
+nestedModule = moduleWith [functionWith [(localName, nestedArray)] unitType []]
+
+deepPlan :: Either MonomorphizationError MonomorphizationPlan
+deepPlan = planCoreMonomorphization deepModule
+
+deepModule :: CoreModule
+deepModule = moduleWith [functionWith [(localName, deepArray)] unitType []]
+
+duplicatePlan :: Either MonomorphizationError MonomorphizationPlan
+duplicatePlan = planCoreMonomorphization duplicateModule
+
+duplicateModule :: CoreModule
+duplicateModule = moduleWith [functionWith [(localName, dynamicIntArray), (secondName, dynamicIntArray)] unitType []]
+
+rootAndDependencyPlan :: Either MonomorphizationError MonomorphizationPlan
+rootAndDependencyPlan =
+    planCoreMonomorphization
+        (moduleWith [functionWith [(localName, nestedArray), (secondName, dynamicIntArray)] unitType []])
+
+repeatedChildPlan :: Either MonomorphizationError MonomorphizationPlan
+repeatedChildPlan =
+    planCoreMonomorphization
+        ( moduleWith
+            [functionWith [(localName, named "Pair" [typeArgument dynamicIntArray, typeArgument dynamicIntArray])] unitType []]
+        )
+
+multiPlan :: Either MonomorphizationError MonomorphizationPlan
+multiPlan = planCoreMonomorphization multiModule
+
+multiModule :: CoreModule
+multiModule =
+    moduleWith
+        [functionWith [(localName, fixedIntArray 1), (secondName, fixedIntArray 2), (thirdName, fixedIntArray 3)] unitType []]
+
+orderedFunctionsModule :: CoreModule
+orderedFunctionsModule =
+    moduleWith
+        [ CoreFunction functionName [(localName, fixedIntArray 1)] unitType []
+        , CoreFunction secondFunctionName [(localName, fixedIntArray 2)] unitType []
+        ]
+
+orderedStatementsModule :: CoreModule
+orderedStatementsModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [CoreEvaluate (CoreLiteral CoreUnit (fixedIntArray 1)), CoreEvaluate (CoreLiteral CoreUnit (fixedIntArray 2))]
+        ]
+
+orderedArgumentsModule :: CoreModule
+orderedArgumentsModule =
+    moduleWith
+        [ functionWith
+            []
+            unitType
+            [ CoreEvaluate
+                ( CoreApply
+                    (CoreVariable calleeName (FunctionType [fixedIntArray 1, fixedIntArray 2] unitType))
+                    [CoreLiteral CoreUnit (fixedIntArray 1), CoreLiteral CoreUnit (fixedIntArray 2)]
+                    unitType
+                )
+            ]
+        ]
+
+valueKindModule :: CoreModule
+valueKindModule =
+    moduleWith
+        [ functionWith
+            [(localName, named "Code" [ValueTemplateArgument (IntegerTemplateValue 65)])]
+            (named "Code" [ValueTemplateArgument (CharacterTemplateValue 65)])
+            []
+        ]
+
+dynamicLiteral :: CoreExpression
+dynamicLiteral = CoreLiteral (CoreString "dynamic") dynamicIntArray
+
+fixedLiteral :: CoreExpression
+fixedLiteral = CoreLiteral (CoreString "fixed") (fixedIntArray 4)
+
+dynamicIntArray :: Type
+dynamicIntArray = namedQualified ["System", "Array"] [typeArgument intType]
+
+fixedIntArray :: Integer -> Type
+fixedIntArray size =
+    namedQualified
+        ["System", "Array"]
+        [typeArgument intType, ValueTemplateArgument (IntegerTemplateValue size)]
+
+builtinIntArray :: Type
+builtinIntArray = named "[]" [typeArgument intType]
+
+nestedArray :: Type
+nestedArray = namedQualified ["System", "Array"] [typeArgument dynamicIntArray]
+
+deepArray :: Type
+deepArray = foldr (\_ nested -> named "Box" [typeArgument nested]) dynamicIntArray [1 :: Int .. 3]
+
+pairType :: Type
+pairType = named "Pair" [typeArgument dynamicIntArray, typeArgument (fixedIntArray 4)]
+
+openBox :: Type
+openBox = named "Box" [typeArgument (TypeVariable typeParameter)]
+
+openArray :: Type
+openArray =
+    namedQualified
+        ["System", "Array"]
+        [typeArgument intType, ValueTemplateArgument (TemplateValueParameter valueParameter)]
+
+malformedArray :: Type
+malformedArray = namedQualified ["System", "Array"] [ValueTemplateArgument (IntegerTemplateValue 4)]
+
+characterBox :: Integer -> Type
+characterBox scalar = named "Character" [ValueTemplateArgument (CharacterTemplateValue scalar)]
+
+typeArgument :: Type -> TemplateArgument
+typeArgument = TypeTemplateArgument
+
+named :: String -> [TemplateArgument] -> Type
+named value = namedQualified [value]
+
+namedQualified :: [String] -> [TemplateArgument] -> Type
+namedQualified parts = NamedType (QualifiedName (map Identifier parts))
+
+functionName
+    , secondFunctionName
+    , localName
+    , secondName
+    , thirdName
+    , calleeName
+    , capturedValueName
+    , closureParameterName ::
+        ResolvedName
+functionName = resolved 1 "Run"
+secondFunctionName = resolved 2 "Next"
+localName = resolved 10 "value"
+secondName = resolved 11 "second"
+thirdName = resolved 12 "third"
+calleeName = resolved 20 "callee"
+capturedValueName = resolved 21 "captured"
+closureParameterName = resolved 22 "argument"
+
+typeParameter, valueParameter :: ResolvedName
+typeParameter = resolved 80 "T"
+valueParameter = resolved 81 "N"
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+rightPlan :: Either MonomorphizationError MonomorphizationPlan -> MonomorphizationPlan
+rightPlan result = case result of
+    Right plan -> plan
+    Left _ -> MonomorphizationPlan [] emptyMonomorphizationStatistics
+
+scalarSource :: String
+scalarSource =
+    unlines
+        [ "namespace Tests;"
+        , "class Program {"
+        , "    public static void Main() {"
+        , "        return;"
+        , "    }"
+        , "}"
+        ]
+
+fixedArraySource :: String
+fixedArraySource =
+    unlines
+        [ "namespace Tests;"
+        , "class Program {"
+        , "    public static void Consume(_ [int; 4] values) {"
+        , "        return;"
+        , "    }"
+        , "}"
+        ]
+
+arrayOverloadSource :: String
+arrayOverloadSource =
+    unlines
+        [ "namespace Tests;"
+        , "class Program {"
+        , "    public static void Consume(_ [int] dynamicValues, _ [int; 4] fixedValues) {"
+        , "        return;"
+        , "    }"
+        , "}"
+        ]
+ test/NestingLimitTests.hs view
@@ -0,0 +1,353 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for the nesting limits of "Progmasoft.Tala.NestingLimits".
+
+Every construct that holds statements or operands is nested to exactly the
+limit, which must be accepted, and to one level more, which must be rejected
+with the diagnostic of that limit at the first node that is too deep. The
+accepted programs are also lowered and run, on the unoptimized and the
+optimized Core, so the limit is known to be a depth the frontend handles and
+not only one it does not reject. Chains that are not nesting, an @else if@
+chain above all, are far longer than either limit and are accepted.
+-}
+module NestingLimitTests (nestingLimitTests) where
+
+import CoreInterpreter
+import Data.List (intercalate)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.NestingLimits
+
+nestingLimitTests :: [(String, Bool)]
+nestingLimitTests = limitTests ++ statementTests ++ expressionTests ++ combinedTests ++ chainTests
+
+-- ---------------------------------------------------------------- sources
+
+-- | A method whose body is the given text on line 3, starting at column 1.
+method :: String -> String
+method statements =
+    unlines
+        [ "class Program {"
+        , "public static int Evaluate(_ int value) {"
+        , statements
+        , "}"
+        , "}"
+        ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "nesting-limit.tala" text)
+
+accepted :: String -> Bool
+accepted text = either (const False) (const True) (compileSource text)
+
+-- | The codes a source is rejected with, in order, or none when it is accepted.
+codesOf :: String -> [String]
+codesOf text = either (map diagnosticCode) (const []) (compileSource text)
+
+-- | The one-based columns on line 3 at which the code is reported.
+columnsOf :: String -> String -> [Int]
+columnsOf code text = case compileSource text of
+    Right _ -> []
+    Left problems ->
+        [ sourceColumn (sourceStart spanValue)
+        | problem <- problems
+        , diagnosticCode problem == code
+        , Just spanValue <- [diagnosticSpan problem]
+        , sourceLine (sourceStart spanValue) == 3
+        ]
+
+-- | The value the method returns for the argument, on both forms of Core.
+valuesOf :: String -> Integer -> [Maybe Integer]
+valuesOf text argument = case compileSource text of
+    Left _ -> [Nothing]
+    Right artifacts ->
+        [ case runFunction (select artifacts) "Evaluate" [IntegerValue argument] of
+            Just (IntegerValue result) -> Just result
+            _ -> Nothing
+        | select <- [artifactCore, artifactOptimizedCore]
+        ]
+
+-- ------------------------------------------------------------------ limits
+
+limitTests :: [(String, Bool)]
+limitTests =
+    [ ("the statement limit is 256 levels", maximumStatementNesting == 256)
+    , ("the expression limit is 1024 levels", maximumExpressionNesting == 1024)
+    ]
+
+-- -------------------------------------------------------------- statements
+
+{- | A statement nest: the opening text repeated, the innermost statement,
+and the closing text repeated. With @count@ openings the innermost statement
+is at level @count + 1@.
+-}
+nest :: Int -> String -> String -> String
+nest count open close = "int total = 0; " ++ concat (replicate count open) ++ "total += 1; " ++ concat (replicate count close)
+
+-- | The column of the innermost statement of 'nest'.
+innermostColumn :: Int -> String -> Int
+innermostColumn count open = 1 + length "int total = 0; " + count * length open
+
+-- | Constructs that put a statement one level below themselves.
+nestingConstructs :: [(String, String, String)]
+nestingConstructs =
+    [ ("if", "if (value > 0) { ", "} ")
+    , ("while", "while (total < 1) { ", "} ")
+    , ("do/while", "do { ", "} while (total < 1); ")
+    , ("for", "for (; total < 1; total += 1) { ", "} ")
+    , ("guard", "guard (total > 0) else { ", "return 0; } ")
+    , ("block", "{ ", "} ")
+    , ("match arm", "match (value) { _ -> { ", "} } ")
+    ]
+
+statementTests :: [(String, Bool)]
+statementTests =
+    concat
+        [ [ ( "a statement at level 256 inside nested " ++ name ++ " statements is accepted"
+            , accepted (method (nest atLimit open close ++ "return total;"))
+            )
+          , ( "a statement at level 257 inside nested " ++ name ++ " statements is rejected at that statement"
+            , columnsOf "TAP0039" (method (nest beyondLimit open close ++ "return total;"))
+                == [innermostColumn beyondLimit open]
+            )
+          , ( "nothing else is reported for nested " ++ name ++ " statements"
+            , codesOf (method (nest beyondLimit open close ++ "return total;")) == ["TAP0039"]
+            )
+          ]
+        | (name, open, close) <- nestingConstructs
+        ]
+        ++ [ ("256 levels of if statements compute their value", valuesOf (method (nest atLimit ifOpen "} " ++ "return total;")) 5 == [Just 1, Just 1])
+           , ("256 levels of if statements skip the innermost one", valuesOf (method (nest atLimit ifOpen "} " ++ "return total;")) 0 == [Just 0, Just 0])
+           , ("256 levels of blocks compute their value", valuesOf (method (nest atLimit "{ " "} " ++ "return total;")) 0 == [Just 1, Just 1])
+           , -- Each level of nested loops once multiplied the time of the
+             -- loop analysis, so that 50 levels did not finish. These nests
+             -- are compiled, optimized and run.
+             ("255 nested while loops compute their value", valuesOf (method (nest atLimit "while (total < 1) { " "} " ++ "return total;")) 0 == [Just 1, Just 1])
+           , ("255 nested for loops compute their value", valuesOf (method (nest atLimit "for (; total < 1; total += 1) { " "} " ++ "return total;")) 0 == [Just 256, Just 256])
+           , ("40 nested counting loops compute their value", valuesOf (method (countingLoops 40)) 1 == [Just 1, Just 1])
+           , ("3 nested counting loops compute their value", valuesOf (method (countingLoops 3)) 4 == [Just 64, Just 64])
+           , ("a statement far beyond the limit is still reported once", codesOf (method (nest 2000 ifOpen "} " ++ "return total;")) == ["TAP0039"])
+           , ("every function reports its own excess", codesOf twoDeepFunctions == ["TAP0039", "TAP0039"])
+           , ("a deep function does not hide an accepted one", codesOf oneDeepFunction == ["TAP0039"])
+           ]
+    where
+        -- The innermost statement of `count` constructs is at level count + 1.
+        atLimit = maximumStatementNesting - 1
+        beyondLimit = maximumStatementNesting
+        ifOpen = "if (value > 0) { "
+        -- Loops that each count to the argument with a counter of their
+        -- own, around one statement: the innermost statement runs
+        -- value ^ depth times.
+        countingLoops :: Int -> String
+        countingLoops depth =
+            "int total = 0; "
+                ++ concat ["for (int c" ++ show level ++ " = 0; c" ++ show level ++ " < value; c" ++ show level ++ " += 1) { " | level <- [1 .. depth]]
+                ++ "total += 1; "
+                ++ concat (replicate depth "} ")
+                ++ "return total;"
+        twoDeepFunctions =
+            unlines
+                [ "class Program {"
+                , "public static int First(_ int value) { " ++ nest beyondLimit ifOpen "} " ++ "return total; }"
+                , "public static int Second(_ int value) { " ++ nest beyondLimit ifOpen "} " ++ "return total; }"
+                , "}"
+                ]
+        oneDeepFunction =
+            unlines
+                [ "class Program {"
+                , "public static int First(_ int value) { return value; }"
+                , "public static int Second(_ int value) { " ++ nest beyondLimit ifOpen "} " ++ "return total; }"
+                , "}"
+                ]
+
+-- ------------------------------------------------------------- expressions
+
+-- | @value + value + ...@ with the given number of operands.
+sumOf :: Int -> String
+sumOf operands = intercalate " + " (replicate operands "value")
+
+-- | @value + (value + (... value))@ with the given number of additions.
+rightNested :: Int -> String
+rightNested additions = concat (replicate additions "value + (") ++ "value" ++ replicate additions ')'
+
+-- | @(((value + 1) + 1) ...)@ with the given number of additions.
+parenthesized :: Int -> String
+parenthesized additions = replicate additions '(' ++ "value" ++ concat (replicate additions " + 1)")
+
+-- | @value == 0 ? 0 : value == 1 ? 1 : ... : 7@ with the given number of tests.
+conditionalChain :: Int -> String
+conditionalChain tests = concat ["value == " ++ show index ++ " ? " ++ show index ++ " : " | index <- [0 .. tests - 1]] ++ "7"
+
+-- | @Twice(Twice(...(value)))@ with the given number of calls.
+calls :: Int -> String
+calls count = concat (replicate count "Twice(") ++ "value" ++ replicate count ')'
+
+withTwice :: String -> String
+withTwice statements =
+    unlines
+        [ "class Program {"
+        , "public static int Evaluate(_ int value) {"
+        , statements
+        , "}"
+        , "public static int Twice(_ int value) { return value + value; }"
+        , "}"
+        ]
+
+expressionTests :: [(String, Bool)]
+expressionTests =
+    [ -- A chain of a left-associative operator is not nesting: the left
+      -- operand of a binary operator is at the level of the operator, and
+      -- every stage walks the chain in a loop.
+      ("a sum of 1024 operands is accepted", accepted (method ("return " ++ sumOf limit ++ ";")))
+    , ("a sum of 1024 operands computes its value", valuesOf (method ("return " ++ sumOf limit ++ ";")) 3 == [Just 3072, Just 3072])
+    , ("a sum of 1025 operands is not nesting", codesOf (method ("return " ++ sumOf (limit + 1) ++ ";")) == [])
+    , ("a sum of 20000 operands computes its value", valuesOf (method ("return " ++ sumOf 20000 ++ ";")) 3 == [Just 60000, Just 60000])
+    , -- Parentheses around a left operand do not change the tree.
+        ( "5000 parentheses around left operands compute their value"
+        , valuesOf (method ("return " ++ parenthesized 5000 ++ ";")) 1 == [Just 5001, Just 5001]
+        )
+    , ("a chain of comparisons joined by && is not nesting", accepted (method ("return (" ++ intercalate " && " (replicate 3000 "value > 0") ++ ") ? 1 : 0;")))
+    , -- A right operand is one level below its operator: n additions that
+      -- nest to the right put the last `value` at level n + 1.
+      ("1023 additions nested to the right are accepted", accepted (method ("return " ++ rightNested (limit - 1) ++ ";")))
+    ,
+        ( "1023 additions nested to the right compute their value"
+        , valuesOf (method ("return " ++ rightNested (limit - 1) ++ ";")) 2 == [Just 2048, Just 2048]
+        )
+    ,
+        ( "1024 additions nested to the right are rejected at the innermost operand"
+        , columnsOf "TAP0040" (method ("return " ++ rightNested limit ++ ";")) == [1 + length "return " + limit * length "value + ("]
+        )
+    , ("nothing else is reported for the deep right operand", codesOf (method ("return " ++ rightNested limit ++ ";")) == ["TAP0040"])
+    , -- A long chain as a right operand costs one level, not its length.
+        ( "a long sum as a right operand is one level"
+        , accepted (method ("return " ++ rightNested 1000 ++ " + (" ++ sumOf 5000 ++ ");"))
+        )
+    , -- Each test of a conditional chain puts the rest of the chain one level deeper.
+      -- The operands of the last test are two levels below its conditional.
+      ("a conditional chain of 1022 tests is accepted", accepted (method ("return " ++ conditionalChain (limit - 2) ++ ";")))
+    , ("a conditional chain of 1022 tests selects its last result", valuesOf (method ("return " ++ conditionalChain (limit - 2) ++ ";")) 5000 == [Just 7, Just 7])
+    , ("a conditional chain of 1022 tests selects a late test", valuesOf (method ("return " ++ conditionalChain (limit - 2) ++ ";")) 1000 == [Just 1000, Just 1000])
+    , ("a conditional chain of 1023 tests is rejected", codesOf (method ("return " ++ conditionalChain (limit - 1) ++ ";")) == ["TAP0040"])
+    , -- n calls put `value` at level n + 1.
+      ("1023 nested calls are accepted", accepted (withTwice ("return " ++ calls (limit - 1) ++ ";")))
+    , ("1024 nested calls are rejected", codesOf (withTwice ("return " ++ calls limit ++ ";")) == ["TAP0040"])
+    , ("an expression far beyond the limit is reported once", codesOf (method ("return " ++ rightNested 5000 ++ ";")) == ["TAP0040"])
+    ,
+        ( "many separate expressions at the limit are accepted"
+        , accepted (method ("int a = " ++ sumOf limit ++ "; int b = " ++ sumOf limit ++ "; return a + b;"))
+        )
+    ]
+    where
+        limit = maximumExpressionNesting
+
+-- ---------------------------------------------------------------- combined
+
+combinedTests :: [(String, Bool)]
+combinedTests =
+    [ -- A value block is one statement level below the statement that holds
+      -- its expression: 255 if expressions put the innermost block at 256.
+        ( "value blocks nested to level 256 are accepted"
+        , accepted (method ("return " ++ valueBlocks (maximumStatementNesting - 1) ++ ";"))
+        )
+    ,
+        ( "value blocks nested to level 256 compute their value"
+        , valuesOf (method ("return " ++ valueBlocks (maximumStatementNesting - 1) ++ ";")) 1 == [Just 1, Just 1]
+        )
+    ,
+        ( "if expressions nested in last position need no parentheses"
+        , valuesOf (method ("return " ++ concat (replicate 100 "if (value > 0) { ") ++ "value" ++ concat (replicate 100 " } else { 0 }") ++ ";")) 4
+            == [Just 4, Just 4]
+        )
+    ,
+        ( "value blocks nested to level 257 are rejected as statements"
+        , codesOf (method ("return " ++ valueBlocks maximumStatementNesting ++ ";")) == ["TAP0039"]
+        )
+    , -- Expressions inside nested statements keep counting from the
+      -- expression around them, so the two limits bound the total depth.
+        ( "an expression split by value blocks is still limited"
+        , codesOf (method ("return " ++ nestedThroughBlocks 4 300 ++ ";")) == ["TAP0040"]
+        )
+    ,
+        ( "an expression split by value blocks below the limit is accepted"
+        , accepted (method ("return " ++ nestedThroughBlocks 4 200 ++ ";"))
+        )
+    ,
+        ( "a statement excess and an expression excess are both reported"
+        , codesOf (method (nest maximumStatementNesting "if (value > 0) { " "} " ++ "return " ++ rightNested maximumExpressionNesting ++ ";"))
+            == ["TAP0039", "TAP0040"]
+        )
+    ,
+        ( "a closure body is one statement level below its expression"
+        , codesOf (method ("auto f = " ++ concat (replicate maximumStatementNesting "\\() -> { return ") ++ "1" ++ concat (replicate maximumStatementNesting "; }") ++ "; return 0;"))
+            == ["TAP0039"]
+        )
+    ]
+    where
+        -- `if (value > 0) { (<inner>) } else { 0 }`, nested the given number
+        -- of times around `value`. The parentheses keep the inner `if` in
+        -- operand position: at the start of a statement it would be the
+        -- `if` statement.
+        valueBlocks :: Int -> String
+        valueBlocks count = concat (replicate count "if (value > 0) { (") ++ "value" ++ concat (replicate count ") } else { 0 }")
+        -- Nested if expressions whose blocks each hold additions that
+        -- nest to the right, the innermost operand of which is the next
+        -- if expression: the deepest operand is below every addition and
+        -- every block around it.
+        nestedThroughBlocks :: Int -> Int -> String
+        nestedThroughBlocks blocks additions =
+            concat (replicate blocks ("if (value > 0) { " ++ concat (replicate additions "value + (")))
+                ++ "value"
+                ++ concat (replicate blocks (replicate additions ')' ++ " } else { 0 }"))
+
+-- ------------------------------------------------------------------ chains
+
+-- | An else-if chain: link @index@ returns @index * 3 + 1@.
+chain :: Int -> String
+chain links =
+    concat
+        [ (if index == 0 then "if" else " else if") ++ " (value == " ++ show index ++ ") { return " ++ show (index * 3 + 1) ++ "; }"
+        | index <- [0 .. links - 1]
+        ]
+        ++ " return 0;"
+
+chainTests :: [(String, Bool)]
+chainTests =
+    [ -- The links of an else-if chain are all at the level of the first if.
+      ("an else-if chain of 1000 links is not nesting", accepted (method (chain 1000)))
+    , ("an else-if chain of 1000 links selects its last link", valuesOf (method (chain 1000)) 999 == [Just 2998, Just 2998])
+    , ("an else-if chain of 1000 links falls through", valuesOf (method (chain 1000)) 1000 == [Just 0, Just 0])
+    , -- The body of every link is one level below the chain, whichever link it belongs to.
+        ( "the bodies of a chain at level 255 are accepted in every link"
+        , accepted (method (around (maximumStatementNesting - 2) threeLinks))
+        )
+    ,
+        ( "the bodies of a chain at level 256 are rejected once"
+        , codesOf (method (around (maximumStatementNesting - 1) threeLinks)) == ["TAP0039"]
+        )
+    , -- An else block that holds exactly one if is the same tree as `else if`.
+      ("else blocks that hold only an if form a chain", accepted (method (nest 600 "if (value < 0) { } else { " "} " ++ "return total;")))
+    , -- The body of an arm is one level below its match, however many arms it has.
+        ( "the arms of a wide match are one level below it"
+        , accepted (method (around (maximumStatementNesting - 2) wideArms))
+            && codesOf (method (around (maximumStatementNesting - 1) wideArms)) == ["TAP0039"]
+        )
+    ,
+        ( "the arms of a narrow match are one level below it"
+        , accepted (method (around (maximumStatementNesting - 2) narrowArms))
+            && codesOf (method (around (maximumStatementNesting - 1) narrowArms)) == ["TAP0039"]
+        )
+    , ("a statement match of 300 arms is not nesting", accepted (method ("match (value) { " ++ concat [show index ++ " -> { return " ++ show index ++ "; }, " | index <- [0 .. 298 :: Int]] ++ "_ -> { return 0; } } return 1;")))
+    ]
+    where
+        -- Twenty arms with block bodies.
+        wideArms = "match (value) { " ++ concat [show index ++ " -> { total = " ++ show index ++ "; }, " | index <- [0 .. 18 :: Int]] ++ "_ -> { total = 0; } } "
+        -- Three arms with block bodies.
+        narrowArms = "match (value) { 1 -> { total = 1; }, 2 -> { total = 2; }, _ -> { total = 0; } } "
+        threeLinks = "if (value == 1) { total = 1; } else if (value == 2) { total = 2; } else if (value == 3) { total = 3; } "
+        -- The statement inside the given number of if statements, at level count + 1.
+        around :: Int -> String -> String
+        around count statement =
+            "int total = 0; " ++ concat (replicate count "if (value >= 0) { ") ++ statement ++ concat (replicate count "} ") ++ "return total;"
+ test/NumericTests.hs view
@@ -0,0 +1,922 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module NumericTests (numericTests) where
+
+import Data.Bits (shiftL)
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.CharacterLiteral
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.ConstantEvaluation
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.Scalar
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.FloatingLiteral
+import Progmasoft.Tala.IntegerEvaluation
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.NumericLiteral
+import Progmasoft.Tala.NumericSemantics
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.TemplateValue (TemplateValueError (..), evaluateTemplateValue)
+
+numericTests :: [(String, Bool)]
+numericTests =
+    lexicalAcceptanceTests
+        ++ lexicalRejectionTests
+        ++ parserValueTests
+        ++ characterLiteralTests
+        ++ floatingLiteralTests
+        ++ scalarMetadataTests
+        ++ coreScalarCatalogTests
+        ++ targetBoundaryTests
+        ++ compileTimeIntegerTests
+        ++ evaluatorParityTests
+        ++ numericContextTests
+        ++ corePropagationTests
+        ++ integerPipelineTests
+        ++ semanticRuleTests
+
+lexicalAcceptanceTests :: [(String, Bool)]
+lexicalAcceptanceTests =
+    [ accepts "decimal zero" "0" IntegerToken
+    , accepts "decimal digits" "26" IntegerToken
+    , accepts "decimal separator" "1'000'000" IntegerToken
+    , accepts "hexadecimal lowercase prefix" "0xff" IntegerToken
+    , accepts "hexadecimal uppercase prefix" "0XFF" IntegerToken
+    , accepts "hexadecimal separator" "0xFF'EE" IntegerToken
+    , accepts "binary lowercase prefix" "0b1010" IntegerToken
+    , accepts "binary uppercase prefix" "0B1010" IntegerToken
+    , accepts "binary separator" "0b1010'1100" IntegerToken
+    , accepts "maximum u8 spelling" "255" IntegerToken
+    , accepts "maximum i16 spelling" "32'767" IntegerToken
+    , accepts "maximum u16 spelling" "65'535" IntegerToken
+    , accepts "maximum i32 spelling" "2'147'483'647" IntegerToken
+    , accepts "maximum u32 spelling" "4'294'967'295" IntegerToken
+    , accepts "maximum i64 spelling" "9'223'372'036'854'775'807" IntegerToken
+    , accepts "maximum u64 spelling" "18'446'744'073'709'551'615" IntegerToken
+    , accepts "maximum i128 spelling" "170'141'183'460'469'231'731'687'303'715'884'105'727" IntegerToken
+    , accepts "maximum u128 spelling" "340'282'366'920'938'463'463'374'607'431'768'211'455" IntegerToken
+    ]
+
+lexicalRejectionTests :: [(String, Bool)]
+lexicalRejectionTests =
+    [ rejectsLexically "octal lowercase prefix" "0o377" "octal"
+    , rejectsLexically "octal uppercase prefix" "0O377" "octal"
+    , rejectsLexically "hexadecimal without digits" "0x" "at least one digit"
+    , rejectsLexically "binary without digits" "0b" "at least one digit"
+    , rejectsLexically "binary digit two" "0b2" "not a valid binary digit"
+    , rejectsLexically "binary hexadecimal tail" "0bA" "not a valid binary digit"
+    , rejectsLexically "hexadecimal letter G" "0xG" "not a valid hexadecimal digit"
+    , rejectsLexically "decimal identifier tail" "123abc" "not a valid decimal digit"
+    , rejectsLexically "leading radix separator" "0x'FF" "cannot begin"
+    , rejectsLexically "trailing radix separator" "0b1010'" "cannot end"
+    , rejectsLexically "consecutive decimal separators" "1''000" "consecutive"
+    , rejectsLexically "consecutive hexadecimal separators" "0xAB''CD" "consecutive"
+    , rejectsLexically "underscore is not a separator" "1_000" "not a valid decimal digit"
+    , rejectsLexically "separator before invalid digit" "0b1'2" "between two valid binary digits"
+    , rejectsLexically "separator after invalid digit" "0xG'0" "between two valid hexadecimal digits"
+    ]
+
+parserValueTests :: [(String, Bool)]
+parserValueTests =
+    [ parsedValue "decimal parser value" "26" 26
+    , parsedValue "separated decimal parser value" "1'000'000" 1000000
+    , parsedValue "hexadecimal parser value" "0xFF" 255
+    , parsedValue "separated hexadecimal parser value" "0xFF'EE" 65518
+    , parsedValue "binary parser value" "0b1010" 10
+    , parsedValue "separated binary parser value" "0b1010'1100" 172
+    , directParse "direct decimal radix" "12'345" DecimalRadix 12345
+    , directParse "direct hexadecimal radix" "0xCA'FE" HexadecimalRadix 51966
+    , directParse "direct binary radix" "0b1101'0010" BinaryRadix 210
+    ]
+
+characterLiteralTests :: [(String, Bool)]
+characterLiteralTests =
+    [ characterValue "single ASCII character" "'A'" 0x00000041
+    , characterValue "two packed characters" "'AB'" 0x00004142
+    , characterValue "three packed characters" "'ABC'" 0x00414243
+    , characterValue "four packed characters" "'ABCD'" 0x41424344
+    , characterValue "escaped newline participates in packing" "'A\\n'" 0x0000410a
+    , characterValue "escaped quote" "'\\\''" 0x27
+    , characterValue "escaped double quote" "'\\\"'" 0x22
+    , characterValue "escaped backslash" "'\\\\'" 0x5c
+    , characterValue "escaped null" "'\\0'" 0x00
+    , characterValue "escaped alert" "'\\a'" 0x07
+    , characterValue "escaped backspace" "'\\b'" 0x08
+    , characterValue "escaped escape" "'\\e'" 0x1b
+    , characterValue "escaped form feed" "'\\f'" 0x0c
+    , characterValue "escaped carriage return" "'\\r'" 0x0d
+    , characterValue "escaped horizontal tab" "'\\t'" 0x09
+    , characterValue "escaped vertical tab" "'\\v'" 0x0b
+    , characterValue "variable hexadecimal escape" "'\\x41'" 0x41
+    , characterValue "four digit Unicode escape" "'\\u0041'" 0x41
+    , characterValue "eight digit Unicode escape" "'\\U00000041'" 0x41
+    , characterValue "Unicode scalar occupies required bytes" "'λ'" 0x03bb
+    , characterFailure "empty character literal" "''" "at least one"
+    , characterFailure "five ASCII values overflow u32" "'ABCDE'" "does not fit u32"
+    , characterFailure "unsupported escape" "'\\q'" "unsupported character escape"
+    , characterFailure "hexadecimal escape requires digits" "'\\x'" "requires at least one digit"
+    , characterFailure "short u escape" "'\\u041'" "requires exactly 4 digits"
+    , characterFailure "short U escape" "'\\U000041'" "requires exactly 8 digits"
+    , characterFailure "surrogate escape" "'\\uD800'" "not a Unicode scalar"
+    , characterFailure "out of range scalar" "'\\U00110000'" "not a Unicode scalar"
+    , characterLexFailure "unterminated character literal" "'A" "unterminated"
+    , characterLexFailure "physical newline character literal" "'A\nB'" "line break"
+    , compiles "character literal targets char" "char letter = 'A';"
+    , rejectedWith "character literal does not target int" "int letter = 'A';" "TAT0002"
+    , ("character literal reaches Core as u32 payload", coreCharacterValue)
+    ]
+
+floatingLiteralTests :: [(String, Bool)]
+floatingLiteralTests =
+    [ floatingValue "decimal fraction" "1.5" "1.5"
+    , floatingValue "lowercase scientific notation" "1e3" "1e3"
+    , floatingValue "fraction with exponent" "1.5e2" "1.5e2"
+    , floatingValue "uppercase signed exponent" "2E-4" "2E-4"
+    , floatingValue "positive exponent sign" "2E+4" "2E+4"
+    , floatingValue "separated integer and fraction" "1'000.250'000" "1000.250000"
+    , floatingFailure "fraction requires right digits" "1." "both sides"
+    , floatingFailure "fraction requires left digits" ".5" "both sides"
+    , floatingFailure "exponent requires digits" "1e" "requires decimal digits"
+    , floatingFailure "signed exponent requires digits" "1e-" "requires decimal digits"
+    , floatingFailure "multiple decimal points" "1.2.3" "more than one decimal point"
+    , floatingFailure "multiple exponent markers" "1e2e3" "more than one exponent"
+    , floatingFailure "separator cannot touch decimal point on left" "1'.5" "between two digits"
+    , floatingFailure "separator cannot touch decimal point on right" "1.'5" "between two digits"
+    , floatingFailure "separator is forbidden in exponent" "1.5e1'00" "not allowed"
+    , floatingFailure "fundamental suffixes are forbidden" "1.5f" "suffixes are not supported"
+    , compiles "untargeted floating literal defaults to float" "auto value = 1.5;"
+    , compiles "sfloat target selects f16 semantics" "sfloat value = 1.5;"
+    , compiles "lfloat target selects f32 semantics" "lfloat value = 1.5;"
+    , compiles "float target selects f64 semantics" "float value = 1.5;"
+    , compiles "double target selects f128 semantics" "double value = 1.5;"
+    , rejectedWith "integer target rejects floating literal" "int value = 1.5;" "TAT0002"
+    , ("floating spelling reaches Core without host rounding", coreFloatingSpelling)
+    ]
+
+scalarMetadataTests :: [(String, Bool)]
+scalarMetadataTests =
+    [ scalarMetadata "char is u32 storage" CharacterScalar CharacterFamily 32 False
+    , scalarMetadata "bool is u8 storage" BooleanScalar BooleanFamily 8 False
+    , scalarMetadata "byte is i8" ByteScalar SignedIntegerFamily 8 True
+    , scalarMetadata "short is i16" ShortScalar SignedIntegerFamily 16 True
+    , scalarMetadata "long is i32" LongScalar SignedIntegerFamily 32 True
+    , scalarMetadata "int is i64" IntScalar SignedIntegerFamily 64 True
+    , scalarMetadata "longint is i128" LongIntScalar SignedIntegerFamily 128 True
+    , scalarMetadata "ubyte is u8" UByteScalar UnsignedIntegerFamily 8 False
+    , scalarMetadata "ushort is u16" UShortScalar UnsignedIntegerFamily 16 False
+    , scalarMetadata "ulong is u32" ULongScalar UnsignedIntegerFamily 32 False
+    , scalarMetadata "uint is u64" UIntScalar UnsignedIntegerFamily 64 False
+    , scalarMetadata "ulongint is u128" ULongIntScalar UnsignedIntegerFamily 128 False
+    , scalarMetadata "sfloat is f16" SFloatScalar FloatingFamily 16 False
+    , scalarMetadata "lfloat is f32" LFloatScalar FloatingFamily 32 False
+    , scalarMetadata "float is f64" FloatScalar FloatingFamily 64 False
+    , scalarMetadata "double is f128" DoubleScalar FloatingFamily 128 False
+    , ("default integer is int", defaultIntegerScalar == IntScalar)
+    , ("default floating point is float", defaultFloatingScalar == FloatScalar)
+    , ("every scalar name round-trips through Type", all scalarRoundTrips scalarTypes)
+    , ("every wrapper name round-trips to its scalar", all wrapperRoundTrips scalarTypes)
+    , ("signed width rank selects int over long", widerScalarType LongScalar IntScalar == Just IntScalar)
+    , ("unsigned width rank selects ulongint", widerScalarType UIntScalar ULongIntScalar == Just ULongIntScalar)
+    , ("floating width rank selects double", widerScalarType SFloatScalar DoubleScalar == Just DoubleScalar)
+    , ("different scalar families have no wider common scalar", widerScalarType IntScalar UIntScalar == Nothing)
+    ]
+
+coreScalarCatalogTests :: [(String, Bool)]
+coreScalarCatalogTests =
+    [
+        ( "Core optimizer integer catalog matches frontend scalar order"
+        , coreIntegerTypeNames == map scalarTypeName integerCoreScalars
+        )
+    , ("Core optimizer integer widths match frontend scalar metadata", all widthMatches integerCoreScalars)
+    , ("Core optimizer integer signedness matches frontend scalar metadata", all signednessMatches integerCoreScalars)
+    ,
+        ( "Core optimizer floating catalog matches frontend floating names"
+        , coreFloatingTypeNames == map scalarTypeName floatingCoreScalars
+        )
+    , ("Boolean is not classified as an integer by Core", not (isCoreIntegerType boolType))
+    ]
+    where
+        integerCoreScalars = filter ((`elem` [CharacterFamily, SignedIntegerFamily, UnsignedIntegerFamily]) . scalarTypeFamily) scalarTypes
+        floatingCoreScalars = filter ((== FloatingFamily) . scalarTypeFamily) scalarTypes
+        widthMatches scalar = coreIntegerBitWidth (scalarTypeToType scalar) == Just (scalarTypeWidth scalar)
+        signednessMatches scalar = coreIntegerIsSigned (scalarTypeToType scalar) == Just (scalarTypeSigned scalar)
+
+targetBoundaryTests :: [(String, Bool)]
+targetBoundaryTests =
+    concat
+        [ signedBoundary "byte" ByteScalar (-128) 127
+        , signedBoundary "short" ShortScalar (-32768) 32767
+        , signedBoundary "long" LongScalar (-2147483648) 2147483647
+        , signedBoundary "int" IntScalar (-9223372036854775808) 9223372036854775807
+        , signedBoundary
+            "longint"
+            LongIntScalar
+            (-170141183460469231731687303715884105728)
+            170141183460469231731687303715884105727
+        , unsignedBoundary "ubyte" UByteScalar 255
+        , unsignedBoundary "ushort" UShortScalar 65535
+        , unsignedBoundary "ulong" ULongScalar 4294967295
+        , unsignedBoundary "uint" UIntScalar 18446744073709551615
+        , unsignedBoundary "ulongint" ULongIntScalar 340282366920938463463374607431768211455
+        ]
+        ++ [ compiles "targeted byte literal" "byte value = 127;"
+           , rejectedWith "byte overflow diagnostic" "byte value = 128;" "TAT0016"
+           , compiles "targeted unsigned literal" "ubyte value = 255;"
+           , rejectedWith "unsigned overflow diagnostic" "ubyte value = 256;" "TAT0016"
+           , rejectedWith "unsigned negative diagnostic" "ubyte value = -1;" "TAT0011"
+           , compiles "targeted longint beyond int" "longint value = 9'223'372'036'854'775'808;"
+           , rejectedWith
+                "un-targeted literal never widens silently"
+                "auto value = 9'223'372'036'854'775'808;"
+                "TAT0017"
+           , compiles "constant expression fits byte target" "byte value = 100 + 27;"
+           , rejectedWith "constant expression overflow is diagnosed" "byte value = 100 + 28;" "TAT0018"
+           , rejectedWith "constant division by zero is diagnosed" "int value = 1 / 0;" "TAT0019"
+           , rejectedWith "constant floor division by zero is diagnosed" "int value = 1 // 0;" "TAT0019"
+           , rejectedWith "constant remainder by zero is diagnosed" "int value = 1 % 0;" "TAT0019"
+           , compiles "integer power is accepted" "int value = 2 ** 8;"
+           , compiles "integer shift is accepted" "int value = 1 << 8;"
+           , rejectedWith "integer power evaluation is bounded" "int value = 2 ** 9223372036854775807;" "TAT0019"
+           , rejectedWith "integer left shift evaluation is bounded" "int value = 1 << 9223372036854775807;" "TAT0019"
+           , compiles "zero power remains bounded for a huge exponent" "int value = 0 ** 9223372036854775807;"
+           , compiles "one power remains bounded for a huge exponent" "int value = 1 ** 9223372036854775807;"
+           , compiles "negative unit power uses parity for a huge exponent" "int value = (-1) ** 9223372036854775807;"
+           , compiles "zero left shift remains bounded for a huge count" "int value = 0 << 9223372036854775807;"
+           , compiles "right shift by a huge count saturates positive values" "int value = 5 >> 9223372036854775807;"
+           , compiles "right shift by a huge count sign-extends negative values" "int value = -5 >> 9223372036854775807;"
+           , rejectedWith "left shift beyond the compile-time magnitude limit is diagnosed" "int value = 1 << 65536;" "TAT0019"
+           , rejectedWith "power beyond the compile-time magnitude limit is diagnosed" "int value = 2 ** 65536;" "TAT0019"
+           , compiles "zero left shift avoids constructing an over-limit result" "int value = 0 << 65536;"
+           , compiles "large right shift avoids constructing an intermediate" "int value = 1 >> 65536;"
+           , compiles "integer bitwise expression is accepted" "int value = !0 & 255 | 4 ^ 1;"
+           , compiles "unsigned bitwise complement uses the selected width" "ubyte value = !0;"
+           , compiles "16-bit unsigned complement uses its selected width" "ushort value = !0;"
+           , compiles "32-bit unsigned complement uses its selected width" "ulong value = !0;"
+           , compiles "64-bit unsigned complement uses its selected width" "uint value = !0;"
+           , compiles "128-bit unsigned bitwise complement uses the selected width" "ulongint value = !0;"
+           , compiles "unsigned complement masks the operand's top bit" "ubyte value = !255;"
+           , rejectedWith "bitwise not rejects floating point" "float value = !1.0;" "TAT0011"
+           , rejectedWith "bitwise binary rejects floating point" "float value = 1.0 & 2.0;" "TAT0012"
+           ]
+
+compileTimeIntegerTests :: [(String, Bool)]
+compileTimeIntegerTests =
+    integerEvaluationTests
+        ++ constantExpressionTests
+        ++ sourceRangeMatrix
+        ++ sourceShiftRangeMatrix
+
+integerEvaluationTests :: [(String, Bool)]
+integerEvaluationTests =
+    [
+        ( "compile-time integer cap accepts its positive edge"
+        , checkCompileTimeInteger (compileTimeMagnitude - 1) == Right (compileTimeMagnitude - 1)
+        )
+    ,
+        ( "compile-time integer cap accepts its negative edge"
+        , checkCompileTimeInteger (1 - compileTimeMagnitude) == Right (1 - compileTimeMagnitude)
+        )
+    ,
+        ( "compile-time integer cap rejects positive overflow"
+        , checkCompileTimeInteger compileTimeMagnitude == Left CompileTimeIntegerLimitExceeded
+        )
+    ,
+        ( "compile-time integer cap rejects negative overflow"
+        , checkCompileTimeInteger (negate compileTimeMagnitude) == Left CompileTimeIntegerLimitExceeded
+        )
+    , ("zero power bypasses an enormous exponent", evaluateCompileTimePower 0 enormousExponent == Right 0)
+    , ("unit power bypasses an enormous exponent", evaluateCompileTimePower 1 enormousExponent == Right 1)
+    , ("negative unit power uses exponent parity", evaluateCompileTimePower (-1) enormousExponent == Right (-1))
+    ,
+        ( "integer power stops at the largest permitted intermediate"
+        , evaluateCompileTimePower 2 65535 == Right (compileTimeMagnitude `quot` 2)
+        )
+    ,
+        ( "integer power rejects its first out-of-limit intermediate"
+        , evaluateCompileTimePower 2 65536 == Left CompileTimeIntegerLimitExceeded
+        )
+    ,
+        ( "integer power reports a negative exponent distinctly"
+        , evaluateCompileTimePower 2 (-1) == Left CompileTimeNegativeExponent
+        )
+    ,
+        ( "bounded multiplication accepts a product below the bit ceiling"
+        , multiplyCompileTimeIntegers (2 ^ (32767 :: Int)) (2 ^ (32767 :: Int)) == Right (2 ^ (65534 :: Int))
+        )
+    ,
+        ( "bounded multiplication accepts the largest represented bit position"
+        , multiplyCompileTimeIntegers (compileTimeMagnitude `quot` 2) 1 == Right (compileTimeMagnitude `quot` 2)
+        )
+    ,
+        ( "bounded multiplication rejects the first out-of-range exact power"
+        , multiplyCompileTimeIntegers (compileTimeMagnitude `quot` 2) 2 == Left CompileTimeIntegerLimitExceeded
+        )
+    ,
+        ( "bounded multiplication rejects products beyond the bit ceiling"
+        , multiplyCompileTimeIntegers (compileTimeMagnitude `quot` 2) 3 == Left CompileTimeIntegerLimitExceeded
+        )
+    ,
+        ( "bounded multiplication handles zero without a large product"
+        , multiplyCompileTimeIntegers 0 (compileTimeMagnitude - 1) == Right 0
+        )
+    , ("zero left shift ignores an enormous shift count", evaluateCompileTimeShiftLeft 0 enormousExponent == Right 0)
+    ,
+        ( "left shift reaches but does not exceed the compile-time width"
+        , evaluateCompileTimeShiftLeft 1 65535 == Right (compileTimeMagnitude `quot` 2)
+        )
+    ,
+        ( "left shift rejects a result beyond the compile-time width"
+        , evaluateCompileTimeShiftLeft 1 65536 == Left CompileTimeIntegerLimitExceeded
+        )
+    , ("positive arithmetic right shift saturates to zero", evaluateCompileTimeShiftRight 3 enormousExponent == Right 0)
+    ,
+        ( "negative arithmetic right shift saturates to minus one"
+        , evaluateCompileTimeShiftRight (-3) enormousExponent == Right (-1)
+        )
+    , ("negative shift count reverses left shift direction", evaluateCompileTimeShiftLeft (-12) (-2) == Right (-3))
+    , ("negative shift count reverses right shift direction", evaluateCompileTimeShiftRight (-12) (-2) == Right (-48))
+    ,
+        ( "reversing a huge right shift is rejected before allocation"
+        , evaluateCompileTimeShiftRight 1 (negate enormousExponent) == Left CompileTimeIntegerLimitExceeded
+        )
+    ]
+
+constantExpressionTests :: [(String, Bool)]
+constantExpressionTests =
+    [
+        ( "unsigned constant complement masks to its annotated width"
+        , evaluateConstantInteger unsignedComplement == Right (Just 0)
+        )
+    ,
+        ( "signed constant complement reports its compile-time limit"
+        , evaluateConstantInteger signedComplementAtLimit == Left ConstantEvaluationLimitExceeded
+        )
+    , ("a runtime name remains non-constant", evaluateConstantInteger runtimeName == Right Nothing)
+    ,
+        ( "a call expression remains non-constant without a pattern-match failure"
+        , evaluateConstantInteger runtimeCall == Right Nothing
+        )
+    ,
+        ( "constant arithmetic limit is rendered for diagnostics"
+        , renderConstantIntegerError ConstantEvaluationLimitExceeded
+            == "constant integer expression exceeds the compile-time evaluation limit"
+        )
+    ]
+    where
+        unsignedComplement = UnaryExpression testSpan BitwiseNot (integerExpression 255 (namedType "ubyte")) (namedType "ubyte")
+        signedComplementAtLimit = UnaryExpression testSpan BitwiseNot (integerExpression (compileTimeMagnitude - 1) intType) intType
+        runtimeName = NameExpression testSpan (ResolvedName (SymbolId 1) (Identifier "runtimeValue")) intType
+        runtimeCall = CallExpression testSpan runtimeName [] intType
+
+sourceRangeMatrix :: [(String, Bool)]
+sourceRangeMatrix = concatMap scalarRangeCases integerScalars
+    where
+        integerScalars = filter isRangeChecked scalarTypes
+        isRangeChecked scalar = scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily]
+        scalarRangeCases scalar = case (integerMinimum scalar, integerMaximum scalar) of
+            (Just minimumValue, Just maximumValue) ->
+                let typeName = scalarTypeName scalar
+                    literal expression = typeName ++ " value = " ++ expression ++ ";"
+                 in [ compiles (typeName ++ " constant lower edge remains in range") (literal (show (minimumValue + 1) ++ " - 1"))
+                    , compiles (typeName ++ " constant upper edge remains in range") (literal (show (maximumValue - 1) ++ " + 1"))
+                    , compiles (typeName ++ " constant multiplication by one remains in range") (literal (show maximumValue ++ " * 1"))
+                    , rejectedWith (typeName ++ " constant lower overflow is diagnosed") (literal (show minimumValue ++ " - 1")) "TAT0018"
+                    , rejectedWith (typeName ++ " constant upper overflow is diagnosed") (literal (show maximumValue ++ " + 1")) "TAT0018"
+                    , rejectedWith
+                        (typeName ++ " constant multiplication overflow is diagnosed")
+                        (literal (show maximumValue ++ " * 2"))
+                        "TAT0018"
+                    ,
+                        ( typeName ++ " signed lower multiplication overflow is diagnosed"
+                        , scalarTypeFamily scalar /= SignedIntegerFamily
+                            || snd (rejectedWith "signed lower multiplication overflow" (literal (show minimumValue ++ " * 2")) "TAT0018")
+                        )
+                    ]
+            _ -> []
+
+-- The frontend's arbitrary-precision evaluator must still hand its exact value
+-- to the selected fixed-width type range check. This catches accidental
+-- widening or truncation at the source-to-Core boundary for every integer ABI.
+sourceShiftRangeMatrix :: [(String, Bool)]
+sourceShiftRangeMatrix = concatMap scalarShiftCases integerScalars
+    where
+        integerScalars = filter isIntegerScalar scalarTypes
+        isIntegerScalar scalar = scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily]
+        scalarShiftCases scalar = case (integerMaximum scalar, scalarTypeSigned scalar) of
+            (Just _, isSigned) ->
+                let typeName = scalarTypeName scalar
+                    width = scalarTypeWidth scalar
+                    highestInRangeShift = width - if isSigned then 2 else 1
+                    firstOutOfRangeShift = width - if isSigned then 1 else 0
+                    declaration shiftAmount = typeName ++ " value = 1 << " ++ show shiftAmount ++ ";"
+                 in [ compiles
+                        (typeName ++ " shift produces its highest in-range positive value")
+                        (declaration highestInRangeShift)
+                    , rejectedWith
+                        (typeName ++ " shift beyond its positive range is diagnosed")
+                        (declaration firstOutOfRangeShift)
+                        "TAT0018"
+                    ]
+            _ -> []
+
+compileTimeMagnitude :: Integer
+compileTimeMagnitude = 1 `shiftL` 65536
+
+enormousExponent :: Integer
+enormousExponent = 2 ^ (63 :: Int) - 1
+
+testSpan :: SourceSpan
+testSpan = SourceSpan "numeric-test.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+
+integerExpression :: Integer -> Type -> Expression ResolvedName Type
+integerExpression value valueType = LiteralExpression testSpan (IntegerLiteral value) valueType
+
+data EvaluatorOutcome
+    = EvaluatedInteger Integer
+    | EvaluatedBoolean Bool
+    | EvaluatedCharacter Integer
+    | EvaluatorDivisionByZero
+    | EvaluatorFloorDivisionByZero
+    | EvaluatorRemainderByZero
+    | EvaluatorNegativeExponent
+    | EvaluatorLimitExceeded
+    | EvaluatorNonconstant
+    | EvaluatorOtherError
+    deriving (Eq, Ord, Read, Show)
+
+evaluatorParityTests :: [(String, Bool)]
+evaluatorParityTests = binaryParityTests ++ unaryParityTests
+
+binaryParityTests :: [(String, Bool)]
+binaryParityTests = map checkOperator binaryOperators
+    where
+        binaryOperators =
+            [ Add
+            , Subtract
+            , Multiply
+            , Divide
+            , FloorDivide
+            , Remainder
+            , Power
+            , ShiftLeft
+            , ShiftRight
+            , BitwiseAnd
+            , BitwiseXor
+            , BitwiseOr
+            , LessThan
+            , LessEqual
+            , GreaterThan
+            , GreaterEqual
+            , Equal
+            , NotEqual
+            , LogicalAnd
+            , LogicalOr
+            ]
+        inputs = [-4 .. 4]
+        checkOperator operator =
+            ( "constant and template evaluators agree for " ++ show operator ++ " across small signed integer inputs"
+            , all (uncurry (sameBinaryOutcome operator)) [(left, right) | left <- inputs, right <- inputs]
+            )
+
+unaryParityTests :: [(String, Bool)]
+unaryParityTests = map checkOperator [UnaryPlus, UnaryNegate, LogicalNot, BitwiseNot]
+    where
+        checkOperator operator =
+            ( "constant and template evaluators agree for unary " ++ show operator
+            , all (sameUnaryOutcome operator) [-8 .. 8]
+            )
+
+sameBinaryOutcome :: BinaryOperator -> Integer -> Integer -> Bool
+sameBinaryOutcome operator left right = constantOutcome resultType constant == templateOutcome (evaluateTemplateValue template)
+    where
+        resultType =
+            if operator `elem` [LessThan, LessEqual, GreaterThan, GreaterEqual, Equal, NotEqual, LogicalAnd, LogicalOr]
+                then boolType
+                else intType
+        constant =
+            evaluateConstantInteger
+                (BinaryExpression testSpan operator (integerExpression left intType) (integerExpression right intType) resultType)
+        template = TemplateBinarySyntax testSpan operator (TemplateIntegerSyntax testSpan left) (TemplateIntegerSyntax testSpan right)
+
+sameUnaryOutcome :: UnaryOperator -> Integer -> Bool
+sameUnaryOutcome operator value = constantOutcome resultType constant == templateOutcome (evaluateTemplateValue template)
+    where
+        resultType = if operator == LogicalNot then boolType else intType
+        constant = evaluateConstantInteger (UnaryExpression testSpan operator (integerExpression value intType) resultType)
+        template = TemplateUnarySyntax testSpan operator (TemplateIntegerSyntax testSpan value)
+
+constantOutcome :: Type -> Either ConstantIntegerError (Maybe Integer) -> EvaluatorOutcome
+constantOutcome _ (Left issue) = case issue of
+    ConstantDivisionByZero -> EvaluatorDivisionByZero
+    ConstantFloorDivisionByZero -> EvaluatorFloorDivisionByZero
+    ConstantRemainderByZero -> EvaluatorRemainderByZero
+    ConstantNegativeExponent -> EvaluatorNegativeExponent
+    ConstantEvaluationLimitExceeded -> EvaluatorLimitExceeded
+constantOutcome _ (Right Nothing) = EvaluatorNonconstant
+constantOutcome resultType (Right (Just value))
+    | resultType == boolType = EvaluatedBoolean (value /= 0)
+    | otherwise = EvaluatedInteger value
+
+templateOutcome :: Either TemplateValueError TemplateValue -> EvaluatorOutcome
+templateOutcome result = case result of
+    Right (IntegerTemplateValue value) -> EvaluatedInteger value
+    Right (BooleanTemplateValue value) -> EvaluatedBoolean value
+    Right (CharacterTemplateValue value) -> EvaluatedCharacter value
+    Right _ -> EvaluatorOtherError
+    Left TemplateValueDivisionByZero -> EvaluatorDivisionByZero
+    Left TemplateValueFloorDivisionByZero -> EvaluatorFloorDivisionByZero
+    Left TemplateValueRemainderByZero -> EvaluatorRemainderByZero
+    Left (TemplateValueNegativeExponent _) -> EvaluatorNegativeExponent
+    Left TemplateValueEvaluationLimitExceeded -> EvaluatorLimitExceeded
+    Left _ -> EvaluatorOtherError
+
+numericContextTests :: [(String, Bool)]
+numericContextTests =
+    [ compiles "numeric zero is a valid condition" "if (0) { return; }"
+    , compiles "numeric one is a valid condition" "if (1) { return; }"
+    , compiles "numeric negative one is a valid condition" "if (-1) { return; }"
+    , compiles "integer literal targets bool" "bool enabled = 1;"
+    , compiles "integer zero targets bool" "bool disabled = 0;"
+    , rejectedWith "string is not a condition" "if (\"yes\") { return; }" "TAT0006"
+    , compiles "same-width long arithmetic" "long value = 2 + 3;"
+    , compiles "same-width byte arithmetic" "byte value = 2 + 3;"
+    , compiles "floating rounded division returns int" "int value = 7.8 // 2.0;"
+    , compiles
+        "integer rounded division keeps contextual long type"
+        "long left = 7; long right = 2; long value = left // right;"
+    , rejectedWith
+        "floating rounded division rejects mixed operand widths"
+        "float left = 7.8; lfloat right = 2.0; int value = left // right;"
+        "TAT0012"
+    , rejectedWith
+        "mixed computed widths are not implicit conversions"
+        "long left = 1; int right = 2; int sum = left + right;"
+        "TAT0012"
+    ]
+
+corePropagationTests :: [(String, Bool)]
+corePropagationTests =
+    [ ("targeted long literal reaches Core with long type", coreLiteralHasType "long" 42)
+    , ("targeted byte literal reaches Core with byte type", coreLiteralHasType "byte" 42)
+    , ("targeted uint literal reaches Core with uint type", coreLiteralHasType "uint" 42)
+    , ("numeric bool lowering emits a Core boolean", numericBoolLowers)
+    , ("numeric branch reaches CorePrep as a canonical boolean", numericBranchCorePrep)
+    , ("mixed numeric logical operands become CorePrep booleans", numericLogicalCorePrep)
+    ]
+
+integerPipelineTests :: [(String, Bool)]
+integerPipelineTests = case compileToCorePrep compilerInput of
+    Left diagnostics -> [("integer pipeline fixture compiles (" ++ show (map diagnosticCode diagnostics) ++ ")", False)]
+    Right artifacts ->
+        let functions = coreModuleFunctions (artifactOptimizedCore artifacts)
+         in if length functions /= length pipelineCases
+                then [("integer pipeline retains every test function", False)]
+                else zipWith matchesFunction pipelineCases functions
+    where
+        integerScalars = filter ((`elem` [SignedIntegerFamily, UnsignedIntegerFamily]) . scalarTypeFamily) scalarTypes
+        pipelineCases = concatMap casesFor integerScalars
+        sourceMethods =
+            [ typeName ++ " " ++ functionName ++ "() { return " ++ expression ++ "; }"
+            | (functionName, typeName, expression, _) <- pipelineCases
+            ]
+        compilerInput = CompilerInput "integer-optimizer-pipeline.tala" ("class Numeric { " ++ unwords sourceMethods ++ " }")
+        casesFor scalar =
+            let typeName = scalarTypeName scalar
+                complementZero = if scalarTypeFamily scalar == SignedIntegerFamily then -1 else 2 ^ scalarTypeWidth scalar - 1
+             in [ ("IntegerCaseAdd" ++ typeName, typeName, "20 + 22", 42)
+                , ("IntegerCaseSubtract" ++ typeName, typeName, "50 - 8", 42)
+                , ("IntegerCaseMultiply" ++ typeName, typeName, "6 * 7", 42)
+                , ("IntegerCaseDivide" ++ typeName, typeName, "84 / 2", 42)
+                , ("IntegerCaseRoundedDivide" ++ typeName, typeName, "7 // 2", 4)
+                , ("IntegerCaseRemainder" ++ typeName, typeName, "7 % 3", 1)
+                , ("IntegerCasePower" ++ typeName, typeName, "3 ** 4", 81)
+                , ("IntegerCaseShiftLeft" ++ typeName, typeName, "3 << 4", 48)
+                , ("IntegerCaseShiftRight" ++ typeName, typeName, "48 >> 4", 3)
+                , ("IntegerCaseBitwiseAnd" ++ typeName, typeName, "14 & 11", 10)
+                , ("IntegerCaseBitwiseXor" ++ typeName, typeName, "14 ^ 11", 5)
+                , ("IntegerCaseBitwiseOr" ++ typeName, typeName, "8 | 3", 11)
+                , ("IntegerCaseBitwiseNot" ++ typeName, typeName, "!0", complementZero)
+                ]
+        matchesFunction (functionName, typeName, expression, expected) function =
+            let body = coreFunctionBody function
+                actualName = identifierText (resolvedSpelling (coreFunctionName function))
+                valueMatches = case body of
+                    [CoreReturn (CoreLiteral (CoreInteger value) resultType)] ->
+                        value == expected && resultType == namedType typeName && coreFunctionReturnType function == resultType
+                    _ -> False
+             in ( "source constant operator "
+                    ++ functionName
+                    ++ " folds "
+                    ++ expression
+                    ++ " to the typed expected value (Core: "
+                    ++ show (actualName, body)
+                    ++ ")"
+                , actualName == functionName && valueMatches
+                )
+
+semanticRuleTests :: [(String, Bool)]
+semanticRuleTests =
+    [ ruleSucceeds "int addition preserves int" intType (binaryNumericRule Add intType intType)
+    , ruleSucceeds
+        "long addition preserves long"
+        (namedType "long")
+        (binaryNumericRule Add (namedType "long") (namedType "long"))
+    , ruleSucceeds
+        "uint multiplication preserves uint"
+        (namedType "uint")
+        (binaryNumericRule Multiply (namedType "uint") (namedType "uint"))
+    , ruleSucceeds
+        "floating division preserves float"
+        (namedType "float")
+        (binaryNumericRule Divide (namedType "float") (namedType "float"))
+    , ruleSucceeds
+        "floating rounded division returns int"
+        intType
+        (binaryNumericRule FloorDivide (namedType "float") (namedType "float"))
+    , ruleSucceeds
+        "integer rounded division preserves its operand width"
+        (namedType "long")
+        (binaryNumericRule FloorDivide (namedType "long") (namedType "long"))
+    , ruleSucceeds "numeric comparison returns bool" boolType (binaryNumericRule LessThan intType intType)
+    , ruleSucceeds "integer equality returns bool" boolType (binaryNumericRule Equal intType intType)
+    , ruleSucceeds "boolean logical and returns bool" boolType (binaryNumericRule LogicalAnd boolType boolType)
+    , ruleSucceeds "numeric logical or returns bool" boolType (binaryNumericRule LogicalOr intType intType)
+    , ruleSucceeds "integer power preserves int" intType (binaryNumericRule Power intType intType)
+    , ruleSucceeds "integer shift-left preserves int" intType (binaryNumericRule ShiftLeft intType intType)
+    , ruleSucceeds "integer shift-right preserves int" intType (binaryNumericRule ShiftRight intType intType)
+    , ruleSucceeds "integer bitwise and preserves int" intType (binaryNumericRule BitwiseAnd intType intType)
+    , ruleSucceeds "integer bitwise xor preserves int" intType (binaryNumericRule BitwiseXor intType intType)
+    , ruleSucceeds "integer bitwise or preserves int" intType (binaryNumericRule BitwiseOr intType intType)
+    , ruleFails "floating shift fails" (binaryNumericRule ShiftLeft (namedType "float") (namedType "float"))
+    , ruleFails "floating bitwise operation fails" (binaryNumericRule BitwiseAnd (namedType "float") (namedType "float"))
+    , ruleFails "mixed signed widths fail" (binaryNumericRule Add (namedType "long") intType)
+    , ruleFails "mixed signedness fails" (binaryNumericRule Add intType (namedType "uint"))
+    , ruleFails "string arithmetic fails" (binaryNumericRule Add stringType stringType)
+    , ruleFails "mismatched equality fails" (binaryNumericRule Equal intType (namedType "long"))
+    , ruleSucceeds "unary plus preserves signed integer" intType (unaryNumericRule UnaryPlus intType)
+    , ruleSucceeds "unary plus preserves unsigned integer" (namedType "uint") (unaryNumericRule UnaryPlus (namedType "uint"))
+    , ruleSucceeds "unary negate preserves float" (namedType "float") (unaryNumericRule UnaryNegate (namedType "float"))
+    , ruleFails "unary negate rejects unsigned integer" (unaryNumericRule UnaryNegate (namedType "uint"))
+    , ruleSucceeds "logical not accepts integer context" boolType (unaryNumericRule LogicalNot intType)
+    , ruleFails "logical not rejects string" (unaryNumericRule LogicalNot stringType)
+    , ruleSucceeds "bitwise not preserves integer" intType (unaryNumericRule BitwiseNot intType)
+    , ruleFails "bitwise not rejects floating point" (unaryNumericRule BitwiseNot (namedType "float"))
+    , ruleSucceeds "untargeted small integer selects int" intType (integerLiteralRule NoNumericContext 42)
+    , ruleFails "untargeted oversized integer fails" (integerLiteralRule NoNumericContext 9223372036854775808)
+    , ruleSucceeds "boolean integer context selects bool" boolType (integerLiteralRule BooleanNumericContext 99)
+    , ruleSucceeds
+        "targeted ubyte selects ubyte"
+        (namedType "ubyte")
+        (integerLiteralRule (TargetNumericType (namedType "ubyte")) 255)
+    , ruleFails "targeted ubyte rejects overflow" (integerLiteralRule (TargetNumericType (namedType "ubyte")) 256)
+    , ruleSucceeds "untargeted floating selects float" (namedType "float") (floatingLiteralRule NoNumericContext)
+    , ruleSucceeds
+        "targeted double selects double"
+        (namedType "double")
+        (floatingLiteralRule (TargetNumericType (namedType "double")))
+    , ("bool accepts boolean context", acceptsBooleanContext boolType)
+    , ("integer accepts boolean context", acceptsBooleanContext intType)
+    , ("floating point accepts boolean context", acceptsBooleanContext (namedType "float"))
+    , ("string rejects boolean context", not (acceptsBooleanContext stringType))
+    ]
+
+ruleSucceeds :: String -> Type -> NumericRuleResult -> (String, Bool)
+ruleSucceeds label expected rule =
+    (label, numericRuleType rule == expected && numericRuleError rule == Nothing)
+
+ruleFails :: String -> NumericRuleResult -> (String, Bool)
+ruleFails label rule = (label, case numericRuleError rule of Just _ -> True; Nothing -> False)
+
+accepts :: String -> String -> TokenKind -> (String, Bool)
+accepts label source expectedKind =
+    (label, case lexOne source of Right [token] -> tokenKind token == expectedKind && tokenText token == source; _ -> False)
+
+rejectsLexically :: String -> String -> String -> (String, Bool)
+rejectsLexically label source fragment =
+    (label, case lexOne source of Left problems -> any (isInfixOf fragment . diagnosticMessage) problems; Right _ -> False)
+
+parsedValue :: String -> String -> Integer -> (String, Bool)
+parsedValue label spelling expected =
+    ( label
+    , case lexWithEof spelling >>= parseExpressionModule of
+        Right value -> value == expected
+        Left _ -> False
+    )
+
+directParse :: String -> String -> IntegerRadix -> Integer -> (String, Bool)
+directParse label spelling radix expected =
+    ( label
+    , case parseIntegerSpelling spelling of
+        Right parsed -> parsedIntegerRadix parsed == radix && parsedIntegerValue parsed == expected
+        Left _ -> False
+    )
+
+characterValue :: String -> String -> Integer -> (String, Bool)
+characterValue label spelling expected =
+    (label, parseCharacterLiteral spelling == Right expected)
+
+characterFailure :: String -> String -> String -> (String, Bool)
+characterFailure label spelling fragment =
+    ( label
+    , case parseCharacterLiteral spelling of
+        Left issue -> fragment `isInfixOf` renderCharacterLiteralError issue
+        Right _ -> False
+    )
+
+characterLexFailure :: String -> String -> String -> (String, Bool)
+characterLexFailure label spelling fragment =
+    ( label
+    , case lexOne spelling of Left problems -> any (isInfixOf fragment . diagnosticMessage) problems; Right _ -> False
+    )
+
+floatingValue :: String -> String -> String -> (String, Bool)
+floatingValue label spelling expected =
+    (label, validateFloatingSpelling spelling == Right expected)
+
+floatingFailure :: String -> String -> String -> (String, Bool)
+floatingFailure label spelling fragment =
+    ( label
+    , case validateFloatingSpelling spelling of
+        Left issue -> fragment `isInfixOf` renderFloatingLiteralError issue
+        Right _ -> False
+    )
+
+scalarMetadata :: String -> ScalarType -> ScalarFamily -> Int -> Bool -> (String, Bool)
+scalarMetadata label scalar family width signed =
+    ( label
+    , scalarTypeFamily scalar == family
+        && scalarTypeWidth scalar == width
+        && scalarTypeSigned scalar == signed
+    )
+
+scalarRoundTrips :: ScalarType -> Bool
+scalarRoundTrips scalar = typeToScalarType (scalarTypeToType scalar) == Just scalar
+
+wrapperRoundTrips :: ScalarType -> Bool
+wrapperRoundTrips scalar = wrapperNameToScalarType (scalarWrapperName scalar) == Just scalar
+
+signedBoundary :: String -> ScalarType -> Integer -> Integer -> [(String, Bool)]
+signedBoundary name scalar minimumValue maximumValue =
+    [ (name ++ " minimum fits", integerFits scalar minimumValue)
+    , (name ++ " maximum fits", integerFits scalar maximumValue)
+    , (name ++ " below minimum is rejected", not (integerFits scalar (minimumValue - 1)))
+    , (name ++ " above maximum is rejected", not (integerFits scalar (maximumValue + 1)))
+    , (name ++ " reported minimum is exact", integerMinimum scalar == Just minimumValue)
+    , (name ++ " reported maximum is exact", integerMaximum scalar == Just maximumValue)
+    ]
+
+unsignedBoundary :: String -> ScalarType -> Integer -> [(String, Bool)]
+unsignedBoundary name scalar maximumValue =
+    [ (name ++ " zero fits", integerFits scalar 0)
+    , (name ++ " maximum fits", integerFits scalar maximumValue)
+    , (name ++ " negative one is rejected", not (integerFits scalar (-1)))
+    , (name ++ " above maximum is rejected", not (integerFits scalar (maximumValue + 1)))
+    , (name ++ " reported minimum is zero", integerMinimum scalar == Just 0)
+    , (name ++ " reported maximum is exact", integerMaximum scalar == Just maximumValue)
+    ]
+
+compiles :: String -> String -> (String, Bool)
+compiles label body = (label, case compileBody body of Right _ -> True; Left _ -> False)
+
+rejectedWith :: String -> String -> String -> (String, Bool)
+rejectedWith label body code =
+    (label, case compileBody body of Left problems -> any ((== code) . diagnosticCode) problems; Right _ -> False)
+
+compileBody :: String -> Either [Diagnostic] FrontendArtifacts
+compileBody body = compile ("class Numeric { void Run() { " ++ body ++ " return; } }")
+
+compile :: String -> Either [Diagnostic] FrontendArtifacts
+compile source = compileToCorePrep (CompilerInput "numeric-test.tala" source)
+
+lexOne :: String -> Either [Diagnostic] [Token]
+lexOne source = do
+    tokens <- runLexer defaultLexer (LexerInput "numeric-test.tala" source)
+    pure (filter ((/= EndOfFileToken) . tokenKind) tokens)
+
+lexWithEof :: String -> Either [Diagnostic] [Token]
+lexWithEof source = runLexer defaultLexer (LexerInput "numeric-test.tala" ("class N { int V() { " ++ source ++ " } }"))
+
+parseExpressionModule :: [Token] -> Either [Diagnostic] Integer
+parseExpressionModule tokens = do
+    ParsedAST tree <- runParser defaultParser (ParserInput "numeric-test.tala" tokens)
+    case syntaxDeclarations tree of
+        [ TypeDeclaration {typeMembers = [FunctionDeclaration {declarationBody = Block [ExpressionStatement _ expression False]}]}
+            ] ->
+                case expression of
+                    LiteralExpression _ (IntegerLiteral value) _ -> Right value
+                    _ -> Left []
+        _ -> Left []
+
+coreLiteralHasType :: String -> Integer -> Bool
+coreLiteralHasType typeName expected = case compileBody (typeName ++ " value = " ++ show expected ++ ";") of
+    Right artifacts -> any matches (concatMap coreFunctionBody (coreModuleFunctions (artifactCore artifacts)))
+    Left _ -> False
+    where
+        matches (CoreBind binding) =
+            coreBindingType binding == namedType typeName
+                && case coreBindingValue binding of
+                    CoreLiteral (CoreInteger value) valueType -> value == expected && valueType == namedType typeName
+                    _ -> False
+        matches _ = False
+
+numericBoolLowers :: Bool
+numericBoolLowers = case compileBody "bool enabled = 1;" of
+    Right artifacts -> any matches (concatMap coreFunctionBody (coreModuleFunctions (artifactCore artifacts)))
+    Left _ -> False
+    where
+        matches (CoreBind binding) = case coreBindingValue binding of CoreLiteral (CoreBoolean True) valueType -> valueType == boolType; _ -> False
+        matches _ = False
+
+numericBranchCorePrep :: Bool
+numericBranchCorePrep = case prepareCore source of
+    Right prepared -> any branchIsBoolean (concatMap corePrepFunctionBlocks (corePrepModuleFunctions prepared))
+    Left _ -> False
+    where
+        parameter = ResolvedName (SymbolId 2) (Identifier "condition")
+        unit = CoreLiteral CoreUnit unitType
+        source =
+            CoreModule
+                (QualifiedName [Identifier "NumericBranch"])
+                [ CoreFunction
+                    (ResolvedName (SymbolId 1) (Identifier "Run"))
+                    [(parameter, intType)]
+                    unitType
+                    [CoreIf (CoreVariable parameter intType) [CoreReturn unit] [CoreReturn unit]]
+                ]
+        branchIsBoolean block = case corePrepBlockTerminator block of
+            CorePrepBranch condition _ _ -> corePrepAtomTypeForTest condition == boolType
+            _ -> False
+
+numericLogicalCorePrep :: Bool
+numericLogicalCorePrep = case prepareCore source of
+    Right prepared ->
+        any hasBooleanSeed instructions
+            && any hasConditional blocks
+            && any hasBooleanAssignment instructions
+            && not (any eagerLogical instructions)
+        where
+            blocks = concatMap corePrepFunctionBlocks (corePrepModuleFunctions prepared)
+            instructions = concatMap corePrepBlockInstructions blocks
+    Left _ -> False
+    where
+        left = ResolvedName (SymbolId 2) (Identifier "left")
+        right = ResolvedName (SymbolId 3) (Identifier "right")
+        source =
+            CoreModule
+                (QualifiedName [Identifier "NumericLogical"])
+                [ CoreFunction
+                    (ResolvedName (SymbolId 1) (Identifier "Evaluate"))
+                    [(left, intType), (right, namedType "float")]
+                    boolType
+                    [ CoreReturn
+                        ( CorePrimitive
+                            CoreLogicalAnd
+                            [CoreVariable left intType, CoreVariable right (namedType "float")]
+                            boolType
+                        )
+                    ]
+                ]
+        hasBooleanSeed (CorePrepBind name resultType True (CorePrepCopy (CorePrepLiteral (CoreBoolean False) literalType))) =
+            "$shortcircuit" `isPrefixOf` identifierText (resolvedSpelling name)
+                && resultType == boolType
+                && literalType == boolType
+        hasBooleanSeed _ = False
+        hasConditional block = case corePrepBlockTerminator block of
+            CorePrepBranch condition _ _ -> corePrepAtomTypeForTest condition == boolType
+            _ -> False
+        hasBooleanAssignment (CorePrepAssign name value) =
+            "$shortcircuit" `isPrefixOf` identifierText (resolvedSpelling name)
+                && corePrepAtomTypeForTest value == boolType
+        hasBooleanAssignment _ = False
+        eagerLogical (CorePrepBind _ _ _ (CorePrepPrimitive primitive _)) = primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
+        eagerLogical (CorePrepEvaluate (CorePrepPrimitive primitive _)) = primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
+        eagerLogical _ = False
+
+corePrepAtomTypeForTest :: CorePrepAtom -> Type
+corePrepAtomTypeForTest atom = case atom of
+    CorePrepVariable _ valueType -> valueType
+    CorePrepLiteral _ valueType -> valueType
+
+coreCharacterValue :: Bool
+coreCharacterValue = case compileBody "char letter = 'A';" of
+    Right artifacts -> any matches (concatMap coreFunctionBody (coreModuleFunctions (artifactCore artifacts)))
+    Left _ -> False
+    where
+        matches (CoreBind binding) = case coreBindingValue binding of
+            CoreLiteral (CoreInteger 0x41) valueType -> valueType == namedType "char"
+            _ -> False
+        matches _ = False
+
+coreFloatingSpelling :: Bool
+coreFloatingSpelling = case compileBody "double precise = 1'000.250'000;" of
+    Right artifacts -> any matches (concatMap coreFunctionBody (coreModuleFunctions (artifactCore artifacts)))
+    Left _ -> False
+    where
+        matches (CoreBind binding) = case coreBindingValue binding of
+            CoreLiteral (CoreFloating "1000.250000") valueType -> valueType == namedType "double"
+            _ -> False
+        matches _ = False
+ test/ParserContractTests.hs view
@@ -0,0 +1,651 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module ParserContractTests (parserContractTests) where
+
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+
+-- These tests exercise the public source/token boundary rather than testing
+-- individual combinators against copies of their own implementation. Diagnostic
+-- assertions include the offending source location, not just a Left result.
+parserContractTests :: [(String, Bool)]
+parserContractTests =
+    diagnosticTests
+        ++ comparisonTests
+        ++ operatorPrecedenceTests
+        ++ literalTests
+        ++ spanTests
+        ++ tokenStreamTests
+        ++ typeSyntaxTests
+        ++ pipelineTests
+        ++ booleanOperandTests
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource source = do
+    tokens <- runLexer defaultLexer (LexerInput "parser-contract.tala" source)
+    runParser defaultParser (ParserInput "parser-contract.tala" tokens)
+
+sourceWith :: String -> String
+sourceWith body = "class Program { int Value() { " ++ body ++ " } }"
+
+accepted :: String -> Bool
+accepted source = case parseSource source of
+    Right _ -> True
+    Left _ -> False
+
+rejectedAt :: String -> String -> String -> Int -> Int -> Bool
+rejectedAt source code message line column = case parseSource source of
+    Left [problem] ->
+        diagnosticStage problem == ParserStage
+            && diagnosticSeverity problem == Error
+            && diagnosticCode problem == code
+            && message `isInfixOf` diagnosticMessage problem
+            && fmap sourceStart (diagnosticSpan problem) == Just (SourcePosition line column)
+            && fmap sourceFile (diagnosticSpan problem) == Just "parser-contract.tala"
+    _ -> False
+
+-- Keep each failure on a separate line so expected columns are independent of
+-- the enclosing class spelling. Consumed optional constructs must retain their
+-- own errors: namespace, else, and capture modes are especially important.
+diagnosticTests :: [(String, Bool)]
+diagnosticTests =
+    [ bad "namespace missing name" "namespace ;" "TAP0006" "identifier" 1 11
+    , bad "namespace missing terminator" "namespace Demo class C {}" "TAP0006" ";" 1 16
+    , bad "namespace missing component" "namespace Demo.;" "TAP0006" "identifier" 1 16
+    , bad "namespace keyword is not an identifier" "namespace class;" "TAP0006" "identifier" 1 11
+    , bodyBad "return missing value" "return +;" "TAP0004" "expression" 9
+    , bodyBad "return missing semicolon" "return 1 }" "TAP0006" ";" 10
+    , bodyBad "binding missing initializer" "int value = ;" "TAP0004" "expression" 13
+    , bodyBad "binding missing equals" "int value 1;" "TAP0006" "=" 11
+    , bodyBad "binding missing semicolon" "int value = 1 }" "TAP0006" ";" 15
+    , bodyBad "final binding missing name" "final int = 1;" "TAP0006" "identifier" 11
+    , bodyBad "if missing parentheses" "if true {}" "TAP0006" "(" 4
+    , bodyBad "if missing condition" "if () {}" "TAP0004" "expression" 5
+    , bodyBad "if missing close paren" "if (true {}" "TAP0006" ")" 10
+    , bodyBad "if missing block" "if (true) return 1;" "TAP0006" "{" 11
+    , bodyBad "else missing block" "if (true) {} else return 1;" "TAP0006" "{" 19
+    , bodyBad "else if missing parentheses" "if (true) {} else if true {}" "TAP0006" "(" 22
+    , bodyBad "else if missing expression" "if (true) {} else if () {}" "TAP0004" "expression" 23
+    , bodyBad "call missing argument" "Call(,);" "TAP0004" "expression" 6
+    , bodyBad "call missing close paren" "Call(1;" "TAP0006" ")" 7
+    , bodyBad "assignment missing value" "value = ;" "TAP0004" "expression" 9
+    , bodyBad "assignment missing semicolon" "value = 1 }" "TAP0006" ";" 11
+    , bodyBad "non-name assignment" "Call() = 1;" "TAP0003" "target" 1
+    , bodyBad "parenthesized incomplete expression" "return (1 +);" "TAP0004" "expression" 12
+    , bodyBad "lambda missing arrow" "auto f = \\(int x) x;" "TAP0006" "->" 19
+    , bodyBad "lambda missing typed parameter" "auto f = \\(int) -> 1;" "TAP0006" "identifier" 12
+    , bodyBad "capture missing name" "auto f = [weak ]\\ -> 1;" "TAP0006" "identifier" 16
+    , bodyBad "capture missing initializer" "auto f = [x = ]\\ -> 1;" "TAP0004" "expression" 15
+    , bodyBad "capture missing closing bracket" "auto f = [x;" "TAP0009" "]" 12
+    , bodyBad "unit diagnostic survives dispatch" "unit value = 1;" "TAP0013" "unit" 1
+    ]
+    where
+        bad label source code message line column = (label, rejectedAt source code message line column)
+        bodyBad label body code message column =
+            bad label ("class Program { int Value() {\n" ++ body ++ "\n} }") code message 2 column
+
+comparisonTests :: [(String, Bool)]
+comparisonTests =
+    [ parses "single less comparison" "return a < b;"
+    , parses "single less-equal comparison" "return a <= b;"
+    , parses "single greater comparison" "return a > b;"
+    , parses "single greater-equal comparison" "return a >= b;"
+    , parses "single equality" "return a == b;"
+    , parses "single inequality" "return a \\= b;"
+    , parses "logical conjunction of comparisons" "return a < b && b < c;"
+    , parses "logical disjunction of equalities" "return a == b || b == c;"
+    , parses "left parenthesized comparison" "return (a < b) < c;"
+    , parses "right parenthesized comparison" "return a < (b < c);"
+    , parses "left parenthesized equality" "return (a == b) == c;"
+    , parses "right parenthesized equality" "return a == (b == c);"
+    , parses "separate precedence groups" "return a < b == c < d;"
+    , parses "arithmetic inside comparison" "return a + b < c * d;"
+    , parses "negated comparison" "return not (a < b);"
+    , chain "less chain" "a < b < c" 7
+    , chain "greater chain" "a > b > c" 7
+    , chain "mixed relational chain" "a <= b > c" 8
+    , chain "reverse mixed relational chain" "a >= b < c" 8
+    , chain "equality chain" "a == b == c" 8
+    , chain "inequality chain" "a \\= b \\= c" 8
+    , chain "mixed equality chain" "a == b \\= c" 8
+    , chain "chain inside parentheses" "(a < b < c)" 8
+    ]
+    where
+        parses label body = (label, accepted (sourceWith body))
+        chain label expression column =
+            ( label
+            , rejectedAt
+                ("class Program { int Value() {\n" ++ expression ++ "\n} }")
+                "TAP0014"
+                "cannot be chained"
+                2
+                column
+            )
+
+-- The operator ladder is a language contract.  These cases intentionally mix
+-- adjacent precedence groups so a parser change cannot silently flatten the
+-- tree while still accepting each token in isolation.
+operatorPrecedenceTests :: [(String, Bool)]
+operatorPrecedenceTests =
+    [ parses "power is right associative" "return 2 ** 3 ** 2;"
+    , parses "power binds before unary negation" "return -2 ** 2;"
+    , parses "bitwise not is an integer prefix operator" "return !mask;"
+    , parses "multiplication binds before addition" "return 1 + 2 * 3;"
+    , parses "addition binds before shift" "return 1 + 2 << 3;"
+    , parses "shift binds before bitwise and" "return 8 >> 1 & 3;"
+    , parses "bitwise and binds before xor" "return 7 & 3 ^ 1;"
+    , parses "bitwise xor binds before or" "return 7 ^ 3 | 1;"
+    , parses "bitwise or binds before comparison" "return flags | mask == expected;"
+    , ("pattern combinators are rejected outside is", not (accepted (sourceWith "return ready and valid or fallback;")))
+    , ("exclamation inequality alias is rejected", not (accepted (sourceWith "return left != right;")))
+    , ("removed membership operator is rejected", not (accepted (sourceWith "return item in values;")))
+    , ("removed not-in operator is rejected", not (accepted (sourceWith "return item not in values;")))
+    , ("removed cast operator is rejected", not (accepted (sourceWith "return value as Target;")))
+    , ("removed nullable cast operator is rejected", not (accepted (sourceWith "return value as? Target;")))
+    , ("removed forced cast operator is rejected", not (accepted (sourceWith "return value as! Target;")))
+    ]
+    where
+        parses label body = (label, accepted (sourceWith body))
+
+-- A string payload that happens to spell punctuation must remain a literal.
+-- In particular a closing-brace payload must not terminate a block early.
+literalTests :: [(String, Bool)]
+literalTests =
+    [ ("literal payload remains data: " ++ show payload, literalResult payload)
+    | payload <-
+        [ "if"
+        , "else"
+        , "return"
+        , "final"
+        , "class"
+        , "namespace"
+        , "static"
+        , "not"
+        , "+"
+        , "-"
+        , "*"
+        , "=="
+        , "{"
+        , "}"
+        , "("
+        , ")"
+        , "["
+        , "]"
+        , ";"
+        , ","
+        , "->"
+        , "true"
+        , "false"
+        ]
+    ]
+        ++ [ ("keyword text cannot act as class keyword", not (accepted "\"class\" Program {}"))
+           , ("String brace cannot open a class", not (accepted "class Program \"{\" }"))
+           , ("String semicolon cannot terminate namespace", not (accepted "namespace Demo \";\" class Program {}"))
+           , ("String else cannot introduce branch", not (accepted (sourceWith "if (true) {} \"else\" {}")))
+           , ("String operator cannot connect operands", not (accepted (sourceWith "return 1 \"+\" 2;")))
+           ]
+
+literalResult :: String -> Bool
+literalResult payload = case statements (sourceWith ("return " ++ show payload ++ ";")) of
+    Just [ReturnStatement _ (Just (LiteralExpression _ (StringLiteral value) ()))] -> value == payload
+    _ -> False
+
+statements :: String -> Maybe [Statement Identifier ()]
+statements source = case parseSource source of
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ _ (Block body) _ _]])) -> Just body
+    _ -> Nothing
+
+spanTests :: [(String, Bool)]
+spanTests =
+    [ ("empty closure retains source file and closing brace", closureSpan "\\ -> {}")
+    , ("explicit empty capture span includes brackets", closureSpan "[]\\ -> {}")
+    , ("strong capture span includes opening bracket", closureSpan "[value]\\ -> {}")
+    , ("weak capture span includes mode and delimiters", closureSpan "[weak value]\\ -> {}")
+    , ("unowned capture span includes mode and delimiters", closureSpan "[unowned value]\\ -> {}")
+    , ("expression closure span includes expression", closureSpan "\\x -> x + 1")
+    , ("typed closure span includes parameter syntax", closureSpan "\\(int x) -> x")
+    , ("empty if span includes closing block delimiter", ifSpan "if (true) {}")
+    , ("empty else span includes final delimiter", ifSpan "if (true) {} else {}")
+    , ("else-if span includes complete chain", ifSpan "if (true) {} else if (false) {} else {}")
+    , ("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 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 }")
+    , ("value block span includes both braces", valueBlockSpans)
+    , ("match arm span starts at its pattern and includes its comma", armSpans)
+    , ("match arm without a comma ends at its body", lastArmSpan)
+    , ("match pattern span includes its parentheses", patternSpans)
+    , ("type pattern span covers the type and the name", typePatternSpan)
+    , ("return span includes semicolon", returnSpan)
+    , ("binding span starts at type", bindingSpan)
+    , ("member span includes access and closing brace", memberSpan)
+    ]
+
+closureSpan :: String -> Bool
+closureSpan spelling = case statements ("class Program { int Value() {\nauto f = " ++ spelling ++ ";\nreturn 1; } }") of
+    Just (BindingStatement _ _ _ _ _ (CallableExpression value _ _ _ _ _) : _) ->
+        exactSpan value 2 10 (10 + length spelling)
+    _ -> False
+
+ifSpan :: String -> Bool
+ifSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
+    Just (IfStatement value _ _ _ : _) -> exactSpan value 2 1 (1 + length spelling)
+    _ -> False
+
+guardSpan :: String -> Bool
+guardSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
+    Just (GuardStatement value _ _ : _) -> exactSpan value 2 1 (1 + length spelling)
+    _ -> False
+
+blockSpan :: String -> Bool
+blockSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
+    Just (BlockStatement value _ : _) -> exactSpan value 2 1 (1 + length spelling)
+    _ -> False
+
+matchStatementSpan :: String -> Bool
+matchStatementSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
+    Just (ExpressionStatement outer (MatchExpression inner _ _ _) True : _) ->
+        exactSpan outer 2 1 (1 + length spelling) && exactSpan inner 2 1 (1 + length spelling)
+    _ -> False
+
+-- The initializer starts at column 10 of `auto r = ...;`.
+matchExpressionSpan :: String -> Bool
+matchExpressionSpan spelling = case statements ("class Program { int Value() {\nauto r = " ++ spelling ++ ";\nreturn r; } }") of
+    Just (BindingStatement _ _ _ _ _ (MatchExpression value _ _ _) : _) -> exactSpan value 2 10 (10 + length spelling)
+    _ -> False
+
+ifExpressionSpan :: String -> Bool
+ifExpressionSpan spelling = case statements ("class Program { int Value() {\nauto r = " ++ spelling ++ ";\nreturn r; } }") of
+    Just (BindingStatement _ _ _ _ _ (ConditionalExpression value _ _ _ _) : _) -> exactSpan value 2 10 (10 + length spelling)
+    _ -> False
+
+-- `auto r = if (true) { 1 } else { 2 };`: the blocks are at columns 20 to 24
+-- 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 _ _) _) : _) ->
+        exactSpan first 2 20 25 && exactSpan second 2 31 36
+    _ -> False
+
+-- `auto r = match (1) { 1 -> 2, _ -> 3 };`: the first arm is `1 -> 2,` at
+-- columns 22 to 28 and the second `_ -> 3` at columns 30 to 35.
+armSpans :: Bool
+armSpans = case statements "class Program { int Value() {\nauto r = match (1) { 1 -> 2, _ -> 3 };\nreturn r; } }" of
+    Just (BindingStatement _ _ _ _ _ (MatchExpression _ _ [first, second] _) : _) ->
+        exactSpan (matchArmSpan first) 2 22 29 && exactSpan (matchArmSpan second) 2 30 36
+    _ -> False
+
+lastArmSpan :: Bool
+lastArmSpan = case statements "class Program { int Value() {\nmatch (1) { _ -> { } }\nreturn 1; } }" of
+    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> exactSpan (matchArmSpan arm) 2 13 21
+    _ -> False
+
+-- `match (1), (2) { (1), _ -> { } }`: the first pattern is `(1)` at columns
+-- 18 to 20 and the second `_` at column 23.
+patternSpans :: Bool
+patternSpans = case statements "class Program { int Value() {\nmatch (1), (2) { (1), _ -> { } }\nreturn 1; } }" of
+    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> case matchArmPatterns arm of
+        [first, second] -> exactSpan (matchPatternSpan first) 2 18 21 && exactSpan (matchPatternSpan second) 2 23 24
+        _ -> False
+    _ -> False
+
+typePatternSpan :: Bool
+typePatternSpan = case statements "class Program { int Value() {\nmatch (1) { int other -> { } }\nreturn 1; } }" of
+    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> case matchArmPatterns arm of
+        [patternValue] -> exactSpan (matchPatternSpan patternValue) 2 13 22
+        _ -> False
+    _ -> False
+
+returnSpan :: Bool
+returnSpan = case statements "class Program { void Value() {\nreturn;\n} }" of
+    Just [ReturnStatement value Nothing] -> exactSpan value 2 1 8
+    _ -> False
+
+bindingSpan :: Bool
+bindingSpan = case statements "class Program { int Value() {\nint value = 1;\nreturn value; } }" of
+    Just (BindingStatement value _ _ _ _ _ : _) -> exactSpan value 2 1 15
+    _ -> False
+
+memberSpan :: Bool
+memberSpan = case parseSource "class Program {\npublic static void Main() {}\n}" of
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [member]])) -> exactSpan (declarationSpan member) 2 1 29
+    _ -> False
+
+exactSpan :: SourceSpan -> Int -> Int -> Int -> Bool
+exactSpan value line start end =
+    sourceFile value == "parser-contract.tala"
+        && sourceStart value == SourcePosition line start
+        && sourceEnd value == SourcePosition line end
+
+tokenStreamTests :: [(String, Bool)]
+tokenStreamTests =
+    [ ("empty token stream is an empty module", parsesTokens [])
+    , ("single EOF is an empty module", parsesTokens [eofToken])
+    ,
+        ( "valid supplied stream need not have EOF"
+        , case tokensOf "class Program {}" of
+            Just tokens -> parsesTokens (filter ((/= EndOfFileToken) . tokenKind) tokens)
+            Nothing -> False
+        )
+    , ("EOF cannot hide a declaration suffix", rejectsTokens [eofToken, wordToken] "TAP0015")
+    , ("duplicate EOF cannot hide trailing input", rejectsTokens [eofToken, eofToken] "TAP0015")
+    ,
+        ( "missing namespace terminator without EOF retains error"
+        , case tokensOf "namespace Demo" of
+            Just tokens -> rejectsTokens (filter ((/= EndOfFileToken) . tokenKind) tokens) "TAP0007"
+            Nothing -> False
+        )
+    ,
+        ( "truncated class without EOF terminates"
+        , case tokensOf "class Program {" of
+            Just tokens -> not (parsesTokens (filter ((/= EndOfFileToken) . tokenKind) tokens))
+            Nothing -> False
+        )
+    , ("String token cannot masquerade as keyword", rejectsTokens [wordToken {tokenKind = StringToken}, eofToken] "TAP0006")
+    ]
+    where
+        spanValue = SourceSpan "tokens.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+        eofToken = Token EndOfFileToken "" spanValue
+        wordToken = Token KeywordToken "class" spanValue
+
+tokensOf :: String -> Maybe [Token]
+tokensOf source = case runLexer defaultLexer (LexerInput "tokens.tala" source) of
+    Left _ -> Nothing
+    Right tokens -> Just tokens
+
+parsesTokens :: [Token] -> Bool
+parsesTokens tokens = case runParser defaultParser (ParserInput "tokens.tala" tokens) of
+    Right _ -> True
+    Left _ -> False
+
+rejectsTokens :: [Token] -> String -> Bool
+rejectsTokens tokens code = case runParser defaultParser (ParserInput "tokens.tala" tokens) of
+    Left [problem] -> diagnosticCode problem == code
+    _ -> False
+
+-- Type spelling is tested at the parsed boundary. This makes sugar choices
+-- observable without relying on later passes that may normalize System types.
+-- In particular Array<T, N> keeps two generic arguments: fixed versus dynamic
+-- behavior belongs to overload and template resolution, not to a second class.
+typeSyntaxTests :: [(String, Bool)]
+typeSyntaxTests =
+    [ ("simple type keeps compact syntax", returnSyntax "int" == Just (ExplicitType (Identifier "int")))
+    ,
+        ( "qualified type preserves every name component"
+        , returnSyntax "System.Text.String"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Text", Identifier "String"])
+                    []
+                )
+        )
+    ,
+        ( "generic type preserves its type argument"
+        , returnSyntax "System.Array<int>"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
+                )
+        )
+    ,
+        ( "Array<T, N> remains one overloaded generic family"
+        , returnSyntax "System.Array<Element, Length>"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [ TemplateTypeSyntax (ExplicitType (Identifier "Element"))
+                    , TemplateTypeSyntax (ExplicitType (Identifier "Length"))
+                    ]
+                )
+        )
+    ,
+        ( "nested generic closing delimiters remain independent"
+        , returnSyntax "System.Array<System.Array<int>>"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [ TemplateTypeSyntax
+                        ( QualifiedTypeSyntax
+                            (QualifiedName [Identifier "System", Identifier "Array"])
+                            [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
+                        )
+                    ]
+                )
+        )
+    ,
+        ( "built-in fixed array remains distinct from System.Array"
+        , returnSyntax "[]int" == Just (BuiltinArrayTypeSyntax (ExplicitType (Identifier "int")))
+        )
+    ,
+        ( "dynamic array sugar has one element type"
+        , returnSyntax "[String]" == Just (ArrayTypeSyntax (ExplicitType (Identifier "String")))
+        )
+    ,
+        ( "dictionary sugar preserves key and value types"
+        , returnSyntax "[String to int]"
+            == Just (DictionaryTypeSyntax (ExplicitType (Identifier "String")) (ExplicitType (Identifier "int")))
+        )
+    ,
+        ( "callable type preserves parameters and result"
+        , returnSyntax "(int, String) -> bool"
+            == Just
+                ( CallableTypeSyntax
+                    [ExplicitType (Identifier "int"), ExplicitType (Identifier "String")]
+                    (ExplicitType (Identifier "bool"))
+                )
+        )
+    ,
+        ( "zero-parameter callable type is accepted"
+        , returnSyntax "() -> void" == Just (CallableTypeSyntax [] (ExplicitType (Identifier "void")))
+        )
+    ,
+        ( "compound binding dispatch recognizes a qualified generic type"
+        , bindingSyntax "System.Array<int> values = Source();"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
+                )
+        )
+    ,
+        ( "compound binding dispatch recognizes dictionary sugar"
+        , bindingSyntax "[String to int] values = Source();"
+            == Just (DictionaryTypeSyntax (ExplicitType (Identifier "String")) (ExplicitType (Identifier "int")))
+        )
+    ,
+        ( "callable parameter dispatch recognizes a compound type"
+        , callableParameterSyntax "\\(System.Array<int> values) -> 1"
+            == Just
+                ( QualifiedTypeSyntax
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
+                )
+        )
+    , ("underscore parameter label remains accepted", parameterNameFor "_ int value" == Just (Identifier "value"))
+    , ("explicit parameter label is accepted", parameterNameFor "input: int value" == Just (Identifier "value"))
+    , ("generic argument list cannot be empty", hasParserCode "TAP0016" (functionWithReturn "Array<>"))
+    , ("qualified type cannot end after a dot", rejected (parseSource (functionWithReturn "System.")))
+    , ("generic type requires a closing delimiter", rejected (parseSource (functionWithReturn "Array<int")))
+    ,
+        ( "fixed array sugar preserves its compile-time size"
+        , case returnSyntax "[int; 3]" of
+            Just (FixedArrayTypeSyntax (ExplicitType (Identifier "int")) (TemplateIntegerSyntax _ 3)) -> True
+            _ -> False
+        )
+    ,
+        ( "dynamic array sugar normalizes to System.Array<T>"
+        , typedParameterType "[int]"
+            == Just
+                ( NamedType
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [TypeTemplateArgument intType]
+                )
+        )
+    ,
+        ( "dictionary sugar normalizes to System.Dictionary<K, V>"
+        , typedParameterType "[String to int]"
+            == Just
+                ( NamedType
+                    (QualifiedName [Identifier "System", Identifier "Dictionary"])
+                    [TypeTemplateArgument stringType, TypeTemplateArgument intType]
+                )
+        )
+    ,
+        ( "built-in array has no invented public class name"
+        , typedParameterType "[]int"
+            == Just (NamedType (QualifiedName [Identifier "[]"]) [TypeTemplateArgument intType])
+        )
+    ,
+        ( "callable syntax becomes a structural FunctionType"
+        , typedParameterType "(int, String) -> bool"
+            == Just (FunctionType [intType, stringType] boolType)
+        )
+    ]
+
+functionWithReturn :: String -> String
+functionWithReturn spelling = "class Program { " ++ spelling ++ " Value() { return Source(); } }"
+
+returnSyntax :: String -> Maybe TypeSyntax
+returnSyntax spelling = case parseSource (functionWithReturn spelling) of
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ syntax _ _ _ _]])) -> Just syntax
+    _ -> Nothing
+
+bindingSyntax :: String -> Maybe TypeSyntax
+bindingSyntax spelling = case statements (sourceWith spelling) of
+    Just (BindingStatement _ _ syntax _ _ _ : _) -> Just syntax
+    _ -> Nothing
+
+callableParameterSyntax :: String -> Maybe TypeSyntax
+callableParameterSyntax spelling = case statements (sourceWith ("auto value = " ++ spelling ++ ";")) of
+    Just (BindingStatement _ _ _ _ _ (CallableExpression _ _ _ [parameter] _ _) : _) ->
+        Just (parameterTypeSyntax parameter)
+    _ -> Nothing
+
+parameterNameFor :: String -> Maybe Identifier
+parameterNameFor spelling = case parseSource ("class Program { void Apply(" ++ spelling ++ ") { return; } }") of
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]])) ->
+        Just (parameterName parameter)
+    _ -> Nothing
+
+typedParameterType :: String -> Maybe Type
+typedParameterType spelling =
+    case compileToCorePrep
+        (CompilerInput "type-contract.tala" ("class Program { void Apply(_ " ++ spelling ++ " value) { return; } }")) of
+        Right artifacts ->
+            case syntaxDeclarations (typedSyntaxTree (artifactTypedAST artifacts)) of
+                [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]] ->
+                    Just (parameterAnnotation parameter)
+                _ -> Nothing
+        Left _ -> Nothing
+
+hasParserCode :: String -> String -> Bool
+hasParserCode code source = case parseSource source of
+    Left problems -> any (\problem -> diagnosticStage problem == ParserStage && diagnosticCode problem == code) problems
+    Right _ -> False
+
+rejected :: Either problems value -> Bool
+rejected result = case result of
+    Left _ -> True
+    Right _ -> False
+
+-- Complete compilation proves that the nested representation of else-if is
+-- understood by name resolution, typing, Core optimization, and CorePrep.
+pipelineTests :: [(String, Bool)]
+pipelineTests =
+    [
+        ( "first branch of else-if folds"
+        , returnsInteger "if (true) { return 1; } else if (true) { return 2; } else { return 3; }" 1
+        )
+    ,
+        ( "middle branch of else-if folds"
+        , returnsInteger "if (false) { return 1; } else if (true) { return 2; } else { return 3; }" 2
+        )
+    ,
+        ( "last branch of else-if folds"
+        , returnsInteger "if (false) { return 1; } else if (false) { return 2; } else { return 3; }" 3
+        )
+    ,
+        ( "unbraced else-if still requires condition parentheses"
+        , not (accepted (sourceWith "if (false) {} else if true {} return 0;"))
+        )
+    , ("parenthesized comparison results type-check", compiles "return (1 < 2) == (2 < 3);")
+    , ("explicit conjunction reaches CorePrep", compiles "return 1 < 2 && 2 < 3;")
+    , ("mutable local dispatch reaches CorePrep", returnsInteger "int value = 1; value = 2; return 3;" 3)
+    , ("immutable local dispatch reaches CorePrep", returnsInteger "final int value = 7; return value;" 7)
+    , ("logical not of name is not a declaration", compiles "bool flag = true; return not flag;")
+    , ("floating comparison reaches CorePrep", compiles "return 2.5 < 3.5;")
+    , ("floating equality reaches CorePrep", compiles "return 2.5 == 3.5;")
+    ,
+        ( "live Boolean equality reaches CorePrep"
+        , case compileToCorePrep
+            (CompilerInput "live.tala" "class Program { bool Same(bool left, bool right) { return left == right; } }") of
+            Right _ -> True
+            Left _ -> False
+        )
+    ]
+
+returnsInteger :: String -> Integer -> Bool
+returnsInteger body expected = case compileToCorePrep (CompilerInput "parser-contract.tala" (sourceWith body)) of
+    Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
+        [function] -> case reverse (coreFunctionBody function) of
+            CoreReturn (CoreLiteral (CoreInteger value) _) : _ -> value == expected
+            _ -> False
+        _ -> False
+    Left _ -> False
+
+compiles :: String -> Bool
+compiles body = case compileToCorePrep (CompilerInput "parser-contract.tala" ("class Program { bool Value() { " ++ body ++ " } }")) of
+    Right _ -> True
+    Left _ -> False
+
+-- Check the computed result, not only successful compilation. Coercing every
+-- integer operand to bool can otherwise make equality silently return true.
+booleanOperandTests :: [(String, Bool)]
+booleanOperandTests =
+    [ ("Boolean result preserves operands: " ++ expression, returnsBoolean expression expected)
+    | (expression, expected) <-
+        [ ("1 == 2", False)
+        , ("2 == 2", True)
+        , ("1 \\= 2", True)
+        , ("2 \\= 2", False)
+        , ("1 < 2", True)
+        , ("2 < 1", False)
+        , ("2 <= 2", True)
+        , ("2 > 1", True)
+        , ("1 >= 2", False)
+        , ("(1 < 2) == (3 < 4)", True)
+        , ("1 < 2 && 3 > 4", False)
+        , ("1 > 2 || 3 < 4", True)
+        , ("not (1 == 2)", True)
+        , ("not (1 < 2)", False)
+        , ("not 0", True)
+        , ("not 7", False)
+        , ("1 \\= 2", True)
+        , ("2 \\= 2", False)
+        , ("1 + 2 == 3", True)
+        , ("1 + 2 == 4", False)
+        , ("true == false", False)
+        , ("true \\= false", True)
+        ]
+    ]
+
+returnsBoolean :: String -> Bool -> Bool
+returnsBoolean expression expected =
+    case compileToCorePrep
+        (CompilerInput "parser-contract.tala" ("class Program { bool Value() { return " ++ expression ++ "; } }")) of
+        Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
+            [function] -> case coreFunctionBody function of
+                [CoreReturn (CoreLiteral (CoreBoolean value) _)] -> value == expected
+                _ -> False
+            _ -> False
+        Left _ -> False
+ test/PatternTests.hs view
@@ -0,0 +1,224 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module PatternTests (patternTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+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
+
+-- Pattern coverage is deliberately organized by compiler boundary. Successful
+-- parsing alone would not catch a lost annotation, repeated subject evaluation,
+-- a broken Core wire tag, or a CorePrep operation that never reaches native code.
+patternTests :: [(String, Bool)]
+patternTests =
+    [ ("wildcard pattern reaches typed Core", wildcardLowersToTrue)
+    , ("literal pattern reaches equality Core", literalLowersToEquality)
+    , ("all relational pattern spellings compile", relationalSpellingsCompile)
+    , ("pattern and binds more tightly than or", combinatorPrecedence)
+    , ("pattern not negates only its nested pattern", notPatternShape)
+    , ("null pattern accepts a reference subject", compilesReference "value is null")
+    , ("null pattern rejects a numeric subject", rejectedWith "TAT0020" (numericSource "value is null"))
+    , ("type pattern accepts reference-to-reference tests", compilesReference "value is Payload")
+    , ("type pattern rejects a numeric target", rejectedWith "TAT0022" (numericSource "value is String"))
+    , ("literal pattern rejects an incompatible subject", rejectedWith "TAT0021" (referenceSource "value is 1"))
+    , ("relational pattern rejects an incompatible subject", rejectedWith "TAT0023" (referenceSource "value is > 1"))
+    , ("pattern subject call appears exactly once in Core", effectfulSubjectEvaluatedOnce)
+    , ("pattern-local symbol is reused by every test", patternSubjectIsShared)
+    , ("pattern Core remains verifier-valid", patternCoreVerifies)
+    , ("Core wire round-trips pattern sequencing", patternCoreWireRoundTrip)
+    , ("CorePrep wire round-trips pattern control flow", patternCorePrepWireRoundTrip)
+    , ("pattern compilation reaches nonempty CorePrep", patternReachesCorePrep)
+    ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource source = compileToCorePrep (CompilerInput "pattern-test.tala" source)
+
+numericSource :: String -> String
+numericSource expression =
+    "class Program { bool Match(_ int value) { return " ++ expression ++ "; } }"
+
+referenceSource :: String -> String
+referenceSource expression =
+    unlines
+        [ "class Payload {}"
+        , "class Program { bool Match(_ Payload value) { return " ++ expression ++ "; } }"
+        ]
+
+compilesReference :: String -> Bool
+compilesReference expression = accepted (compileSource (referenceSource expression))
+
+accepted :: Either problems value -> Bool
+accepted result = case result of
+    Right _ -> True
+    Left _ -> False
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code source = case compileSource source of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+singleReturn :: String -> Maybe CoreExpression
+singleReturn source = do
+    artifacts <- either (const Nothing) Just (compileSource source)
+    function <- case coreModuleFunctions (artifactCore artifacts) of
+        [value] -> Just value
+        values -> case reverse values of
+            value : _ -> Just value
+            [] -> Nothing
+    case coreFunctionBody function of
+        [CoreReturn expression] -> Just expression
+        _ -> Nothing
+
+wildcardLowersToTrue :: Bool
+wildcardLowersToTrue = case singleReturn (numericSource "value is _") of
+    Just (CoreLet _ intBinding (CoreVariable _ intSubject) (CoreLiteral (CoreBoolean True) resultType) letType) ->
+        intBinding == intType && intSubject == intType && resultType == boolType && letType == boolType
+    _ -> False
+
+literalLowersToEquality :: Bool
+literalLowersToEquality = case singleReturn (numericSource "value is 42") of
+    Just
+        (CoreLet subject _ _ (CorePrimitive CoreEqual [CoreVariable used _, CoreLiteral (CoreInteger 42) _] result) letType) ->
+            resolvedSymbol subject == resolvedSymbol used && result == boolType && letType == boolType
+    _ -> False
+
+relationalSpellingsCompile :: Bool
+relationalSpellingsCompile =
+    all
+        (accepted . compileSource . numericSource . ("value is " ++))
+        ["< 1", "<= 1", "> 1", ">= 1", "== 1", "\\= 1"]
+
+combinatorPrecedence :: Bool
+combinatorPrecedence = case parsePatternSource (referenceSource "value is null or Payload and not null") of
+    Just (OrPattern _ _ (AndPattern _ _ _ _) _) -> True
+    _ -> False
+
+parsePatternSource :: String -> Maybe (Pattern Identifier ())
+parsePatternSource source = do
+    tokens <- either (const Nothing) Just (runLexer defaultLexer (LexerInput "pattern-test.tala" source))
+    ParsedAST tree <- either (const Nothing) Just (runParser defaultParser (ParserInput "pattern-test.tala" tokens))
+    expression <- case [ value
+                       | TypeDeclaration {typeMembers = members} <- syntaxDeclarations tree
+                       , FunctionDeclaration {declarationName = Identifier "Match", declarationBody = Block statements} <- members
+                       , ReturnStatement _ (Just value) <- statements
+                       ] of
+        [value] -> Just value
+        _ -> Nothing
+    case expression of
+        IsPatternExpression _ _ patternValue _ -> Just patternValue
+        _ -> Nothing
+
+notPatternShape :: Bool
+notPatternShape = case singleReturn (numericSource "value is not > 0") of
+    Just (CoreLet _ _ _ (CorePrimitive CoreLogicalNot [CorePrimitive CoreGreaterThan _ _] result) letType) ->
+        result == boolType && letType == boolType
+    _ -> False
+
+effectfulSource :: String
+effectfulSource =
+    unlines
+        [ "class Program {"
+        , "  String Source() { return \"value\"; }"
+        , "  bool Match() { return Source() is not null and String; }"
+        , "}"
+        ]
+
+effectfulSubjectEvaluatedOnce :: Bool
+effectfulSubjectEvaluatedOnce = case singleReturn effectfulSource of
+    Just expression@CoreLet {} -> countCalls expression == 1
+    _ -> False
+
+patternSubjectIsShared :: Bool
+patternSubjectIsShared = case singleReturn effectfulSource of
+    Just (CoreLet subject _ _ body _) ->
+        let uses = expressionSymbols body
+         in length uses >= 2 && all (== resolvedSymbol subject) uses
+    _ -> False
+
+countCalls :: CoreExpression -> Int
+countCalls expression = case expression of
+    CoreVariable {} -> 0
+    CoreLiteral {} -> 0
+    CoreApply callee arguments _ -> 1 + countCalls callee + sum (map countCalls arguments)
+    CorePrimitive _ arguments _ -> sum (map countCalls arguments)
+    CoreLet _ _ value body _ -> countCalls value + countCalls body
+    CoreConditional condition whenTrue whenFalse _ -> sum (map countCalls [condition, whenTrue, whenFalse])
+    CoreClosure captures _ _ body _ ->
+        sum (map (countCalls . coreCaptureValue) captures) + sum (map statementCalls body)
+
+statementCalls :: CoreStatement -> Int
+statementCalls statement = case statement of
+    CoreBind binding -> countCalls (coreBindingValue binding)
+    CoreAssign _ value -> countCalls value
+    CoreReturn value -> countCalls value
+    CoreIf condition whenTrue whenFalse ->
+        countCalls condition + sum (map statementCalls whenTrue) + sum (map statementCalls whenFalse)
+    CoreEvaluate value -> countCalls value
+    CoreWhile condition body -> countCalls condition + sum (map statementCalls body)
+    CoreDoWhile body condition -> sum (map statementCalls body) + countCalls condition
+    CoreFor condition updates body ->
+        countCalls condition + sum (map statementCalls updates) + sum (map statementCalls body)
+    CoreBreak -> 0
+    CoreContinue -> 0
+
+expressionSymbols :: CoreExpression -> [SymbolId]
+expressionSymbols expression = case expression of
+    CoreVariable name _ -> [resolvedSymbol name]
+    CoreLiteral {} -> []
+    CoreApply callee arguments _ -> expressionSymbols callee ++ concatMap expressionSymbols arguments
+    CorePrimitive _ arguments _ -> concatMap expressionSymbols arguments
+    CoreLet _ _ value body _ -> expressionSymbols value ++ expressionSymbols body
+    CoreConditional condition whenTrue whenFalse _ -> concatMap expressionSymbols [condition, whenTrue, whenFalse]
+    CoreClosure captures _ _ body _ ->
+        concatMap (expressionSymbols . coreCaptureValue) captures ++ concatMap statementSymbols body
+
+statementSymbols :: CoreStatement -> [SymbolId]
+statementSymbols statement = case statement of
+    CoreBind binding -> expressionSymbols (coreBindingValue binding)
+    CoreAssign _ value -> expressionSymbols value
+    CoreReturn value -> expressionSymbols value
+    CoreIf condition whenTrue whenFalse ->
+        expressionSymbols condition ++ concatMap statementSymbols whenTrue ++ concatMap statementSymbols whenFalse
+    CoreEvaluate value -> expressionSymbols value
+    CoreWhile condition body -> expressionSymbols condition ++ concatMap statementSymbols body
+    CoreDoWhile body condition -> concatMap statementSymbols body ++ expressionSymbols condition
+    CoreFor condition updates body ->
+        expressionSymbols condition ++ concatMap statementSymbols updates ++ concatMap statementSymbols body
+    CoreBreak -> []
+    CoreContinue -> []
+
+patternArtifacts :: Maybe FrontendArtifacts
+patternArtifacts = either (const Nothing) Just (compileSource effectfulSource)
+
+patternCoreVerifies :: Bool
+patternCoreVerifies = case patternArtifacts of
+    Just artifacts -> verifyCore (artifactCore artifacts) == Right (artifactCore artifacts)
+    Nothing -> False
+
+patternCoreWireRoundTrip :: Bool
+patternCoreWireRoundTrip = case patternArtifacts of
+    Just artifacts ->
+        case encodeCore defaultCoreWireLimits (artifactCore artifacts) >>= decodeCore defaultCoreWireLimits of
+            Right decoded -> decoded == artifactCore artifacts
+            Left _ -> False
+    Nothing -> False
+
+patternCorePrepWireRoundTrip :: Bool
+patternCorePrepWireRoundTrip = case patternArtifacts of
+    Just artifacts -> case encodeCorePrep (artifactCorePrep artifacts) >>= decodeCorePrep of
+        Right decoded -> decoded == artifactCorePrep artifacts
+        Left _ -> False
+    Nothing -> False
+
+patternReachesCorePrep :: Bool
+patternReachesCorePrep = case patternArtifacts of
+    Just artifacts -> not (null (corePrepModuleFunctions (artifactCorePrep artifacts)))
+    Nothing -> False
+ test/QualifiedNameTests.hs view
@@ -0,0 +1,138 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Names written through what declares them.
+
+A declaration of a namespace may be named through the namespace:
+@Demo.Program@ is @Program@ inside the namespace @Demo@. A static method may
+be referred to through its type without being called: @Program::Log@ is the
+method as a callable value, like the bare @Log@ inside @Program@. A selector
+with a dot is not a reference: @Program.Log@ without a call is rejected.
+
+Each program is run in the reference evaluator, before and after
+optimization, and what it writes is compared with text written by hand. The
+programs that must be rejected are held with the code of the diagnostic.
+-}
+module QualifiedNameTests (qualifiedNameTests) where
+
+import CoreInterpreter
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+
+qualifiedNameTests :: [(String, Bool)]
+qualifiedNameTests =
+    concat
+        [ [ ("unoptimized: " ++ label, written artifactCore (source body) == Just expected)
+          , ("optimized: " ++ label, written artifactOptimizedCore (source body) == Just expected)
+          ]
+        | (label, source, body, expected) <- outputs
+        ]
+        ++ [ (label ++ " (" ++ code ++ ")", rejectedWith code (source body))
+           | (label, source, body, code) <- rejections
+           ]
+
+-- | The label, the program around the body of @Main@, the body and the text.
+outputs :: [(String, String -> String, String, String)]
+outputs =
+    [ ("a method is called through its namespace", demo, "Console.Println(Demo.Program.Half(8));", "4\n")
+    , ("a method called through its namespace writes where it stands", demo, "int x = Demo.Program.Log(1); Console.Println(9);", "1\n9\n")
+    , ("a method of another class is called through the namespace", demo, "Console.Println(Demo.Other.Triple(3));", "9\n")
+    , ("an enum member is named through the namespace", demo, "Color c = Demo.Color.Green; Console.Println(c == Color.Green && c \\= Demo.Color.Red);", "true\n")
+    , ("the namespace and the bare name are the same declaration", demo, "Console.Println(Demo.Program.Half(8) == Program.Half(8) && Program.Half(8) == Half(8));", "true\n")
+    , ("a namespace of several parts is written whole", nested, "Console.Println(Outer.Inner.Program.Half(10));", "5\n")
+    , ("a method named through its type is a value", demo, "auto f = Program::Log; int x = f(1); Console.Println(9);", "1\n9\n")
+    , ("a method named through its type is handed to a method", demo, "int x = Run(Program::Log); Console.Println(9);", "3\n9\n")
+    , ("a method of another class is a value", demo, "Console.Println(Run(Other::Triple));", "9\n")
+    , ("a method named through namespace and type is a value", demo, "Console.Println(Run(Demo.Other::Triple));", "9\n")
+    , ("a quiet method named through its type is called by need", demo, "int x = Run(Program::Half) / Zero(); Console.Println(9);", "9\n")
+    , ("the place selects among the overloads of a method value", demo, "Console.Println(Run(Other::Pick));", "13\n")
+    , ("a method value is called as often as its call is reached", demo, "auto f = Other::Triple; int t = 0; for (int i = 0; i < 4; i += 1) { t += f(i); } Console.Println(t);", "18\n")
+    , -- A class of the program named like the namespace is the class.
+      ("a class named like the namespace hides the namespace", shadowed, "Console.Println(Demo.Seven());", "7\n")
+    ]
+
+-- | The label, the program, the body of @Main@ and the code it is rejected with.
+rejections :: [(String, String -> String, String, String)]
+rejections =
+    [ ("a type the namespace does not declare", demo, "Console.Println(Demo.Missing.Half(8));", "TAN0001")
+    , ("another namespace is not known", demo, "Console.Println(Elsewhere.Program.Half(8));", "TAN0001")
+    , ("a part of a namespace is not the namespace", nested, "Console.Println(Outer.Program.Half(10));", "TAN0001")
+    , ("the last part of a namespace is not the namespace", nested, "Console.Println(Inner.Program.Half(10));", "TAN0001")
+    , ("a local name hides the namespace", demo, "int Demo = 1; Console.Println(Demo.Program.Half(8));", "TAT0032")
+    , ("a selector with a dot is not a method reference", demo, "auto f = Program.Log;", "TAT0034")
+    , ("a selector with a dot is not an argument", demo, "Console.Println(Run(Program.Log));", "TAT0009")
+    , ("a method reference through a value", demo, "int v = 1; auto f = v::Half;", "TAT0081")
+    , ("a method reference through a name that is not declared", demo, "auto f = Missing::Half;", "TAN0001")
+    , ("a method value of a name the type does not declare", demo, "auto f = Program::Missing;", "TAT0029")
+    , ("a method value of a private method of another class", demo, "auto f = Other::Hidden;", "TAT0033")
+    , ("a method value with overloads and no expected type", demo, "auto f = Other::Pick;", "TAT0080")
+    , ("a call that expects none of the overloads of a method value", demo, "Console.Println(Wide(Other::Pick));", "TAT0009")
+    , ("a callable parameter without a written type", demo, "auto f = \\x -> 7;", "TAT0082")
+    , ("a captured value beside a parameter without a written type", demo, "int s = 4; auto f = [s] \\x -> s;", "TAT0082")
+    ]
+
+demo :: String -> String
+demo body =
+    unlines
+        [ "namespace Demo;"
+        , "enum Color { Red, Green }"
+        , "public class Other {"
+        , "    public static int Triple(_ int v) { return v * 3; }"
+        , "    public static int Pick(_ int v) { return v + 10; }"
+        , "    public static int Pick(_ int v, _ int w) { return v + w; }"
+        , "    private static int Hidden(_ int v) { return v; }"
+        , "}"
+        , "public class Program {"
+        , "    public static int Zero() { return 0; }"
+        , "    public static int Half(_ int v) { return v / 2; }"
+        , "    public static int Log(_ int v) { Console.Println(v); return v; }"
+        , "    public static int Run(_ (int) -> int f) { return f(3); }"
+        , "    public static int Wide(_ (int, int, int) -> int f) { return f(1, 2, 3); }"
+        , "    public static void Main() {"
+        , "        " ++ body
+        , "    }"
+        , "}"
+        ]
+
+nested :: String -> String
+nested body =
+    unlines
+        [ "namespace Outer.Inner;"
+        , "public class Program {"
+        , "    public static int Half(_ int v) { return v / 2; }"
+        , "    public static void Main() {"
+        , "        " ++ body
+        , "    }"
+        , "}"
+        ]
+
+shadowed :: String -> String
+shadowed body =
+    unlines
+        [ "namespace Demo;"
+        , "public class Demo {"
+        , "    public static int Seven() { return 7; }"
+        , "}"
+        , "public class Program {"
+        , "    public static void Main() {"
+        , "        " ++ body
+        , "    }"
+        , "}"
+        ]
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource text = compileToCorePrep (CompilerInput "qualified.tala" text)
+
+-- | What @Main@ writes to standard output.
+written :: (FrontendArtifacts -> CoreModule) -> String -> Maybe String
+written select text = case compileSource text of
+    Right artifacts -> case runFunctionWriting 200000 (select artifacts) "Main" [] of
+        Just (_, Written output "") -> Just output
+        _ -> Nothing
+    Left _ -> Nothing
+
+rejectedWith :: String -> String -> Bool
+rejectedWith code text = case compileSource text of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+ test/RuntimeCallTests.hs view
@@ -0,0 +1,564 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | The runtime call in Core, its catalog, and the reading of formats.
+
+A runtime call is one Core primitive whose first operand is a literal that
+names a function of the runtime catalog. These cases hold the three things
+the rest of the compiler builds on:
+
+* the catalog, row by row. An identity is written into artifacts and a
+  symbol is linked against, so a row that changed would silently change what
+  compiled programs call; the native stages pin the same rows in
+  @RuntimeCallPipelineTests.cpp@;
+* the rule each verifier applies to a call, broken in each way it can be
+  broken, in Core and in CorePrep, and both wire formats;
+* the reading of a format into pieces, which is the one place the output
+  format grammar is decided.
+
+The programs that use all of it together are in "ConsoleTests".
+-}
+module RuntimeCallTests (runtimeCallTests) where
+
+import CoreInterpreter
+import Data.List (nub)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.CorePrep.Wire
+import Progmasoft.Tala.Core.Optimizer
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.RuntimeCall
+import Progmasoft.Tala.TypeChecker.Format
+
+runtimeCallTests :: [(String, Bool)]
+runtimeCallTests =
+    catalogTests ++ acceptanceTests ++ coreTests ++ corePrepTests ++ optimizerTests ++ formatTests
+
+-- ------------------------------------------------------------------ catalog
+
+-- | A row as the tests state it: identity, symbol, parameters, result, observable.
+type Row = (RuntimeFunction, Integer, String, [RuntimeParameter], RuntimeResult, Bool)
+
+rows :: [Row]
+rows =
+    [ (TextConcat, 1, "tala_text_concat", [TextParameter, TextParameter], TextResult, False)
+    , (TextFromSigned, 2, "tala_text_from_signed", [SignedParameter], TextResult, False)
+    , (TextFromUnsigned, 3, "tala_text_from_unsigned", [UnsignedParameter], TextResult, False)
+    , (TextFromBool, 4, "tala_text_from_bool", [BoolParameter], TextResult, False)
+    , (TextFromChar, 5, "tala_text_from_char", [CharParameter], TextResult, False)
+    , (TextFormatSigned, 6, "tala_text_format_signed", conversion SignedParameter, TextResult, False)
+    , (TextFormatUnsigned, 7, "tala_text_format_unsigned", conversion UnsignedParameter, TextResult, False)
+    , (TextFormatFloating, 8, "tala_text_format_floating", conversion FloatingParameter, TextResult, False)
+    , (TextFormatString, 9, "tala_text_format_string", conversion TextParameter, TextResult, False)
+    , (TextFormatChar, 10, "tala_text_format_char", conversion CharParameter, TextResult, False)
+    , (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)
+    ]
+    where
+        conversion value = [CountParameter, CountParameter, CountParameter, value]
+
+catalogTests :: [(String, Bool)]
+catalogTests =
+    [ ("the runtime catalog has thirteen functions", length runtimeFunctions == 13)
+    , ("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 symbols are distinct", length (nub (map runtimeFunctionSymbol runtimeFunctions)) == length runtimeFunctions)
+    , ("runtime identities are the numbers from one", map runtimeFunctionIdentity runtimeFunctions == [1 .. 13])
+    , ("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 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 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])
+    , ("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)
+    , -- 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])
+    , ("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])
+    ]
+        ++ concat
+            [ [ ("runtime identity of " ++ show function, runtimeFunctionIdentity function == identity)
+              , ("runtime function of identity " ++ show identity, runtimeFunctionOf identity == Just function)
+              , ("runtime symbol of " ++ show function, runtimeFunctionSymbol function == symbol)
+              , ("runtime parameters of " ++ show function, runtimeParameters function == parameters)
+              , ("runtime result of " ++ show function, runtimeResult function == result)
+              , ("runtime observability of " ++ show function, runtimeObservable function == observable)
+              ]
+            | (function, identity, symbol, parameters, result, observable) <- rows
+            ]
+    where
+        declared symbol = ResolvedName symbol (Identifier "Console")
+
+{- | Which types each kind of parameter takes. The runtime functions are
+written for 64 bits, so the wider types are not taken.
+-}
+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 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))
+    ]
+    where
+        parameterKinds =
+            [SignedParameter, UnsignedParameter, FloatingParameter, BoolParameter, CharParameter, TextParameter, CountParameter]
+        spellings =
+            [ "bool"
+            , "char"
+            , "byte"
+            , "short"
+            , "long"
+            , "int"
+            , "longint"
+            , "ubyte"
+            , "ushort"
+            , "ulong"
+            , "uint"
+            , "ulongint"
+            , "sfloat"
+            , "lfloat"
+            , "float"
+            , "double"
+            , "String"
+            , "unit"
+            ]
+        accepted parameter = [spelling | spelling <- spellings, runtimeAccepts parameter (namedType spelling)]
+
+-- --------------------------------------------------------------------- Core
+
+name :: Int -> String -> ResolvedName
+name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+runName, countName, lineName :: ResolvedName
+runName = name 1 "Run"
+countName = name 2 "count"
+lineName = name 3 "line"
+
+count :: CoreExpression
+count = CoreVariable countName intType
+
+integer :: Integer -> CoreExpression
+integer value = CoreLiteral (CoreInteger value) intType
+
+text :: String -> CoreExpression
+text value = CoreLiteral (CoreString value) stringType
+
+-- | A runtime call of the given function with the given result type.
+runtime :: RuntimeFunction -> [CoreExpression] -> Type -> CoreExpression
+runtime function arguments = CorePrimitive CoreRuntimeCall (integer (runtimeFunctionIdentity function) : arguments)
+
+-- | @Run(count)@ with the given body.
+moduleOf :: [CoreStatement] -> CoreModule
+moduleOf body =
+    CoreModuleWithSources
+        (QualifiedName [Identifier "Calls"])
+        [CoreFunction runName [(countName, intType)] intType body]
+        []
+        []
+
+{- | @line = "Count: " + format(count); write(line); return count@, the shape
+a call of @Console.Printfn("Count: %5d", count)@ lowers to.
+-}
+writingModule :: CoreModule
+writingModule =
+    moduleOf
+        [ CoreBind
+            ( CoreBinding
+                lineName
+                stringType
+                False
+                ( runtime
+                    TextConcat
+                    [text "Count: ", runtime TextFormatSigned [integer 0, integer 5, integer absent, count] stringType]
+                    stringType
+                )
+            )
+        , CoreEvaluate (runtime ConsoleWrite [CoreVariable lineName stringType, integer consoleOutputLine] unitType)
+        , CoreReturn count
+        ]
+
+-- | A module whose one statement before the return evaluates the expression.
+evaluating :: CoreExpression -> CoreModule
+evaluating expression = moduleOf [CoreEvaluate expression, CoreReturn count]
+
+-- | A module that binds the expression at the given type.
+binding :: Type -> CoreExpression -> CoreModule
+binding valueType expression = moduleOf [CoreBind (CoreBinding lineName valueType False expression), CoreReturn count]
+
+verifiesCore :: CoreModule -> Bool
+verifiesCore moduleValue = verifyCore moduleValue == Right moduleValue
+
+rejectsCore :: CoreModule -> Bool
+rejectsCore moduleValue = case verifyCore moduleValue of
+    Left problems -> any ((== "TAC1075") . diagnosticCode) problems
+    Right _ -> False
+
+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" ""))
+    , -- 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 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)))
+    , -- 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
+            ( 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)))
+    , -- 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)))
+    ]
+
+-- | A call of the function with an argument of a type each parameter takes.
+wellFormed :: RuntimeFunction -> CoreModule
+wellFormed function = case runtimeResult function of
+    NoResult -> evaluating call
+    _ -> binding (runtimeResultType function) call
+    where
+        call = runtime function (map argument (runtimeParameters function)) (runtimeResultType function)
+        argument parameter = case parameter of
+            SignedParameter -> count
+            UnsignedParameter -> CoreLiteral (CoreInteger 7) (namedType "uint")
+            FloatingParameter -> CoreLiteral (CoreFloating "1.5") (namedType "float")
+            BoolParameter -> CoreLiteral (CoreBoolean True) boolType
+            CharParameter -> CoreLiteral (CoreInteger 65) (namedType "char")
+            TextParameter -> text "t"
+            CountParameter -> integer 0
+
+-- ----------------------------------------------------------------- CorePrep
+
+corePrepTests :: [(String, Bool)]
+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
+            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])
+    , ("CorePrep rejects a call without operands", rejectsCorePrep stringType [])
+    , ("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 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"])
+    ]
+    where
+        literal value = CorePrepLiteral (CoreInteger value) intType
+        identity = literal . runtimeFunctionIdentity
+        string value = CorePrepLiteral (CoreString value) stringType
+
+preparedAndVerified :: CoreModule -> Bool
+preparedAndVerified moduleValue = case prepareCore moduleValue of
+    Right prepared -> verifyCorePrep prepared == Right prepared
+    Left _ -> False
+
+identityStaysLiteral :: Bool
+identityStaysLiteral = case prepareCore writingModule of
+    Right prepared ->
+        and
+            [ case atoms of
+                CorePrepLiteral (CoreInteger _) valueType : _ -> valueType == intType
+                _ -> False
+            | function <- corePrepModuleFunctions prepared
+            , block <- corePrepFunctionBlocks function
+            , CorePrepPrimitive CoreRuntimeCall atoms <- map operationOf (corePrepBlockInstructions block)
+            ]
+    Left _ -> False
+    where
+        operationOf instruction = case instruction of
+            CorePrepBind _ _ _ operation -> operation
+            CorePrepEvaluate operation -> operation
+            CorePrepAssign _ atom -> CorePrepCopy atom
+
+-- | A CorePrep function that binds a runtime call with the given atoms.
+handmade :: Type -> [CorePrepAtom] -> CorePrepModule
+handmade valueType atoms =
+    CorePrepModule
+        (QualifiedName [Identifier "Calls"])
+        [ CorePrepFunction
+            runName
+            ""
+            [(countName, intType)]
+            intType
+            0
+            [ CorePrepBlock
+                0
+                [CorePrepBind lineName valueType False (CorePrepPrimitive CoreRuntimeCall atoms)]
+                (CorePrepReturn (CorePrepVariable countName intType))
+            ]
+        ]
+        []
+
+rejectsCorePrep :: Type -> [CorePrepAtom] -> Bool
+rejectsCorePrep valueType atoms = case verifyCorePrep (handmade valueType atoms) of
+    Left problems -> any ((== "TAC0026") . diagnosticCode) problems
+    Right _ -> False
+
+acceptsCorePrep :: Type -> [CorePrepAtom] -> Bool
+acceptsCorePrep valueType atoms = let moduleValue = handmade valueType atoms in verifyCorePrep moduleValue == Right moduleValue
+
+-- ---------------------------------------------------------------- optimizer
+
+optimizerTests :: [(String, Bool)]
+optimizerTests =
+    [ -- A write whose result nothing reads stays: the write is the point.
+      ("the optimizer keeps a console write", writes (optimized writingModule) == Just "Count:    42\n")
+    , ("the optimizer keeps the order of writes", writes (optimized twoWrites) == Just "first\nsecond\n")
+    , ("the optimizer does not repeat a write", writes (optimized boundWrite) == Just "once\n")
+    , -- 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])
+    ]
+    where
+        write value = CoreEvaluate (runtime ConsoleWrite [text value, integer consoleOutputLine] unitType)
+        twoWrites = moduleOf [write "first", write "second", CoreReturn count]
+        -- The same line is written once however often its string is read.
+        boundWrite =
+            moduleOf
+                [ CoreBind (CoreBinding lineName stringType False (text "once"))
+                , CoreEvaluate (runtime ConsoleWrite [CoreVariable lineName stringType, integer consoleOutputLine] unitType)
+                , CoreReturn count
+                ]
+        unusedText =
+            moduleOf
+                [ CoreBind (CoreBinding lineName stringType False (runtime TextConcat [text "a", text "b"] stringType))
+                , CoreReturn count
+                ]
+        writes moduleValue = do
+            value <- moduleValue
+            (_, Written output _) <- runFunctionWriting 1000 value "Run" [IntegerValue 42]
+            Just output
+
+optimized :: CoreModule -> Maybe CoreModule
+optimized moduleValue = either (const Nothing) Just (runCoreOptimizer defaultCoreOptimizer moduleValue)
+
+-- ------------------------------------------------------------------ formats
+
+formatTests :: [(String, Bool)]
+formatTests =
+    [ ("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"
+            == Right
+                [ LiteralPiece "a"
+                , ConversionPiece (plain SignedDecimal 2)
+                , LiteralPiece "b"
+                , 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")
+    , -- %% 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"])
+    , ("%n is the line terminator", parseFormat "a%nb" == Right [LiteralPiece "a", NewlinePiece, LiteralPiece "b"])
+    , ("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 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 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)])
+    , ("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)
+    , ("a star takes one more", argumentCount "%*d" == Just 2)
+    , ("two stars take two more", argumentCount "%*.*f" == Just 3)
+    , ("the arguments of a format are those of its conversions", argumentCount "%s: %*d (%.2f)%n" == Just 4)
+    , -- What the runtime is given for the flags.
+      ("no flag is no bit", bitsOf "%d" == Just 0)
+    , ("each flag is its bit", map bitsOf ["%-d", "%0d", "%+d", "% d", "%#x", "%'d"] == map Just [1, 2, 4, 8, 16 + 64, 32])
+    , ("flags are added", bitsOf "%+08d" == Just (4 + 2))
+    , ("%x is the hexadecimal flag of the integer conversions", bitsOf "%x" == Just 64)
+    , -- The position a problem is reported at counts from one.
+      ("a problem names the position of its conversion", fmap formatProblemOffset (problemOf "abc%q") == Just 4)
+    , ("the position counts the conversions before it", fmap formatProblemOffset (problemOf "%d%5d%q") == Just 6)
+    , ("a doubled percent sign counts as two characters", fmap formatProblemOffset (problemOf "%%%q") == Just 3)
+    ]
+        ++ [ ("the format " ++ show format ++ " is rejected with " ++ code, fmap formatProblemCode (problemOf format) == Just code)
+           | (format, code) <- rejected
+           ]
+        ++ [ ("the format " ++ show format ++ " is accepted", either (const False) (const True) (parseFormat format))
+           | format <- acceptedFormats
+           ]
+    where
+        plain kind offset = Conversion kind [] NoSize NoSize offset
+        single format = case parseFormat format of
+            Right [ConversionPiece value] -> Just value
+            _ -> Nothing
+        kindOf letter = conversionKind <$> single ['%', letter]
+        flagsOf = fmap conversionFlags . single
+        widthOf = fmap conversionWidth . single
+        precisionOf = fmap conversionPrecision . single
+        bitsOf = fmap conversionFlagBits . single
+        argumentCount format = case parseFormat format of
+            Right found -> Just (sum [conversionArgumentCount value | ConversionPiece value <- found])
+            Left _ -> Nothing
+        problemOf format = either Just (const Nothing) (parseFormat format)
+        rejected =
+            [ -- Not a conversion, or not finished.
+              ("%", "TAT0075")
+            , ("abc%", "TAT0075")
+            , ("%q", "TAT0075")
+            , ("%D", "TAT0075")
+            , ("%i", "TAT0075")
+            , ("%e", "TAT0075")
+            , ("%g", "TAT0075")
+            , ("%X", "TAT0075")
+            , ("%o", "TAT0075")
+            , ("%5", "TAT0075")
+            , ("%-", "TAT0075")
+            , ("%.", "TAT0075")
+            , ("%.f", "TAT0075")
+            , ("%5.d", "TAT0075")
+            , ("% ", "TAT0075")
+            , ("%1234567890d", "TAT0075")
+            , ("%.1234567890f", "TAT0075")
+            , -- A flag the conversion does not take.
+              ("%+s", "TAT0076")
+            , ("%0s", "TAT0076")
+            , ("% s", "TAT0076")
+            , ("%#s", "TAT0076")
+            , ("%'s", "TAT0076")
+            , ("%#d", "TAT0076")
+            , ("%'x", "TAT0076")
+            , ("%+x", "TAT0076")
+            , ("%+u", "TAT0076")
+            , ("% u", "TAT0076")
+            , ("%#u", "TAT0076")
+            , ("%#f", "TAT0076")
+            , ("%0c", "TAT0076")
+            , ("%+c", "TAT0076")
+            , ("%'c", "TAT0076")
+            , ("%0b", "TAT0076")
+            , ("%+b", "TAT0076")
+            , -- Flags that exclude each other, and a flag written twice.
+              ("%-08d", "TAT0076")
+            , ("%0-8d", "TAT0076")
+            , ("%+ d", "TAT0076")
+            , ("% +d", "TAT0076")
+            , ("%++d", "TAT0076")
+            , ("%--d", "TAT0076")
+            , ("%00d", "TAT0076")
+            , -- A precision the conversion does not take.
+              ("%.2d", "TAT0076")
+            , ("%.2u", "TAT0076")
+            , ("%.2x", "TAT0076")
+            , ("%.1c", "TAT0076")
+            , ("%.1b", "TAT0076")
+            , ("%.*d", "TAT0076")
+            , -- %% and %n take nothing.
+              ("%5%", "TAT0076")
+            , ("%-%", "TAT0076")
+            , ("%.2%", "TAT0076")
+            , ("%5n", "TAT0076")
+            , ("%-n", "TAT0076")
+            , ("%*n", "TAT0076")
+            , -- Specified, and waiting for the object model.
+              ("%A", "TAT0079")
+            , ("%O", "TAT0079")
+            , ("%.4A", "TAT0079")
+            , ("%#A", "TAT0079")
+            ]
+        acceptedFormats =
+            [ "%d"
+            , "%5d"
+            , "%-5d"
+            , "%05d"
+            , "%+d"
+            , "% d"
+            , "%'d"
+            , "%+'010d"
+            , "%-+'12d"
+            , "%*d"
+            , "%-*d"
+            , "%u"
+            , "%08u"
+            , "%'u"
+            , "%-'12u"
+            , "%x"
+            , "%#x"
+            , "%08x"
+            , "%#010x"
+            , "%-#12x"
+            , "%f"
+            , "%.2f"
+            , "%10.3f"
+            , "%-10.3f"
+            , "%010.3f"
+            , "%+.1f"
+            , "% .1f"
+            , "%'.2f"
+            , "%*.*f"
+            , "%.*f"
+            , "%s"
+            , "%10s"
+            , "%-10s"
+            , "%.5s"
+            , "%10.5s"
+            , "%*s"
+            , "%.*s"
+            , "%c"
+            , "%3c"
+            , "%-3c"
+            , "%b"
+            , "%6b"
+            , "%-6b"
+            , "%n"
+            , "%%"
+            , "Name: %s, Age: %d"
+            , "%d%%"
+            , "%s%s%s"
+            ]
+ test/RuntimeText.hs view
@@ -0,0 +1,119 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | A reference for the text functions of the runtime, used only by tests.
+
+The runtime library that programs call is written in C++ and works on
+machine integers, a buffer of scalars and a bounded natural number. This
+module states what each of its functions returns in the most direct way the
+language of the tests allows: unbounded integers, exact rationals and lists.
+It shares nothing with the library, so a test that runs a program in the
+reference evaluator and through the native pipeline compares two
+independent answers.
+
+A floating-point value is an exact rational here: the value of the binary
+number, not of the decimal literal it was written as. @%f@ rounds that exact
+value to the digits asked for, a tie to the even digit.
+-}
+module RuntimeText
+    ( Conversion (..)
+    , plain
+    , textOfSigned
+    , textOfBool
+    , formatInteger
+    , formatFloating
+    , formatText
+    , lineTerminator
+    ) where
+
+import Data.Bits ((.&.))
+import Numeric (showHex)
+import Progmasoft.Tala.RuntimeCall
+
+-- | The flags, the width and the precision of one conversion, as the runtime takes them.
+data Conversion = Conversion
+    { conversionFlags :: Integer
+    , conversionWidth :: Integer
+    , conversionPrecision :: Integer
+    }
+    deriving (Eq, Show)
+
+-- | A conversion with nothing written in it.
+plain :: Conversion
+plain = Conversion 0 absent absent
+
+has :: Conversion -> Integer -> Bool
+has conversion flag = conversionFlags conversion .&. flag /= 0
+
+{- | The line terminator the reference writes. The native runtime writes the
+terminator of its platform; a test that compares the two reads a carriage
+return and line feed as this.
+-}
+lineTerminator :: String
+lineTerminator = "\n"
+
+-- | An integer in decimal, with a minus sign when negative.
+textOfSigned :: Integer -> String
+textOfSigned = show
+
+textOfBool :: Bool -> String
+textOfBool flag = if flag then "true" else "false"
+
+{- | The field of a conversion: what stands before the body, the body, and the
+padding that brings the two to the width. Zeros stand between the two.
+-}
+field :: Conversion -> Bool -> String -> String -> String
+field conversion zerosAllowed prefix body
+    | has conversion flagLeft = prefix ++ body ++ replicate padding ' '
+    | zerosAllowed && has conversion flagZero = prefix ++ replicate padding '0' ++ body
+    | otherwise = replicate padding ' ' ++ prefix ++ body
+    where
+        padding = max 0 (fromInteger (conversionWidth conversion) - length prefix - length body)
+
+sign :: Conversion -> Bool -> String
+sign conversion negative
+    | negative = "-"
+    | has conversion flagPlus = "+"
+    | has conversion flagSpace = " "
+    | otherwise = ""
+
+-- | Apostrophes between groups of three digits, counted from the right.
+grouped :: String -> String
+grouped digits = reverse (go (reverse digits))
+    where
+        go (a : b : c : rest@(_ : _)) = a : b : c : '\'' : go rest
+        go remaining = remaining
+
+{- | @%d@, @%u@ and @%x@. A negative number is its sign and its magnitude in
+either base.
+-}
+formatInteger :: Conversion -> Integer -> String
+formatInteger conversion value = field conversion True prefix body
+    where
+        hexadecimal = has conversion flagHexadecimal
+        magnitude = abs value
+        digits = if hexadecimal then showHex magnitude "" else show magnitude
+        body = if not hexadecimal && has conversion flagGroup then grouped digits else digits
+        prefix = sign conversion (value < 0) ++ (if hexadecimal && has conversion flagAlternate then "0x" else "")
+
+{- | @%f@ of an exact value. The flag says whether the value is negative zero,
+which a rational cannot say for itself.
+-}
+formatFloating :: Conversion -> Bool -> Rational -> String
+formatFloating conversion negativeZero value = field conversion True (sign conversion negative) body
+    where
+        negative = value < 0 || negativeZero
+        precision = if conversionPrecision conversion < 0 then 6 else conversionPrecision conversion
+        -- 'round' on a rational rounds a tie to the even integer.
+        scaled = round (abs value * 10 ^ precision) :: Integer
+        digits = show scaled
+        padded = replicate (fromInteger precision + 1 - length digits) '0' ++ digits
+        (whole, fraction) = splitAt (length padded - fromInteger precision) padded
+        integer = if has conversion flagGroup then grouped whole else whole
+        body = if precision > 0 then integer ++ "." ++ fraction else integer
+
+-- | @%s@ and @%c@: at most the precision's number of characters, when there is one.
+formatText :: Conversion -> String -> String
+formatText conversion value = field conversion False "" kept
+    where
+        kept = if conversionPrecision conversion >= 0 then take (fromInteger (conversionPrecision conversion)) value else value
+ test/ScalarWireTests.hs view
@@ -0,0 +1,328 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module ScalarWireTests (scalarWireTests) where
+
+import Data.Word (Word8)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+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
+
+scalarWireTests :: [(String, Bool)]
+scalarWireTests =
+    versionTests
+        ++ coreRoundTripTests
+        ++ corePrepRoundTripTests
+        ++ coreVerifierTests
+        ++ corePrepVerifierTests
+        ++ malformedWireTests
+
+versionTests :: [(String, Bool)]
+versionTests =
+    [ ("Core wire current version is 10", currentCoreWireVersion == CoreWireVersion 10)
+    , ("CorePrep wire current version is 8", currentWireVersion == WireVersion 8)
+    , ("Core numeric payload default is bounded", maximumCoreNumericBytes defaultCoreWireLimits == 4096)
+    , ("CorePrep numeric payload default is bounded", maximumNumericBytes defaultWireLimits == 4096)
+    ]
+
+scalarTypes :: [(String, Type)]
+scalarTypes =
+    [ ("char", named "char")
+    , ("byte", named "byte")
+    , ("short", named "short")
+    , ("long", named "long")
+    , ("int", intType)
+    , ("longint", named "longint")
+    , ("ubyte", named "ubyte")
+    , ("ushort", named "ushort")
+    , ("ulong", named "ulong")
+    , ("uint", named "uint")
+    , ("ulongint", named "ulongint")
+    , ("sfloat", named "sfloat")
+    , ("lfloat", named "lfloat")
+    , ("float", named "float")
+    , ("double", named "double")
+    ]
+
+integerTypes :: [(String, Type, Integer, Integer)]
+integerTypes =
+    [ ("char", named "char", 0, 2 ^ (32 :: Int) - 1)
+    , ("byte", named "byte", negate (2 ^ (7 :: Int)), 2 ^ (7 :: Int) - 1)
+    , ("short", named "short", negate (2 ^ (15 :: Int)), 2 ^ (15 :: Int) - 1)
+    , ("long", named "long", negate (2 ^ (31 :: Int)), 2 ^ (31 :: Int) - 1)
+    , ("int", intType, negate (2 ^ (63 :: Int)), 2 ^ (63 :: Int) - 1)
+    , ("longint", named "longint", negate (2 ^ (127 :: Int)), 2 ^ (127 :: Int) - 1)
+    , ("ubyte", named "ubyte", 0, 2 ^ (8 :: Int) - 1)
+    , ("ushort", named "ushort", 0, 2 ^ (16 :: Int) - 1)
+    , ("ulong", named "ulong", 0, 2 ^ (32 :: Int) - 1)
+    , ("uint", named "uint", 0, 2 ^ (64 :: Int) - 1)
+    , ("ulongint", named "ulongint", 0, 2 ^ (128 :: Int) - 1)
+    ]
+
+floatingTypes :: [(String, Type)]
+floatingTypes =
+    [ ("sfloat", named "sfloat")
+    , ("lfloat", named "lfloat")
+    , ("float", named "float")
+    , ("double", named "double")
+    ]
+
+named :: String -> Type
+named spelling = NamedType (QualifiedName [Identifier spelling]) []
+
+resolved :: Int -> String -> ResolvedName
+resolved identifier spelling = ResolvedName (SymbolId identifier) (Identifier spelling)
+
+literalFor :: Type -> CoreLiteral
+literalFor valueType
+    | valueType == unitType = CoreUnit
+    | valueType == boolType = CoreBoolean True
+    | valueType == stringType = CoreString "Tala λ 😀"
+    | valueType `elem` map snd floatingTypes = CoreFloating "-12345.625e-2"
+    | otherwise = CoreInteger 42
+
+coreModuleFor :: Type -> CoreLiteral -> CoreModule
+coreModuleFor valueType literal =
+    CoreModule
+        (QualifiedName [Identifier "Scalar", Identifier "Core"])
+        [CoreFunction (resolved 1 "Value") [] valueType [CoreReturn (CoreLiteral literal valueType)]]
+
+corePrepModuleFor :: Type -> CoreLiteral -> CorePrepModule
+corePrepModuleFor valueType literal =
+    CorePrepModule
+        (QualifiedName [Identifier "Scalar", Identifier "CorePrep"])
+        [ CorePrepFunction
+            (resolved 1 "Value")
+            ""
+            []
+            valueType
+            1
+            [CorePrepBlock 1 [] (CorePrepReturn (CorePrepLiteral literal valueType))]
+        ]
+        []
+
+coreRoundTrip :: Type -> Bool
+coreRoundTrip valueType =
+    let source = coreModuleFor valueType (literalFor valueType)
+     in (encodeCore defaultCoreWireLimits source >>= decodeCore defaultCoreWireLimits) == Right source
+
+corePrepRoundTrip :: Type -> Bool
+corePrepRoundTrip valueType =
+    let source = corePrepModuleFor valueType (literalFor valueType)
+     in (encodeCorePrep source >>= decodeCorePrep) == Right source
+
+coreRoundTripTests :: [(String, Bool)]
+coreRoundTripTests =
+    [ ("Core wire round-trips unit", coreRoundTrip unitType)
+    , ("Core wire round-trips bool", coreRoundTrip boolType)
+    , ("Core wire round-trips String", coreRoundTrip stringType)
+    ]
+        ++ [("Core wire round-trips " ++ name, coreRoundTrip valueType) | (name, valueType) <- scalarTypes]
+        ++ concatMap coreIntegerBoundaryRoundTrips integerTypes
+        ++ [("Core wire preserves " ++ name ++ " spelling", coreFloatingRoundTrip valueType) | (name, valueType) <- floatingTypes]
+
+corePrepRoundTripTests :: [(String, Bool)]
+corePrepRoundTripTests =
+    [ ("CorePrep wire round-trips unit", corePrepRoundTrip unitType)
+    , ("CorePrep wire round-trips bool", corePrepRoundTrip boolType)
+    , ("CorePrep wire round-trips String", corePrepRoundTrip stringType)
+    ]
+        ++ [("CorePrep wire round-trips " ++ name, corePrepRoundTrip valueType) | (name, valueType) <- scalarTypes]
+        ++ concatMap corePrepIntegerBoundaryRoundTrips integerTypes
+        ++ [ ("CorePrep wire preserves " ++ name ++ " spelling", corePrepFloatingRoundTrip valueType)
+           | (name, valueType) <- floatingTypes
+           ]
+
+coreIntegerBoundaryRoundTrips :: (String, Type, Integer, Integer) -> [(String, Bool)]
+coreIntegerBoundaryRoundTrips (name, valueType, minimumValue, maximumValue) =
+    [ ("Core wire round-trips " ++ name ++ " minimum", roundTrip minimumValue)
+    , ("Core wire round-trips " ++ name ++ " maximum", roundTrip maximumValue)
+    ]
+    where
+        roundTrip value =
+            let source = coreModuleFor valueType (CoreInteger value)
+             in (encodeCore defaultCoreWireLimits source >>= decodeCore defaultCoreWireLimits) == Right source
+
+corePrepIntegerBoundaryRoundTrips :: (String, Type, Integer, Integer) -> [(String, Bool)]
+corePrepIntegerBoundaryRoundTrips (name, valueType, minimumValue, maximumValue) =
+    [ ("CorePrep wire round-trips " ++ name ++ " minimum", roundTrip minimumValue)
+    , ("CorePrep wire round-trips " ++ name ++ " maximum", roundTrip maximumValue)
+    ]
+    where
+        roundTrip value =
+            let source = corePrepModuleFor valueType (CoreInteger value)
+             in (encodeCorePrep source >>= decodeCorePrep) == Right source
+
+coreFloatingRoundTrip :: Type -> Bool
+coreFloatingRoundTrip valueType =
+    all
+        ( \spelling ->
+            let source = coreModuleFor valueType (CoreFloating spelling)
+             in (encodeCore defaultCoreWireLimits source >>= decodeCore defaultCoreWireLimits) == Right source
+        )
+        ["0", "1.0", ".5", "5.", "-1.25", "6.022e23", "1e-9"]
+
+corePrepFloatingRoundTrip :: Type -> Bool
+corePrepFloatingRoundTrip valueType =
+    all
+        ( \spelling ->
+            let source = corePrepModuleFor valueType (CoreFloating spelling)
+             in (encodeCorePrep source >>= decodeCorePrep) == Right source
+        )
+        ["0", "1.0", ".5", "5.", "-1.25", "6.022e23", "1e-9"]
+
+hasDiagnostic :: String -> Either [Diagnostic] value -> Bool
+hasDiagnostic code result = case result of
+    Left issues -> any ((== code) . diagnosticCode) issues
+    Right _ -> False
+
+coreVerifierTests :: [(String, Bool)]
+coreVerifierTests =
+    concatMap verifyIntegerBoundaries integerTypes
+        ++ [ ("Core verifier accepts every floating scalar", all validFloating floatingTypes)
+           , ("Core verifier rejects floating payload on int", invalidCoreLiteral intType (CoreFloating "1.0"))
+           , ("Core verifier rejects integer payload on float", invalidCoreLiteral (named "float") (CoreInteger 1))
+           , ("Core verifier rejects invalid floating exponent", invalidCoreLiteral (named "double") (CoreFloating "1e"))
+           , ("Core verifier rejects String payload on bool", invalidCoreLiteral boolType (CoreString "wrong"))
+           ]
+    where
+        validFloating (_, valueType) =
+            verifyCore (coreModuleFor valueType (CoreFloating "1.25e2")) == Right (coreModuleFor valueType (CoreFloating "1.25e2"))
+
+corePrepVerifierTests :: [(String, Bool)]
+corePrepVerifierTests =
+    concatMap verifyCorePrepIntegerBoundaries integerTypes
+        ++ [ ("CorePrep verifier accepts every floating scalar", all validFloating floatingTypes)
+           , ("CorePrep verifier rejects floating payload on int", invalidCorePrepLiteral intType (CoreFloating "1.0"))
+           , ("CorePrep verifier rejects integer payload on float", invalidCorePrepLiteral (named "float") (CoreInteger 1))
+           , ("CorePrep verifier rejects invalid floating exponent", invalidCorePrepLiteral (named "double") (CoreFloating "1e"))
+           , ("CorePrep verifier rejects String payload on bool", invalidCorePrepLiteral boolType (CoreString "wrong"))
+           ,
+               ( "CorePrep verifier accepts discarded integer rounded division"
+               , validDiscardedFloorDivide (named "long") (CoreInteger 7) (CoreInteger 2)
+               )
+           ,
+               ( "CorePrep verifier accepts discarded floating rounded division"
+               , validDiscardedFloorDivide (named "float") (CoreFloating "7.8") (CoreFloating "2.0")
+               )
+           , ("CorePrep verifier rejects mismatched discarded rounded division operands", rejectsMismatchedDiscardedFloorDivide)
+           ]
+    where
+        validFloating (_, valueType) =
+            verifyCorePrep (corePrepModuleFor valueType (CoreFloating "1.25e2"))
+                == Right (corePrepModuleFor valueType (CoreFloating "1.25e2"))
+
+verifyIntegerBoundaries :: (String, Type, Integer, Integer) -> [(String, Bool)]
+verifyIntegerBoundaries (name, valueType, minimumValue, maximumValue) =
+    [ ("Core verifier accepts " ++ name ++ " minimum", valid minimumValue)
+    , ("Core verifier accepts " ++ name ++ " maximum", valid maximumValue)
+    , ("Core verifier rejects " ++ name ++ " below minimum", invalid (minimumValue - 1))
+    , ("Core verifier rejects " ++ name ++ " above maximum", invalid (maximumValue + 1))
+    ]
+    where
+        valid value = verifyCore (coreModuleFor valueType (CoreInteger value)) == Right (coreModuleFor valueType (CoreInteger value))
+        invalid value = invalidCoreLiteral valueType (CoreInteger value)
+
+verifyCorePrepIntegerBoundaries :: (String, Type, Integer, Integer) -> [(String, Bool)]
+verifyCorePrepIntegerBoundaries (name, valueType, minimumValue, maximumValue) =
+    [ ("CorePrep verifier accepts " ++ name ++ " minimum", valid minimumValue)
+    , ("CorePrep verifier accepts " ++ name ++ " maximum", valid maximumValue)
+    , ("CorePrep verifier rejects " ++ name ++ " below minimum", invalid (minimumValue - 1))
+    , ("CorePrep verifier rejects " ++ name ++ " above maximum", invalid (maximumValue + 1))
+    ]
+    where
+        valid value =
+            verifyCorePrep (corePrepModuleFor valueType (CoreInteger value))
+                == Right (corePrepModuleFor valueType (CoreInteger value))
+        invalid value = invalidCorePrepLiteral valueType (CoreInteger value)
+
+invalidCoreLiteral :: Type -> CoreLiteral -> Bool
+invalidCoreLiteral valueType literal = hasDiagnostic "TAC1029" (verifyCore (coreModuleFor valueType literal))
+
+invalidCorePrepLiteral :: Type -> CoreLiteral -> Bool
+invalidCorePrepLiteral valueType literal = hasDiagnostic "TAC0023" (verifyCorePrep (corePrepModuleFor valueType literal))
+
+validDiscardedFloorDivide :: Type -> CoreLiteral -> CoreLiteral -> Bool
+validDiscardedFloorDivide valueType left right =
+    let moduleValue = discardedFloorDivideModule valueType left valueType right
+     in verifyCorePrep moduleValue == Right moduleValue
+
+rejectsMismatchedDiscardedFloorDivide :: Bool
+rejectsMismatchedDiscardedFloorDivide =
+    hasDiagnostic
+        "TAC0019"
+        (verifyCorePrep (discardedFloorDivideModule intType (CoreInteger 7) (named "long") (CoreInteger 2)))
+
+discardedFloorDivideModule :: Type -> CoreLiteral -> Type -> CoreLiteral -> CorePrepModule
+discardedFloorDivideModule leftType left rightType right =
+    CorePrepModule
+        (QualifiedName [Identifier "Scalar", Identifier "DiscardedFloorDivide"])
+        [ CorePrepFunction
+            (resolved 1 "Run")
+            ""
+            []
+            unitType
+            1
+            [ CorePrepBlock
+                1
+                [ CorePrepEvaluate
+                    ( CorePrepPrimitive
+                        CoreFloorDivide
+                        [ CorePrepLiteral left leftType
+                        , CorePrepLiteral right rightType
+                        ]
+                    )
+                ]
+                (CorePrepReturn (CorePrepLiteral CoreUnit unitType))
+            ]
+        ]
+        []
+
+malformedWireTests :: [(String, Bool)]
+malformedWireTests =
+    [ ("Core wire rejects v2 input", rejectsCoreVersion 2)
+    , ("Core wire rejects the previous v9 schema", rejectsCoreVersion 9)
+    , ("Core wire rejects future input", rejectsCoreVersion 11)
+    , ("CorePrep wire rejects v2 input", rejectsCorePrepVersion 2)
+    , ("CorePrep wire rejects the previous v7 schema", rejectsCorePrepVersion 7)
+    , ("CorePrep wire rejects future input", rejectsCorePrepVersion 9)
+    , ("Core wire enforces numeric byte limit", coreNumericLimit)
+    , ("CorePrep wire enforces numeric byte limit", corePrepNumericLimit)
+    ]
+
+rejectsCoreVersion :: Word8 -> Bool
+rejectsCoreVersion version = case encodeCore defaultCoreWireLimits (coreModuleFor unitType CoreUnit) of
+    Left _ -> False
+    Right (_ : _ : _ : _ : _ : remaining) -> case decodeCore defaultCoreWireLimits ([0x54, 0x41, 0x43, 0x52, version] ++ remaining) of
+        Left issue -> coreWireErrorKind issue == CoreUnsupportedVersion
+        Right _ -> False
+    Right _ -> False
+
+rejectsCorePrepVersion :: Word8 -> Bool
+rejectsCorePrepVersion version = case encodeCorePrep (corePrepModuleFor unitType CoreUnit) of
+    Left _ -> False
+    Right (_ : _ : _ : _ : _ : remaining) -> case decodeCorePrep ([0x54, 0x41, 0x43, 0x50, version] ++ remaining) of
+        Left issue -> wireErrorKind issue == UnsupportedVersion
+        Right _ -> False
+    Right _ -> False
+
+coreNumericLimit :: Bool
+coreNumericLimit =
+    let limits = defaultCoreWireLimits {maximumCoreNumericBytes = 1}
+        source = coreModuleFor (named "long") (CoreInteger 256)
+     in case encodeCore limits source of
+            Left issue -> coreWireErrorKind issue == CoreLimitExceeded
+            Right _ -> False
+
+corePrepNumericLimit :: Bool
+corePrepNumericLimit =
+    let limits = defaultWireLimits {maximumNumericBytes = 1}
+        source = corePrepModuleFor (named "long") (CoreInteger 256)
+     in case encodeCorePrepWith limits source of
+            Left issue -> wireErrorKind issue == LimitExceeded
+            Right _ -> False
+ test/ShortCircuitTests.hs view
@@ -0,0 +1,479 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+module ShortCircuitTests (shortCircuitTests) where
+
+import Data.List (isPrefixOf, nub)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
+import Progmasoft.Tala.Diagnostic (Diagnostic)
+
+shortCircuitTests :: [(String, Bool)]
+shortCircuitTests =
+    [ ("logical AND lowers to a branch and join", andHasExpectedShape)
+    , ("logical OR reverses the short-circuit edge", orHasExpectedShape)
+    , ("logical primitives never survive CorePrep", noEagerLogicalPrimitives)
+    , ("AND result storage starts as false", andSeedIsFalse)
+    , ("OR result storage starts as true", orSeedIsTrue)
+    , ("short-circuit result storage is mutable Bool", resultStorageIsMutableBool)
+    , ("right operand assigns the short-circuit result", rightOperandAssignsResult)
+    , ("numeric left operands become Bool before branching", numericLeftIsBooleanized)
+    , ("numeric right operands become Bool before assignment", numericRightIsBooleanized)
+    , ("Boolean operands do not acquire redundant comparisons", booleanOperandsAvoidComparisons)
+    , ("nested conjunction creates two conditional regions", nestedAndCreatesTwoRegions)
+    , ("nested disjunction creates two conditional regions", nestedOrCreatesTwoRegions)
+    , ("mixed nested logical expressions create three regions", mixedTreeCreatesThreeRegions)
+    , ("logical expressions in bindings resume at their join", bindingContinuesAfterJoin)
+    , ("logical expressions in assignments resume at their join", assignmentContinuesAfterJoin)
+    , ("logical expressions in returns terminate the join", returnTerminatesJoin)
+    , ("logical expressions used as call arguments preserve order", callArgumentContinuesAtJoin)
+    , ("logical expressions used as a callee preserve order", calleeContinuesAtJoin)
+    , ("logical expressions used as if conditions compose CFG", conditionComposesControlFlow)
+    , ("logical expressions used as closure captures compose CFG", captureComposesControlFlow)
+    , ("every generated block id is unique", generatedBlockIdsAreUnique)
+    , ("every generated branch target exists", generatedTargetsExist)
+    , ("every short-circuit result has exactly one seed", eachResultHasOneSeed)
+    , ("every short-circuit result has exactly one conditional assignment", eachResultHasOneAssignment)
+    , ("temporary symbols remain globally unique", generatedSymbolsAreUnique)
+    , ("the CorePrep verifier accepts simple AND", verifierAcceptsAnd)
+    , ("the CorePrep verifier accepts simple OR", verifierAcceptsOr)
+    , ("the CorePrep verifier accepts nested logical control flow", verifierAcceptsNested)
+    , ("false AND keeps its right call behind a branch", andCallIsConditional)
+    , ("true OR keeps its right call behind a branch", orCallIsConditional)
+    ]
+
+andHasExpectedShape :: Bool
+andHasExpectedShape = case preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse)) of
+    Just function ->
+        length (corePrepFunctionBlocks function) == 3
+            && branchTargets function == [(1, 2)]
+            && jumpTargets function == [2]
+    Nothing -> False
+
+orHasExpectedShape :: Bool
+orHasExpectedShape = case preparedFunction (returning (logical CoreLogicalOr boolFalse boolTrue)) of
+    Just function -> branchTargets function == [(2, 1)] && jumpTargets function == [2]
+    Nothing -> False
+
+noEagerLogicalPrimitives :: Bool
+noEagerLogicalPrimitives = all (not . containsEagerLogical) (allInstructions mixedPrepared)
+
+andSeedIsFalse :: Bool
+andSeedIsFalse = seedValues andPrepared == [False]
+
+orSeedIsTrue :: Bool
+orSeedIsTrue = seedValues orPrepared == [True]
+
+resultStorageIsMutableBool :: Bool
+resultStorageIsMutableBool = case shortCircuitSeeds andPrepared of
+    [CorePrepBind _ valueType mutable _] -> valueType == boolType && mutable
+    _ -> False
+
+rightOperandAssignsResult :: Bool
+rightOperandAssignsResult = case (shortCircuitNames andPrepared, shortCircuitAssignments andPrepared) of
+    ([seed], [assigned]) -> seed == assigned
+    _ -> False
+
+numericLeftIsBooleanized :: Bool
+numericLeftIsBooleanized =
+    hasComparisonToZero intType numericPrepared
+        && all ((== boolType) . atomType) (branchConditions numericPrepared)
+
+numericRightIsBooleanized :: Bool
+numericRightIsBooleanized =
+    hasComparisonToZero floatType numericPrepared
+        && all ((== boolType) . atomType) (assignmentValues numericPrepared)
+
+booleanOperandsAvoidComparisons :: Bool
+booleanOperandsAvoidComparisons = not (any isNotEqual (allInstructions andPrepared))
+
+nestedAndCreatesTwoRegions :: Bool
+nestedAndCreatesTwoRegions = length (branchConditions nestedAndPrepared) == 2
+
+nestedOrCreatesTwoRegions :: Bool
+nestedOrCreatesTwoRegions = length (branchConditions nestedOrPrepared) == 2
+
+mixedTreeCreatesThreeRegions :: Bool
+mixedTreeCreatesThreeRegions =
+    length (branchConditions mixedPrepared) == 3
+        && length (shortCircuitSeeds mixedPrepared) == 3
+        && length (shortCircuitAssignments mixedPrepared) == 3
+
+bindingContinuesAfterJoin :: Bool
+bindingContinuesAfterJoin = case preparedFunction statements of
+    Just function -> any joinContainsBinding (corePrepFunctionBlocks function)
+    Nothing -> False
+    where
+        value = name 20 "value"
+        statements =
+            [ CoreBind (CoreBinding value boolType False (logical CoreLogicalAnd boolTrue boolFalse))
+            , CoreReturn unitLiteral
+            ]
+        joinContainsBinding block = any bindsValue (corePrepBlockInstructions block)
+        bindsValue (CorePrepBind bound _ _ _) = bound == value
+        bindsValue _ = False
+
+assignmentContinuesAfterJoin :: Bool
+assignmentContinuesAfterJoin = case preparedFunction statements of
+    Just function -> any joinContainsAssignment (corePrepFunctionBlocks function)
+    Nothing -> False
+    where
+        value = name 20 "value"
+        statements =
+            [ CoreBind (CoreBinding value boolType True boolFalse)
+            , CoreAssign value (logical CoreLogicalOr boolFalse boolTrue)
+            , CoreReturn unitLiteral
+            ]
+        joinContainsAssignment block = any assignsValue (corePrepBlockInstructions block)
+        assignsValue (CorePrepAssign assigned _) = assigned == value
+        assignsValue _ = False
+
+returnTerminatesJoin :: Bool
+returnTerminatesJoin = case preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse)) of
+    Just function -> case reverse (corePrepFunctionBlocks function) of
+        block : _ -> case corePrepBlockTerminator block of
+            CorePrepReturn value -> atomType value == boolType
+            _ -> False
+        [] -> False
+    Nothing -> False
+
+callArgumentContinuesAtJoin :: Bool
+callArgumentContinuesAtJoin = case preparedFunction (returning call) of
+    Just function -> exactlyOneCall function && callBlockIsJoin function
+    Nothing -> False
+    where
+        call = CoreApply functionVariable [logical CoreLogicalAnd boolTrue boolFalse] boolType
+
+calleeContinuesAtJoin :: Bool
+calleeContinuesAtJoin = case preparedFunction (returning call) of
+    Just function -> exactlyOneCall function && length (branchConditions (Just function)) == 1
+    Nothing -> False
+    where
+        argument = name 51 "argument"
+        callableType = FunctionType [boolType] boolType
+        callee =
+            CoreClosure
+                []
+                [(argument, boolType)]
+                boolType
+                [CoreReturn (CoreVariable argument boolType)]
+                callableType
+        call = CoreApply callee [logical CoreLogicalOr boolFalse boolTrue] boolType
+
+conditionComposesControlFlow :: Bool
+conditionComposesControlFlow = case preparedFunction statements of
+    Just function ->
+        length (branchConditions (Just function)) == 2
+            && allTargetsExist function
+    Nothing -> False
+    where
+        statements =
+            [ CoreIf
+                (logical CoreLogicalAnd boolTrue boolFalse)
+                [CoreReturn unitLiteral]
+                [CoreReturn unitLiteral]
+            ]
+
+captureComposesControlFlow :: Bool
+captureComposesControlFlow = case preparedModule statements of
+    Right moduleValue ->
+        length (corePrepModuleFunctions moduleValue) == 2
+            && length (branchConditions (preparedFunctionFromModule moduleValue)) == 1
+    Left _ -> False
+    where
+        capture = CoreCapture StrongCapture (name 40 "enabled") boolType (logical CoreLogicalOr boolFalse boolTrue)
+        closure = CoreClosure [capture] [] boolType [CoreReturn (CoreVariable (name 40 "enabled") boolType)] nullaryBoolFunction
+        statements = [CoreEvaluate closure, CoreReturn unitLiteral]
+
+generatedBlockIdsAreUnique :: Bool
+generatedBlockIdsAreUnique = case mixedPrepared of
+    Just function -> unique (map corePrepBlockId (corePrepFunctionBlocks function))
+    Nothing -> False
+
+generatedTargetsExist :: Bool
+generatedTargetsExist = maybe False allTargetsExist mixedPrepared
+
+eachResultHasOneSeed :: Bool
+eachResultHasOneSeed =
+    let names = shortCircuitNames mixedPrepared
+     in unique names && length names == 3
+
+eachResultHasOneAssignment :: Bool
+eachResultHasOneAssignment =
+    let seeds = shortCircuitNames mixedPrepared
+        assignments = shortCircuitAssignments mixedPrepared
+     in all (\seed -> count seed assignments == 1) seeds
+
+generatedSymbolsAreUnique :: Bool
+generatedSymbolsAreUnique = unique (generatedBindingSymbols mixedPrepared)
+
+verifierAcceptsAnd :: Bool
+verifierAcceptsAnd = maybe False (verifierAccepts . singletonModule) andPrepared
+
+verifierAcceptsOr :: Bool
+verifierAcceptsOr = maybe False (verifierAccepts . singletonModule) orPrepared
+
+verifierAcceptsNested :: Bool
+verifierAcceptsNested = maybe False (verifierAccepts . singletonModule) mixedPrepared
+
+andCallIsConditional :: Bool
+andCallIsConditional = callIsSeparatedFromEntry CoreLogicalAnd boolFalse
+
+orCallIsConditional :: Bool
+orCallIsConditional = callIsSeparatedFromEntry CoreLogicalOr boolTrue
+
+andPrepared :: Maybe CorePrepFunction
+andPrepared = preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse))
+
+orPrepared :: Maybe CorePrepFunction
+orPrepared = preparedFunction (returning (logical CoreLogicalOr boolFalse boolTrue))
+
+numericPrepared :: Maybe CorePrepFunction
+numericPrepared =
+    preparedFunction
+        ( returning
+            ( logical
+                CoreLogicalAnd
+                (CoreVariable (name 2 "integer") intType)
+                (CoreVariable (name 3 "floating") floatType)
+            )
+        )
+
+nestedAndPrepared :: Maybe CorePrepFunction
+nestedAndPrepared =
+    preparedFunction
+        (returning (logical CoreLogicalAnd boolTrue (logical CoreLogicalAnd boolFalse boolTrue)))
+
+nestedOrPrepared :: Maybe CorePrepFunction
+nestedOrPrepared =
+    preparedFunction
+        (returning (logical CoreLogicalOr (logical CoreLogicalOr boolFalse boolTrue) boolFalse))
+
+mixedPrepared :: Maybe CorePrepFunction
+mixedPrepared =
+    preparedFunction
+        ( returning
+            ( logical
+                CoreLogicalAnd
+                (logical CoreLogicalOr boolFalse boolTrue)
+                (logical CoreLogicalAnd boolTrue boolFalse)
+            )
+        )
+
+preparedFunction :: [CoreStatement] -> Maybe CorePrepFunction
+preparedFunction statements = case preparedModule statements of
+    Right moduleValue -> case corePrepModuleFunctions moduleValue of
+        function : _ -> Just function
+        [] -> Nothing
+    Left _ -> Nothing
+
+preparedModule :: [CoreStatement] -> Either [Diagnostic] CorePrepModule
+preparedModule statements = case prepareCore moduleValue of
+    Right prepared -> Right prepared
+    Left _ -> error "prepareCore currently cannot fail after verified Core"
+    where
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "ShortCircuit"]) [testFunction statements] [] []
+
+preparedFunctionFromModule :: CorePrepModule -> Maybe CorePrepFunction
+preparedFunctionFromModule moduleValue = case corePrepModuleFunctions moduleValue of
+    function : _ -> Just function
+    [] -> Nothing
+
+testFunction :: [CoreStatement] -> CoreFunction
+testFunction statements = CoreFunction (name 1 "Evaluate") parameters returnType statements
+    where
+        parameters =
+            [ (name 2 "integer", intType)
+            , (name 3 "floating", floatType)
+            , (name 31 "leftFunction", nullaryBoolFunction)
+            , (name 32 "rightFunction", nullaryBoolFunction)
+            , (name 50 "Predicate", FunctionType [boolType] boolType)
+            ]
+        returnType = case reverse statements of
+            CoreReturn expression : _ -> expressionType expression
+            _ -> unitType
+
+returning :: CoreExpression -> [CoreStatement]
+returning expression = [CoreReturn expression]
+
+logical :: CorePrimitive -> CoreExpression -> CoreExpression -> CoreExpression
+logical primitive left right = logicalWithType primitive left right boolType
+
+logicalWithType :: CorePrimitive -> CoreExpression -> CoreExpression -> Type -> CoreExpression
+logicalWithType primitive left right resultType = CorePrimitive primitive [left, right] resultType
+
+boolTrue :: CoreExpression
+boolTrue = CoreLiteral (CoreBoolean True) boolType
+
+boolFalse :: CoreExpression
+boolFalse = CoreLiteral (CoreBoolean False) boolType
+
+unitLiteral :: CoreExpression
+unitLiteral = CoreLiteral CoreUnit unitType
+
+floatType :: Type
+floatType = namedType "float"
+
+nullaryBoolFunction :: Type
+nullaryBoolFunction = FunctionType [] boolType
+
+functionVariable :: CoreExpression
+functionVariable = CoreVariable (name 50 "Predicate") (FunctionType [boolType] boolType)
+
+name :: Int -> String -> ResolvedName
+name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+allBlocks :: Maybe CorePrepFunction -> [CorePrepBlock]
+allBlocks = maybe [] corePrepFunctionBlocks
+
+allInstructions :: Maybe CorePrepFunction -> [CorePrepInstruction]
+allInstructions = concatMap corePrepBlockInstructions . allBlocks
+
+branchConditions :: Maybe CorePrepFunction -> [CorePrepAtom]
+branchConditions = foldMap condition . allBlocks
+    where
+        condition block = case corePrepBlockTerminator block of
+            CorePrepBranch value _ _ -> [value]
+            _ -> []
+
+branchTargets :: CorePrepFunction -> [(Int, Int)]
+branchTargets = foldMap targets . corePrepFunctionBlocks
+    where
+        targets block = case corePrepBlockTerminator block of
+            CorePrepBranch _ trueTarget falseTarget -> [(trueTarget, falseTarget)]
+            _ -> []
+
+jumpTargets :: CorePrepFunction -> [Int]
+jumpTargets = foldMap targets . corePrepFunctionBlocks
+    where
+        targets block = case corePrepBlockTerminator block of
+            CorePrepJump target -> [target]
+            _ -> []
+
+shortCircuitSeeds :: Maybe CorePrepFunction -> [CorePrepInstruction]
+shortCircuitSeeds = filter isSeed . allInstructions
+    where
+        isSeed (CorePrepBind bound valueType mutable (CorePrepCopy (CorePrepLiteral (CoreBoolean _) literalType))) =
+            isShortCircuitName bound && valueType == boolType && mutable && literalType == boolType
+        isSeed _ = False
+
+seedValues :: Maybe CorePrepFunction -> [Bool]
+seedValues = foldMap seed . shortCircuitSeeds
+    where
+        seed (CorePrepBind _ _ _ (CorePrepCopy (CorePrepLiteral (CoreBoolean value) _))) = [value]
+        seed _ = []
+
+shortCircuitNames :: Maybe CorePrepFunction -> [ResolvedName]
+shortCircuitNames = foldMap bound . shortCircuitSeeds
+    where
+        bound (CorePrepBind result _ _ _) = [result]
+        bound _ = []
+
+shortCircuitAssignments :: Maybe CorePrepFunction -> [ResolvedName]
+shortCircuitAssignments = foldMap assigned . allInstructions
+    where
+        assigned (CorePrepAssign result _) | isShortCircuitName result = [result]
+        assigned _ = []
+
+assignmentValues :: Maybe CorePrepFunction -> [CorePrepAtom]
+assignmentValues = foldMap assigned . allInstructions
+    where
+        assigned (CorePrepAssign result value) | isShortCircuitName result = [value]
+        assigned _ = []
+
+generatedBindingSymbols :: Maybe CorePrepFunction -> [SymbolId]
+generatedBindingSymbols = foldMap symbol . allInstructions
+    where
+        symbol (CorePrepBind bound _ _ _) | "$" `isPrefixOf` identifierText (resolvedSpelling bound) = [resolvedSymbol bound]
+        symbol _ = []
+
+isShortCircuitName :: ResolvedName -> Bool
+isShortCircuitName = ("$shortcircuit" `isPrefixOf`) . identifierText . resolvedSpelling
+
+containsEagerLogical :: CorePrepInstruction -> Bool
+containsEagerLogical instruction = case instruction of
+    CorePrepBind _ _ _ (CorePrepPrimitive primitive _) -> primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
+    CorePrepEvaluate (CorePrepPrimitive primitive _) -> primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
+    _ -> False
+
+isNotEqual :: CorePrepInstruction -> Bool
+isNotEqual (CorePrepBind _ _ _ (CorePrepPrimitive CoreNotEqual _)) = True
+isNotEqual _ = False
+
+hasComparisonToZero :: Type -> Maybe CorePrepFunction -> Bool
+hasComparisonToZero valueType = any comparison . allInstructions
+    where
+        comparison (CorePrepBind _ resultType _ (CorePrepPrimitive CoreNotEqual operands)) =
+            resultType == boolType
+                && any ((== valueType) . atomType) operands
+                && any isZero operands
+        comparison _ = False
+        -- The zero has the literal form of the operand type, as the native
+        -- adapter spells it: a floating operand is compared with a floating
+        -- zero, never with an integer payload under a floating type.
+        isZero (CorePrepLiteral literal literalType) =
+            literalType == valueType
+                && literal == (if valueType == floatType then CoreFloating "0" else CoreInteger 0)
+        isZero _ = False
+
+atomType :: CorePrepAtom -> Type
+atomType atom = case atom of
+    CorePrepVariable _ valueType -> valueType
+    CorePrepLiteral _ valueType -> valueType
+
+allTargetsExist :: CorePrepFunction -> Bool
+allTargetsExist function = all (`elem` ids) targets
+    where
+        blocks = corePrepFunctionBlocks function
+        ids = map corePrepBlockId blocks
+        targets = foldMap blockTargets blocks
+        blockTargets block = case corePrepBlockTerminator block of
+            CorePrepBranch _ trueTarget falseTarget -> [trueTarget, falseTarget]
+            CorePrepJump target -> [target]
+            _ -> []
+
+exactlyOneCall :: CorePrepFunction -> Bool
+exactlyOneCall function = count True (map isCall (concatMap corePrepBlockInstructions (corePrepFunctionBlocks function))) == 1
+    where
+        isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
+        isCall (CorePrepEvaluate (CorePrepCall _ _)) = True
+        isCall _ = False
+
+callBlockIsJoin :: CorePrepFunction -> Bool
+callBlockIsJoin function = case jumpTargets function of
+    target : _ ->
+        any
+            (\block -> corePrepBlockId block == target && any isCall (corePrepBlockInstructions block))
+            (corePrepFunctionBlocks function)
+    [] -> False
+    where
+        isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
+        isCall (CorePrepEvaluate (CorePrepCall _ _)) = True
+        isCall _ = False
+
+callIsSeparatedFromEntry :: CorePrimitive -> CoreExpression -> Bool
+callIsSeparatedFromEntry primitive left = case preparedFunction (returning expression) of
+    Just function ->
+        let entry = corePrepFunctionEntry function
+            callBlocks = [corePrepBlockId block | block <- corePrepFunctionBlocks function, any isCall (corePrepBlockInstructions block)]
+         in length callBlocks == 1 && callBlocks /= [entry]
+    Nothing -> False
+    where
+        expression = logical primitive left (CoreApply functionVariable [boolTrue] boolType)
+        isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
+        isCall _ = False
+
+singletonModule :: CorePrepFunction -> CorePrepModule
+singletonModule function = CorePrepModule (QualifiedName [Identifier "ShortCircuit"]) [function] []
+
+verifierAccepts :: CorePrepModule -> Bool
+verifierAccepts moduleValue = case verifyCorePrep moduleValue of
+    Right _ -> True
+    Left _ -> False
+
+unique :: (Eq a) => [a] -> Bool
+unique values = length values == length (nub values)
+
+count :: (Eq a) => a -> [a] -> Int
+count needle = length . filter (== needle)
+ test/SourceSetTests.hs view
@@ -0,0 +1,399 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module SourceSetTests (sourceSetTests) where
+
+import Control.Exception (finally)
+import Data.ByteString qualified as ByteString
+import Data.List (sort)
+import System.Directory
+    ( canonicalizePath
+    , createDirectory
+    , createDirectoryIfMissing
+    , getTemporaryDirectory
+    , removeDirectoryRecursive
+    , removeFile
+    )
+import System.FilePath (takeDirectory, (</>))
+import System.IO (hClose, openTempFile)
+import System.Info (os)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.SourceSet
+
+sourceSetTests :: [(String, IO Bool)]
+sourceSetTests =
+    [ ("source globs distinguish segments and recursive wildcards", pure globSemantics)
+    , ("source loader discovers deterministic project-relative order", discoveryOrder)
+    , ("source loader applies project-relative exclusions", exclusionPolicy)
+    , ("overlapping source roots do not duplicate compilation units", overlappingRoots)
+    , ("uppercase source extensions are not treated as .tala", caseSensitiveExtension)
+    , ("malformed UTF-8 is rejected before lexing", invalidUtf8)
+    , ("tool source discovery does not impose compiler UTF-8", encodingNeutralDiscovery)
+    , ("explicit file mode uses the strict source decoder", explicitFileDecoder)
+    , ("explicit file mode requires the exact .tala extension", explicitFileExtension)
+    , ("explicit file compilation uses a safe relative Core identity", explicitFileIdentity)
+    , ("a configured source root cannot escape the project", escapingRoot)
+    , ("an empty discovered source set is diagnosed", emptySourceSet)
+    , ("project compiler merges files that declare one namespace", namespaceMerge)
+    , ("project Core keeps every source and one owner per function", sourceOwnership)
+    , ("source ownership survives the versioned Core wire round trip", sourceOwnershipWireRoundTrip)
+    , ("project source paths use portable relative spelling", portableProjectSourcePaths)
+    , ("empty namespace source files remain in the project catalog", emptyNamespaceSourceIsRetained)
+    , ("project compiler selects entry by namespace and class", entrySelection)
+    , ("project compiler validates every namespace", validatesEveryNamespace)
+    , ("project compiler accumulates independent file diagnostics", accumulatesFileDiagnostics)
+    , ("duplicate types across physical files are rejected", duplicateAcrossFiles)
+    , ("missing entry namespaces are diagnosed", missingEntryNamespace)
+    , ("entry names must include a namespace", unqualifiedEntry)
+    , ("entry Main remains public static void across merged units", mergedEntryContract)
+    ]
+
+globSemantics :: Bool
+globSemantics =
+    and
+        [ matchesGlob "Sources/**/*.tala" "Sources/Main.tala"
+        , matchesGlob "Sources/**/*.tala" "Sources/Talac/Parser.tala"
+        , matchesGlob "Sources/Generated/**" "Sources/Generated/Nested/File.tala"
+        , matchesGlob "Sources/?ain.tala" "Sources/Main.tala"
+        , matchesGlob "Sources/*.tala" "Sources/Main.tala"
+        , not (matchesGlob "Sources/*.tala" "Sources/Nested/Main.tala")
+        , if os == "mingw32"
+            then matchesGlob "Sources/Main.tala" "Sources/main.tala"
+            else not (matchesGlob "Sources/Main.tala" "Sources/main.tala")
+        , not (matchesGlob "Tests/**" "Sources/Tests/Main.tala")
+        ]
+
+discoveryOrder :: IO Bool
+discoveryOrder = withTemporaryTree $ \root -> do
+    writeSource root "Sources/Zeta.tala" minimalSource
+    writeSource root "Sources/Alpha.tala" minimalSource
+    writeSource root "Sources/Nested/Middle.tala" minimalSource
+    result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
+    pure $ case result of
+        Right sources ->
+            map loadedSourceRelativePath sources
+                == ["Sources/Alpha.tala", "Sources/Nested/Middle.tala", "Sources/Zeta.tala"]
+        Left _ -> False
+
+exclusionPolicy :: IO Bool
+exclusionPolicy = withTemporaryTree $ \root -> do
+    writeSource root "Sources/Main.tala" minimalSource
+    writeSource root "Sources/Generated/Bindings.tala" "this would not parse"
+    writeSource root "Sources/Skip.tala" "this would not parse"
+    writeSource root "Sources/Nested/Keep.tala" minimalSource
+    result <-
+        loadSourceSet
+            ( SourceSetRequest
+                root
+                ["Sources"]
+                ["Sources/Generated/**", "Sources/Skip.tala"]
+            )
+    pure $ case result of
+        Right sources ->
+            map loadedSourceRelativePath sources
+                == ["Sources/Main.tala", "Sources/Nested/Keep.tala"]
+        Left _ -> False
+
+overlappingRoots :: IO Bool
+overlappingRoots = withTemporaryTree $ \root -> do
+    writeSource root "Sources/Main.tala" minimalSource
+    writeSource root "Sources/Nested/Helper.tala" minimalSource
+    result <- loadSourceSet (SourceSetRequest root ["Sources", "Sources/Nested"] [])
+    pure $ case result of
+        Right sources ->
+            map loadedSourceRelativePath sources
+                == ["Sources/Main.tala", "Sources/Nested/Helper.tala"]
+        Left _ -> False
+
+caseSensitiveExtension :: IO Bool
+caseSensitiveExtension = withTemporaryTree $ \root -> do
+    writeSource root "Sources/Main.TALA" minimalSource
+    result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
+    pure (hasDiagnostic "TAS0007" result)
+
+invalidUtf8 :: IO Bool
+invalidUtf8 = withTemporaryTree $ \root -> do
+    let path = root </> "Sources" </> "Invalid.tala"
+    createDirectoryIfMissing True (root </> "Sources")
+    ByteString.writeFile path (ByteString.pack [0x63, 0x6c, 0x61, 0x73, 0x73, 0x20, 0xc3, 0x28])
+    result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
+    pure (hasDiagnostic "TAS0020" result)
+
+encodingNeutralDiscovery :: IO Bool
+encodingNeutralDiscovery = withTemporaryTree $ \root -> do
+    let path = root </> "Sources" </> "Utf16.tala"
+    createDirectoryIfMissing True (root </> "Sources")
+    ByteString.writeFile path (ByteString.pack [0xff, 0xfe, 0x63, 0x00])
+    expected <- canonicalizePath path
+    result <- discoverSourceSet (SourceSetRequest root ["Sources"] [])
+    pure $ case result of
+        Right [discovered] -> discovered == expected
+        _ -> False
+
+explicitFileDecoder :: IO Bool
+explicitFileDecoder = withTemporaryTree $ \root -> do
+    let path = root </> "Türkçe.tala"
+    ByteString.writeFile path (ByteString.pack [0xc3, 0x28])
+    result <- loadSourceFile path
+    pure (hasDiagnostic "TAS0020" result)
+
+explicitFileExtension :: IO Bool
+explicitFileExtension = withTemporaryTree $ \root -> do
+    let path = root </> "Main.TALA"
+    writeFile path minimalSource
+    result <- loadSourceFile path
+    pure (hasDiagnostic "TAS0003" result)
+
+explicitFileIdentity :: IO Bool
+explicitFileIdentity = withTemporaryTree $ \root -> do
+    writeSource root "Sources/Main.tala" minimalSource
+    let path = root </> "Sources" </> "Main.tala"
+    loaded <- loadSourceFile path
+    pure $ case loaded of
+        Left _ -> False
+        Right source ->
+            loadedSourceRelativePath source == "Main.tala"
+                && case compileToCorePrep
+                    (CompilerInput (loadedSourceRelativePath source) (loadedSourceText source)) of
+                    Left _ -> False
+                    Right artifacts ->
+                        let owners = map snd (coreModuleFunctionSources (artifactOptimizedCore artifacts))
+                         in not (null owners) && all (== "Main.tala") owners
+
+escapingRoot :: IO Bool
+escapingRoot = withTemporaryTree $ \root -> do
+    let project = root </> "Project"
+        outside = root </> "Outside"
+    createDirectory project
+    createDirectory outside
+    writeSource outside "Escape.tala" minimalSource
+    result <- loadSourceSet (SourceSetRequest project ["../Outside"] [])
+    pure (hasDiagnostic "TAS0009" result)
+
+emptySourceSet :: IO Bool
+emptySourceSet = withTemporaryTree $ \root -> do
+    createDirectoryIfMissing True (root </> "Sources")
+    writeFile (root </> "Sources" </> "README.txt") "not source"
+    result <- loadSourceSet (SourceSetRequest root ["Sources"] [])
+    pure (hasDiagnostic "TAS0007" result)
+
+namespaceMerge :: IO Bool
+namespaceMerge = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        length (projectNamespaces project) == 1
+            && artifactSourceFiles (projectEntryNamespace project) == ["helper.tala", "main.tala"]
+            && length (coreModuleFunctions (projectEntryCore project)) == 2
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "main.tala" applicationMain
+            , CompilerInput "helper.tala" applicationHelper
+            ]
+
+-- Physical source ownership is carried beside semantic symbols. It must stay
+-- deterministic as files are merged, lowered, and written to the Core wire.
+sourceOwnership :: IO Bool
+sourceOwnership = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        let core = projectEntryCore project
+            prepared = artifactCorePrep (artifactFrontend (projectEntryNamespace project))
+            expectedSources = ["helper.tala", "main.tala"]
+            owners = map snd (coreModuleFunctionSources core)
+         in sort (coreModuleSourceFiles core) == expectedSources
+                && sort (corePrepModuleSourceFiles prepared) == expectedSources
+                && sort owners == expectedSources
+                && sort (map corePrepFunctionSourceFile (corePrepModuleFunctions prepared)) == expectedSources
+                && all (`elem` coreModuleSourceFiles core) owners
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "main.tala" applicationMain
+            , CompilerInput "helper.tala" applicationHelper
+            ]
+
+sourceOwnershipWireRoundTrip :: IO Bool
+sourceOwnershipWireRoundTrip = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        let core = projectEntryCore project
+         in case encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits of
+                Right decoded ->
+                    coreModuleSourceFiles decoded == coreModuleSourceFiles core
+                        && coreModuleFunctionSources decoded == coreModuleFunctionSources core
+                        && decoded == core
+                Left _ -> False
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "main.tala" applicationMain
+            , CompilerInput "helper.tala" applicationHelper
+            ]
+
+portableProjectSourcePaths :: IO Bool
+portableProjectSourcePaths = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        let core = projectEntryCore project
+            paths = coreModuleSourceFiles core ++ map snd (coreModuleFunctionSources core)
+         in paths == ["Sources/helper.tala", "Sources/main.tala", "Sources/helper.tala", "Sources/main.tala"]
+                && all (not . any (== '\\')) paths
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "Sources\\main.tala" applicationMain
+            , CompilerInput "Sources\\helper.tala" applicationHelper
+            ]
+
+emptyNamespaceSourceIsRetained :: IO Bool
+emptyNamespaceSourceIsRetained = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        let core = projectEntryCore project
+            prepared = artifactCorePrep (artifactFrontend (projectEntryNamespace project))
+         in sort (coreModuleSourceFiles core) == ["empty.tala", "helper.tala", "main.tala"]
+                && sort (corePrepModuleSourceFiles prepared) == ["empty.tala", "helper.tala", "main.tala"]
+                && length (coreModuleFunctions core) == 2
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "main.tala" applicationMain
+            , CompilerInput "helper.tala" applicationHelper
+            , CompilerInput "empty.tala" "namespace Application;"
+            ]
+
+entrySelection :: IO Bool
+entrySelection = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Right project ->
+        length (projectNamespaces project) == 2
+            && artifactNamespace (projectEntryNamespace project) == Just applicationNamespace
+            && coreModuleName (projectEntryCore project) == applicationNamespace
+    Left _ -> False
+    where
+        inputs =
+            [ CompilerInput "library.tala" "namespace Library; class Utility { public static int Value() { return 7; } }"
+            , CompilerInput "main.tala" applicationMain
+            ]
+
+validatesEveryNamespace :: IO Bool
+validatesEveryNamespace =
+    pure $
+        hasDiagnostic
+            "TAN0001"
+            ( compileProjectToCorePrep
+                applicationEntry
+                [ CompilerInput "main.tala" applicationMain
+                , CompilerInput "broken.tala" "namespace Broken; class Utility { int Value() { return missing; } }"
+                ]
+            )
+
+accumulatesFileDiagnostics :: IO Bool
+accumulatesFileDiagnostics = pure $ case compileProjectToCorePrep applicationEntry inputs of
+    Left diagnostics ->
+        length diagnostics == 2
+            && map (fmap sourceFile . diagnosticSpan) diagnostics
+                == [Just "first.tala", Just "second.tala"]
+    Right _ -> False
+    where
+        inputs =
+            [ CompilerInput "first.tala" "namespace First; class One { void Run() { @; } }"
+            , CompilerInput "second.tala" "namespace Second; class Two { void Run() { #; } }"
+            ]
+
+duplicateAcrossFiles :: IO Bool
+duplicateAcrossFiles =
+    pure $
+        hasDiagnostic
+            "TAR0001"
+            ( compileProjectToCorePrep
+                applicationEntry
+                [ CompilerInput "first.tala" applicationMain
+                , CompilerInput "second.tala" "namespace Application; class Program { public static void Main() { return; } }"
+                ]
+            )
+
+missingEntryNamespace :: IO Bool
+missingEntryNamespace =
+    pure $
+        hasDiagnostic
+            "TAE0011"
+            ( compileProjectToCorePrep
+                (QualifiedName [Identifier "Missing", Identifier "Program"])
+                [CompilerInput "main.tala" applicationMain]
+            )
+
+unqualifiedEntry :: IO Bool
+unqualifiedEntry =
+    pure $
+        hasDiagnostic
+            "TAE0001"
+            ( compileProjectToCorePrep
+                (QualifiedName [Identifier "Program"])
+                [CompilerInput "main.tala" applicationMain]
+            )
+
+mergedEntryContract :: IO Bool
+mergedEntryContract =
+    pure $
+        hasDiagnostic
+            "TAE0008"
+            ( compileProjectToCorePrep
+                applicationEntry
+                [ CompilerInput "helper.tala" applicationHelper
+                , CompilerInput
+                    "main.tala"
+                    "namespace Application; class Program { public static int Main() { return 0; } }"
+                ]
+            )
+
+applicationNamespace :: QualifiedName
+applicationNamespace = QualifiedName [Identifier "Application"]
+
+applicationEntry :: QualifiedName
+applicationEntry = QualifiedName [Identifier "Application", Identifier "Program"]
+
+applicationMain :: String
+applicationMain =
+    unlines
+        [ "namespace Application;"
+        , "class Program {"
+        , "  public static void Main() {"
+        , "    return;"
+        , "  }"
+        , "}"
+        ]
+
+applicationHelper :: String
+applicationHelper =
+    unlines
+        [ "namespace Application;"
+        , "class Helper {"
+        , "  public static int Value() {"
+        , "    return 42;"
+        , "  }"
+        , "}"
+        ]
+
+minimalSource :: String
+minimalSource = "namespace Example; class Value { public static int Get() { return 1; } }"
+
+hasDiagnostic :: String -> Either [Diagnostic] value -> Bool
+hasDiagnostic code result = case result of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+withTemporaryTree :: (FilePath -> IO Bool) -> IO Bool
+withTemporaryTree action = do
+    temporary <- getTemporaryDirectory
+    (path, handle) <- openTempFile temporary "tala-source-set-"
+    hClose handle
+    removeFile path
+    createDirectory path
+    action path `finally` removeDirectoryRecursive path
+
+writeSource :: FilePath -> FilePath -> String -> IO ()
+writeSource root relative content = do
+    let path = root </> relative
+    createDirectoryIfMissing True (takeDirectory path)
+    writeFile path content
+ test/SourceTextTests.hs view
@@ -0,0 +1,132 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module SourceTextTests (sourceTextTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.SourceText
+
+sourceTextTests :: [(String, Bool)]
+sourceTextTests =
+    [ ("lossless source fragments reconstruct mixed source", reconstructsMixedSource)
+    , ("source fragments classify every protected spelling", classifiesProtectedSpelling)
+    , ("source fragment spans cross CRLF exactly once", spansTrackCrLf)
+    , ("source mask hides structural characters in protected text", masksProtectedBraces)
+    , ("source mask retains physical line endings", maskRetainsLineEndings)
+    , ("multi-line raw strings protect every crossed line", rawStringProtectedLines)
+    , ("multi-line long comments protect every crossed line", longCommentProtectedLines)
+    , ("single-line protected fragments do not protect indentation", singleLineFragmentsRemainIndentable)
+    , ("an attached double dash begins a line comment", attachedDoubleDashIsComment)
+    , ("a double dash after a literal begins a line comment", literalDoubleDashIsComment)
+    , ("spaced double dash begins a line comment", spacedDoubleDashIsComment)
+    , ("unterminated normal strings have a lossless-scan diagnostic", rejectsUnterminatedString)
+    , ("unterminated character literals have a lossless-scan diagnostic", rejectsUnterminatedCharacter)
+    , ("unterminated raw strings have a lossless-scan diagnostic", rejectsUnterminatedRawString)
+    , ("unterminated long comments have a lossless-scan diagnostic", rejectsUnterminatedLongComment)
+    ]
+
+mixedSource :: String
+mixedSource =
+    "namespace Demo;\r\n"
+        ++ "-- ordinary { comment }\n"
+        ++ "class Program {\n"
+        ++ "  String text = \"escaped \\\" }\";\n"
+        ++ "  char marker = '}';\n"
+        ++ "  auto raw = [=[\n{ untouched }\n]=];\n"
+        ++ "  --[==[ long { comment ]=] still open ]==]\n"
+        ++ "}\n"
+
+fragmentsOf :: String -> Either [Diagnostic] [SourceFragment]
+fragmentsOf = scanSourceFragments "source-text.tala"
+
+reconstructsMixedSource :: Bool
+reconstructsMixedSource = (reconstructSource <$> fragmentsOf mixedSource) == Right mixedSource
+
+classifiesProtectedSpelling :: Bool
+classifiesProtectedSpelling = case fragmentsOf mixedSource of
+    Right fragments ->
+        all
+            (`elem` map sourceFragmentKind fragments)
+            [ CodeFragment
+            , LineCommentFragment
+            , LongCommentFragment
+            , StringLiteralFragment
+            , CharacterLiteralFragment
+            , RawStringLiteralFragment
+            ]
+    Left _ -> False
+
+spansTrackCrLf :: Bool
+spansTrackCrLf = case fragmentsOf "code\r\n\"text\"" of
+    Right [code, literal] ->
+        sourceFragmentSpan code == SourceSpan "source-text.tala" (SourcePosition 1 1) (SourcePosition 2 1)
+            && sourceFragmentSpan literal == SourceSpan "source-text.tala" (SourcePosition 2 1) (SourcePosition 2 7)
+    _ -> False
+
+masksProtectedBraces :: Bool
+masksProtectedBraces = case fragmentsOf "class A { String x = \"}\"; -- {\n}" of
+    Right fragments -> maskedSource fragments == "class A { String x =    ;     \n}"
+    Left _ -> False
+
+maskRetainsLineEndings :: Bool
+maskRetainsLineEndings = case fragmentsOf "-- a\r\n\"b\nc\"\r\n" of
+    Right fragments -> filter (`elem` "\r\n") (maskedSource fragments) == "\r\n\n\r\n"
+    Left _ -> False
+
+rawStringProtectedLines :: Bool
+rawStringProtectedLines = case fragmentsOf "auto value = [=[\n one \n two \n]=];" of
+    Right fragments -> protectedLineNumbers fragments == [1, 2, 3, 4]
+    Left _ -> False
+
+longCommentProtectedLines :: Bool
+longCommentProtectedLines = case fragmentsOf "--[=[ first\n second\n]=]\nclass A {}" of
+    Right fragments -> protectedLineNumbers fragments == [1, 2, 3]
+    Left _ -> False
+
+singleLineFragmentsRemainIndentable :: Bool
+singleLineFragmentsRemainIndentable = case fragmentsOf "\"text\" -- comment\n" of
+    Right fragments -> null (protectedLineNumbers fragments)
+    Left _ -> False
+
+attachedDoubleDashIsComment :: Bool
+attachedDoubleDashIsComment = case fragmentsOf "value--;" of
+    Right [code, comment] ->
+        sourceFragmentKind code == CodeFragment
+            && sourceFragmentText code == "value"
+            && sourceFragmentKind comment == LineCommentFragment
+            && sourceFragmentText comment == "--;"
+    _ -> False
+
+literalDoubleDashIsComment :: Bool
+literalDoubleDashIsComment = case fragmentsOf "\"value\"--;" of
+    Right [literal, comment] ->
+        sourceFragmentKind literal == StringLiteralFragment
+            && sourceFragmentKind comment == LineCommentFragment
+            && sourceFragmentText comment == "--;"
+    _ -> False
+
+spacedDoubleDashIsComment :: Bool
+spacedDoubleDashIsComment = case fragmentsOf "value -- explanation" of
+    Right [code, comment] ->
+        sourceFragmentKind code == CodeFragment
+            && sourceFragmentText code == "value "
+            && sourceFragmentKind comment == LineCommentFragment
+    _ -> False
+
+rejectsUnterminatedString :: Bool
+rejectsUnterminatedString = hasCode "TAL0101" (fragmentsOf "\"open")
+
+rejectsUnterminatedCharacter :: Bool
+rejectsUnterminatedCharacter = hasCode "TAL0102" (fragmentsOf "'x")
+
+rejectsUnterminatedRawString :: Bool
+rejectsUnterminatedRawString = hasCode "TAL0103" (fragmentsOf "[=[open")
+
+rejectsUnterminatedLongComment :: Bool
+rejectsUnterminatedLongComment = hasCode "TAL0104" (fragmentsOf "--[=[open")
+
+hasCode :: String -> Either [Diagnostic] value -> Bool
+hasCode code result = case result of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+ test/SourceWarningTests.hs view
@@ -0,0 +1,111 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Tests for the source warnings, which today are the two spellings of the
+removed decrement operator.
+
+A warning must never change what compiles: every text below that the
+compiler accepts is still accepted, and the warning only points at the place
+where @--@ became a comment.
+-}
+module SourceWarningTests (sourceWarningTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.SourceWarnings
+
+sourceWarningTests :: [(String, Bool)]
+sourceWarningTests =
+    [ ("an attached double dash after a name is warned about", positions "count--;" == [(1, 6)])
+    , ("an attached double dash after a parenthesis is warned about", positions "(count)--;" == [(1, 8)])
+    , ("an attached double dash after an index is warned about", positions "items[i]--;" == [(1, 9)])
+    , ("a leading double dash before a terminated name is warned about", positions "--count;" == [(1, 1)])
+    , ("a leading double dash tolerates blanks around the terminator", positions "  --count ;  " == [(1, 3)])
+    , ("every occurrence is reported in source order", positions "a--;\nb = 1;\n--c;" == [(1, 2), (3, 1)])
+    , ("the warning is a lexer-stage warning with its own code", attachedShape)
+    , ("the two spellings give different advice", differentAdvice)
+    , ("a spaced comment after code is not warned about", quiet "count -- one less")
+    , ("a comment on its own line is not warned about", quiet "-- count the items")
+    , ("a comment that starts with a word and continues is not warned about", quiet "--count the items;")
+    , ("commented-out code that is not a bare name is not warned about", quiet "--count = 5;")
+    , ("an attached comment after an operator is not warned about", quiet "total = left +-- right\n1;")
+    , ("a documentation comment is never a decrement spelling", quiet "value--| documented\n--!module;")
+    , ("a long comment is never a decrement spelling", quiet "value--[[ long ]] + 1; --[[name;]]")
+    , ("a double dash inside a string is not a comment at all", quiet "String text = \"count--;\";")
+    , ("a double dash inside a raw string is not a comment at all", quiet "String text = [[--count;]];")
+    , ("compound subtraction is the replacement and is not warned about", quiet "count -= 1; total = a - -b;")
+    , ("a text with a lexical error has no warnings", quiet "count--;\n\"unterminated")
+    , ("an empty text has no warnings", quiet "")
+    , ("a warned program still compiles when it is otherwise valid", warnedProgramStillCompiles)
+    , ("the warning does not turn a valid program into an error", null (lefts (compileToCorePrep validWarned)))
+    ]
+
+warnings :: String -> [Diagnostic]
+warnings = sourceWarnings "warnings.tala"
+
+-- | Line and column of each warning's start.
+positions :: String -> [(Int, Int)]
+positions text =
+    [ (sourceLine start, sourceColumn start)
+    | diagnostic <- warnings text
+    , Just spanValue <- [diagnosticSpan diagnostic]
+    , let start = sourceStart spanValue
+    ]
+
+quiet :: String -> Bool
+quiet = null . warnings
+
+attachedShape :: Bool
+attachedShape = case warnings "count--;" of
+    [Diagnostic LexerStage Warning code (Just spanValue) _] ->
+        code == decrementSpellingCode
+            && code == "TAL0009"
+            && sourceFile spanValue == "warnings.tala"
+            -- The span covers exactly the two characters of the sign.
+            && sourceColumn (sourceEnd spanValue) - sourceColumn (sourceStart spanValue) == 2
+            && sourceLine (sourceEnd spanValue) == sourceLine (sourceStart spanValue)
+    _ -> False
+
+differentAdvice :: Bool
+differentAdvice = case (warnings "count--;", warnings "--count;") of
+    ([attached], [leading]) ->
+        diagnosticMessage attached /= diagnosticMessage leading
+            && all (mentions "no decrement operator") [attached, leading]
+            && mentions "space before" attached
+            && mentions "space after" leading
+    _ -> False
+    where
+        mentions needle diagnostic = needle `isInfixOfText` diagnosticMessage diagnostic
+
+isInfixOfText :: String -> String -> Bool
+isInfixOfText needle haystack = any (startsWith needle) (suffixes haystack)
+    where
+        startsWith prefix text = take (length prefix) text == prefix
+        suffixes text = case text of
+            [] -> [[]]
+            _ : rest -> text : suffixes rest
+
+-- `value--;` leaves the name as the final expression of its block, so the
+-- program is accepted. The warning is the only sign that nothing is
+-- subtracted.
+validWarned :: CompilerInput
+validWarned =
+    CompilerInput
+        "warnings.tala"
+        ( unlines
+            [ "class Program {"
+            , "    public static int Evaluate(_ int limit) {"
+            , "        int value = limit;"
+            , "        value--;"
+            , "    }"
+            , "}"
+            ]
+        )
+
+warnedProgramStillCompiles :: Bool
+warnedProgramStillCompiles =
+    length (sourceWarnings "warnings.tala" (compilerSourceText validWarned)) == 1
+
+lefts :: Either [left] right -> [left]
+lefts = either id (const [])
+ test/SpecializationTests.hs view
@@ -0,0 +1,208 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module SpecializationTests (specializationTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Core.Specialization
+import Progmasoft.Tala.Core.Template
+
+specializationTests :: [(String, Bool)]
+specializationTests =
+    [ ("specialization preparation substitutes type and value parameters", preparesBothKinds)
+    , ("specialization preparation supports partial substitution diagnostics", rejectsPartiallyOpen)
+    , ("specialization preparation rejects an untouched generic type", rejectsOpenType)
+    , ("specialization preparation rejects duplicate type bindings", rejectsDuplicateTypes)
+    , ("specialization preparation rejects duplicate value bindings", rejectsDuplicateValues)
+    , ("specialization preparation rejects cross-kind binding conflicts", rejectsBindingKindConflict)
+    , ("specialization preparation validates the substituted result", rejectsInvalidSubstitution)
+    , ("specialization catalog starts empty", emptyCatalogContract)
+    , ("specialization catalog assigns positive insertion-order ids", insertionOrderIds)
+    , ("specialization catalog coalesces identical concrete types", coalescesIdentity)
+    , ("specialization catalog distinguishes fixed array sizes", distinguishesArraySizes)
+    , ("specialization catalog distinguishes argument order", distinguishesArgumentOrder)
+    , ("specialization lookup works by id and structural type", lookupContract)
+    , ("specialization snapshot follows id order", snapshotOrder)
+    , ("specialization batches preserve duplicate request results", batchDuplicates)
+    , ("specialization batches roll back on invalid input", batchFailureIsAtomic)
+    , ("specialization catalog rejects malformed array families", rejectsMalformedArray)
+    , ("specialization identity preserves value argument kinds", distinguishesValueKinds)
+    ]
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+named :: String -> [TemplateArgument] -> Type
+named name = NamedType (QualifiedName [Identifier name])
+
+systemArray :: [TemplateArgument] -> Type
+systemArray = NamedType (QualifiedName [Identifier "System", Identifier "Array"])
+
+typeArgument :: Type -> TemplateArgument
+typeArgument = TypeTemplateArgument
+
+integerArgument :: Integer -> TemplateArgument
+integerArgument = ValueTemplateArgument . IntegerTemplateValue
+
+booleanArgument :: Bool -> TemplateArgument
+booleanArgument = ValueTemplateArgument . BooleanTemplateValue
+
+characterArgument :: Integer -> TemplateArgument
+characterArgument = ValueTemplateArgument . CharacterTemplateValue
+
+fixedArray :: Type -> Integer -> Type
+fixedArray element size = systemArray [typeArgument element, integerArgument size]
+
+typeParameterName, valueParameterName :: ResolvedName
+typeParameterName = resolved 10 "T"
+valueParameterName = resolved 20 "N"
+
+genericArray :: Type
+genericArray =
+    systemArray
+        [ typeArgument (TypeVariable typeParameterName)
+        , ValueTemplateArgument (TemplateValueParameter valueParameterName)
+        ]
+
+preparesBothKinds :: Bool
+preparesBothKinds =
+    prepareSpecialization
+        [(SymbolId 10, stringType)]
+        [(SymbolId 20, IntegerTemplateValue 32)]
+        genericArray
+        == Right (fixedArray stringType 32)
+
+rejectsPartiallyOpen :: Bool
+rejectsPartiallyOpen =
+    prepareSpecialization [(SymbolId 10, stringType)] [] genericArray
+        == Left (OpenSpecialization [SymbolId 20])
+
+rejectsOpenType :: Bool
+rejectsOpenType =
+    prepareSpecialization [] [] genericArray
+        == Left (OpenSpecialization [SymbolId 10, SymbolId 20])
+
+rejectsDuplicateTypes :: Bool
+rejectsDuplicateTypes =
+    prepareSpecialization
+        [(SymbolId 10, intType), (SymbolId 10, stringType)]
+        []
+        (TypeVariable typeParameterName)
+        == Left (DuplicateTypeBinding [SymbolId 10])
+
+rejectsDuplicateValues :: Bool
+rejectsDuplicateValues =
+    prepareSpecialization
+        []
+        [(SymbolId 20, IntegerTemplateValue 4), (SymbolId 20, IntegerTemplateValue 8)]
+        genericArray
+        == Left (DuplicateValueBinding [SymbolId 20])
+
+rejectsBindingKindConflict :: Bool
+rejectsBindingKindConflict =
+    prepareSpecialization
+        [(SymbolId 10, intType)]
+        [(SymbolId 10, IntegerTemplateValue 4)]
+        intType
+        == Left (ConflictingBindingKinds [SymbolId 10])
+
+rejectsInvalidSubstitution :: Bool
+rejectsInvalidSubstitution =
+    case prepareSpecialization
+        [(SymbolId 10, intType)]
+        [(SymbolId 20, IntegerTemplateValue (-1))]
+        genericArray of
+        Left (InvalidSpecialization issues) ->
+            any ((== TemplateNegativeArraySize) . templateIssueKind) issues
+        _ -> False
+
+emptyCatalogContract :: Bool
+emptyCatalogContract =
+    catalogSize emptyCatalog == 0
+        && null (specializationSnapshot emptyCatalog)
+        && findSpecialization (SpecializationId 1) emptyCatalog == Nothing
+
+insertionOrderIds :: Bool
+insertionOrderIds = case internSpecializations [fixedArray intType 4, fixedArray intType 5] emptyCatalog of
+    Right ([first, second], catalog) ->
+        specializationId first == SpecializationId 1
+            && specializationId second == SpecializationId 2
+            && catalogSize catalog == 2
+    _ -> False
+
+coalescesIdentity :: Bool
+coalescesIdentity = case internSpecialization (fixedArray intType 4) emptyCatalog of
+    Right (first, True, once) -> case internSpecialization (fixedArray intType 4) once of
+        Right (second, False, twice) ->
+            first == second && once == twice && catalogSize twice == 1
+        _ -> False
+    _ -> False
+
+distinguishesArraySizes :: Bool
+distinguishesArraySizes = case internSpecializations [fixedArray intType 4, fixedArray intType 5] emptyCatalog of
+    Right ([first, second], _) -> specializationIdentity first /= specializationIdentity second
+    _ -> False
+
+distinguishesArgumentOrder :: Bool
+distinguishesArgumentOrder =
+    let first = named "Mix" [typeArgument intType, integerArgument 4]
+        second = named "Mix" [integerArgument 4, typeArgument intType]
+     in case internSpecializations [first, second] emptyCatalog of
+            Right ([left, right], catalog) ->
+                specializationIdentity left /= specializationIdentity right
+                    && catalogSize catalog == 2
+            _ -> False
+
+lookupContract :: Bool
+lookupContract = case internSpecialization (fixedArray stringType 16) emptyCatalog of
+    Right (entry, _, catalog) ->
+        findSpecialization (specializationId entry) catalog == Just entry
+            && findSpecializationByType (specializationType entry) catalog == Just entry
+            && findSpecialization (SpecializationId 99) catalog == Nothing
+    _ -> False
+
+snapshotOrder :: Bool
+snapshotOrder = case internSpecializations inputs emptyCatalog of
+    Right (entries, catalog) -> specializationSnapshot catalog == entries
+    _ -> False
+    where
+        inputs = [fixedArray intType 8, named "Flag" [booleanArgument True], named "Code" [characterArgument 65]]
+
+batchDuplicates :: Bool
+batchDuplicates = case internSpecializations [value, value, value] emptyCatalog of
+    Right (entries, catalog) ->
+        length entries == 3
+            && all ((== SpecializationId 1) . specializationId) entries
+            && catalogSize catalog == 1
+    _ -> False
+    where
+        value = fixedArray intType 64
+
+batchFailureIsAtomic :: Bool
+batchFailureIsAtomic =
+    let malformed = systemArray []
+     in case internSpecializations [fixedArray intType 4, malformed] emptyCatalog of
+            Left (InvalidSpecialization _) -> catalogSize emptyCatalog == 0
+            _ -> False
+
+rejectsMalformedArray :: Bool
+rejectsMalformedArray = case internSpecialization (systemArray [integerArgument 8]) emptyCatalog of
+    Left (InvalidSpecialization issues) ->
+        any ((== TemplateMalformedArrayFamily) . templateIssueKind) issues
+    _ -> False
+
+distinguishesValueKinds :: Bool
+distinguishesValueKinds = case internSpecializations values emptyCatalog of
+    Right (entries, catalog) ->
+        catalogSize catalog == 3
+            && length (map specializationIdentity entries) == 3
+            && allDifferent (map specializationIdentity entries)
+    _ -> False
+    where
+        values =
+            [ named "Value" [integerArgument 1]
+            , named "Value" [booleanArgument True]
+            , named "Value" [characterArgument 1]
+            ]
+        allDifferent [first, second, third] = first /= second && first /= third && second /= third
+        allDifferent _ = False
+ test/StaticMemberCoreTests.hs view
@@ -0,0 +1,513 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Core-boundary tests for selected static method identities.
+
+Semantic success alone is insufficient: each overload call must become a
+CoreApply to the selected function SymbolId, and CorePrep must preserve that
+identity when it atomizes the call.
+-}
+module StaticMemberCoreTests (staticMemberCoreTests) where
+
+import Data.List (sort)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Diagnostic
+
+staticMemberCoreTests :: [(String, Bool)]
+staticMemberCoreTests =
+    [ ("Core keeps every overload as a distinct function", coreKeepsDistinctFunctions)
+    , ("Core call target is the selected overload, not the lexical seed", coreTargetIsSelected)
+    , ("Core source metadata has one entry per overload identity", coreOwnersAreUnique)
+    , ("Core target and source owner share the same function identity", coreCallOwnerIdentityAgrees)
+    , ("CorePrep retains the exact overload target", corePrepTargetIsSelected)
+    , ("CorePrep retains the selected function declaration", corePrepFunctionIdentityIsStable)
+    , ("two calls to one overload preserve target multiplicity", repeatedCoreCallsKeepMultiplicity)
+    , ("calls to two overloads retain their distinct target identities", mixedCoreCallsKeepDistinctTargets)
+    , ("unqualified overload dispatch lowers to its declaring method", unqualifiedCoreTargetIsSelected)
+    , ("core lowering does not encode a selector as a synthesized name", coreTargetIsSourceFunction)
+    , ("overload lowering preserves argument order", coreCallArgumentsPreserveOrder)
+    , ("CorePrep preserves the argument order of the selected overload", corePrepCallArgumentsPreserveOrder)
+    , ("overload lowering preserves the chosen function type", coreCallFunctionTypeIsExact)
+    , ("overloaded methods with different owners do not alias", ownerNamesDoNotAlias)
+    , ("all Core function ids remain globally unique in the unit", allCoreFunctionIdsAreUnique)
+    , ("all CorePrep function ids remain globally unique in the unit", allCorePrepFunctionIdsAreUnique)
+    , ("CorePrep output remains verified after overload lowering", corePrepExistsAndIsWellFormed)
+    , ("Core retains one method symbol for each static overload", coreOverloadSymbolCountIsExact)
+    , ("CorePrep retains the source file for each overload", corePrepOwnerFilesAreRetained)
+    , ("overload resolution is independent of body return literals", bodyLiteralDoesNotChangeBinding)
+    , ("the selected overload identity survives a branch in its body", branchBodyDoesNotChangeBinding)
+    , ("the selected overload identity survives arithmetic around the call", arithmeticCallRetainsBinding)
+    , ("the selected overload identity survives a local initializer", initializerCallRetainsBinding)
+    , ("the selected overload identity survives a condition", conditionCallRetainsBinding)
+    , ("the selected overload identity survives a loop condition", loopCallRetainsBinding)
+    ]
+        ++ scalarCoreSelectionTests
+
+coreKeepsDistinctFunctions :: Bool
+coreKeepsDistinctFunctions = case compiled fixture of
+    Right artifacts -> case selectFunctions (artifactCore artifacts) of
+        [first, second] -> resolvedSymbol (coreFunctionName first) /= resolvedSymbol (coreFunctionName second)
+        _ -> False
+    Left _ -> False
+
+coreTargetIsSelected :: Bool
+coreTargetIsSelected = case compiled fixture of
+    Right artifacts -> matchingTargets intType (artifactCore artifacts) == namedFunctionSymbols "Select" intType (artifactCore artifacts)
+    Left _ -> False
+
+coreOwnersAreUnique :: Bool
+coreOwnersAreUnique = case compiled fixture of
+    Right artifacts ->
+        let selectedIds = map (symbolIdValue . resolvedSymbol . coreFunctionName) (selectFunctions (artifactCore artifacts))
+            owners = map fst (coreModuleFunctionSources (artifactCore artifacts))
+         in all (\identity -> count identity owners == 1) selectedIds
+    Left _ -> False
+
+coreCallOwnerIdentityAgrees :: Bool
+coreCallOwnerIdentityAgrees = case compiled fixture of
+    Right artifacts -> case matchingTargets intType (artifactCore artifacts) of
+        [target] ->
+            let moduleValue = artifactCore artifacts
+                owners = [owner | (identity, owner) <- coreModuleFunctionSources moduleValue, identity == symbolIdValue target]
+             in owners == ["overload-core.tala"]
+        _ -> False
+    Left _ -> False
+
+corePrepTargetIsSelected :: Bool
+corePrepTargetIsSelected = case compiled fixture of
+    Right artifacts ->
+        matchingCorePrepTargets intType (artifactCorePrep artifacts)
+            == namedCorePrepFunctionSymbols "Select" intType (artifactCorePrep artifacts)
+    Left _ -> False
+
+corePrepFunctionIdentityIsStable :: Bool
+corePrepFunctionIdentityIsStable = case compiled fixture of
+    Right artifacts ->
+        let coreIds = sort (map (resolvedSymbol . coreFunctionName) (selectFunctions (artifactCore artifacts)))
+            prepIds = sort (map (resolvedSymbol . corePrepFunctionName) (selectCorePrepFunctions (artifactCorePrep artifacts)))
+         in coreIds == prepIds
+    Left _ -> False
+
+repeatedCoreCallsKeepMultiplicity :: Bool
+repeatedCoreCallsKeepMultiplicity = case compiled repeatedCalls of
+    Right artifacts -> case matchingTargets intType (artifactCore artifacts) of
+        [first, second] -> first == second
+        _ -> False
+    Left _ -> False
+
+mixedCoreCallsKeepDistinctTargets :: Bool
+mixedCoreCallsKeepDistinctTargets = case compiled mixedCalls of
+    Right artifacts ->
+        let targets = matchingTargetsAny "Select" (artifactCore artifacts)
+         in length targets == 2 && length (unique targets) == 2
+    Left _ -> False
+
+unqualifiedCoreTargetIsSelected :: Bool
+unqualifiedCoreTargetIsSelected = case compiled unqualifiedCall of
+    Right artifacts ->
+        let moduleValue = artifactCore artifacts
+            targets = matchingTargets intType moduleValue
+            selected = namedFunctionSymbols "Select" intType moduleValue
+         in targets == selected && not (null targets)
+    Left _ -> False
+
+coreTargetIsSourceFunction :: Bool
+coreTargetIsSourceFunction = case compiled fixture of
+    Right artifacts ->
+        let moduleValue = artifactCore artifacts
+            targetNames = map resolvedSpelling (matchingTargetsAny "Select" moduleValue)
+            declarations = map (resolvedSpelling . coreFunctionName) (coreModuleFunctions moduleValue)
+         in not (null targetNames) && all (`elem` declarations) targetNames
+    Left _ -> False
+
+coreCallArgumentsPreserveOrder :: Bool
+coreCallArgumentsPreserveOrder = case compiled twoArguments of
+    Right artifacts -> case callArgumentsFor "Select" (artifactCore artifacts) of
+        [[CoreVariable first _, CoreVariable second _]] ->
+            resolvedSpelling first == Identifier "left"
+                && resolvedSpelling second == Identifier "right"
+                && resolvedSymbol first /= resolvedSymbol second
+        _ -> False
+    Left _ -> False
+
+corePrepCallArgumentsPreserveOrder :: Bool
+corePrepCallArgumentsPreserveOrder = case compiled twoArguments of
+    Right artifacts -> case corePrepCallArgumentsFor "Select" (artifactCorePrep artifacts) of
+        [[CorePrepVariable first _, CorePrepVariable second _]] ->
+            resolvedSpelling first == Identifier "left"
+                && resolvedSpelling second == Identifier "right"
+                && resolvedSymbol first /= resolvedSymbol second
+        _ -> False
+    Left _ -> False
+
+coreCallFunctionTypeIsExact :: Bool
+coreCallFunctionTypeIsExact = case compiled fixture of
+    Right artifacts -> case callTypesFor "Select" (artifactCore artifacts) of
+        [FunctionType [parameter] result] -> parameter == intType && result == intType
+        _ -> False
+    Left _ -> False
+
+ownerNamesDoNotAlias :: Bool
+ownerNamesDoNotAlias = case compiled twoOwners of
+    Right artifacts ->
+        let first = namedFunctions "FirstRead" (artifactCore artifacts)
+            second = namedFunctions "SecondRead" (artifactCore artifacts)
+         in case (first, second) of
+                ([left], [right]) -> resolvedSymbol (coreFunctionName left) /= resolvedSymbol (coreFunctionName right)
+                _ -> False
+    Left _ -> False
+
+allCoreFunctionIdsAreUnique :: Bool
+allCoreFunctionIdsAreUnique = case compiled fixture of
+    Right artifacts -> uniqueSymbols (map (resolvedSymbol . coreFunctionName) (coreModuleFunctions (artifactCore artifacts)))
+    Left _ -> False
+
+allCorePrepFunctionIdsAreUnique :: Bool
+allCorePrepFunctionIdsAreUnique = case compiled fixture of
+    Right artifacts -> uniqueSymbols (map (resolvedSymbol . corePrepFunctionName) (corePrepModuleFunctions (artifactCorePrep artifacts)))
+    Left _ -> False
+
+corePrepExistsAndIsWellFormed :: Bool
+corePrepExistsAndIsWellFormed = case compiled fixture of
+    Right artifacts ->
+        let prepared = artifactCorePrep artifacts
+         in not (null (corePrepModuleFunctions prepared))
+                && all ((> 0) . symbolIdValue . resolvedSymbol . corePrepFunctionName) (corePrepModuleFunctions prepared)
+                && all (not . null . corePrepFunctionBlocks) (corePrepModuleFunctions prepared)
+    Left _ -> False
+
+coreOverloadSymbolCountIsExact :: Bool
+coreOverloadSymbolCountIsExact = case compiled fixture of
+    Right artifacts -> length (selectFunctions (artifactCore artifacts)) == 2
+    Left _ -> False
+
+corePrepOwnerFilesAreRetained :: Bool
+corePrepOwnerFilesAreRetained = case compiled fixture of
+    Right artifacts ->
+        let prepared = corePrepModuleFunctions (artifactCorePrep artifacts)
+            selected = selectCorePrepFunctions (artifactCorePrep artifacts)
+         in length selected == 2
+                && all ((== "overload-core.tala") . corePrepFunctionSourceFile) selected
+                && all (`elem` prepared) selected
+    Left _ -> False
+
+bodyLiteralDoesNotChangeBinding :: Bool
+bodyLiteralDoesNotChangeBinding =
+    exactTargetFor
+        "class Catalog { public static int Select(long value) { if (value > 0) { return 0; } else { return 2; } } public static int Select(int value) { if (value > 0) { return 1; } else { return 3; } } } class Caller { int Invoke(int value) { return Catalog.Select(value); } }"
+        intType
+
+branchBodyDoesNotChangeBinding :: Bool
+branchBodyDoesNotChangeBinding = exactTargetFor branchFixture intType
+
+arithmeticCallRetainsBinding :: Bool
+arithmeticCallRetainsBinding =
+    exactTargetFor
+        "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 2; } } public static int Select(int value) { if (value > 0) { return value; } else { return 3; } } } class Caller { int Invoke(int value) { return Catalog.Select(value) + 1; } }"
+        intType
+
+initializerCallRetainsBinding :: Bool
+initializerCallRetainsBinding =
+    exactTargetFor
+        "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 2; } } public static int Select(int value) { if (value > 0) { return value; } else { return 3; } } } class Caller { int Invoke(int value) { int result = Catalog.Select(value); return result; } }"
+        intType
+
+conditionCallRetainsBinding :: Bool
+conditionCallRetainsBinding =
+    exactTargetFor
+        "class Catalog { public static bool Select(long value) { if (value > 0) { return true; } else { return false; } } public static bool Select(int value) { if (value > 0) { return false; } else { return true; } } } class Caller { int Invoke(int value) { if (Catalog.Select(value)) { return 1; } else { return 0; } } }"
+        intType
+
+loopCallRetainsBinding :: Bool
+loopCallRetainsBinding =
+    exactTargetFor
+        "class Catalog { public static bool Select(long value) { if (value > 0) { return true; } else { return false; } } public static bool Select(int value) { if (value > 0) { return false; } else { return true; } } } class Caller { int Invoke(int value) { while (Catalog.Select(value)) { return 1; } return 0; } }"
+        intType
+
+scalarCoreSelectionTests :: [(String, Bool)]
+scalarCoreSelectionTests =
+    [ ( "Core and CorePrep bind " ++ scalarTypeName selected ++ " instead of " ++ scalarTypeName distractor
+      , exactTargetFor (scalarBranchFixture (scalarTypeName selected) (scalarTypeName distractor)) (scalarTypeToType selected)
+      )
+    | (selected, distractor) <- scalarPairs
+    ]
+
+scalarPairs :: [(ScalarType, ScalarType)]
+scalarPairs =
+    [ (selected, nextScalar selected)
+    | selected <- scalarTypes
+    ]
+    where
+        nextScalar value = case dropWhile (/= value) scalarTypes of
+            _ : following : _ -> following
+            _ -> case scalarTypes of
+                first : _ -> first
+                [] -> value
+
+exactTargetFor :: String -> Type -> Bool
+exactTargetFor source targetType = case compiled source of
+    Right artifacts ->
+        let core = artifactCore artifacts
+            prepared = artifactCorePrep artifacts
+            coreTargets = matchingTargets targetType core
+            functionSymbols = namedFunctionSymbols "Select" targetType core
+            preparedTargets = matchingCorePrepTargets targetType prepared
+            preparedFunctions = namedCorePrepFunctionSymbols "Select" targetType prepared
+         in singletonAgreement coreTargets functionSymbols
+                && singletonAgreement preparedTargets preparedFunctions
+                && coreTargets == preparedTargets
+    Left _ -> False
+
+singletonAgreement :: (Eq value) => [value] -> [value] -> Bool
+singletonAgreement [first] [second] = first == second
+singletonAgreement _ _ = False
+
+compiled :: String -> Either [Diagnostic] FrontendArtifacts
+compiled = compileToCorePrep . CompilerInput "overload-core.tala"
+
+selectFunctions :: CoreModule -> [CoreFunction]
+selectFunctions = namedFunctions "Select"
+
+namedFunctions :: String -> CoreModule -> [CoreFunction]
+namedFunctions spelling moduleValue =
+    [ function
+    | function <- coreModuleFunctions moduleValue
+    , resolvedSpelling (coreFunctionName function) == Identifier spelling
+    ]
+
+namedFunctionSymbols :: String -> Type -> CoreModule -> [SymbolId]
+namedFunctionSymbols spelling parameterType moduleValue =
+    [ resolvedSymbol (coreFunctionName function)
+    | function <- namedFunctions spelling moduleValue
+    , [(_, actualType)] <- [coreFunctionParameters function]
+    , actualType == parameterType
+    ]
+
+matchingTargets :: Type -> CoreModule -> [SymbolId]
+matchingTargets targetType moduleValue =
+    [ resolvedSymbol target
+    | target <- matchingTargetsAny "Select" moduleValue
+    , maybe False (== targetType) (callParameterType target moduleValue)
+    ]
+
+matchingTargetsAny :: String -> CoreModule -> [ResolvedName]
+matchingTargetsAny spelling moduleValue = filter ((== Identifier spelling) . resolvedSpelling) (coreCallTargets moduleValue)
+
+matchingCorePrepTargets :: Type -> CorePrepModule -> [SymbolId]
+matchingCorePrepTargets targetType moduleValue =
+    [ resolvedSymbol target
+    | target <- corePrepCallTargets moduleValue
+    , maybe False (== targetType) (corePrepParameterType target moduleValue)
+    ]
+
+namedCorePrepFunctionSymbols :: String -> Type -> CorePrepModule -> [SymbolId]
+namedCorePrepFunctionSymbols spelling parameterType moduleValue =
+    [ resolvedSymbol (corePrepFunctionName function)
+    | function <- corePrepModuleFunctions moduleValue
+    , resolvedSpelling (corePrepFunctionName function) == Identifier spelling
+    , [(_, actualType)] <- [corePrepFunctionParameters function]
+    , actualType == parameterType
+    ]
+
+callParameterType :: ResolvedName -> CoreModule -> Maybe Type
+callParameterType target moduleValue = do
+    function <- findCoreFunction target (coreModuleFunctions moduleValue)
+    (_, parameterType) <- exactlyOneValue (coreFunctionParameters function)
+    pure parameterType
+
+corePrepParameterType :: ResolvedName -> CorePrepModule -> Maybe Type
+corePrepParameterType target moduleValue = do
+    function <- findCorePrepFunction target (corePrepModuleFunctions moduleValue)
+    (_, parameterType) <- exactlyOneValue (corePrepFunctionParameters function)
+    pure parameterType
+
+findCoreFunction :: ResolvedName -> [CoreFunction] -> Maybe CoreFunction
+findCoreFunction _ [] = Nothing
+findCoreFunction requested (function : remaining)
+    | resolvedSymbol requested == resolvedSymbol (coreFunctionName function) = Just function
+    | otherwise = findCoreFunction requested remaining
+
+findCorePrepFunction :: ResolvedName -> [CorePrepFunction] -> Maybe CorePrepFunction
+findCorePrepFunction _ [] = Nothing
+findCorePrepFunction requested (function : remaining)
+    | resolvedSymbol requested == resolvedSymbol (corePrepFunctionName function) = Just function
+    | otherwise = findCorePrepFunction requested remaining
+
+callArgumentsFor :: String -> CoreModule -> [[CoreExpression]]
+callArgumentsFor spelling moduleValue = concatMap (functionCalls . coreFunctionBody) (coreModuleFunctions moduleValue)
+    where
+        functionCalls = concatMap statementCalls
+        statementCalls statement = case statement of
+            CoreBind binding -> expressionCalls (coreBindingValue binding)
+            CoreAssign _ value -> expressionCalls value
+            CoreReturn value -> expressionCalls value
+            CoreIf condition yes no -> expressionCalls condition ++ concatMap statementCalls yes ++ concatMap statementCalls no
+            CoreEvaluate value -> expressionCalls value
+            CoreWhile condition body -> expressionCalls condition ++ concatMap statementCalls body
+            CoreDoWhile body condition -> concatMap statementCalls body ++ expressionCalls condition
+            CoreFor condition updates body -> expressionCalls condition ++ concatMap statementCalls updates ++ concatMap statementCalls body
+            CoreBreak -> []
+            CoreContinue -> []
+        expressionCalls expression = case expression of
+            CoreVariable _ _ -> []
+            CoreLiteral _ _ -> []
+            CoreApply (CoreVariable target _) arguments _ ->
+                (if resolvedSpelling target == Identifier spelling then [arguments] else [])
+                    ++ concatMap expressionCalls arguments
+            CoreApply callee arguments _ -> expressionCalls callee ++ concatMap expressionCalls arguments
+            CorePrimitive _ arguments _ -> concatMap expressionCalls arguments
+            CoreLet _ _ value body _ -> expressionCalls value ++ expressionCalls body
+            CoreConditional condition whenTrue whenFalse _ -> concatMap expressionCalls [condition, whenTrue, whenFalse]
+            CoreClosure _ _ _ statements _ -> concatMap statementCalls statements
+
+callTypesFor :: String -> CoreModule -> [Type]
+callTypesFor spelling moduleValue = concatMap (functionCallTypes . coreFunctionBody) (coreModuleFunctions moduleValue)
+    where
+        functionCallTypes = concatMap statementTypes
+        statementTypes statement = case statement of
+            CoreBind binding -> expressionTypes (coreBindingValue binding)
+            CoreAssign _ value -> expressionTypes value
+            CoreReturn value -> expressionTypes value
+            CoreIf condition yes no -> expressionTypes condition ++ concatMap statementTypes yes ++ concatMap statementTypes no
+            CoreEvaluate value -> expressionTypes value
+            CoreWhile condition body -> expressionTypes condition ++ concatMap statementTypes body
+            CoreDoWhile body condition -> concatMap statementTypes body ++ expressionTypes condition
+            CoreFor condition updates body -> expressionTypes condition ++ concatMap statementTypes updates ++ concatMap statementTypes body
+            CoreBreak -> []
+            CoreContinue -> []
+        expressionTypes expression = case expression of
+            CoreVariable _ _ -> []
+            CoreLiteral _ _ -> []
+            CoreApply (CoreVariable target functionType) arguments _ ->
+                (if resolvedSpelling target == Identifier spelling then [functionType] else [])
+                    ++ concatMap expressionTypes arguments
+            CoreApply callee arguments _ -> expressionTypes callee ++ concatMap expressionTypes arguments
+            CorePrimitive _ arguments _ -> concatMap expressionTypes arguments
+            CoreLet _ _ value body _ -> expressionTypes value ++ expressionTypes body
+            CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTypes [condition, whenTrue, whenFalse]
+            CoreClosure _ _ _ statements _ -> concatMap statementTypes statements
+
+coreCallTargets :: CoreModule -> [ResolvedName]
+coreCallTargets moduleValue = concatMap (concatMap statementTargets . coreFunctionBody) (coreModuleFunctions moduleValue)
+
+statementTargets :: CoreStatement -> [ResolvedName]
+statementTargets statement = case statement of
+    CoreBind binding -> expressionTargets (coreBindingValue binding)
+    CoreAssign _ value -> expressionTargets value
+    CoreReturn value -> expressionTargets value
+    CoreIf condition yes no -> expressionTargets condition ++ concatMap statementTargets yes ++ concatMap statementTargets no
+    CoreEvaluate value -> expressionTargets value
+    CoreWhile condition body -> expressionTargets condition ++ concatMap statementTargets body
+    CoreDoWhile body condition -> concatMap statementTargets body ++ expressionTargets condition
+    CoreFor condition updates body -> expressionTargets condition ++ concatMap statementTargets updates ++ concatMap statementTargets body
+    CoreBreak -> []
+    CoreContinue -> []
+
+expressionTargets :: CoreExpression -> [ResolvedName]
+expressionTargets expression = case expression of
+    CoreVariable {} -> []
+    CoreLiteral {} -> []
+    CoreApply callee arguments _ -> calledTarget callee ++ expressionTargets callee ++ concatMap expressionTargets arguments
+    CorePrimitive _ arguments _ -> concatMap expressionTargets arguments
+    CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body
+    CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse]
+    CoreClosure _ _ _ body _ -> concatMap statementTargets body
+    where
+        calledTarget (CoreVariable name _) = [name]
+        calledTarget _ = []
+
+corePrepCallTargets :: CorePrepModule -> [ResolvedName]
+corePrepCallTargets moduleValue = concatMap (concatMap blockTargets . corePrepFunctionBlocks) (corePrepModuleFunctions moduleValue)
+    where
+        blockTargets block = concatMap instructionTargets (corePrepBlockInstructions block)
+        instructionTargets instruction = case instruction of
+            CorePrepBind _ _ _ operation -> operationTargets operation
+            CorePrepAssign _ _ -> []
+            CorePrepEvaluate operation -> operationTargets operation
+        operationTargets operation = case operation of
+            CorePrepCall (CorePrepVariable name _) _ -> [name]
+            CorePrepCall _ _ -> []
+            CorePrepCopy _ -> []
+            CorePrepPrimitive _ _ -> []
+            CorePrepMakeClosure _ _ -> []
+
+corePrepCallArgumentsFor :: String -> CorePrepModule -> [[CorePrepAtom]]
+corePrepCallArgumentsFor spelling moduleValue = concatMap (concatMap blockArguments . corePrepFunctionBlocks) (corePrepModuleFunctions moduleValue)
+    where
+        blockArguments block = concatMap instructionArguments (corePrepBlockInstructions block)
+        instructionArguments instruction = case instruction of
+            CorePrepBind _ _ _ operation -> operationArguments operation
+            CorePrepAssign _ _ -> []
+            CorePrepEvaluate operation -> operationArguments operation
+        operationArguments operation = case operation of
+            CorePrepCall (CorePrepVariable target _) arguments
+                | resolvedSpelling target == Identifier spelling -> [arguments]
+            _ -> []
+
+selectCorePrepFunctions :: CorePrepModule -> [CorePrepFunction]
+selectCorePrepFunctions moduleValue =
+    [ function
+    | function <- corePrepModuleFunctions moduleValue
+    , resolvedSpelling (corePrepFunctionName function) == Identifier "Select"
+    ]
+
+uniqueSymbols :: [SymbolId] -> Bool
+uniqueSymbols symbols = length symbols == length (unique symbols)
+
+unique :: (Eq value) => [value] -> [value]
+unique [] = []
+unique (value : remaining) = value : unique (filter (/= value) remaining)
+
+count :: (Eq value) => value -> [value] -> Int
+count requested = length . filter (== requested)
+
+exactlyOneValue :: [value] -> Maybe value
+exactlyOneValue values = case values of
+    [value] -> Just value
+    _ -> Nothing
+
+fixture :: String
+fixture =
+    "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 0; } } public static int Select(int value) { if (value > 0) { return value; } else { return 0; } } } class Caller { public static int Invoke(int value) { return Catalog.Select(value); } }"
+
+branchFixture :: String
+branchFixture =
+    "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 0; } } public static int Select(int value) { if (value > 0) { return value; } else { return 0; } } } class Caller { int Invoke(int value) { if (value > 0) { return Catalog.Select(value); } else { return 0; } } }"
+
+repeatedCalls :: String
+repeatedCalls =
+    "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 0; } } public static int Select(int value) { if (value > 0) { return value; } else { return 0; } } } class Caller { int Invoke(int value) { return Catalog.Select(value) + Catalog.Select(value); } }"
+
+mixedCalls :: String
+mixedCalls =
+    "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 0; } } public static int Select(int value) { if (value > 0) { return value; } else { return 0; } } } class Caller { int Invoke(int integer, long wider) { return Catalog.Select(integer) + Catalog.Select(wider); } }"
+
+unqualifiedCall :: String
+unqualifiedCall =
+    "class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 0; } } public static int Select(int value) { if (value > 0) { return value; } else { return 0; } } int Invoke(int value) { return Select(value); } }"
+
+twoArguments :: String
+twoArguments =
+    "class Catalog { public static int Select(int first, int second) { if (first > 0) { return second; } else { return 0; } } public static int Select(long first, long second) { if (first > 0) { return 1; } else { return 0; } } } class Caller { int Invoke(int left, int right) { return Catalog.Select(left, right); } }"
+
+twoOwners :: String
+twoOwners =
+    "class First { public static int FirstRead(int value) { if (value > 0) { return value; } else { return 0; } } } class Second { public static int SecondRead(int value) { if (value > 0) { return value; } else { return 0; } } } class Caller { int Invoke(int value) { return First.FirstRead(value) + Second.SecondRead(value); } }"
+
+scalarBranchFixture :: String -> String -> String
+scalarBranchFixture selected distractor =
+    "class Catalog { public static int Select("
+        ++ distractor
+        ++ " other) { "
+        ++ distractor
+        ++ " copy = other; if (other == copy) { return 1; } else { return 0; } } public static int Select("
+        ++ selected
+        ++ " value) { "
+        ++ selected
+        ++ " copy = value; if (value == copy) { return 2; } else { return 0; } } public static int Invoke("
+        ++ selected
+        ++ " value) { return Select(value); } }"
+ test/StaticMemberOverloadTests.hs view
@@ -0,0 +1,345 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Arity and ordered parameter-vector tests for static member overloads.
+
+The scalar cross-product is deliberately resolved through the real frontend,
+not a test-only ranking function. Every selected target is checked against
+the typed declaration that owns its SymbolId. This catches a category of bug
+that a successful parse or a return-type-only assertion would miss.
+-}
+module StaticMemberOverloadTests (staticMemberOverloadTests) where
+
+import Data.List (find)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+
+staticMemberOverloadTests :: [(String, Bool)]
+staticMemberOverloadTests =
+    [ ("a two-parameter call selects by its ordered type vector", orderedPairSelection)
+    , ("reversing overload declaration order does not change the selected vector", reversedPairOrderSelection)
+    , ("an overload may differ at the first parameter position", firstPositionSelection)
+    , ("an overload may differ at the second parameter position", secondPositionSelection)
+    , ("a three-parameter call checks all positions", threePositionSelection)
+    , ("a mismatch in each parameter position is rejected", everyPositionMismatchRejected)
+    , ("two-parameter duplicate signatures are rejected", duplicateTwoParameterSignature)
+    , ("wrong arity is diagnosed when no overload has the supplied count", wrongArityAmongOverloads)
+    , ("matching arity with a wrong ordered type vector reports type mismatch", wrongVectorReportsTypeMismatch)
+    , ("the selected return type comes from the matching declaration", selectedResultTypeIsPreserved)
+    , ("multi-argument resolution preserves method identity in the typed tree", selectedMethodIdentityIsStable)
+    ]
+        ++ pairSelectionMatrix
+        ++ triplePositionMatrix
+        ++ scalarPositionMismatchMatrix
+
+orderedPairSelection :: Bool
+orderedPairSelection =
+    selectedParameterTypes (pairProgram IntScalar LongScalar False)
+        == Just [scalarTypeToType IntScalar, scalarTypeToType LongScalar]
+
+reversedPairOrderSelection :: Bool
+reversedPairOrderSelection =
+    selectedParameterTypes (pairProgram IntScalar LongScalar True)
+        == Just [scalarTypeToType IntScalar, scalarTypeToType LongScalar]
+
+firstPositionSelection :: Bool
+firstPositionSelection =
+    selectedParameterTypes (pairProgram BooleanScalar IntScalar False) == Just [boolType, intType]
+
+secondPositionSelection :: Bool
+secondPositionSelection =
+    selectedParameterTypes (pairProgram IntScalar BooleanScalar False) == Just [intType, boolType]
+
+threePositionSelection :: Bool
+threePositionSelection =
+    selectedParameterTypes (threeParameterProgram [ByteScalar, IntScalar, DoubleScalar])
+        == Just (map scalarTypeToType [ByteScalar, IntScalar, DoubleScalar])
+
+everyPositionMismatchRejected :: Bool
+everyPositionMismatchRejected =
+    and
+        [ hasDiagnostic "TAT0009" (threeParameterMismatchProgram position)
+        | position <- [0 .. 2]
+        ]
+
+duplicateTwoParameterSignature :: Bool
+duplicateTwoParameterSignature =
+    hasDiagnostic
+        "TAT0028"
+        "class Catalog { public static int Select(int first, long second) { return 1; } public static long Select(int left, long right) { return 2; } }"
+
+wrongArityAmongOverloads :: Bool
+wrongArityAmongOverloads =
+    hasDiagnostic
+        "TAT0008"
+        "class Catalog { public static int Select(int value) { return value; } public static int Select(int first, int second) { return first; } } class Caller { int Invoke(int value) { return Catalog.Select(value, value, value); } }"
+
+wrongVectorReportsTypeMismatch :: Bool
+wrongVectorReportsTypeMismatch =
+    hasDiagnostic
+        "TAT0009"
+        "class Catalog { public static int Select(int first, long second) { return first; } public static int Select(long first, int second) { return second; } } class Caller { int Invoke(long first, long second) { return Catalog.Select(first, second); } }"
+
+selectedResultTypeIsPreserved :: Bool
+selectedResultTypeIsPreserved =
+    case selectedFunctionType resultTypeProgram of
+        Just (FunctionType [first, second] result) ->
+            first == scalarTypeToType IntScalar
+                && second == scalarTypeToType LongScalar
+                && result == stringType
+        _ -> False
+
+selectedMethodIdentityIsStable :: Bool
+selectedMethodIdentityIsStable =
+    case selectedDeclaration (pairProgram LongScalar IntScalar False) of
+        Just (target, parametersValue) ->
+            resolvedSpelling target == Identifier "Select"
+                && map parameterAnnotation parametersValue == map scalarTypeToType [LongScalar, IntScalar]
+                && resolvedSymbol target /= SymbolId 0
+        Nothing -> False
+
+pairSelectionMatrix :: [(String, Bool)]
+pairSelectionMatrix =
+    [ ( "two-parameter vector " ++ vectorName [first, second] ++ " wins in " ++ orderName reversed
+      , selectedParameterTypes (pairProgram first second reversed)
+            == Just (map scalarTypeToType [first, second])
+      )
+    | first <- scalarTypes
+    , second <- scalarTypes
+    , first /= second
+    , reversed <- [False, True]
+    ]
+
+triplePositionMatrix :: [(String, Bool)]
+triplePositionMatrix =
+    [ ( "three-parameter vector " ++ vectorName expected ++ " wins against a mismatch at position " ++ show position
+      , selectedParameterTypes (threeOverloadProgram expected distractor)
+            == Just (map scalarTypeToType expected)
+      )
+    | selected <- scalarTypes
+    , selected /= IntScalar
+    , position <- [0 .. 2]
+    , let expected = replaceAt position selected [IntScalar, IntScalar, IntScalar]
+    , let otherPosition = (position + 1) `mod` 3
+    , let distractor = replaceAt otherPosition selected expected
+    ]
+
+scalarPositionMismatchMatrix :: [(String, Bool)]
+scalarPositionMismatchMatrix =
+    [ ( "three-parameter candidate rejects "
+            ++ scalarTypeName target
+            ++ " versus "
+            ++ scalarTypeName actual
+            ++ " at position "
+            ++ show position
+      , hasDiagnostic "TAT0009" (typedThreeParameterProgram target actual position)
+      )
+    | target <- scalarTypes
+    , let actual = nextScalar target
+    , position <- [0 .. 2]
+    ]
+
+pairProgram :: ScalarType -> ScalarType -> Bool -> String
+pairProgram first second reverseDeclarations =
+    "class Catalog { "
+        ++ method (if reverseDeclarations then second else first) (if reverseDeclarations then first else second) "first" "second"
+        ++ method (if reverseDeclarations then first else second) (if reverseDeclarations then second else first) "left" "right"
+        ++ " } class Caller { "
+        ++ "int Invoke("
+        ++ scalarTypeName first
+        ++ " first, "
+        ++ scalarTypeName second
+        ++ " second) { return Catalog.Select(first, second); } }"
+
+threeParameterProgram :: [ScalarType] -> String
+threeParameterProgram signature =
+    "class Catalog { public static int Select("
+        ++ parameters signature ["first", "second", "third"]
+        ++ ") { return 1; } } class Caller { int Invoke("
+        ++ parameters signature ["first", "second", "third"]
+        ++ ") { return Catalog.Select(first, second, third); } }"
+
+threeOverloadProgram :: [ScalarType] -> [ScalarType] -> String
+threeOverloadProgram selected distractor =
+    "class Catalog { public static int Select("
+        ++ parameters selected ["first", "second", "third"]
+        ++ ") { return 1; } public static int Select("
+        ++ parameters distractor ["left", "middle", "right"]
+        ++ ") { return 2; } } class Caller { int Invoke("
+        ++ parameters selected ["first", "second", "third"]
+        ++ ") { return Catalog.Select(first, second, third); } }"
+
+typedThreeParameterProgram :: ScalarType -> ScalarType -> Int -> String
+typedThreeParameterProgram target actual changedPosition =
+    let expected = [IntScalar, IntScalar, IntScalar]
+        candidate = replaceAt changedPosition target expected
+        callerTypes = replaceAt changedPosition actual candidate
+     in "class Catalog { public static int Select("
+            ++ parameters candidate ["first", "second", "third"]
+            ++ ") { return 1; } } class Caller { int Invoke("
+            ++ parameters callerTypes ["first", "second", "third"]
+            ++ ") { return Catalog.Select(first, second, third); } }"
+
+threeParameterMismatchProgram :: Int -> String
+threeParameterMismatchProgram position = typedThreeParameterProgram IntScalar LongScalar position
+
+resultTypeProgram :: String
+resultTypeProgram =
+    "class Catalog { public static String Select(int first, long second) { return \"selected\"; } public static String Select(long first, int second) { return \"other\"; } } class Caller { String Invoke(int first, long second) { return Catalog.Select(first, second); } }"
+
+method :: ScalarType -> ScalarType -> String -> String -> String
+method first second firstName secondName =
+    "public static int Select("
+        ++ scalarTypeName first
+        ++ " "
+        ++ firstName
+        ++ ", "
+        ++ scalarTypeName second
+        ++ " "
+        ++ secondName
+        ++ ") { return 1; } "
+
+parameters :: [ScalarType] -> [String] -> String
+parameters types names =
+    joinComma [scalarTypeName valueType ++ " " ++ name | (valueType, name) <- zip types names]
+
+joinComma :: [String] -> String
+joinComma [] = ""
+joinComma [value] = value
+joinComma (value : remaining) = value ++ ", " ++ joinComma remaining
+
+replaceAt :: Int -> value -> [value] -> [value]
+replaceAt position replacement values =
+    [if index == position then replacement else value | (index, value) <- zip [0 ..] values]
+
+nextScalar :: ScalarType -> ScalarType
+nextScalar value = case dropWhile (/= value) scalarTypes of
+    _ : following : _ -> following
+    _ -> case scalarTypes of
+        first : _ -> first
+        [] -> value
+
+vectorName :: [ScalarType] -> String
+vectorName = joinComma . map scalarTypeName
+
+orderName :: Bool -> String
+orderName False = "forward declaration order"
+orderName True = "reverse declaration order"
+
+selectedParameterTypes :: String -> Maybe [Type]
+selectedParameterTypes source = do
+    (_, parametersValue) <- selectedDeclaration source
+    pure (map parameterAnnotation parametersValue)
+
+selectedFunctionType :: String -> Maybe Type
+selectedFunctionType source = do
+    (target, _) <- selectedDeclaration source
+    result <- either (const Nothing) Just (analyze source)
+    call <- findCall (semanticTypedAST result)
+    case call of
+        CallExpression _ (NameExpression _ name functionType) _ _
+            | resolvedSymbol name == resolvedSymbol target -> Just functionType
+        _ -> Nothing
+
+selectedDeclaration :: String -> Maybe (ResolvedName, [Parameter ResolvedName Type])
+selectedDeclaration source = do
+    result <- either (const Nothing) Just (analyze source)
+    call <- findCall (semanticTypedAST result)
+    target <- case call of
+        CallExpression _ (NameExpression _ name _) _ _ -> Just name
+        _ -> Nothing
+    declaration <- find (sameIdentity target) (methodDeclarations (semanticTypedAST result))
+    case declaration of
+        FunctionDeclaration {declarationName = name, declarationParameters = parametersValue} -> Just (name, parametersValue)
+        _ -> Nothing
+    where
+        sameIdentity target FunctionDeclaration {declarationName = candidate} = resolvedSymbol target == resolvedSymbol candidate
+        sameIdentity _ _ = False
+
+methodDeclarations :: TypedAST -> [Declaration ResolvedName Type]
+methodDeclarations (TypedAST (SyntaxTree _ declarations)) = concatMap membersOf declarations
+    where
+        membersOf declaration@TypeDeclaration {} = declaration : concatMap membersOf (typeMembers declaration)
+        membersOf declaration@TemplateTypeDeclaration {} = declaration : concatMap membersOf (typeMembers declaration)
+        membersOf declaration@FunctionDeclaration {} = [declaration]
+        membersOf EnumDeclaration {} = []
+
+findCall :: TypedAST -> Maybe (Expression ResolvedName Type)
+findCall (TypedAST (SyntaxTree _ declarations)) = find isSelected (concatMap callsInDeclaration declarations)
+    where
+        isSelected (CallExpression _ (NameExpression _ name _) _ _) = resolvedSpelling name == Identifier "Select"
+        isSelected _ = False
+
+callsInDeclaration :: Declaration ResolvedName Type -> [Expression ResolvedName Type]
+callsInDeclaration declaration = case declaration of
+    TypeDeclaration {typeMembers = members} -> concatMap callsInDeclaration members
+    TemplateTypeDeclaration {typeMembers = members} -> concatMap callsInDeclaration members
+    FunctionDeclaration {declarationBody = body} -> callsInBlock body
+    EnumDeclaration {} -> []
+
+callsInBlock :: Block ResolvedName Type -> [Expression ResolvedName Type]
+callsInBlock (Block statements) = concatMap callsInStatement statements
+
+callsInStatement :: Statement ResolvedName Type -> [Expression ResolvedName Type]
+callsInStatement statement = case statement of
+    BindingStatement _ _ _ _ _ value -> callsInExpression value
+    AssignmentStatement _ _ _ value -> callsInExpression value
+    ReturnStatement _ value -> maybe [] callsInExpression value
+    IfStatement _ condition yes no -> callsInExpression condition ++ callsInBlock yes ++ maybe [] callsInBlock no
+    WhileStatement _ condition body -> callsInExpression condition ++ callsInBlock body
+    DoWhileStatement _ body condition -> callsInBlock body ++ callsInExpression condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] callsInStatement initializer
+            ++ maybe [] callsInExpression condition
+            ++ concatMap callsInStatement updates
+            ++ callsInBlock body
+    ForEachStatement _ _ _ _ _ source body -> callsInExpression source ++ callsInBlock body
+    IncrementStatement {} -> []
+    CompoundAssignmentStatement _ _ _ _ value -> callsInExpression value
+    DiscardStatement _ value -> callsInExpression value
+    BreakStatement _ value -> maybe [] callsInExpression value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> callsInExpression condition ++ callsInBlock block
+    BlockStatement _ block -> callsInBlock block
+    ExpressionStatement _ value _ -> callsInExpression value
+
+callsInExpression :: Expression ResolvedName Type -> [Expression ResolvedName Type]
+callsInExpression expression = case expression of
+    NameExpression {} -> []
+    LiteralExpression {} -> []
+    MemberAccessExpression _ receiver _ _ -> callsInExpression receiver
+    MethodReferenceExpression _ receiver _ _ -> callsInExpression receiver
+    CallExpression _ callee arguments _ ->
+        [expression | isSelectedCall expression]
+            ++ callsInExpression callee
+            ++ concatMap callsInExpression arguments
+    UnaryExpression _ _ value _ -> callsInExpression value
+    BinaryExpression _ _ left right _ -> callsInExpression left ++ callsInExpression right
+    IsPatternExpression _ subject _ _ -> callsInExpression subject
+    ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap callsInExpression [condition, whenTrue, whenFalse]
+    CoalesceExpression _ left fallback _ -> callsInExpression left ++ callsInExpression fallback
+    AssignmentExpression _ _ _ value _ -> callsInExpression value
+    IncrementExpression {} -> []
+    LoopExpression _ loop _ -> callsInStatement loop
+    BlockExpression _ block _ -> callsInBlock block
+    MatchExpression _ subjects arms _ -> concatMap callsInExpression (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ _ _ body _ -> callsInCallableBody body
+    where
+        isSelectedCall (CallExpression _ (NameExpression _ name _) _ _) = resolvedSpelling name == Identifier "Select"
+        isSelectedCall _ = False
+
+callsInCallableBody :: CallableBody ResolvedName Type -> [Expression ResolvedName Type]
+callsInCallableBody body = case body of
+    CallableExpressionBody expression -> callsInExpression expression
+    CallableBlockBody block -> callsInBlock block
+
+analyze :: String -> Either [Diagnostic] SemanticArtifacts
+analyze = analyzeSemantics . CompilerInput "static-member-overloads.tala"
+
+hasDiagnostic :: String -> String -> Bool
+hasDiagnostic code source = case analyze source of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+ test/StaticMemberParserTests.hs view
@@ -0,0 +1,459 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Grammar-boundary tests for member selectors. These assert the parsed tree,
+not merely successful token consumption, so later stages receive an explicit
+receiver and a separately preserved member spelling.
+-}
+module StaticMemberParserTests (staticMemberParserTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+
+staticMemberParserTests :: [(String, Bool)]
+staticMemberParserTests =
+    [ ("one-level member call has a selector node", oneLevelCall)
+    , ("member call keeps its receiver as a name", receiverIsPreserved)
+    , ("member selector keeps the source member spelling", memberSpellingIsPreserved)
+    , ("method call wraps the selector rather than flattening its name", callWrapsSelector)
+    , ("qualified selectors associate from the left", selectorsAssociateLeft)
+    , ("qualified selector chain preserves all path components", selectorPathIsPreserved)
+    , ("ordinary call remains an ordinary call", ordinaryCallHasNoSelector)
+    , ("selector may be the receiver of an outer call", selectorCanBeNested)
+    , ("selector call may appear as a binding initializer", selectorBindingInitializer)
+    , ("selector call may appear in a return expression", selectorReturnExpression)
+    , ("selector call may appear below arithmetic", selectorArithmeticOperand)
+    , ("selector call may appear below a comparison", selectorComparisonOperand)
+    , ("selector call may appear in a condition", selectorCondition)
+    , ("selector call may appear in a callable expression", selectorCallableExpression)
+    , ("selector spelling is case-sensitive", selectorCaseIsPreserved)
+    , ("selector span begins at its receiver", selectorSpanBeginsAtReceiver)
+    , ("selector span ends at the selected name", selectorSpanEndsAtMember)
+    , ("call span includes its closing parenthesis", callSpanIncludesClose)
+    , ("selector after a parenthesized receiver parses", parenthesizedReceiver)
+    , ("a trailing dot without a member is rejected", trailingDotRejected)
+    , ("a dot without a receiver is a target-typed member selection", leadingDotIsTargetTyped)
+    , ("two dots without a component are rejected", repeatedDotRejected)
+    , ("a numeric token cannot be a member name", numericMemberRejected)
+    , ("a keyword cannot be a member name", keywordMemberRejected)
+    , ("a missing call close remains a parser diagnostic", missingCloseRejected)
+    , ("a missing call argument remains a parser diagnostic", missingArgumentRejected)
+    , ("a second selector after a call is retained", selectorAfterCall)
+    , ("a selector after a nested call is retained", nestedCallSelector)
+    , ("comment punctuation does not create a selector", commentIsNotSelector)
+    , ("string punctuation does not create a selector", stringIsNotSelector)
+    , ("raw string punctuation does not create a selector", rawStringIsNotSelector)
+    , ("character punctuation does not create a selector", characterIsNotSelector)
+    , ("selector expression is not an assignment target", selectorAssignmentRejected)
+    , ("selector expression can be grouped without flattening", groupedSelector)
+    , ("a selector is not confused with a namespace declaration", namespaceAndSelector)
+    , ("a selector can be nested in a second argument", secondArgumentSelector)
+    , ("multiple selector calls retain argument order", selectorArgumentsPreserved)
+    , ("zero-argument selector call has an empty argument list", zeroArgumentCall)
+    , ("member selector without a call is syntactically representable", bareSelector)
+    , ("selector paths can contain many components", longSelectorPath)
+    , ("member access remains available after unary grouping", unaryGroupedSelector)
+    , ("member access remains available under Boolean negation", negatedSelector)
+    , ("member access remains available under bitwise negation", bitwiseNegatedSelector)
+    , ("member access remains available in a nested block", nestedBlockSelector)
+    , ("member access remains available in an else block", elseBlockSelector)
+    , ("member access remains available in a while condition", whileConditionSelector)
+    , ("member access remains available in a loop body", loopBodySelector)
+    , ("a malformed selector reports the parser stage", malformedSelectorHasStage)
+    , ("a malformed selector keeps a source position", malformedSelectorHasSpan)
+    , ("a selector's member name is not resolved by the parser", selectorIsUnresolvedSyntax)
+    ]
+
+oneLevelCall :: Bool
+oneLevelCall = case parseSource "class Program { void Run() { Counter.Current(); return; } }" of
+    Right
+        ( ParsedAST
+                ( SyntaxTree
+                        _
+                        [ TypeDeclaration
+                                { typeMembers =
+                                    [ FunctionDeclaration
+                                            { declarationBody = Block (ExpressionStatement _ (CallExpression _ (MemberAccessExpression {}) [] _) True : _)
+                                            }
+                                        ]
+                                }
+                            ]
+                    )
+            ) -> True
+    _ -> False
+
+receiverIsPreserved :: Bool
+receiverIsPreserved = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
+    Just
+        (CallExpression _ (MemberAccessExpression _ (NameExpression _ (Identifier "Counter") _) (Identifier "Current") _) _ _) -> True
+    _ -> False
+
+memberSpellingIsPreserved :: Bool
+memberSpellingIsPreserved = case firstCall "class Program { void Run() { Math.Sqrt(4.0); return; } }" of
+    Just (CallExpression _ (MemberAccessExpression _ _ (Identifier "Sqrt") _) _ _) -> True
+    _ -> False
+
+callWrapsSelector :: Bool
+callWrapsSelector = case firstCall "class Program { void Run() { Service.Start(); return; } }" of
+    Just CallExpression {} -> case firstCallCallee "class Program { void Run() { Service.Start(); return; } }" of
+        Just (MemberAccessExpression {}) -> True
+        _ -> False
+    _ -> False
+
+selectorsAssociateLeft :: Bool
+selectorsAssociateLeft = case firstCallCallee "class Program { void Run() { A.B.C(); return; } }" of
+    Just
+        ( MemberAccessExpression
+                _
+                (MemberAccessExpression _ (NameExpression _ (Identifier "A") _) (Identifier "B") _)
+                (Identifier "C")
+                _
+            ) -> True
+    _ -> False
+
+selectorPathIsPreserved :: Bool
+selectorPathIsPreserved = case firstCallCallee "class Program { void Run() { One.Two.Three.Four(); return; } }" of
+    Just callee -> selectorPath callee == map Identifier ["One", "Two", "Three", "Four"]
+    _ -> False
+
+ordinaryCallHasNoSelector :: Bool
+ordinaryCallHasNoSelector = case firstCallCallee "class Program { void Run() { Execute(); return; } }" of
+    Just NameExpression {} -> True
+    _ -> False
+
+selectorCanBeNested :: Bool
+selectorCanBeNested = case firstCall "class Program { void Run() { Consume(Counter.Current()); return; } }" of
+    Just (CallExpression _ _ [CallExpression _ (MemberAccessExpression {}) [] _] _) -> True
+    _ -> False
+
+selectorBindingInitializer :: Bool
+selectorBindingInitializer = case bindingInitializer "class Program { void Run() { int value = Counter.Current(); return; } }" of
+    Just CallExpression {} -> True
+    _ -> False
+
+selectorReturnExpression :: Bool
+selectorReturnExpression = case firstExpression "class Program { int Run() { return Counter.Current(); } }" of
+    Just (CallExpression _ (MemberAccessExpression {}) [] _) -> True
+    _ -> False
+
+selectorArithmeticOperand :: Bool
+selectorArithmeticOperand = case firstExpression "class Program { int Run() { return Counter.Current() + 1; } }" of
+    Just (BinaryExpression _ Add CallExpression {} _ _) -> True
+    _ -> False
+
+selectorComparisonOperand :: Bool
+selectorComparisonOperand = case firstExpression "class Program { bool Run() { return Counter.Current() < 1; } }" of
+    Just (BinaryExpression _ LessThan CallExpression {} _ _) -> True
+    _ -> False
+
+selectorCondition :: Bool
+selectorCondition = case firstExpression "class Program { void Run() { if (Counter.Ready()) { return; } return; } }" of
+    Just CallExpression {} -> True
+    _ -> False
+
+selectorCallableExpression :: Bool
+selectorCallableExpression = case bindingInitializer "class Program { void Run() { auto action = \\() -> Counter.Current(); return; } }" of
+    Just CallableExpression {} -> True
+    _ -> False
+
+selectorCaseIsPreserved :: Bool
+selectorCaseIsPreserved =
+    selectorPathFor "class Program { void Run() { Counter.cUrReNt(); return; } }"
+        == Just [Identifier "Counter", Identifier "cUrReNt"]
+
+selectorSpanBeginsAtReceiver :: Bool
+selectorSpanBeginsAtReceiver = case firstCallCallee "class Program { void Run() { Counter.Current(); return; } }" of
+    Just expression -> sourceStart (spanOf expression) == SourcePosition 1 30
+    _ -> False
+
+selectorSpanEndsAtMember :: Bool
+selectorSpanEndsAtMember = case firstCallCallee "class Program { void Run() { Counter.Current(); return; } }" of
+    Just expression -> sourceEnd (spanOf expression) == SourcePosition 1 45
+    _ -> False
+
+callSpanIncludesClose :: Bool
+callSpanIncludesClose = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
+    Just expression -> sourceEnd (spanOf expression) == SourcePosition 1 47
+    _ -> False
+
+parenthesizedReceiver :: Bool
+parenthesizedReceiver =
+    selectorPathFor "class Program { void Run() { (Counter).Current(); return; } }"
+        == Just [Identifier "Counter", Identifier "Current"]
+
+trailingDotRejected :: Bool
+trailingDotRejected = parseSource "class Program { void Run() { Counter.; return; } }" `isLeft` True
+
+-- `.Member` selects from the enum that the context expects. The parser keeps
+-- the missing receiver as the unit literal, which no source can write; whether
+-- there is an enum to select from is the type checker's question.
+leadingDotIsTargetTyped :: Bool
+leadingDotIsTargetTyped = case firstExpression "class Program { void Run() { .Current(); return; } }" of
+    Just (CallExpression _ (MemberAccessExpression _ (LiteralExpression _ UnitLiteral _) (Identifier "Current") _) [] _) -> True
+    _ -> False
+
+repeatedDotRejected :: Bool
+repeatedDotRejected = parseSource "class Program { void Run() { Counter..Current(); return; } }" `isLeft` True
+
+numericMemberRejected :: Bool
+numericMemberRejected = parseSource "class Program { void Run() { Counter.7(); return; } }" `isLeft` True
+
+keywordMemberRejected :: Bool
+keywordMemberRejected = parseSource "class Program { void Run() { Counter.return(); return; } }" `isLeft` True
+
+missingCloseRejected :: Bool
+missingCloseRejected = parseSource "class Program { void Run() { Counter.Current(1; return; } }" `isLeft` True
+
+missingArgumentRejected :: Bool
+missingArgumentRejected = parseSource "class Program { void Run() { Counter.Current(, 1); return; } }" `isLeft` True
+
+selectorAfterCall :: Bool
+selectorAfterCall =
+    selectorPathFor "class Program { void Run() { Counter.Factory().Current(); return; } }"
+        == Just [Identifier "Counter", Identifier "Factory", Identifier "Current"]
+
+nestedCallSelector :: Bool
+nestedCallSelector =
+    selectorPathFor "class Program { void Run() { Counter.Factory(Next()).Current(); return; } }"
+        == Just [Identifier "Counter", Identifier "Factory", Identifier "Current"]
+
+commentIsNotSelector :: Bool
+commentIsNotSelector = case parseSource "class Program { void Run() { Execute(); -- Fake.Member()\n return; } }" of
+    Right _ -> True
+    _ -> False
+
+stringIsNotSelector :: Bool
+stringIsNotSelector = firstCall "class Program { void Run() { Print(\"Fake.Member()\"); return; } }" `hasArguments` 1
+
+rawStringIsNotSelector :: Bool
+rawStringIsNotSelector = firstCall "class Program { void Run() { Print([[Fake.Member()]]); return; } }" `hasArguments` 1
+
+characterIsNotSelector :: Bool
+characterIsNotSelector = firstCall "class Program { void Run() { Print('.'); return; } }" `hasArguments` 1
+
+selectorAssignmentRejected :: Bool
+selectorAssignmentRejected = parseSource "class Program { void Run() { Counter.Current = 1; return; } }" `isLeft` True
+
+groupedSelector :: Bool
+groupedSelector =
+    selectorPathFor "class Program { void Run() { (Counter.Current)(); return; } }"
+        == Just [Identifier "Counter", Identifier "Current"]
+
+namespaceAndSelector :: Bool
+namespaceAndSelector = case parseSource "namespace Demo.Core; class Program { void Run() { Counter.Current(); return; } }" of
+    Right (ParsedAST (SyntaxTree (Just (QualifiedName [Identifier "Demo", Identifier "Core"])) _)) -> True
+    _ -> False
+
+secondArgumentSelector :: Bool
+secondArgumentSelector = firstCall "class Program { void Run() { Pair(1, Counter.Current()); return; } }" `hasArguments` 2
+
+selectorArgumentsPreserved :: Bool
+selectorArgumentsPreserved = case firstCall "class Program { void Run() { Counter.Sum(1, 2); return; } }" of
+    Just
+        ( CallExpression
+                _
+                (MemberAccessExpression {})
+                [LiteralExpression _ (IntegerLiteral 1) _, LiteralExpression _ (IntegerLiteral 2) _]
+                _
+            ) -> True
+    _ -> False
+
+zeroArgumentCall :: Bool
+zeroArgumentCall = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
+    Just (CallExpression _ _ [] _) -> True
+    _ -> False
+
+bareSelector :: Bool
+bareSelector = case firstExpression "class Program { void Run() { Counter.Current; return; } }" of
+    Just (MemberAccessExpression {}) -> True
+    _ -> False
+
+longSelectorPath :: Bool
+longSelectorPath =
+    selectorPathFor "class Program { void Run() { A.B.C.D.E.F.G.H.I.J(); return; } }"
+        == Just (map Identifier ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"])
+
+unaryGroupedSelector :: Bool
+unaryGroupedSelector = case firstExpression "class Program { int Run() { return -(Counter.Current()); } }" of
+    Just (UnaryExpression _ UnaryNegate CallExpression {} _) -> True
+    _ -> False
+
+negatedSelector :: Bool
+negatedSelector = case firstExpression "class Program { bool Run() { return not Counter.Ready(); } }" of
+    Just (UnaryExpression _ LogicalNot CallExpression {} _) -> True
+    _ -> False
+
+bitwiseNegatedSelector :: Bool
+bitwiseNegatedSelector = case firstExpression "class Program { int Run() { return !Counter.Mask(); } }" of
+    Just (UnaryExpression _ BitwiseNot CallExpression {} _) -> True
+    _ -> False
+
+nestedBlockSelector :: Bool
+nestedBlockSelector = case parseSource "class Program { void Run() { if (true) { if (true) { Counter.Current(); } } return; } }" of
+    Right _ -> True
+    _ -> False
+
+elseBlockSelector :: Bool
+elseBlockSelector = case parseSource "class Program { void Run() { if (true) { return; } else { Counter.Current(); } return; } }" of
+    Right _ -> True
+    _ -> False
+
+whileConditionSelector :: Bool
+whileConditionSelector = case parseSource "class Program { void Run() { while (Counter.Ready()) { break; } return; } }" of
+    Right _ -> True
+    _ -> False
+
+loopBodySelector :: Bool
+loopBodySelector = case parseSource "class Program { void Run() { while (true) { Counter.Step(); break; } return; } }" of
+    Right _ -> True
+    _ -> False
+
+malformedSelectorHasStage :: Bool
+malformedSelectorHasStage = case parseSource "class Program { void Run() { Counter.; return; } }" of
+    Left (diagnostic : _) -> diagnosticStage diagnostic == ParserStage
+    _ -> False
+
+malformedSelectorHasSpan :: Bool
+malformedSelectorHasSpan = case parseSource "class Program { void Run() { Counter.; return; } }" of
+    Left diagnostics -> any ((/= Nothing) . diagnosticSpan) diagnostics
+    _ -> False
+
+selectorIsUnresolvedSyntax :: Bool
+selectorIsUnresolvedSyntax = case parseSource "class Program { void Run() { Counter.Current(); return; } }" of
+    Right (ParsedAST (SyntaxTree _ [TypeDeclaration {typeMembers = [FunctionDeclaration {declarationBody = body}]}])) ->
+        case blockStatements body of
+            ExpressionStatement _ (CallExpression _ (MemberAccessExpression _ _ (Identifier "Current") ()) _ ()) True : _ -> True
+            _ -> False
+    _ -> False
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource source = do
+    tokens <- runLexer defaultLexer (LexerInput "static-member-parser.tala" source)
+    runParser defaultParser (ParserInput "static-member-parser.tala" tokens)
+
+firstCall :: String -> Maybe (Expression Identifier ())
+firstCall source = case firstExpression source of
+    Just expression -> case expression of
+        call@CallExpression {} -> Just call
+        _ -> nestedCall expression
+    Nothing -> Nothing
+    where
+        nestedCall (CallExpression _ _ arguments _) = firstJust [firstCallIn value | value <- arguments]
+        nestedCall (MemberAccessExpression _ receiver _ _) = nestedCall receiver
+        nestedCall (UnaryExpression _ _ value _) = nestedCall value
+        nestedCall (BinaryExpression _ _ left right _) = firstJust [nestedCall left, nestedCall right]
+        nestedCall _ = Nothing
+
+firstCallIn :: Expression Identifier () -> Maybe (Expression Identifier ())
+firstCallIn expression = case expression of
+    call@CallExpression {} -> Just call
+    MemberAccessExpression _ receiver _ _ -> firstCallIn receiver
+    MethodReferenceExpression _ receiver _ _ -> firstCallIn receiver
+    UnaryExpression _ _ value _ -> firstCallIn value
+    BinaryExpression _ _ left right _ -> firstJust [firstCallIn left, firstCallIn right]
+    _ -> Nothing
+
+firstCallCallee :: String -> Maybe (Expression Identifier ())
+firstCallCallee source = do
+    CallExpression _ callee _ _ <- firstCall source
+    pure callee
+
+firstExpression :: String -> Maybe (Expression Identifier ())
+firstExpression source = do
+    ParsedAST (SyntaxTree _ declarations) <- either (const Nothing) Just (parseSource source)
+    declaration <- firstMember declarations
+    case declaration of
+        FunctionDeclaration {declarationBody = Block statements} -> firstExpressionStatement statements
+        _ -> Nothing
+
+firstMember :: [Declaration name annotation] -> Maybe (Declaration name annotation)
+firstMember declarations =
+    firstJust [firstMember members | TypeDeclaration {typeMembers = members} <- declarations]
+        `orElse` firstFunction declarations
+    where
+        firstFunction values = firstJust [Just declaration | declaration@FunctionDeclaration {} <- values]
+
+firstExpressionStatement :: [Statement name annotation] -> Maybe (Expression name annotation)
+firstExpressionStatement statements = firstJust (map direct statements) `orElse` firstJust (map nested statements)
+    where
+        direct statement = case statement of
+            ExpressionStatement _ expression _ -> Just expression
+            ReturnStatement _ value -> value
+            IfStatement _ condition _ _ -> Just condition
+            WhileStatement _ condition _ -> Just condition
+            DoWhileStatement _ _ condition -> Just condition
+            ForStatement _ _ condition _ _ -> condition
+            ForEachStatement _ _ _ _ _ source _ -> Just source
+            BreakStatement _ value -> value
+            _ -> Nothing
+        nested (IfStatement _ _ yes no) = firstExpressionStatement (blockStatements yes ++ maybe [] blockStatements no)
+        nested (WhileStatement _ _ body) = firstExpressionStatement (blockStatements body)
+        nested (DoWhileStatement _ body _) = firstExpressionStatement (blockStatements body)
+        nested (ForStatement _ _ _ _ body) = firstExpressionStatement (blockStatements body)
+        nested (ForEachStatement _ _ _ _ _ _ body) = firstExpressionStatement (blockStatements body)
+        nested _ = Nothing
+
+bindingInitializer :: String -> Maybe (Expression Identifier ())
+bindingInitializer source = do
+    ParsedAST (SyntaxTree _ declarations) <- either (const Nothing) Just (parseSource source)
+    findBinding declarations
+    where
+        findBinding declarations =
+            firstJust
+                [ findBlock body
+                | TypeDeclaration {typeMembers = members} <- declarations
+                , FunctionDeclaration {declarationBody = body} <- members
+                ]
+        findBlock (Block statements) = firstJust [Just value | BindingStatement _ _ _ _ _ value <- statements] `orElse` firstJust (map nested statements)
+        nested (IfStatement _ _ yes no) = findBlock (Block (blockStatements yes ++ maybe [] blockStatements no))
+        nested _ = Nothing
+
+selectorPath :: Expression Identifier annotation -> [Identifier]
+selectorPath expression = case expression of
+    NameExpression _ name _ -> [name]
+    MemberAccessExpression _ receiver member _ -> selectorPath receiver ++ [member]
+    MethodReferenceExpression _ receiver member _ -> selectorPath receiver ++ [member]
+    CallExpression _ callee _ _ -> selectorPath callee
+    _ -> []
+
+selectorPathFor :: String -> Maybe [Identifier]
+selectorPathFor source = selectorPath <$> firstCallCallee source
+
+hasArguments :: Maybe (Expression Identifier ()) -> Int -> Bool
+hasArguments (Just (CallExpression _ _ arguments _)) expected = length arguments == expected
+hasArguments _ _ = False
+
+spanOf :: Expression name annotation -> SourceSpan
+spanOf expression = case expression of
+    NameExpression spanValue _ _ -> spanValue
+    LiteralExpression spanValue _ _ -> spanValue
+    MemberAccessExpression spanValue _ _ _ -> spanValue
+    MethodReferenceExpression spanValue _ _ _ -> spanValue
+    CallExpression spanValue _ _ _ -> spanValue
+    UnaryExpression spanValue _ _ _ -> spanValue
+    BinaryExpression spanValue _ _ _ _ -> spanValue
+    IsPatternExpression spanValue _ _ _ -> spanValue
+    ConditionalExpression spanValue _ _ _ _ -> spanValue
+    CoalesceExpression spanValue _ _ _ -> spanValue
+    AssignmentExpression spanValue _ _ _ _ -> spanValue
+    IncrementExpression spanValue _ _ _ -> spanValue
+    LoopExpression spanValue _ _ -> spanValue
+    BlockExpression spanValue _ _ -> spanValue
+    MatchExpression spanValue _ _ _ -> spanValue
+    CallableExpression spanValue _ _ _ _ _ -> spanValue
+
+isLeft :: Either [a] b -> Bool -> Bool
+isLeft value expected = case value of
+    Left _ -> expected
+    Right _ -> not expected
+
+firstJust :: [Maybe value] -> Maybe value
+firstJust values = case values of
+    Just value : _ -> Just value
+    Nothing : remaining -> firstJust remaining
+    [] -> Nothing
+
+orElse :: Maybe value -> Maybe value -> Maybe value
+orElse first second = case first of Just _ -> first; Nothing -> second
+ test/StaticMemberProjectTests.hs view
@@ -0,0 +1,334 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Project-boundary tests for static member binding across physical files.
+
+The project compiler groups physical files by namespace before semantic
+analysis. These tests ensure that a caller does not depend on input file order
+and that a matching type in an unrelated namespace is not imported by spelling.
+-}
+module StaticMemberProjectTests (staticMemberProjectTests) where
+
+import Data.List (sort)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+
+staticMemberProjectTests :: [(String, IO Bool)]
+staticMemberProjectTests =
+    [ ("later physical file contributes a type to the namespace catalog", laterFileTypeIsVisible)
+    , ("static overload binding is stable when project inputs are reversed", reversedInputsKeepSelection)
+    , ("the project Core records separate identities for same-spelled overloads", projectOverloadsHaveUniqueIds)
+    , ("the selected cross-file call targets the exact parameter type", crossFileCallSelectsExactSignature)
+    , ("project Core assigns each overload to its actual source file", overloadSourceOwnersAreDistinct)
+    , ("source file path spelling remains portable for overload owners", overloadOwnerPathsArePortable)
+    , ("an unrelated namespace does not satisfy a type receiver", namespaceDoesNotLeakTypes)
+    , ("an invalid unrelated namespace is still validated", unrelatedNamespaceStillValidated)
+    , ("a type with the same simple name in two namespaces has separate identities", namespaceTypeSymbolsAreSeparate)
+    , ("project input permutation preserves successful selection", inputPermutationKeepsSelectedSignature)
+    , ("each namespace returns its own overloaded Core module", namespaceModulesKeepLocalFunctions)
+    , ("the selected namespace exposes only its own source catalog", selectedNamespaceSourcesAreLocal)
+    , ("source owner metadata names every overload exactly once", everyOverloadHasOneSourceOwner)
+    , ("project compilation rejects a missing type before overload lookup", missingTypeIsNameResolutionError)
+    , ("same namespace calls remain case-sensitive across files", crossFileMemberSpellingIsCaseSensitive)
+    , ("cross-file wrong arity preserves the overload diagnostic", crossFileArityDiagnostic)
+    , ("cross-file wrong type preserves the exact-match diagnostic", crossFileTypeDiagnostic)
+    , ("cross-file instance methods are not callable through a type", crossFileInstanceMethodRejected)
+    , ("cross-file private methods remain inaccessible", crossFilePrivateMethodRejected)
+    , ("cross-file public candidates survive private sibling filtering", crossFilePublicCandidateSurvives)
+    , ("source ownership stays stable with a namespace-only source file", namespaceOnlySourceKeepsOwners)
+    , ("all project namespaces remain represented after successful compile", everyProjectNamespaceIsRetained)
+    ]
+
+laterFileTypeIsVisible :: IO Bool
+laterFileTypeIsVisible = pure (projectCompiles [programSource, catalogSource])
+
+reversedInputsKeepSelection :: IO Bool
+reversedInputsKeepSelection = pure (projectCompiles [catalogSource, programSource])
+
+projectOverloadsHaveUniqueIds :: IO Bool
+projectOverloadsHaveUniqueIds = pure $ case compileProject [programSource, catalogSource] of
+    Right project -> case namedFunctions (Identifier "Select") (projectEntryCore project) of
+        [first, second] -> resolvedSymbol (coreFunctionName first) /= resolvedSymbol (coreFunctionName second)
+        _ -> False
+    Left _ -> False
+
+crossFileCallSelectsExactSignature :: IO Bool
+crossFileCallSelectsExactSignature = pure $ case compileProject [programSource, catalogSource] of
+    Right project ->
+        let moduleValue = projectEntryCore project
+            expected =
+                [ resolvedSymbol (coreFunctionName function)
+                | function <- namedFunctionsWithParameter (Identifier "Select") intType moduleValue
+                ]
+            actual = [resolvedSymbol target | target <- coreCallTargets moduleValue, resolvedSpelling target == Identifier "Select"]
+         in exactlyOne expected && exactlyOne actual && actual == expected
+    Left _ -> False
+
+overloadSourceOwnersAreDistinct :: IO Bool
+overloadSourceOwnersAreDistinct = pure $ case compileProject [programSource, catalogSource] of
+    Right project ->
+        let moduleValue = projectEntryCore project
+            selectedOwners =
+                [ owner
+                | function <- namedFunctions (Identifier "Select") moduleValue
+                , let functionId = symbolIdValue (resolvedSymbol (coreFunctionName function))
+                , (identity, owner) <- coreModuleFunctionSources moduleValue
+                , identity == functionId
+                ]
+         in sort selectedOwners == ["Sources/Catalog.tala", "Sources/Catalog.tala"]
+                && length selectedOwners == 2
+    Left _ -> False
+
+overloadOwnerPathsArePortable :: IO Bool
+overloadOwnerPathsArePortable = pure $ case compileProject [programSource, catalogSource] of
+    Right project ->
+        let owners = [owner | (_, owner) <- coreModuleFunctionSources (projectEntryCore project)]
+         in all (not . any (== '\\')) owners && all (not . null) owners
+    Left _ -> False
+
+namespaceDoesNotLeakTypes :: IO Bool
+namespaceDoesNotLeakTypes = pure $ hasCode "TAN0001" $ compileProjectToCorePrep entry [programSource, libraryCatalogSource]
+
+unrelatedNamespaceStillValidated :: IO Bool
+unrelatedNamespaceStillValidated =
+    pure $ hasCode "TAN0001" $ compileProjectToCorePrep entry [programSource, applicationCatalogSource, brokenLibrarySource]
+
+namespaceTypeSymbolsAreSeparate :: IO Bool
+namespaceTypeSymbolsAreSeparate = pure $ case compileProjectToCorePrep entry [programSource, applicationCatalogSource, libraryCatalogSource] of
+    Right project ->
+        let typeNames = concatMap namespaceCatalogNames (projectNamespaces project)
+         in length (filter (== Identifier "Catalog") typeNames) == 2
+    Left _ -> False
+
+inputPermutationKeepsSelectedSignature :: IO Bool
+inputPermutationKeepsSelectedSignature = pure (selectionFor [programSource, catalogSource] == selectionFor [catalogSource, programSource])
+
+namespaceModulesKeepLocalFunctions :: IO Bool
+namespaceModulesKeepLocalFunctions = pure $ case compileProjectToCorePrep entry [programSource, applicationCatalogSource, libraryCatalogSource] of
+    Right project ->
+        let modules = map (coreModuleFunctions . artifactCore . artifactFrontend) (projectNamespaces project)
+            names = map (sort . map (resolvedSpelling . coreFunctionName)) modules
+         in length modules == 2
+                && names
+                    == [ sort [Identifier "Evaluate", Identifier "Main", Identifier "Select", Identifier "Select"]
+                       , [Identifier "Select"]
+                       ]
+    Left _ -> False
+
+selectedNamespaceSourcesAreLocal :: IO Bool
+selectedNamespaceSourcesAreLocal = pure $ case compileProjectToCorePrep entry [programSource, applicationCatalogSource, libraryCatalogSource] of
+    Right project ->
+        let selected = projectEntryNamespace project
+         in sort (artifactSourceFiles selected) == ["Sources/ApplicationCatalog.tala", "Sources/Program.tala"]
+                && sort (map snd (coreModuleFunctionSources (projectEntryCore project)))
+                    == [ "Sources/ApplicationCatalog.tala"
+                       , "Sources/ApplicationCatalog.tala"
+                       , "Sources/Program.tala"
+                       , "Sources/Program.tala"
+                       ]
+    Left _ -> False
+
+everyOverloadHasOneSourceOwner :: IO Bool
+everyOverloadHasOneSourceOwner = pure $ case compileProject [programSource, catalogSource] of
+    Right project ->
+        let moduleValue = projectEntryCore project
+            overloadIds = map (symbolIdValue . resolvedSymbol . coreFunctionName) (namedFunctions (Identifier "Select") moduleValue)
+            ownerIds = map fst (coreModuleFunctionSources moduleValue)
+         in length overloadIds == 2
+                && all (\identity -> length (filter (== identity) ownerIds) == 1) overloadIds
+    Left _ -> False
+
+missingTypeIsNameResolutionError :: IO Bool
+missingTypeIsNameResolutionError = pure $ hasCode "TAN0001" $ compileProjectToCorePrep entry [programWithMissingCatalog]
+
+crossFileMemberSpellingIsCaseSensitive :: IO Bool
+crossFileMemberSpellingIsCaseSensitive = pure $ hasCode "TAT0029" $ compileProjectToCorePrep entry [programWithMember "select", catalogSource]
+
+crossFileArityDiagnostic :: IO Bool
+crossFileArityDiagnostic = pure $ hasCode "TAT0008" $ compileProjectToCorePrep entry [programWithCall "Catalog.Select()", catalogSource]
+
+crossFileTypeDiagnostic :: IO Bool
+crossFileTypeDiagnostic =
+    pure $ hasCode "TAT0009" $ compileProjectToCorePrep entry [programWithCall "Catalog.Select(\"wrong\")", catalogSource]
+
+crossFileInstanceMethodRejected :: IO Bool
+crossFileInstanceMethodRejected = pure $ hasCode "TAT0031" $ compileProjectToCorePrep entry [programSource, instanceCatalogSource]
+
+crossFilePrivateMethodRejected :: IO Bool
+crossFilePrivateMethodRejected = pure $ hasCode "TAT0033" $ compileProjectToCorePrep entry [programSource, privateCatalogSource]
+
+crossFilePublicCandidateSurvives :: IO Bool
+crossFilePublicCandidateSurvives = pure $ projectCompiles [programSource, mixedAccessCatalogSource]
+
+namespaceOnlySourceKeepsOwners :: IO Bool
+namespaceOnlySourceKeepsOwners = pure $ case compileProject [programSource, catalogSource, namespaceOnlySource] of
+    Right project ->
+        let moduleValue = projectEntryCore project
+            ownerPaths = map snd (coreModuleFunctionSources moduleValue)
+         in length (filter (== "Sources/OnlyNamespace.tala") (coreModuleSourceFiles moduleValue)) == 1
+                && "Sources/OnlyNamespace.tala" `notElem` ownerPaths
+    Left _ -> False
+
+everyProjectNamespaceIsRetained :: IO Bool
+everyProjectNamespaceIsRetained = pure $ case compileProjectToCorePrep entry [programSource, applicationCatalogSource, libraryCatalogSource] of
+    Right project -> sort (map artifactNamespace (projectNamespaces project)) == [Just entryNamespace, Just libraryNamespace]
+    Left _ -> False
+
+selectionFor :: [CompilerInput] -> Maybe Type
+selectionFor inputs = do
+    project <- either (const Nothing) Just (compileProjectToCorePrep entry inputs)
+    let targets = [target | target <- coreCallTargets (projectEntryCore project), resolvedSpelling target == Identifier "Select"]
+    target <- exactlyOneValue targets
+    function <- findFunctionBySymbol target (coreModuleFunctions (projectEntryCore project))
+    parameter <- exactlyOneValue (map snd (coreFunctionParameters function))
+    pure parameter
+
+projectCompiles :: [CompilerInput] -> Bool
+projectCompiles = either (const False) (const True) . compileProjectToCorePrep entry
+
+compileProject :: [CompilerInput] -> Either [Diagnostic] ProjectFrontendArtifacts
+compileProject = compileProjectToCorePrep entry
+
+findFunctionBySymbol :: ResolvedName -> [CoreFunction] -> Maybe CoreFunction
+findFunctionBySymbol _ [] = Nothing
+findFunctionBySymbol requested (function : remaining)
+    | resolvedSymbol requested == resolvedSymbol (coreFunctionName function) = Just function
+    | otherwise = findFunctionBySymbol requested remaining
+
+namedFunctions :: Identifier -> CoreModule -> [CoreFunction]
+namedFunctions spelling moduleValue =
+    [ function
+    | function <- coreModuleFunctions moduleValue
+    , resolvedSpelling (coreFunctionName function) == spelling
+    ]
+
+namedFunctionsWithParameter :: Identifier -> Type -> CoreModule -> [CoreFunction]
+namedFunctionsWithParameter spelling parameterType moduleValue =
+    [ function
+    | function <- namedFunctions spelling moduleValue
+    , [(_, actualType)] <- [coreFunctionParameters function]
+    , actualType == parameterType
+    ]
+
+coreCallTargets :: CoreModule -> [ResolvedName]
+coreCallTargets moduleValue = concatMap (concatMap statementTargets . coreFunctionBody) (coreModuleFunctions moduleValue)
+
+statementTargets :: CoreStatement -> [ResolvedName]
+statementTargets statement = case statement of
+    CoreBind binding -> expressionTargets (coreBindingValue binding)
+    CoreAssign _ value -> expressionTargets value
+    CoreReturn value -> expressionTargets value
+    CoreIf condition yes no -> expressionTargets condition ++ concatMap statementTargets yes ++ concatMap statementTargets no
+    CoreEvaluate value -> expressionTargets value
+    CoreWhile condition body -> expressionTargets condition ++ concatMap statementTargets body
+    CoreDoWhile body condition -> concatMap statementTargets body ++ expressionTargets condition
+    CoreFor condition updates body -> expressionTargets condition ++ concatMap statementTargets updates ++ concatMap statementTargets body
+    CoreBreak -> []
+    CoreContinue -> []
+
+expressionTargets :: CoreExpression -> [ResolvedName]
+expressionTargets expression = case expression of
+    CoreVariable {} -> []
+    CoreLiteral {} -> []
+    CoreApply callee arguments _ -> target callee ++ expressionTargets callee ++ concatMap expressionTargets arguments
+    CorePrimitive _ arguments _ -> concatMap expressionTargets arguments
+    CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body
+    CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse]
+    CoreClosure _ _ _ body _ -> concatMap statementTargets body
+    where
+        target (CoreVariable name _) = [name]
+        target _ = []
+
+namespaceCatalogNames :: NamespaceArtifacts -> [Identifier]
+namespaceCatalogNames artifacts =
+    [ resolvedSpelling (declarationName declaration)
+    | declaration <- syntaxDeclarations (typedSyntaxTree (artifactTypedAST (artifactFrontend artifacts)))
+    , case declaration of TypeDeclaration {} -> True; TemplateTypeDeclaration {} -> True; _ -> False
+    ]
+
+programSource :: CompilerInput
+programSource = programWithCall "Catalog.Select(value)"
+
+programWithCall :: String -> CompilerInput
+programWithCall call =
+    CompilerInput
+        "Sources/Program.tala"
+        ( "namespace Application; class Program { public static int Evaluate(int value) { return "
+            ++ call
+            ++ "; } public static void Main() { return; } }"
+        )
+
+programWithMember :: String -> CompilerInput
+programWithMember member = programWithCall ("Catalog." ++ member ++ "(value)")
+
+programWithMissingCatalog :: CompilerInput
+programWithMissingCatalog = programWithCall "Catalog.Select(value)"
+
+applicationCatalogSource :: CompilerInput
+applicationCatalogSource =
+    CompilerInput
+        "Sources/ApplicationCatalog.tala"
+        "namespace Application; class Catalog { public static int Select(long value) { return 1; } public static int Select(int value) { return value; } }"
+
+catalogSource :: CompilerInput
+catalogSource =
+    CompilerInput
+        "Sources/Catalog.tala"
+        "namespace Application; class Catalog { public static int Select(long value) { if (value > 0) { return 1; } else { return 2; } } public static int Select(int value) { if (value > 0) { return value; } else { return 3; } } }"
+
+instanceCatalogSource :: CompilerInput
+instanceCatalogSource =
+    CompilerInput
+        "Sources/Catalog.tala"
+        "namespace Application; class Catalog { public int Select(int value) { return value; } }"
+
+privateCatalogSource :: CompilerInput
+privateCatalogSource =
+    CompilerInput
+        "Sources/Catalog.tala"
+        "namespace Application; class Catalog { private static int Select(int value) { return value; } }"
+
+mixedAccessCatalogSource :: CompilerInput
+mixedAccessCatalogSource =
+    CompilerInput
+        "Sources/Catalog.tala"
+        "namespace Application; class Catalog { private static int Select(String value) { return 1; } public static int Select(int value) { return value; } }"
+
+libraryCatalogSource :: CompilerInput
+libraryCatalogSource =
+    CompilerInput
+        "Sources/LibraryCatalog.tala"
+        "namespace Library; class Catalog { public static int Select(int value) { return value; } }"
+
+brokenLibrarySource :: CompilerInput
+brokenLibrarySource =
+    CompilerInput "Sources/Broken.tala" "namespace Library; class Broken { int Read() { return missing; } }"
+
+namespaceOnlySource :: CompilerInput
+namespaceOnlySource = CompilerInput "Sources/OnlyNamespace.tala" "namespace Application;"
+
+entry :: QualifiedName
+entry = QualifiedName [Identifier "Application", Identifier "Program"]
+
+entryNamespace :: QualifiedName
+entryNamespace = QualifiedName [Identifier "Application"]
+
+libraryNamespace :: QualifiedName
+libraryNamespace = QualifiedName [Identifier "Library"]
+
+hasCode :: String -> Either [Diagnostic] value -> Bool
+hasCode code result = case result of
+    Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
+    Right _ -> False
+
+exactlyOne :: [value] -> Bool
+exactlyOne values = case values of
+    [_] -> True
+    _ -> False
+
+exactlyOneValue :: [value] -> Maybe value
+exactlyOneValue values = case values of
+    [value] -> Just value
+    _ -> Nothing
+ test/StaticMemberSemanticTests.hs view
@@ -0,0 +1,619 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Semantic conformance tests for type-qualified static method calls.
+
+The source fragments intentionally use only declaration and call forms already
+present in Spec/Language/Decls.tala. These tests pin down candidate collection,
+exact parameter matching, access filtering, diagnostics, and the stable symbol
+that must survive from semantic analysis into Core.
+-}
+module StaticMemberSemanticTests (staticMemberSemanticTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+
+staticMemberSemanticTests :: [(String, Bool)]
+staticMemberSemanticTests =
+    [ ("a single public static method resolves", singlePublicMethod)
+    , ("a default-access static method resolves in its source unit", defaultAccessMethod)
+    , ("an internal static method resolves in its source unit", internalMethod)
+    , ("a private static method resolves in its declaring class", privateMethodWithinOwner)
+    , ("a protected static method resolves in its declaring class", protectedMethodWithinOwner)
+    , ("a private static method is hidden from another class", privateMethodOutsideOwner)
+    , ("a protected static method is hidden from an unrelated class", protectedMethodOutsideOwner)
+    , ("an instance method is not callable through the type surface", instanceMethodThroughType)
+    , ("a static method is not callable through a value receiver", staticMethodThroughValue)
+    , ("a local identifier that shadows a type is not a type receiver", localShadowsType)
+    , ("a missing member reports the member-not-found diagnostic", missingMethod)
+    , ("a case-mismatched member reports the member-not-found diagnostic", caseSensitiveMember)
+    , ("a bare member selector is rejected until it denotes a method call", bareMethodSelector)
+    , ("a method reference is the method and not its result", methodReferenceAsResult)
+    , ("a member selected on a value is rejected until members of values exist", valueMemberSelector)
+    , ("an overload call with too few arguments reports arity", tooFewArguments)
+    , ("an overload call with too many arguments reports arity", tooManyArguments)
+    , ("an overload call rejects a mismatched argument type", mismatchedArgumentType)
+    , ("an overload call rejects an implicit numeric conversion", implicitNumericConversionRejected)
+    , ("an overload call rejects a String-to-scalar conversion", stringConversionRejected)
+    , ("an overload call rejects a scalar-to-String conversion", scalarConversionRejected)
+    , ("distinct parameter lists form a valid overload set", distinctParameterLists)
+    , ("same-name methods on different owners remain independent", overloadFamiliesAreOwnerScoped)
+    , ("the selected overload is stored as a stable symbol reference", selectedSymbolIsRecorded)
+    , ("each overload declaration keeps its own SymbolId", overloadDeclarationsHaveDistinctIds)
+    , ("method symbols remain distinct across type declarations", methodsAcrossOwnersHaveDistinctIds)
+    , ("the Core call target agrees with the selected overload symbol", coreCallTargetAgrees)
+    , ("overload selection is independent of declaration order", declarationOrderDoesNotSelect)
+    , ("equal arity does not imply equal overload viability", equalArityUsesTypes)
+    , ("a call can select the second overload after rejecting the first", secondCandidateWins)
+    , ("a call can select the first overload after rejecting the second", firstCandidateWins)
+    , ("an overloaded method remains callable from its owner", ownerCallUsesOverloadSet)
+    , ("an overload selected by a typed local preserves its annotation", selectedAnnotationIsKept)
+    , ("a method returning void remains a valid call statement", voidOverloadCall)
+    , ("an overload returning a value can be used as a return expression", valueOverloadReturn)
+    , ("an overload returning a value can initialize a typed local", valueOverloadInitializer)
+    , ("a selector receiver is checked before member lookup", receiverMustNameDeclaredType)
+    , ("a local value receiver does not become a dynamic dispatch", valueReceiverIsNotDynamic)
+    , ("a nested call receiver is not silently treated as a type", nestedCallReceiverNotType)
+    , ("a qualified selector is rejected when the type is absent", unknownTypeReceiver)
+    , ("a public overload remains selectable beside an inaccessible overload", publicCandidateSurvivesPrivateSibling)
+    , ("a private-only overload family reports access failure", privateOnlyFamilyReportsAccess)
+    , ("access modifiers do not create a second overload identity", accessDoesNotDistinguishSignature)
+    , ("return types do not create a second overload identity", returnDoesNotDistinguishSignature)
+    , ("static and instance declarations do not create distinct signatures", staticnessDoesNotDistinguishSignature)
+    , ("parameter order contributes to overload identity", parameterOrderIsSignificant)
+    , ("different owners may reuse a member spelling and signature", ownerScopePermitsSameSignature)
+    , ("a type name is case-sensitive", typeNameIsCaseSensitive)
+    , ("a returned call has the selected method result type", returnedCallHasResultType)
+    , ("a value receiver's own type is not guessed from its spelling", valueReceiverTypeIsNotGuessed)
+    , ("unqualified owner calls share the declared overload family", ownerUnqualifiedCall)
+    , ("unqualified calls do not resolve methods on other classes", foreignUnqualifiedCallIsRejected)
+    , ("selector diagnostics retain a source span", methodDiagnosticHasSpan)
+    , ("selector diagnostics belong to the type-checker stage", methodDiagnosticHasStage)
+    , ("selector diagnostics are errors rather than warnings", methodDiagnosticSeverity)
+    ]
+        ++ scalarPairSelectionTests
+        ++ scalarDuplicateSignatureTests
+        ++ scalarArityTests
+
+singlePublicMethod :: Bool
+singlePublicMethod = succeeds (staticProgram "public static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")
+
+defaultAccessMethod :: Bool
+defaultAccessMethod = succeeds (staticProgram "static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")
+
+internalMethod :: Bool
+internalMethod = succeeds (staticProgram "internal static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")
+
+privateMethodWithinOwner :: Bool
+privateMethodWithinOwner =
+    succeeds ("class Catalog { private static int Read() { return 7; } int Use() { return Catalog.Read(); } }")
+
+protectedMethodWithinOwner :: Bool
+protectedMethodWithinOwner =
+    succeeds ("class Catalog { protected static int Read() { return 7; } int Use() { return Catalog.Read(); } }")
+
+privateMethodOutsideOwner :: Bool
+privateMethodOutsideOwner =
+    hasCode
+        "TAT0033"
+        "class Catalog { private static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"
+
+protectedMethodOutsideOwner :: Bool
+protectedMethodOutsideOwner =
+    hasCode
+        "TAT0033"
+        "class Catalog { protected static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"
+
+instanceMethodThroughType :: Bool
+instanceMethodThroughType = hasCode "TAT0031" "class Catalog { int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"
+
+staticMethodThroughValue :: Bool
+staticMethodThroughValue =
+    hasCode
+        "TAT0032"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use(Catalog value) { return value.Read(); } }"
+
+localShadowsType :: Bool
+localShadowsType =
+    hasCode
+        "TAT0032"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use(int Catalog) { return Catalog.Read(); } }"
+
+missingMethod :: Bool
+missingMethod = hasCode "TAT0029" "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"
+
+caseSensitiveMember :: Bool
+caseSensitiveMember =
+    hasCode
+        "TAT0029"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog.read(); } }"
+
+bareMethodSelector :: Bool
+bareMethodSelector =
+    hasCode
+        "TAT0034"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read; } }"
+
+-- The reference is the method itself, so returning it as an int is a
+-- mismatch of types.
+methodReferenceAsResult :: Bool
+methodReferenceAsResult =
+    hasCode
+        "TAT0005"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog::Read; } }"
+
+valueMemberSelector :: Bool
+valueMemberSelector =
+    hasCode
+        "TAT0034"
+        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { int value = 1; return value.Read; } }"
+
+tooFewArguments :: Bool
+tooFewArguments =
+    hasCode
+        "TAT0008"
+        "class Catalog { public static int Read(int first, int second) { return first; } } class Caller { int Use() { return Catalog.Read(1); } }"
+
+tooManyArguments :: Bool
+tooManyArguments =
+    hasCode
+        "TAT0008"
+        "class Catalog { public static int Read(int first) { return first; } } class Caller { int Use() { return Catalog.Read(1, 2); } }"
+
+mismatchedArgumentType :: Bool
+mismatchedArgumentType =
+    hasCode
+        "TAT0009"
+        "class Catalog { public static int Read(String text) { return 1; } } class Caller { int Use() { return Catalog.Read(1); } }"
+
+implicitNumericConversionRejected :: Bool
+implicitNumericConversionRejected =
+    hasCode
+        "TAT0009"
+        "class Catalog { public static int Read(long value) { return 1; } } class Caller { int Use(int value) { return Catalog.Read(value); } }"
+
+stringConversionRejected :: Bool
+stringConversionRejected =
+    hasCode
+        "TAT0009"
+        "class Catalog { public static int Read(String text) { return 1; } } class Caller { int Use(char value) { return Catalog.Read(value); } }"
+
+scalarConversionRejected :: Bool
+scalarConversionRejected =
+    hasCode
+        "TAT0009"
+        "class Catalog { public static int Read(int value) { return value; } } class Caller { int Use(String value) { return Catalog.Read(value); } }"
+
+distinctParameterLists :: Bool
+distinctParameterLists =
+    succeeds
+        "class Catalog { public static int Read(int value) { return value; } public static int Read(String value) { return 1; } } class Caller { int Use(int value) { return Catalog.Read(value); } }"
+
+overloadFamiliesAreOwnerScoped :: Bool
+overloadFamiliesAreOwnerScoped =
+    succeeds
+        "class First { public static int Read(int value) { return value; } } class Second { public static int Read(int value) { return value; } } class Caller { int Use() { return First.Read(1) + Second.Read(2); } }"
+
+selectedSymbolIsRecorded :: Bool
+selectedSymbolIsRecorded =
+    sameSuccessfulSymbol
+        (selectedMethodSymbol "int" selectedIntLongProgram)
+        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)
+
+overloadDeclarationsHaveDistinctIds :: Bool
+overloadDeclarationsHaveDistinctIds = case methodDeclarationSymbols "Select" selectedIntLongProgram of
+    [first, second] -> first /= second
+    _ -> False
+
+methodsAcrossOwnersHaveDistinctIds :: Bool
+methodsAcrossOwnersHaveDistinctIds = case methodDeclarationSymbols "Read" source of
+    [first, second] -> first /= second
+    _ -> False
+    where
+        source =
+            "class First { public static int Read(int value) { return value; } } class Second { public static int Read(int value) { return value; } }"
+
+coreCallTargetAgrees :: Bool
+coreCallTargetAgrees = case compileToCorePrep (CompilerInput "static-member.tala" selectedIntLongProgram) of
+    Right artifacts ->
+        case [ resolvedSymbol target
+             | target <- coreCallTargets (artifactCore artifacts)
+             , resolvedSpelling target == Identifier "Select"
+             ] of
+            selected : _ -> Just selected == expectedSelectedMethodSymbol "int" selectedIntLongProgram
+            [] -> False
+    Left _ -> False
+
+declarationOrderDoesNotSelect :: Bool
+declarationOrderDoesNotSelect =
+    sameSuccessfulSymbol
+        (selectedMethodSymbol "int" selectedIntLongProgram)
+        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)
+        && sameSuccessfulSymbol
+            (selectedMethodSymbol "long" selectedLongIntProgram)
+            (expectedSelectedMethodSymbol "long" selectedLongIntProgram)
+
+equalArityUsesTypes :: Bool
+equalArityUsesTypes = succeeds selectedIntLongProgram && succeeds selectedLongIntProgram
+
+secondCandidateWins :: Bool
+secondCandidateWins =
+    sameSuccessfulSymbol
+        (selectedMethodSymbol "int" selectedIntLongProgram)
+        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)
+
+firstCandidateWins :: Bool
+firstCandidateWins =
+    sameSuccessfulSymbol
+        (selectedMethodSymbol "long" selectedLongIntProgram)
+        (expectedSelectedMethodSymbol "long" selectedLongIntProgram)
+
+ownerCallUsesOverloadSet :: Bool
+ownerCallUsesOverloadSet =
+    sameSuccessfulSymbol
+        (selectedMethodSymbol "int" source)
+        (expectedSelectedMethodSymbol "int" source)
+    where
+        source =
+            "class Catalog { public static int Select(long value) { return 1; } public static int Select(int value) { return value; } int Use(int value) { return Select(value); } }"
+
+selectedAnnotationIsKept :: Bool
+selectedAnnotationIsKept = case analyze
+    "class Catalog { public static int Select(int value) { return value; } int Use(int value) { Select(value); return 0; } }" of
+    Right result -> case callsNamed "Select" (typedTree result) of
+        CallExpression _ (NameExpression _ target (FunctionType [parameter] resultType)) [NameExpression _ _ argumentType] _ : _ ->
+            parameter == intType
+                && resultType == intType
+                && argumentType == intType
+                && Just (resolvedSymbol target)
+                    == expectedSelectedMethodSymbol
+                        "int"
+                        "class Catalog { public static int Select(int value) { return value; } int Use(int value) { Select(value); return 0; } }"
+        _ -> False
+    Left _ -> False
+
+voidOverloadCall :: Bool
+voidOverloadCall =
+    succeeds
+        "class Catalog { public static void Select(int value) { return; } public static void Select(String value) { return; } } class Caller { void Use(int value) { Catalog.Select(value); return; } }"
+
+valueOverloadReturn :: Bool
+valueOverloadReturn = succeeds selectedIntLongProgram
+
+valueOverloadInitializer :: Bool
+valueOverloadInitializer =
+    succeeds
+        "class Catalog { public static int Select(int value) { return value; } public static long Select(long value) { return value; } } class Caller { int Use(int value) { int chosen = Catalog.Select(value); return chosen; } }"
+
+receiverMustNameDeclaredType :: Bool
+receiverMustNameDeclaredType = hasCode "TAN0001" "class Caller { int Use() { return Missing.Read(); } }"
+
+valueReceiverIsNotDynamic :: Bool
+valueReceiverIsNotDynamic = staticMethodThroughValue
+
+nestedCallReceiverNotType :: Bool
+nestedCallReceiverNotType =
+    hasCode
+        "TAT0032"
+        "class Catalog { public static Catalog Create(Catalog value) { return value; } public static int Read() { return 1; } } class Caller { int Use(Catalog value) { return Catalog.Create(value).Read(); } }"
+
+unknownTypeReceiver :: Bool
+unknownTypeReceiver = receiverMustNameDeclaredType
+
+publicCandidateSurvivesPrivateSibling :: Bool
+publicCandidateSurvivesPrivateSibling =
+    succeeds
+        "class Catalog { private static int Select(String value) { return 1; } public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Catalog.Select(value); } }"
+
+privateOnlyFamilyReportsAccess :: Bool
+privateOnlyFamilyReportsAccess = privateMethodOutsideOwner
+
+accessDoesNotDistinguishSignature :: Bool
+accessDoesNotDistinguishSignature =
+    hasCode
+        "TAT0028"
+        "class Catalog { public static int Select(int value) { return value; } private static int Select(int other) { return other; } }"
+
+returnDoesNotDistinguishSignature :: Bool
+returnDoesNotDistinguishSignature =
+    hasCode
+        "TAT0028"
+        "class Catalog { public static int Select(int value) { return value; } public static long Select(int other) { return other; } }"
+
+staticnessDoesNotDistinguishSignature :: Bool
+staticnessDoesNotDistinguishSignature =
+    hasCode
+        "TAT0028"
+        "class Catalog { public static int Select(int value) { return value; } public int Select(int other) { return other; } }"
+
+parameterOrderIsSignificant :: Bool
+parameterOrderIsSignificant =
+    succeeds
+        "class Catalog { public static int Select(int left, long right) { return left; } public static long Select(long left, int right) { return left; } } class Caller { int Use(int left, long right) { return Catalog.Select(left, right); } }"
+
+ownerScopePermitsSameSignature :: Bool
+ownerScopePermitsSameSignature = overloadFamiliesAreOwnerScoped
+
+typeNameIsCaseSensitive :: Bool
+typeNameIsCaseSensitive =
+    hasCode
+        "TAN0001"
+        "class Catalog { public static int Read() { return 1; } } class Caller { int Use() { return catalog.Read(); } }"
+
+returnedCallHasResultType :: Bool
+returnedCallHasResultType = selectedAnnotationIsKept
+
+valueReceiverTypeIsNotGuessed :: Bool
+valueReceiverTypeIsNotGuessed = staticMethodThroughValue
+
+ownerUnqualifiedCall :: Bool
+ownerUnqualifiedCall = ownerCallUsesOverloadSet
+
+foreignUnqualifiedCallIsRejected :: Bool
+foreignUnqualifiedCallIsRejected =
+    hasCode
+        "TAN0001"
+        "class Catalog { public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Select(value); } }"
+
+methodDiagnosticHasSpan :: Bool
+methodDiagnosticHasSpan =
+    hasDiagnostic
+        "TAT0029"
+        (\diagnostic -> diagnosticSpan diagnostic /= Nothing)
+        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"
+
+methodDiagnosticHasStage :: Bool
+methodDiagnosticHasStage =
+    hasDiagnostic
+        "TAT0029"
+        ((== TypeCheckerStage) . diagnosticStage)
+        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"
+
+methodDiagnosticSeverity :: Bool
+methodDiagnosticSeverity =
+    hasDiagnostic
+        "TAT0029"
+        ((== Error) . diagnosticSeverity)
+        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"
+
+selectedIntLongProgram :: String
+selectedIntLongProgram =
+    "class Catalog { public static int Select(long value) { return 1; } public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Catalog.Select(value); } }"
+
+selectedLongIntProgram :: String
+selectedLongIntProgram =
+    "class Catalog { public static long Select(int value) { return 1; } public static long Select(long value) { return value; } } class Caller { long Use(long value) { return Catalog.Select(value); } }"
+
+sameSuccessfulSymbol :: Maybe SymbolId -> Maybe SymbolId -> Bool
+sameSuccessfulSymbol (Just first) (Just second) = first == second
+sameSuccessfulSymbol _ _ = False
+
+methodDeclarationSymbols :: String -> String -> [SymbolId]
+methodDeclarationSymbols name = map (resolvedSymbol . declarationName) . filter isSelected . typedDeclarations
+    where
+        isSelected FunctionDeclaration {declarationName = candidate} = resolvedSpelling candidate == Identifier name
+        isSelected _ = False
+
+scalarPairSelectionTests :: [(String, Bool)]
+scalarPairSelectionTests =
+    [ ( "exact overload resolution selects " ++ scalarTypeName requested ++ " over " ++ scalarTypeName distractor
+      , sameSuccessfulSymbol
+            (selectedMethodSymbol (scalarTypeName requested) source)
+            (expectedSelectedMethodSymbol (scalarTypeName requested) source)
+      )
+    | requested <- scalarTypes
+    , distractor <- scalarTypes
+    , requested /= distractor
+    , let source = scalarPairProgram (scalarTypeName requested) (scalarTypeName distractor)
+    ]
+
+scalarDuplicateSignatureTests :: [(String, Bool)]
+scalarDuplicateSignatureTests =
+    [ ( "duplicate " ++ scalarTypeName valueType ++ " parameter signatures are rejected"
+      , hasCode "TAT0028" (scalarDuplicateProgram (scalarTypeName valueType))
+      )
+    | valueType <- scalarTypes
+    ]
+
+scalarArityTests :: [(String, Bool)]
+scalarArityTests =
+    [ ( "an " ++ scalarTypeName valueType ++ " overload preserves its wrong-arity diagnostic"
+      , hasCode "TAT0008" (scalarArityProgram (scalarTypeName valueType))
+      )
+    | valueType <- scalarTypes
+    ]
+
+scalarPairProgram :: String -> String -> String
+scalarPairProgram requested distractor =
+    "class Catalog { public static void Select("
+        ++ distractor
+        ++ " other) { return; } public static void Select("
+        ++ requested
+        ++ " other) { return; } public static void Invoke("
+        ++ requested
+        ++ " value) { Select(value); return; } }"
+
+scalarDuplicateProgram :: String -> String
+scalarDuplicateProgram valueType =
+    "class Catalog { public static void Select("
+        ++ valueType
+        ++ " value) { return; } public static void Select("
+        ++ valueType
+        ++ " other) { return; } }"
+
+scalarArityProgram :: String -> String
+scalarArityProgram valueType =
+    "class Catalog { public static void Select("
+        ++ valueType
+        ++ " value) { return; } } class Caller { void Invoke() { Catalog.Select(); return; } }"
+
+selectedMethodSymbol :: String -> String -> Maybe SymbolId
+selectedMethodSymbol selectedType source = do
+    result <- either (const Nothing) Just (analyze source)
+    selectedCall <- firstMatch (callsNamed "Select" (typedTree result))
+    functionName <- case selectedCall of
+        CallExpression _ (NameExpression _ name _) _ _ -> Just name
+        _ -> Nothing
+    pure (resolvedSymbol functionName)
+    where
+        firstMatch [] = Nothing
+        firstMatch (call : remaining) = case call of
+            CallExpression _ (NameExpression _ name _) _ _
+                | any (selectedDeclaration name) (typedDeclarations source) -> Just call
+            _ -> firstMatch remaining
+        selectedDeclaration name declaration = case declaration of
+            FunctionDeclaration {declarationName = candidate, declarationParameters = [parameter]} ->
+                resolvedSymbol candidate == resolvedSymbol name
+                    && parameterAnnotation parameter == scalarTypeToType (scalarForName selectedType)
+                    && resolvedSpelling candidate == Identifier "Select"
+            _ -> False
+
+expectedSelectedMethodSymbol :: String -> String -> Maybe SymbolId
+expectedSelectedMethodSymbol selectedType source = do
+    declaration <- firstMatch (typedDeclarations source)
+    pure (resolvedSymbol (declarationName declaration))
+    where
+        firstMatch [] = Nothing
+        firstMatch (declaration : remaining) = case declaration of
+            FunctionDeclaration {declarationName = name, declarationParameters = [parameter]}
+                | resolvedSpelling name == Identifier "Select"
+                , parameterAnnotation parameter == scalarTypeToType (scalarForName selectedType) ->
+                    Just declaration
+            _ -> firstMatch remaining
+
+scalarForName :: String -> ScalarType
+scalarForName spelling = case [scalar | scalar <- scalarTypes, scalarTypeName scalar == spelling] of
+    scalar : _ -> scalar
+    [] -> IntScalar
+
+typedDeclarations :: String -> [Declaration ResolvedName Type]
+typedDeclarations source = case analyze source of
+    Right result -> flattenDeclarations (syntaxDeclarations (typedSyntaxTree (typedTree result)))
+    Left _ -> []
+
+flattenDeclarations :: [Declaration name annotation] -> [Declaration name annotation]
+flattenDeclarations declarations = concatMap flatten declarations
+    where
+        flatten declaration@TypeDeclaration {} = declaration : flattenDeclarations (typeMembers declaration)
+        flatten declaration@TemplateTypeDeclaration {} = declaration : flattenDeclarations (typeMembers declaration)
+        flatten declaration = [declaration]
+
+analyze :: String -> Either [Diagnostic] SemanticArtifacts
+analyze = analyzeSemantics . CompilerInput "static-member.tala"
+
+typedTree :: SemanticArtifacts -> TypedAST
+typedTree = semanticTypedAST
+
+succeeds :: String -> Bool
+succeeds = either (const False) (const True) . analyze
+
+hasCode :: String -> String -> Bool
+hasCode code = hasDiagnostic code (const True)
+
+hasDiagnostic :: String -> (Diagnostic -> Bool) -> String -> Bool
+hasDiagnostic code predicate source = case analyze source of
+    Left diagnostics -> any (\diagnostic -> diagnosticCode diagnostic == code && predicate diagnostic) diagnostics
+    Right _ -> False
+
+staticProgram :: String -> String -> String
+staticProgram method caller = "class Catalog { " ++ method ++ " } class Caller { " ++ caller ++ " }"
+
+callsNamed :: String -> TypedAST -> [Expression ResolvedName Type]
+callsNamed name (TypedAST (SyntaxTree _ declarations)) = filter isNamedCall (concatMap declarationCalls declarations)
+    where
+        isNamedCall (CallExpression _ (NameExpression _ target _) _ _) = resolvedSpelling target == Identifier name
+        isNamedCall _ = False
+
+declarationCalls :: Declaration ResolvedName Type -> [Expression ResolvedName Type]
+declarationCalls declaration = case declaration of
+    TypeDeclaration {typeMembers = members} -> concatMap declarationCalls members
+    TemplateTypeDeclaration {typeMembers = members} -> concatMap declarationCalls members
+    FunctionDeclaration {declarationBody = body} -> blockCalls body
+    EnumDeclaration {} -> []
+
+blockCalls :: Block ResolvedName Type -> [Expression ResolvedName Type]
+blockCalls (Block statements) = concatMap statementCalls statements
+
+statementCalls :: Statement ResolvedName Type -> [Expression ResolvedName Type]
+statementCalls statement = case statement of
+    BindingStatement _ _ _ _ _ value -> expressionCalls value
+    AssignmentStatement _ _ _ value -> expressionCalls value
+    ReturnStatement _ value -> maybe [] expressionCalls value
+    IfStatement _ condition yes no -> expressionCalls condition ++ blockCalls yes ++ maybe [] blockCalls no
+    WhileStatement _ condition body -> expressionCalls condition ++ blockCalls body
+    DoWhileStatement _ body condition -> blockCalls body ++ expressionCalls condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementCalls initializer
+            ++ maybe [] expressionCalls condition
+            ++ concatMap statementCalls updates
+            ++ blockCalls body
+    ForEachStatement _ _ _ _ _ source body -> expressionCalls source ++ blockCalls body
+    IncrementStatement {} -> []
+    CompoundAssignmentStatement _ _ _ _ value -> expressionCalls value
+    DiscardStatement _ value -> expressionCalls value
+    BreakStatement _ value -> maybe [] expressionCalls value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionCalls condition ++ blockCalls block
+    BlockStatement _ block -> blockCalls block
+    ExpressionStatement _ value _ -> expressionCalls value
+
+expressionCalls :: Expression ResolvedName Type -> [Expression ResolvedName Type]
+expressionCalls expression = case expression of
+    NameExpression {} -> []
+    LiteralExpression {} -> []
+    MemberAccessExpression _ receiver _ _ -> expressionCalls receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionCalls receiver
+    CallExpression _ callee arguments _ ->
+        [expression | isCall expression] ++ expressionCalls callee ++ concatMap expressionCalls arguments
+    UnaryExpression _ _ value _ -> expressionCalls value
+    BinaryExpression _ _ left right _ -> expressionCalls left ++ expressionCalls right
+    IsPatternExpression _ subject _ _ -> expressionCalls subject
+    ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap expressionCalls [condition, whenTrue, whenFalse]
+    CoalesceExpression _ left fallback _ -> expressionCalls left ++ expressionCalls fallback
+    AssignmentExpression _ _ _ value _ -> expressionCalls value
+    IncrementExpression {} -> []
+    LoopExpression _ loop _ -> statementCalls loop
+    BlockExpression _ block _ -> blockCalls block
+    MatchExpression _ subjects arms _ -> concatMap expressionCalls (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ _ _ body _ -> callableBodyCalls body
+    where
+        isCall CallExpression {} = True
+        isCall _ = False
+
+callableBodyCalls :: CallableBody ResolvedName Type -> [Expression ResolvedName Type]
+callableBodyCalls body = case body of
+    CallableExpressionBody expression -> expressionCalls expression
+    CallableBlockBody block -> blockCalls block
+
+coreCallTargets :: CoreModule -> [ResolvedName]
+coreCallTargets moduleValue = concatMap (concatMap statementTargets . coreFunctionBody) (coreModuleFunctions moduleValue)
+
+statementTargets :: CoreStatement -> [ResolvedName]
+statementTargets statement = case statement of
+    CoreBind binding -> expressionTargets (coreBindingValue binding)
+    CoreAssign _ value -> expressionTargets value
+    CoreReturn value -> expressionTargets value
+    CoreIf condition yes no -> expressionTargets condition ++ concatMap statementTargets yes ++ concatMap statementTargets no
+    CoreEvaluate value -> expressionTargets value
+    CoreWhile condition body -> expressionTargets condition ++ concatMap statementTargets body
+    CoreDoWhile body condition -> concatMap statementTargets body ++ expressionTargets condition
+    CoreFor condition updates body -> expressionTargets condition ++ concatMap statementTargets updates ++ concatMap statementTargets body
+    CoreBreak -> []
+    CoreContinue -> []
+
+expressionTargets :: CoreExpression -> [ResolvedName]
+expressionTargets expression = case expression of
+    CoreVariable {} -> []
+    CoreLiteral {} -> []
+    CoreApply callee arguments _ -> calledTarget callee ++ expressionTargets callee ++ concatMap expressionTargets arguments
+    CorePrimitive _ arguments _ -> concatMap expressionTargets arguments
+    CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body
+    CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse]
+    CoreClosure _ _ _ statements _ -> concatMap statementTargets statements
+    where
+        calledTarget (CoreVariable name _) = [name]
+        calledTarget _ = []
+ test/TemplateApplicationTests.hs view
@@ -0,0 +1,400 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateApplicationTests (templateApplicationTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+
+templateApplicationTests :: [(String, Bool)]
+templateApplicationTests =
+    [ ("catalog records a qualified template name", catalogRecordsQualifiedName)
+    , ("catalog records the declaration symbol", catalogRecordsDeclarationSymbol)
+    , ("catalog records member semantic signatures", catalogRecordsMembers)
+    , ("catalog excludes ordinary declarations", catalogExcludesOrdinaryDeclarations)
+    , ("catalog preserves source declaration order", catalogPreservesOrder)
+    , ("catalog describes a type parameter", catalogDescribesTypeParameter)
+    , ("catalog describes a value parameter", catalogDescribesValueParameter)
+    , ("catalog describes a template parameter", catalogDescribesTemplateParameter)
+    , ("catalog preserves pack status", catalogPreservesPackStatus)
+    , ("catalog preserves default syntax", catalogPreservesDefault)
+    , ("lookup finds an exact qualified name", lookupFindsQualifiedName)
+    , ("lookup does not fall back to a short name", lookupRejectsShortName)
+    , ("lookup reports unknown declarations", lookupReportsUnknown)
+    , ("lookup reports ambiguous catalog entries", lookupReportsAmbiguous)
+    , ("minimum arity counts required parameters", minimumArityCountsRequired)
+    , ("minimum arity excludes defaults", minimumArityExcludesDefaults)
+    , ("minimum arity excludes packs", minimumArityExcludesPacks)
+    , ("maximum arity is exact without packs", maximumArityIsExact)
+    , ("maximum arity is unbounded with a pack", maximumArityIsUnbounded)
+    , ("binding accepts one type argument", bindingAcceptsType)
+    , ("binding accepts one value argument", bindingAcceptsValue)
+    , ("binding accepts a template declaration name", bindingAcceptsTemplateName)
+    , ("binding rejects a value for a type parameter", bindingRejectsValueForType)
+    , ("binding rejects a type for a value parameter", bindingRejectsTypeForValue)
+    , ("binding rejects a function type for a template parameter", bindingRejectsFunctionForTemplate)
+    , ("binding reports too few arguments", bindingReportsTooFew)
+    , ("binding reports too many arguments", bindingReportsTooMany)
+    , ("binding fills an omitted type default", bindingFillsTypeDefault)
+    , ("binding fills an omitted value default", bindingFillsValueDefault)
+    , ("binding resolves a later type default", bindingResolvesLaterTypeDefault)
+    , ("binding resolves a later value default", bindingResolvesLaterValueDefault)
+    , ("binding rejects a cyclic type default", bindingRejectsCyclicTypeDefault)
+    , ("binding places zero arguments in an empty pack", bindingCreatesEmptyPack)
+    , ("binding places every surplus argument in a final pack", bindingFillsFinalPack)
+    , ("binding reserves required suffix arguments after a pack", bindingReservesRequiredSuffix)
+    , ("binding keeps explicit and default origins distinct", bindingPreservesArgumentOrigin)
+    , ("substitution replaces a type variable", substitutionReplacesTypeVariable)
+    , ("substitution replaces nested named arguments", substitutionReplacesNestedType)
+    , ("substitution replaces callable parameter and result types", substitutionReplacesCallableType)
+    , ("substitution replaces a value parameter", substitutionReplacesValueParameter)
+    , ("substitution preserves concrete scalar values", substitutionPreservesConcreteValue)
+    , ("substitution preserves ErrorType", substitutionPreservesErrorType)
+    , ("substitution reports an unbound type variable", substitutionReportsMissingType)
+    , ("substitution reports an unbound value variable", substitutionReportsMissingValue)
+    , ("rendering names an unknown template", renderingNamesUnknownTemplate)
+    , ("rendering reports arity values", renderingReportsArity)
+    , ("rendering identifies a mismatched parameter", renderingIdentifiesParameter)
+    ]
+
+analyzeCatalog :: String -> Maybe TemplateCatalog
+analyzeCatalog source = case analyzeSemantics (CompilerInput "application-test.tala" source) of
+    Right artifacts -> Just (buildTemplateCatalog (semanticTypedAST artifacts))
+    Left _ -> Nothing
+
+singleDescriptor :: String -> Maybe TemplateDeclarationDescriptor
+singleDescriptor source = do
+    TemplateCatalog declarations <- analyzeCatalog source
+    case declarations of { [declaration] -> Just declaration; _ -> Nothing }
+
+qualified :: [String] -> QualifiedName
+qualified = QualifiedName . map Identifier
+
+typeArgument :: String -> TemplateArgument
+typeArgument name = TypeTemplateArgument (namedType name)
+
+valueArgument :: Integer -> TemplateArgument
+valueArgument = ValueTemplateArgument . IntegerTemplateValue
+
+application :: QualifiedName -> [TemplateArgument] -> TemplateApplication
+application = TemplateApplication
+
+bindSource :: String -> QualifiedName -> [TemplateArgument] -> Either [TemplateApplicationError] TemplateBinding
+bindSource source name arguments = case analyzeCatalog source of
+    Just catalog -> bindTemplateApplication catalog (application name arguments)
+    Nothing -> Left []
+
+catalogRecordsQualifiedName :: Bool
+catalogRecordsQualifiedName = case singleDescriptor "namespace Library; template<typename T> class Box {}" of
+    Just descriptor -> templateDeclarationName descriptor == qualified ["Library", "Box"]
+    Nothing -> False
+
+catalogRecordsDeclarationSymbol :: Bool
+catalogRecordsDeclarationSymbol = case singleDescriptor "template<typename T> class Box {}" of
+    Just descriptor -> symbolIdValue (templateDeclarationSymbol descriptor) > 0
+    Nothing -> False
+
+catalogRecordsMembers :: Bool
+catalogRecordsMembers = case singleDescriptor source of
+    Just descriptor -> case templateDeclarationMembers descriptor of
+        [(name, FunctionType [TypeVariable input] (TypeVariable output))] ->
+            identifierText (resolvedSpelling name) == "Identity" && resolvedSymbol input == resolvedSymbol output
+        _ -> False
+    Nothing -> False
+    where
+        source = "template<typename T> class Box { T Identity(_ T value) { return value; } }"
+
+catalogExcludesOrdinaryDeclarations :: Bool
+catalogExcludesOrdinaryDeclarations = case analyzeCatalog "class App { void Run() { return; } }" of
+    Just (TemplateCatalog []) -> True
+    _ -> False
+
+catalogPreservesOrder :: Bool
+catalogPreservesOrder = case analyzeCatalog source of
+    Just (TemplateCatalog declarations) -> map templateDeclarationName declarations == map (qualified . (: [])) ["Left", "Right"]
+    Nothing -> False
+    where
+        source = "template<typename T> class Left {} template<typename U> class Right {}"
+
+catalogDescribesTypeParameter :: Bool
+catalogDescribesTypeParameter = case singleDescriptor "template<typename T> class Box {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of
+        [parameter] -> templateDescriptorCategory parameter == TypeParameterCategory
+        _ -> False
+    Nothing -> False
+
+catalogDescribesValueParameter :: Bool
+catalogDescribesValueParameter = case singleDescriptor "template<int N> class Buffer {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of
+        [parameter] -> templateDescriptorCategory parameter == ValueParameterCategory intType
+        _ -> False
+    Nothing -> False
+
+catalogDescribesTemplateParameter :: Bool
+catalogDescribesTemplateParameter = case singleDescriptor "template<template<typename> class C> class Box {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of
+        [parameter] -> case templateDescriptorCategory parameter of TemplateParameterCategory [_] -> True; _ -> False
+        _ -> False
+    Nothing -> False
+
+catalogPreservesPackStatus :: Bool
+catalogPreservesPackStatus = case singleDescriptor "template<typename... T> class Tuple {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of [parameter] -> templateDescriptorIsPack parameter; _ -> False
+    Nothing -> False
+
+catalogPreservesDefault :: Bool
+catalogPreservesDefault = case singleDescriptor "template<typename T = int> class Box {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of
+        [parameter] -> templateDescriptorDefault parameter == Just (TemplateTypeDefault (ExplicitType (Identifier "int")))
+        _ -> False
+    Nothing -> False
+
+lookupFindsQualifiedName :: Bool
+lookupFindsQualifiedName = case analyzeCatalog "namespace A; template<typename T> class Box {}" of
+    Just catalog -> case lookupTemplateDeclaration (qualified ["A", "Box"]) catalog of Right _ -> True; Left _ -> False
+    Nothing -> False
+
+lookupRejectsShortName :: Bool
+lookupRejectsShortName = case analyzeCatalog "namespace A; template<typename T> class Box {}" of
+    Just catalog -> lookupTemplateDeclaration (qualified ["Box"]) catalog == Left (UnknownTemplateDeclaration (qualified ["Box"]))
+    Nothing -> False
+
+lookupReportsUnknown :: Bool
+lookupReportsUnknown =
+    lookupTemplateDeclaration (qualified ["Missing"]) (TemplateCatalog [])
+        == Left (UnknownTemplateDeclaration (qualified ["Missing"]))
+
+lookupReportsAmbiguous :: Bool
+lookupReportsAmbiguous = case singleDescriptor "template<typename T> class Box {}" of
+    Just descriptor -> case lookupTemplateDeclaration (qualified ["Box"]) (TemplateCatalog [descriptor, descriptor]) of
+        Left (AmbiguousTemplateDeclaration _ symbols) -> length symbols == 2
+        _ -> False
+    Nothing -> False
+
+minimumArityCountsRequired :: Bool
+minimumArityCountsRequired = case singleDescriptor "template<typename T, int N> class Box {}" of
+    Just value -> minimumTemplateArity value == 2
+    Nothing -> False
+
+minimumArityExcludesDefaults :: Bool
+minimumArityExcludesDefaults = case singleDescriptor "template<typename T = int, int N = 4> class Box {}" of
+    Just value -> minimumTemplateArity value == 0
+    Nothing -> False
+
+minimumArityExcludesPacks :: Bool
+minimumArityExcludesPacks = case singleDescriptor "template<typename... T> class Box {}" of
+    Just value -> minimumTemplateArity value == 0
+    Nothing -> False
+
+maximumArityIsExact :: Bool
+maximumArityIsExact = case singleDescriptor "template<typename T, int N> class Box {}" of
+    Just value -> maximumTemplateArity value == Just 2
+    Nothing -> False
+
+maximumArityIsUnbounded :: Bool
+maximumArityIsUnbounded = case singleDescriptor "template<typename... T> class Box {}" of
+    Just value -> maximumTemplateArity value == Nothing
+    Nothing -> False
+
+bindingAcceptsType :: Bool
+bindingAcceptsType = case bindSource "template<typename T> class Box {}" (qualified ["Box"]) [typeArgument "String"] of
+    Right _ -> True
+    Left _ -> False
+
+bindingAcceptsValue :: Bool
+bindingAcceptsValue = case bindSource "template<int N> class Buffer {}" (qualified ["Buffer"]) [valueArgument 4] of
+    Right _ -> True
+    Left _ -> False
+
+bindingAcceptsTemplateName :: Bool
+bindingAcceptsTemplateName = case bindSource source (qualified ["Wrapper"]) [typeArgument "System.Array"] of Right _ -> True; Left _ -> False
+    where
+        source = "template<template<typename> class C> class Wrapper {}"
+
+bindingRejectsValueForType :: Bool
+bindingRejectsValueForType = categoryFailure (bindSource "template<typename T> class Box {}" (qualified ["Box"]) [valueArgument 1])
+
+bindingRejectsTypeForValue :: Bool
+bindingRejectsTypeForValue = categoryFailure (bindSource "template<int N> class Box {}" (qualified ["Box"]) [typeArgument "int"])
+
+bindingRejectsFunctionForTemplate :: Bool
+bindingRejectsFunctionForTemplate = categoryFailure (bindSource source (qualified ["Box"]) [TypeTemplateArgument (FunctionType [] voidType)])
+    where
+        source = "template<template<typename> class C> class Box {}"
+
+categoryFailure :: Either [TemplateApplicationError] value -> Bool
+categoryFailure result = case result of Left [TemplateArgumentCategoryMismatch {}] -> True; _ -> False
+
+bindingReportsTooFew :: Bool
+bindingReportsTooFew = case bindSource "template<typename T> class Box {}" (qualified ["Box"]) [] of
+    Left [TooFewTemplateArguments _ 1 0] -> True
+    _ -> False
+
+bindingReportsTooMany :: Bool
+bindingReportsTooMany = case bindSource "template<typename T> class Box {}" (qualified ["Box"]) [typeArgument "int", typeArgument "long"] of
+    Left [TooManyTemplateArguments _ 1 2] -> True
+    _ -> False
+
+bindingFillsTypeDefault :: Bool
+bindingFillsTypeDefault = defaultBindingIs (typeArgument "int") (bindSource "template<typename T = int> class Box {}" (qualified ["Box"]) [])
+
+bindingFillsValueDefault :: Bool
+bindingFillsValueDefault = defaultBindingIs (valueArgument 8) (bindSource "template<int N = 8> class Box {}" (qualified ["Box"]) [])
+
+defaultBindingIs :: TemplateArgument -> Either [TemplateApplicationError] TemplateBinding -> Bool
+defaultBindingIs expected result = case result of
+    Right binding -> case templateBindingArguments binding of [(_, [DefaultTemplateArgument actual])] -> actual == expected; _ -> False
+    Left _ -> False
+
+bindingResolvesLaterTypeDefault :: Bool
+bindingResolvesLaterTypeDefault = case bindSource source (qualified ["Pair"]) [] of
+    Right binding -> map (map unwrap . snd) (templateBindingArguments binding) == [[typeArgument "int"], [typeArgument "int"]]
+    Left _ -> False
+    where
+        source = "template<typename T = U, typename U = int> class Pair {}"
+        unwrap (ExplicitTemplateArgument value) = value
+        unwrap (DefaultTemplateArgument value) = value
+
+bindingResolvesLaterValueDefault :: Bool
+bindingResolvesLaterValueDefault = case bindSource source (qualified ["Buffer"]) [] of
+    Right binding -> map (map unwrap . snd) (templateBindingArguments binding) == [[valueArgument 4], [valueArgument 4]]
+    Left _ -> False
+    where
+        source = "template<int N = M, int M = 4> class Buffer {}"
+        unwrap (ExplicitTemplateArgument value) = value
+        unwrap (DefaultTemplateArgument value) = value
+
+bindingRejectsCyclicTypeDefault :: Bool
+bindingRejectsCyclicTypeDefault = case bindSource source (qualified ["Cycle"]) [] of Left [UnresolvedTemplateDefault {}] -> True; _ -> False
+    where
+        source = "template<typename T = U, typename U = T> class Cycle {}"
+
+bindingCreatesEmptyPack :: Bool
+bindingCreatesEmptyPack = case bindSource "template<typename... T> class Tuple {}" (qualified ["Tuple"]) [] of
+    Right binding -> case templateBindingArguments binding of [(_, [])] -> True; _ -> False
+    Left _ -> False
+
+bindingFillsFinalPack :: Bool
+bindingFillsFinalPack = case bindSource "template<typename... T> class Tuple {}" (qualified ["Tuple"]) arguments of
+    Right binding -> case templateBindingArguments binding of [(_, values)] -> length values == 3; _ -> False
+    Left _ -> False
+    where
+        arguments = map typeArgument ["int", "String", "bool"]
+
+bindingReservesRequiredSuffix :: Bool
+bindingReservesRequiredSuffix = case bindSource source (qualified ["Tuple"]) arguments of
+    Right binding -> map (length . snd) (templateBindingArguments binding) == [2, 1]
+    Left _ -> False
+    where
+        source = "template<typename... T, int N> class Tuple {}"
+        arguments = [typeArgument "int", typeArgument "String", valueArgument 2]
+
+bindingPreservesArgumentOrigin :: Bool
+bindingPreservesArgumentOrigin = case bindSource source (qualified ["Pair"]) [typeArgument "String"] of
+    Right binding -> case templateBindingArguments binding of
+        [(_, [ExplicitTemplateArgument _]), (_, [DefaultTemplateArgument _])] -> True
+        _ -> False
+    Left _ -> False
+    where
+        source = "template<typename T, typename U = int> class Pair {}"
+
+typeBinding :: Maybe TemplateBinding
+typeBinding = case bindSource "template<typename T> class Box {}" (qualified ["Box"]) [typeArgument "String"] of
+    Right value -> Just value
+    Left _ -> Nothing
+
+valueBinding :: Maybe TemplateBinding
+valueBinding = case bindSource "template<int N> class Buffer {}" (qualified ["Buffer"]) [valueArgument 16] of
+    Right value -> Just value
+    Left _ -> Nothing
+
+firstParameterName :: TemplateBinding -> ResolvedName
+firstParameterName binding = case templateBindingArguments binding of
+    (name, _) : _ -> name
+    [] -> ResolvedName (SymbolId (-1)) (Identifier "<missing-template-parameter>")
+
+substitutionReplacesTypeVariable :: Bool
+substitutionReplacesTypeVariable = case typeBinding of
+    Just binding -> substituteType binding (TypeVariable (firstParameterName binding)) == Right (namedType "String")
+    Nothing -> False
+
+substitutionReplacesNestedType :: Bool
+substitutionReplacesNestedType = case typeBinding of
+    Just binding ->
+        let variable = TypeVariable (firstParameterName binding)
+            input = NamedType (qualified ["System", "Array"]) [TypeTemplateArgument variable]
+            expected = NamedType (qualified ["System", "Array"]) [typeArgument "String"]
+         in substituteType binding input == Right expected
+    Nothing -> False
+
+substitutionReplacesCallableType :: Bool
+substitutionReplacesCallableType = case typeBinding of
+    Just binding ->
+        let variable = TypeVariable (firstParameterName binding)
+         in substituteType binding (FunctionType [variable] variable)
+                == Right (FunctionType [namedType "String"] (namedType "String"))
+    Nothing -> False
+
+substitutionReplacesValueParameter :: Bool
+substitutionReplacesValueParameter = case valueBinding of
+    Just binding ->
+        substituteTemplateValue binding (TemplateValueParameter (firstParameterName binding)) == Right (IntegerTemplateValue 16)
+    Nothing -> False
+
+substitutionPreservesConcreteValue :: Bool
+substitutionPreservesConcreteValue = case valueBinding of
+    Just binding -> substituteTemplateValue binding (IntegerTemplateValue 3) == Right (IntegerTemplateValue 3)
+    Nothing -> False
+
+substitutionPreservesErrorType :: Bool
+substitutionPreservesErrorType = case typeBinding of Just binding -> substituteType binding ErrorType == Right ErrorType; Nothing -> False
+
+substitutionReportsMissingType :: Bool
+substitutionReportsMissingType = case typeBinding of
+    Just binding -> case substituteType binding (TypeVariable (ResolvedName (SymbolId 999) (Identifier "Missing"))) of
+        Left UnresolvedTemplateDefault {} -> True
+        _ -> False
+    Nothing -> False
+
+substitutionReportsMissingValue :: Bool
+substitutionReportsMissingValue = case valueBinding of
+    Just binding -> case substituteTemplateValue binding (TemplateValueParameter (ResolvedName (SymbolId 999) (Identifier "Missing"))) of
+        Left UnresolvedTemplateDefault {} -> True
+        _ -> False
+    Nothing -> False
+
+renderingNamesUnknownTemplate :: Bool
+renderingNamesUnknownTemplate =
+    renderTemplateApplicationError (UnknownTemplateDeclaration (qualified ["A", "Box"]))
+        == "unknown template declaration A.Box"
+
+renderingReportsArity :: Bool
+renderingReportsArity =
+    renderTemplateApplicationError (TooFewTemplateArguments (qualified ["Box"]) 2 1)
+        == "Box requires at least 2 template arguments, but received 1"
+
+renderingIdentifiesParameter :: Bool
+renderingIdentifiesParameter = case singleDescriptor "template<typename T> class Box {}" of
+    Just descriptor -> case templateDeclarationParameters descriptor of
+        [parameter] ->
+            "parameter T"
+                `contains` renderTemplateApplicationError
+                    ( TemplateArgumentCategoryMismatch
+                        (qualified ["Box"])
+                        (templateDescriptorName parameter)
+                        0
+                        TypeParameterCategory
+                        (valueArgument 1)
+                    )
+        _ -> False
+    Nothing -> False
+
+contains :: String -> String -> Bool
+contains needle haystack = any (needle `prefixOf`) (tails haystack)
+    where
+        tails [] = [[]]
+        tails value@(_ : rest) = value : tails rest
+        prefixOf [] _ = True
+        prefixOf _ [] = False
+        prefixOf (left : leftRest) (right : rightRest) = left == right && prefixOf leftRest rightRest
+ test/TemplateDeclarationTests.hs view
@@ -0,0 +1,445 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateDeclarationTests (templateDeclarationTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.Parser
+
+templateDeclarationTests :: [(String, Bool)]
+templateDeclarationTests =
+    [ ("lexer reserves template declaration words", lexerReservesTemplateWords)
+    , ("lexer emits ellipsis as one maximal token", lexerEmitsEllipsis)
+    , ("parser preserves a type parameter", parserPreservesTypeParameter)
+    , ("parser preserves a value parameter", parserPreservesValueParameter)
+    , ("parser preserves a template-template parameter", parserPreservesTemplateParameter)
+    , ("parser preserves a type parameter pack", parserPreservesTypePack)
+    , ("parser preserves a value parameter pack", parserPreservesValuePack)
+    , ("parser preserves a template-template parameter pack", parserPreservesTemplatePack)
+    , ("parser preserves a type default", parserPreservesTypeDefault)
+    , ("parser preserves a later type default reference", parserPreservesLaterDefault)
+    , ("parser preserves a scalar value default", parserPreservesValueDefault)
+    , ("parser preserves a named value default", parserPreservesNamedValueDefault)
+    , ("parser preserves a boolean value default", parserPreservesBooleanDefault)
+    , ("parser preserves a character value default", parserPreservesCharacterDefault)
+    , ("parser preserves a qualified type default", parserPreservesQualifiedDefault)
+    , ("parser preserves a generic type default", parserPreservesGenericDefault)
+    , ("parser preserves nested template-template shapes", parserPreservesNestedTemplateShape)
+    , ("parser preserves mixed template parameter order", parserPreservesMixedOrder)
+    , ("parser rejects an empty template parameter list", parserRejectsEmptyParameters)
+    , ("parser rejects an empty template-template signature", parserRejectsEmptyTemplateShape)
+    , ("parser rejects a missing template close delimiter", parserRejectsMissingClose)
+    , ("parser rejects a missing template parameter name", parserRejectsMissingName)
+    , ("parser rejects a missing class after a template prefix", parserRejectsMissingClass)
+    , ("parser rejects a dangling template default", parserRejectsDanglingDefault)
+    , ("renamer assigns positive identities to template parameters", renamerAssignsPositiveIds)
+    , ("renamer keeps source-order template identities", renamerKeepsParameterOrder)
+    , ("renamer diagnoses duplicate template parameters", renamerRejectsDuplicateParameters)
+    , ("renamer scopes template value parameters in method bodies", renamerScopesValueParameter)
+    , ("renamer scopes template parameters independently per declaration", renamerIsolatesTemplates)
+    , ("resolver preserves the declaration and parameter identities", resolverPreservesIdentities)
+    , ("type checker represents T as a type variable", checkerBuildsTypeVariable)
+    , ("type checker represents N as a template value parameter", checkerBuildsValueVariable)
+    , ("type checker carries a type variable through callable types", checkerBuildsCallableType)
+    , ("type checker carries type variables through arrays", checkerBuildsArrayType)
+    , ("type checker carries value variables through fixed arrays", checkerBuildsFixedArrayType)
+    , ("type checker carries variables through dictionaries", checkerBuildsDictionaryType)
+    , ("type checker annotates value template parameters", checkerAnnotatesValueParameter)
+    , ("type checker annotates type template parameters", checkerAnnotatesTypeParameter)
+    , ("type checker annotates template-template parameters", checkerAnnotatesTemplateParameter)
+    , ("type checker accepts a later type default reference", checkerAcceptsLaterDefault)
+    , ("type checker accepts a named value default reference", checkerAcceptsNamedValueDefault)
+    , ("type checker rejects a default on a parameter pack", checkerRejectsPackDefault)
+    , ("type checker rejects unresolved value defaults", checkerRejectsUnknownValueDefault)
+    , ("open templates do not emit unspecialized Core functions", openTemplateStopsBeforeCore)
+    , ("ordinary declarations still pass semantic analysis", ordinaryDeclarationUnaffected)
+    , ("template and ordinary declarations coexist", templateAndOrdinaryCoexist)
+    , ("template member calls use semantic member signatures", templateMemberCallsResolve)
+    , ("template identity methods retain exact type variables", identityMethodRetainsType)
+    , ("template fixed-array identity retains both parameters", fixedArrayMethodRetainsParameters)
+    ]
+
+lexTokens :: String -> Either [Diagnostic] [Token]
+lexTokens source = runLexer defaultLexer (LexerInput "template-test.tala" source)
+
+parseSource :: String -> Either [Diagnostic] ParsedAST
+parseSource source = do
+    tokens <- lexTokens source
+    runParser defaultParser (ParserInput "template-test.tala" tokens)
+
+analyzeSource :: String -> Either [Diagnostic] SemanticArtifacts
+analyzeSource = analyzeSemantics . CompilerInput "template-test.tala"
+
+onlyParsedTemplate :: String -> Maybe (Declaration Identifier ())
+onlyParsedTemplate source = case parseSource source of
+    Right (ParsedAST (SyntaxTree _ [declaration@TemplateTypeDeclaration {}])) -> Just declaration
+    _ -> Nothing
+
+onlyTypedTemplate :: String -> Maybe (Declaration ResolvedName Type)
+onlyTypedTemplate source = case analyzeSource source of
+    Right artifacts -> case syntaxDeclarations (typedSyntaxTree (semanticTypedAST artifacts)) of
+        [declaration@TemplateTypeDeclaration {}] -> Just declaration
+        _ -> Nothing
+    Left _ -> Nothing
+
+hasCode :: String -> Either [Diagnostic] value -> Bool
+hasCode code result = case result of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+identifier :: String -> Identifier
+identifier = Identifier
+
+parameterNames :: Declaration name annotation -> [name]
+parameterNames TemplateTypeDeclaration {declarationTemplateParameters = parameters} = map templateParameterName parameters
+parameterNames _ = []
+
+lexerReservesTemplateWords :: Bool
+lexerReservesTemplateWords = case lexTokens "template<typename T> class Box {}" of
+    Right tokens ->
+        [(tokenText token, tokenKind token) | token <- tokens, tokenText token `elem` ["template", "typename"]]
+            == [("template", KeywordToken), ("typename", KeywordToken)]
+    Left _ -> False
+
+lexerEmitsEllipsis :: Bool
+lexerEmitsEllipsis = case lexTokens "template<typename... Types> class Tuple {}" of
+    Right tokens -> length [() | token <- tokens, tokenText token == "..." && tokenKind token == SymbolToken] == 1
+    Left _ -> False
+
+parserPreservesTypeParameter :: Bool
+parserPreservesTypeParameter = case onlyParsedTemplate "template<typename T> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [TemplateParameter _ name _ TemplateTypeParameter False Nothing] -> name == identifier "T"
+        _ -> False
+    Nothing -> False
+
+parserPreservesValueParameter :: Bool
+parserPreservesValueParameter = case onlyParsedTemplate "template<int Size> class Buffer {}" of
+    Just declaration -> case templateParameters declaration of
+        [TemplateParameter _ name _ (TemplateValueParameterKind valueType) False Nothing] ->
+            name == identifier "Size" && valueType == ExplicitType (identifier "int")
+        _ -> False
+    Nothing -> False
+
+parserPreservesTemplateParameter :: Bool
+parserPreservesTemplateParameter = case onlyParsedTemplate "template<template<typename> class Container> class Wrapper {}" of
+    Just declaration -> case templateParameters declaration of
+        [TemplateParameter _ name _ (TemplateTemplateParameter [shape]) False Nothing] ->
+            name == identifier "Container"
+                && templateParameterShapeKind shape == TemplateTypeParameterShape
+                && not (templateParameterShapeIsPack shape)
+        _ -> False
+    Nothing -> False
+
+parserPreservesTypePack :: Bool
+parserPreservesTypePack = case onlyParsedTemplate "template<typename... Types> class Tuple {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> templateParameterKind parameter == TemplateTypeParameter && templateParameterIsPack parameter
+        _ -> False
+    Nothing -> False
+
+parserPreservesValuePack :: Bool
+parserPreservesValuePack = case onlyParsedTemplate "template<int... Values> class Integers {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] ->
+            templateParameterKind parameter == TemplateValueParameterKind (ExplicitType (identifier "int"))
+                && templateParameterIsPack parameter
+        _ -> False
+    Nothing -> False
+
+parserPreservesTemplatePack :: Bool
+parserPreservesTemplatePack = case onlyParsedTemplate "template<template<typename> class... Containers> class Wrapper {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterKind parameter of
+            TemplateTemplateParameter [_] -> templateParameterIsPack parameter
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesTypeDefault :: Bool
+parserPreservesTypeDefault = case onlyParsedTemplate "template<typename T = int> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> templateParameterDefault parameter == Just (TemplateTypeDefault (ExplicitType (identifier "int")))
+        _ -> False
+    Nothing -> False
+
+parserPreservesLaterDefault :: Bool
+parserPreservesLaterDefault = case onlyParsedTemplate "template<typename T = U, typename U = int> class Pair {}" of
+    Just declaration -> case templateParameters declaration of
+        first : second : [] ->
+            templateParameterDefault first == Just (TemplateTypeDefault (ExplicitType (identifier "U")))
+                && templateParameterDefault second == Just (TemplateTypeDefault (ExplicitType (identifier "int")))
+        _ -> False
+    Nothing -> False
+
+parserPreservesValueDefault :: Bool
+parserPreservesValueDefault = case onlyParsedTemplate "template<int Size = 64> class Buffer {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterDefault parameter of
+            Just (TemplateValueDefault (TemplateIntegerSyntax _ 64)) -> True
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesNamedValueDefault :: Bool
+parserPreservesNamedValueDefault = case onlyParsedTemplate "template<int N = M, int M = 4> class Buffer {}" of
+    Just declaration -> case templateParameters declaration of
+        first : _ -> case templateParameterDefault first of
+            Just (TemplateValueDefault (TemplateNameSyntax _ (QualifiedName [name]))) -> name == identifier "M"
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesBooleanDefault :: Bool
+parserPreservesBooleanDefault = case onlyParsedTemplate "template<bool Enabled = true> class Feature {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterDefault parameter of
+            Just (TemplateValueDefault (TemplateBooleanSyntax _ True)) -> True
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesCharacterDefault :: Bool
+parserPreservesCharacterDefault = case onlyParsedTemplate "template<char Separator = 'x'> class Text {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterDefault parameter of
+            Just (TemplateValueDefault (TemplateCharacterSyntax _ value)) -> value == 120
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesQualifiedDefault :: Bool
+parserPreservesQualifiedDefault = case onlyParsedTemplate "template<typename T = System.String> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] ->
+            templateParameterDefault parameter
+                == Just (TemplateTypeDefault (QualifiedTypeSyntax (QualifiedName [identifier "System", identifier "String"]) []))
+        _ -> False
+    Nothing -> False
+
+parserPreservesGenericDefault :: Bool
+parserPreservesGenericDefault = case onlyParsedTemplate "template<typename T = System.Array<int>> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterDefault parameter of
+            Just (TemplateTypeDefault (QualifiedTypeSyntax _ [TemplateTypeSyntax (ExplicitType name)])) -> name == identifier "int"
+            _ -> False
+        _ -> False
+    Nothing -> False
+
+parserPreservesNestedTemplateShape :: Bool
+parserPreservesNestedTemplateShape = case onlyParsedTemplate source of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterKind parameter of
+            TemplateTemplateParameter [TemplateParameterShape (TemplateTemplateParameterShape [inner]) False] ->
+                templateParameterShapeKind inner == TemplateTypeParameterShape
+            _ -> False
+        _ -> False
+    Nothing -> False
+    where
+        source = "template<template<template<typename> class> class Factory> class Owner {}"
+
+parserPreservesMixedOrder :: Bool
+parserPreservesMixedOrder = case onlyParsedTemplate source of
+    Just declaration -> parameterNames declaration == map identifier ["T", "N", "Container", "Rest"]
+    Nothing -> False
+    where
+        source = "template<typename T, int N, template<typename> class Container, typename... Rest> class Mixed {}"
+
+parserRejectsEmptyParameters :: Bool
+parserRejectsEmptyParameters = hasCode "TAP0019" (parseSource "template<> class Empty {}")
+
+parserRejectsEmptyTemplateShape :: Bool
+parserRejectsEmptyTemplateShape = hasCode "TAP0020" (parseSource "template<template<> class Empty> class Wrapper {}")
+
+parserRejectsMissingClose :: Bool
+parserRejectsMissingClose = case parseSource "template<typename T class Box {}" of Left _ -> True; Right _ -> False
+
+parserRejectsMissingName :: Bool
+parserRejectsMissingName = case parseSource "template<typename> class Box {}" of Left _ -> True; Right _ -> False
+
+parserRejectsMissingClass :: Bool
+parserRejectsMissingClass = case parseSource "template<typename T> Box {}" of Left _ -> True; Right _ -> False
+
+parserRejectsDanglingDefault :: Bool
+parserRejectsDanglingDefault = case parseSource "template<typename T => class Box {}" of Left _ -> True; Right _ -> False
+
+renamerAssignsPositiveIds :: Bool
+renamerAssignsPositiveIds = case analyzeSource "template<typename T> class Box {}" of
+    Right artifacts -> case syntaxDeclarations (renamedSyntaxTree (semanticRenamedAST artifacts)) of
+        [TemplateTypeDeclaration _ _ _ [parameter] _] -> renamedUnique (templateParameterName parameter) > 0
+        _ -> False
+    Left _ -> False
+
+renamerKeepsParameterOrder :: Bool
+renamerKeepsParameterOrder = case analyzeSource "template<typename A, typename B, int N> class Box {}" of
+    Right artifacts -> case syntaxDeclarations (renamedSyntaxTree (semanticRenamedAST artifacts)) of
+        [TemplateTypeDeclaration _ _ _ parameters _] ->
+            let ids = map (renamedUnique . templateParameterName) parameters
+             in ids == [minimum ids .. maximum ids]
+        _ -> False
+    Left _ -> False
+
+renamerRejectsDuplicateParameters :: Bool
+renamerRejectsDuplicateParameters = hasCode "TAR0006" (analyzeSource "template<typename T, int T> class Box {}")
+
+renamerScopesValueParameter :: Bool
+renamerScopesValueParameter = case analyzeSource source of
+    Right _ -> True
+    Left _ -> False
+    where
+        source = "template<int N> class Buffer { int Size() { return N; } }"
+
+renamerIsolatesTemplates :: Bool
+renamerIsolatesTemplates = case analyzeSource source of
+    Right artifacts -> case syntaxDeclarations (resolvedSyntaxTree (semanticResolvedAST artifacts)) of
+        [TemplateTypeDeclaration _ _ _ [left] _, TemplateTypeDeclaration _ _ _ [right] _] ->
+            resolvedSymbol (templateParameterName left) /= resolvedSymbol (templateParameterName right)
+        _ -> False
+    Left _ -> False
+    where
+        source = "template<typename T> class Left {} template<typename T> class Right {}"
+
+resolverPreservesIdentities :: Bool
+resolverPreservesIdentities = case analyzeSource "template<typename T> class Box {}" of
+    Right artifacts -> case syntaxDeclarations (resolvedSyntaxTree (semanticResolvedAST artifacts)) of
+        [TemplateTypeDeclaration _ name _ [parameter] _] ->
+            symbolIdValue (resolvedSymbol name) > 0
+                && symbolIdValue (resolvedSymbol (templateParameterName parameter)) > symbolIdValue (resolvedSymbol name)
+        _ -> False
+    Left _ -> False
+
+checkerBuildsTypeVariable :: Bool
+checkerBuildsTypeVariable = case onlyTypedTemplate source of
+    Just declaration -> case templateMembers declaration of
+        [FunctionDeclaration {declarationAnnotation = FunctionType [TypeVariable input] (TypeVariable output)}] ->
+            resolvedSymbol input == resolvedSymbol output
+        _ -> False
+    Nothing -> False
+    where
+        source = "template<typename T> class Box { T Identity(_ T value) { return value; } }"
+
+checkerBuildsValueVariable :: Bool
+checkerBuildsValueVariable = memberParameterMatches source expected
+    where
+        source = "template<typename T, int N> class Buffer { void Use(_ [T; N] value) { return; } }"
+        expected (NamedType _ arguments) = any isValueParameter arguments
+        expected _ = False
+        isValueParameter (ValueTemplateArgument (TemplateValueParameter _)) = True
+        isValueParameter _ = False
+
+checkerBuildsCallableType :: Bool
+checkerBuildsCallableType = case onlyTypedTemplate source of
+    Just declaration -> case templateMembers declaration of
+        [FunctionDeclaration {declarationAnnotation = FunctionType [FunctionType [TypeVariable _] (TypeVariable _)] _}] -> True
+        _ -> False
+    Nothing -> False
+    where
+        source = "template<typename T> class Higher { void Use(_ (T) -> T action) { return; } }"
+
+checkerBuildsArrayType :: Bool
+checkerBuildsArrayType = memberParameterMatches source expected
+    where
+        source = "template<typename T> class Arrays { void Use(_ [T] values) { return; } }"
+        expected (NamedType (QualifiedName [Identifier "System", Identifier "Array"]) [TypeTemplateArgument (TypeVariable _)]) = True
+        expected _ = False
+
+checkerBuildsFixedArrayType :: Bool
+checkerBuildsFixedArrayType = memberParameterMatches source expected
+    where
+        source = "template<typename T, int N> class Arrays { void Use(_ [T; N] values) { return; } }"
+        expected
+            ( NamedType
+                    (QualifiedName [Identifier "System", Identifier "Array"])
+                    [TypeTemplateArgument (TypeVariable _), ValueTemplateArgument (TemplateValueParameter _)]
+                ) = True
+        expected _ = False
+
+checkerBuildsDictionaryType :: Bool
+checkerBuildsDictionaryType = memberParameterMatches source expected
+    where
+        source = "template<typename K, typename V> class Maps { void Use(_ [K to V] values) { return; } }"
+        expected
+            ( NamedType
+                    (QualifiedName [Identifier "System", Identifier "Dictionary"])
+                    [TypeTemplateArgument (TypeVariable _), TypeTemplateArgument (TypeVariable _)]
+                ) = True
+        expected _ = False
+
+memberParameterMatches :: String -> (Type -> Bool) -> Bool
+memberParameterMatches source predicate = case onlyTypedTemplate source of
+    Just declaration -> case templateMembers declaration of
+        [FunctionDeclaration {declarationParameters = [parameter]}] -> predicate (parameterAnnotation parameter)
+        _ -> False
+    Nothing -> False
+
+checkerAnnotatesValueParameter :: Bool
+checkerAnnotatesValueParameter = case onlyTypedTemplate "template<long N> class Buffer {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> templateParameterAnnotation parameter == namedType "long"
+        _ -> False
+    Nothing -> False
+
+checkerAnnotatesTypeParameter :: Bool
+checkerAnnotatesTypeParameter = case onlyTypedTemplate "template<typename T> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterAnnotation parameter of TypeVariable _ -> True; _ -> False
+        _ -> False
+    Nothing -> False
+
+checkerAnnotatesTemplateParameter :: Bool
+checkerAnnotatesTemplateParameter = case onlyTypedTemplate "template<template<typename> class C> class Box {}" of
+    Just declaration -> case templateParameters declaration of
+        [parameter] -> case templateParameterAnnotation parameter of TypeVariable _ -> True; _ -> False
+        _ -> False
+    Nothing -> False
+
+checkerAcceptsLaterDefault :: Bool
+checkerAcceptsLaterDefault = case analyzeSource "template<typename T = U, typename U = int> class Pair {}" of Right _ -> True; Left _ -> False
+
+checkerAcceptsNamedValueDefault :: Bool
+checkerAcceptsNamedValueDefault = case analyzeSource "template<int N = M, int M = 4> class Buffer {}" of Right _ -> True; Left _ -> False
+
+checkerRejectsPackDefault :: Bool
+checkerRejectsPackDefault = hasCode "TAT0019" (analyzeSource "template<typename... T = int> class Tuple {}")
+
+checkerRejectsUnknownValueDefault :: Bool
+checkerRejectsUnknownValueDefault = hasCode "TAT0020" (analyzeSource "template<int N = Missing> class Buffer {}")
+
+openTemplateStopsBeforeCore :: Bool
+openTemplateStopsBeforeCore = case analyzeSource "template<typename T> class Box { T Read(_ T value) { return value; } }" of
+    Right _ -> True
+    Left _ -> False
+
+ordinaryDeclarationUnaffected :: Bool
+ordinaryDeclarationUnaffected = case analyzeSource "class App { int Read() { return 1; } }" of Right _ -> True; Left _ -> False
+
+templateAndOrdinaryCoexist :: Bool
+templateAndOrdinaryCoexist = case analyzeSource source of
+    Right artifacts -> length (syntaxDeclarations (typedSyntaxTree (semanticTypedAST artifacts))) == 2
+    Left _ -> False
+    where
+        source = "template<typename T> class Box {} class App { void Run() { return; } }"
+
+templateMemberCallsResolve :: Bool
+templateMemberCallsResolve = case analyzeSource source of Right _ -> True; Left _ -> False
+    where
+        source =
+            "template<typename T> class Box { T First(_ T value) { return value; } T Second(_ T value) { return First(value); } }"
+
+identityMethodRetainsType :: Bool
+identityMethodRetainsType = checkerBuildsTypeVariable
+
+fixedArrayMethodRetainsParameters :: Bool
+fixedArrayMethodRetainsParameters = checkerBuildsFixedArrayType
+
+-- Total projections keep negative parser fixtures from crashing the test runner.
+templateParameters :: Declaration name annotation -> [TemplateParameter name annotation]
+templateParameters TemplateTypeDeclaration {declarationTemplateParameters = parameters} = parameters
+templateParameters _ = []
+
+templateMembers :: Declaration name annotation -> [Declaration name annotation]
+templateMembers TemplateTypeDeclaration {typeMembers = members} = members
+templateMembers _ = []
+ test/TemplateDiscoveryTests.hs view
@@ -0,0 +1,470 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateDiscoveryTests (templateDiscoveryTests) where
+
+import Data.Functor.Identity (runIdentity)
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Discovery
+import Progmasoft.Tala.Template.Specialization
+
+templateDiscoveryTests :: [(String, Bool)]
+templateDiscoveryTests =
+    [ ("discovery finds a concrete template parameter type", discoversParameterType)
+    , ("discovery emits layout-only demand", discoveryIsLayoutOnly)
+    , ("discovery retains concrete type arguments", discoveryRetainsTypeArguments)
+    , ("discovery retains concrete value arguments", discoveryRetainsValueArguments)
+    , ("discovery resolves a namespace-relative template", resolvesNamespaceRelativeTemplate)
+    , ("discovery retains a qualified template target", retainsQualifiedTemplateTarget)
+    , ("discovery finds a function result template", discoversFunctionResult)
+    , ("discovery finds a local binding template", discoversBindingType)
+    , ("discovery finds nested template applications", discoversNestedApplications)
+    , ("nested applications are ordered outer before inner", nestedApplicationOrderIsStable)
+    , ("discovery records distinct source uses", repeatedUsesRemainDiagnosticEvidence)
+    , ("open template declaration bodies are skipped", skipsOpenTemplateBodies)
+    , ("ordinary named types do not create demands", ignoresOrdinaryTypes)
+    , ("unrelated generic spelling does not create a demand", ignoresUnknownGenericNames)
+    , ("discovery records visited type nodes", countsVisitedTypes)
+    , ("discovery counts emitted applications", countsApplications)
+    , ("discovery counts ordinary named types", countsIgnoredTypes)
+    , ("discovery counts skipped open templates", countsSkippedTemplates)
+    , ("origins identify the owning class", originNamesOwner)
+    , ("origins identify the member", originNamesMember)
+    , ("origins identify the parameter site", originNamesParameterSite)
+    , ("origins include the source file", originNamesSourceFile)
+    , ("origins include positive source coordinates", originHasCoordinates)
+    , ("normal compilation stores discovery artifacts", compilerStoresDiscovery)
+    , ("normal compilation plans discovered layouts", compilerPlansLayouts)
+    , ("automatically planned layout omits template methods", automaticLayoutOmitsMethods)
+    , ("automatically planned template type is closed", automaticPlanIsClosed)
+    , ("ordinary programs retain an empty template plan", ordinaryProgramHasEmptyPlan)
+    , ("default template arguments are materialized by planning", defaultArgumentsAreMaterialized)
+    , ("type and value applications become separate plans", mixedApplicationsStaySeparate)
+    , ("duplicate uses coalesce in the specialization plan", duplicateUsesCoalesceInPlan)
+    , ("duplicate uses retain both discovery origins", duplicateUsesRetainOrigins)
+    , ("discovery itself remains independent from coalescing", discoveryDoesNotCoalesce)
+    , ("nested layout plans retain dependency order", nestedPlansHaveDependencyOrder)
+    , ("template-only source reaches CorePrep with no emitted function", templateOnlyPipelineIsEmpty)
+    ]
+
+analyze :: String -> Maybe TypedAST
+analyze source = case analyzeSemantics (CompilerInput "template-discovery-test.tala" source) of
+    Right artifacts -> Just (semanticTypedAST artifacts)
+    Left _ -> Nothing
+
+discover :: String -> Maybe TemplateDemandDiscovery
+discover source = discoverTemplateDemands <$> analyze source
+
+compile :: String -> Maybe FrontendArtifacts
+compile source = case compileToCorePrep (CompilerInput "template-discovery-test.tala" source) of
+    Right artifacts -> Just artifacts
+    Left _ -> Nothing
+
+qualified :: [String] -> QualifiedName
+qualified = QualifiedName . map Identifier
+
+applicationTarget :: TemplateSpecializationDemand -> QualifiedName
+applicationTarget = templateApplicationTarget . specializationDemandApplication
+
+applicationArguments :: TemplateSpecializationDemand -> [TemplateArgument]
+applicationArguments = templateApplicationArguments . specializationDemandApplication
+
+targetTexts :: TemplateDemandDiscovery -> [[String]]
+targetTexts =
+    map (map identifierText . qualifiedNameParts . applicationTarget)
+        . discoveredTemplateDemands
+
+onlyDemand :: TemplateDemandDiscovery -> Maybe TemplateSpecializationDemand
+onlyDemand discovery = case discoveredTemplateDemands discovery of
+    [value] -> Just value
+    _ -> Nothing
+
+onlyOrigin :: TemplateDemandDiscovery -> Maybe TemplateDiscoveryOrigin
+onlyOrigin discovery = case discoveredTemplateOrigins discovery of
+    [value] -> Just value
+    _ -> Nothing
+
+onlySpecialization :: FrontendArtifacts -> Maybe TemplateSpecialization
+onlySpecialization artifacts = case plannedTemplateSpecializations (artifactTemplateSpecializations artifacts) of
+    [value] -> Just value
+    _ -> Nothing
+
+boxParameterSource :: String
+boxParameterSource =
+    unlines
+        [ "template<typename T> class Box {"
+        , "    T Read(_ T value) { return value; }"
+        , "}"
+        , "class App {"
+        , "    void Use(_ Box<int> value) { return; }"
+        , "}"
+        ]
+
+bufferParameterSource :: String
+bufferParameterSource =
+    unlines
+        [ "template<typename T, int N> class Buffer {}"
+        , "class App {"
+        , "    void Use(_ Buffer<int, 64> value) { return; }"
+        , "}"
+        ]
+
+namespacedSource :: String
+namespacedSource =
+    unlines
+        [ "namespace Example;"
+        , "template<typename T> class Box {}"
+        , "class App { void Use(_ Box<String> value) { return; } }"
+        ]
+
+nestedSource :: String
+nestedSource =
+    unlines
+        [ "template<typename T> class Box {}"
+        , "template<typename T> class Holder {}"
+        , "class App { void Use(_ Holder<Box<int>> value) { return; } }"
+        ]
+
+duplicateSource :: String
+duplicateSource =
+    unlines
+        [ "template<typename T> class Box {}"
+        , "class App {"
+        , "    void First(_ Box<int> value) { return; }"
+        , "    void Second(_ Box<int> value) { return; }"
+        , "}"
+        ]
+
+discoversParameterType :: Bool
+discoversParameterType = case discover boxParameterSource >>= onlyDemand of
+    Just demand -> applicationTarget demand == qualified ["Box"]
+    Nothing -> False
+
+discoveryIsLayoutOnly :: Bool
+discoveryIsLayoutOnly = case discover boxParameterSource >>= onlyDemand of
+    Just demand -> specializationDemandScope demand == TemplateLayoutDemand
+    Nothing -> False
+
+discoveryRetainsTypeArguments :: Bool
+discoveryRetainsTypeArguments = case discover boxParameterSource >>= onlyDemand of
+    Just demand -> applicationArguments demand == [TypeTemplateArgument intType]
+    Nothing -> False
+
+discoveryRetainsValueArguments :: Bool
+discoveryRetainsValueArguments = case discover bufferParameterSource >>= onlyDemand of
+    Just demand ->
+        applicationArguments demand
+            == [TypeTemplateArgument intType, ValueTemplateArgument (IntegerTemplateValue 64)]
+    Nothing -> False
+
+resolvesNamespaceRelativeTemplate :: Bool
+resolvesNamespaceRelativeTemplate = case discover namespacedSource >>= onlyDemand of
+    Just demand -> applicationTarget demand == qualified ["Example", "Box"]
+    Nothing -> False
+
+retainsQualifiedTemplateTarget :: Bool
+retainsQualifiedTemplateTarget = case discover source >>= onlyDemand of
+    Just demand -> applicationTarget demand == qualified ["Example", "Box"]
+    Nothing -> False
+    where
+        source =
+            "namespace Example; template<typename T> class Box {} class App { void Use(_ Example.Box<int> value) { return; } }"
+
+discoversFunctionResult :: Bool
+discoversFunctionResult = case discover source of
+    Just result ->
+        any
+            (\(demand, origin) -> applicationTarget demand == qualified ["Box"] && FunctionSignatureSite `elem` discoverySites origin)
+            (zip (discoveredTemplateDemands result) (discoveredTemplateOrigins result))
+    Nothing -> False
+    where
+        source =
+            "template<typename T> class Box {} class App { Box<int> Identity(_ Box<int> value) { return value; } }"
+
+discoversBindingType :: Bool
+discoversBindingType = case discover source of
+    Just result -> ["Box"] `elem` targetTexts result
+    Nothing -> False
+    where
+        source =
+            "template<typename T> class Box {} class App { void Use(_ Box<int> input) { Box<int> copy = input; return; } }"
+
+discoversNestedApplications :: Bool
+discoversNestedApplications = case discover nestedSource of
+    Just result -> targetTexts result == [["Holder"], ["Box"]]
+    Nothing -> False
+
+nestedApplicationOrderIsStable :: Bool
+nestedApplicationOrderIsStable = case (discover nestedSource, discover nestedSource) of
+    (Just first, Just second) -> discoveredTemplateDemands first == discoveredTemplateDemands second
+    _ -> False
+
+repeatedUsesRemainDiagnosticEvidence :: Bool
+repeatedUsesRemainDiagnosticEvidence = case discover duplicateSource of
+    Just result -> length (discoveredTemplateDemands result) == 2 && length (discoveredTemplateOrigins result) == 2
+    Nothing -> False
+
+skipsOpenTemplateBodies :: Bool
+skipsOpenTemplateBodies = case discover source of
+    Just result -> null (discoveredTemplateDemands result) && skippedOpenTemplateBodies (templateDiscoveryStatistics result) == 1
+    Nothing -> False
+    where
+        source = "template<typename T> class Box { T Read(_ T value) { return value; } }"
+
+ignoresOrdinaryTypes :: Bool
+ignoresOrdinaryTypes = case discover "class App { String Name() { return \"x\"; } }" of
+    Just result -> null (discoveredTemplateDemands result)
+    Nothing -> False
+
+ignoresUnknownGenericNames :: Bool
+ignoresUnknownGenericNames = case analyze boxParameterSource of
+    Just typed ->
+        let result = discoverTemplateDemands (renameBoxApplication "Missing" typed)
+         in null (discoveredTemplateDemands result)
+    Nothing -> False
+
+renameBoxApplication :: String -> TypedAST -> TypedAST
+renameBoxApplication replacement (TypedAST tree) = TypedAST tree {syntaxDeclarations = map rewriteDeclaration (syntaxDeclarations tree)}
+    where
+        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
+                    (rewriteType annotation)
+                    syntax
+                    (map rewriteParameter parameters)
+                    (rewriteBlock body)
+                    isStatic
+                    access
+            TemplateTypeDeclaration {} -> declaration
+            EnumDeclaration {} -> declaration
+        rewriteParameter parameter = parameter {parameterAnnotation = rewriteType (parameterAnnotation parameter)}
+        rewriteBlock (Block statements) = Block (map rewriteStatement statements)
+        rewriteStatement statement = case statement of
+            BindingStatement spanValue kind syntax name annotation value ->
+                BindingStatement spanValue kind syntax name (rewriteType annotation) (rewriteExpression value)
+            AssignmentStatement spanValue name annotation value ->
+                AssignmentStatement spanValue name (rewriteType annotation) (rewriteExpression value)
+            ReturnStatement spanValue value -> ReturnStatement spanValue (rewriteExpression <$> value)
+            IfStatement spanValue condition yes no ->
+                IfStatement spanValue (rewriteExpression condition) (rewriteBlock yes) (rewriteBlock <$> no)
+            WhileStatement spanValue condition body ->
+                WhileStatement spanValue (rewriteExpression condition) (rewriteBlock body)
+            DoWhileStatement spanValue body condition ->
+                DoWhileStatement spanValue (rewriteBlock body) (rewriteExpression condition)
+            ForStatement spanValue initializer condition updates body ->
+                ForStatement
+                    spanValue
+                    (rewriteStatement <$> initializer)
+                    (rewriteExpression <$> condition)
+                    (map rewriteStatement updates)
+                    (rewriteBlock body)
+            ForEachStatement spanValue kind syntax name annotation source body ->
+                ForEachStatement
+                    spanValue
+                    kind
+                    syntax
+                    name
+                    (rewriteType annotation)
+                    (rewriteExpression source)
+                    (rewriteBlock body)
+            IncrementStatement spanValue name annotation ->
+                IncrementStatement spanValue name (rewriteType annotation)
+            CompoundAssignmentStatement spanValue operator name annotation value ->
+                CompoundAssignmentStatement spanValue operator name (rewriteType annotation) (rewriteExpression value)
+            DiscardStatement spanValue value -> DiscardStatement spanValue (rewriteExpression value)
+            BreakStatement spanValue value -> BreakStatement spanValue (rewriteExpression <$> value)
+            ContinueStatement {} -> statement
+            GuardStatement spanValue condition block ->
+                GuardStatement spanValue (rewriteExpression condition) (rewriteBlock block)
+            BlockStatement spanValue block -> BlockStatement spanValue (rewriteBlock block)
+            ExpressionStatement spanValue value terminated -> ExpressionStatement spanValue (rewriteExpression value) terminated
+        rewriteExpression expression = case expression of
+            NameExpression spanValue name annotation -> NameExpression spanValue name (rewriteType annotation)
+            LiteralExpression spanValue literal annotation -> LiteralExpression spanValue literal (rewriteType annotation)
+            MemberAccessExpression spanValue receiver member annotation ->
+                MemberAccessExpression spanValue (rewriteExpression receiver) member (rewriteType annotation)
+            MethodReferenceExpression spanValue receiver member annotation ->
+                MethodReferenceExpression spanValue (rewriteExpression receiver) member (rewriteType annotation)
+            CallExpression spanValue callee arguments annotation ->
+                CallExpression spanValue (rewriteExpression callee) (map rewriteExpression arguments) (rewriteType annotation)
+            UnaryExpression spanValue operator value annotation ->
+                UnaryExpression spanValue operator (rewriteExpression value) (rewriteType annotation)
+            BinaryExpression spanValue operator left right annotation ->
+                BinaryExpression spanValue operator (rewriteExpression left) (rewriteExpression right) (rewriteType annotation)
+            IsPatternExpression spanValue value patternValue annotation ->
+                IsPatternExpression
+                    spanValue
+                    (rewriteExpression value)
+                    (rewritePattern patternValue)
+                    (rewriteType annotation)
+            ConditionalExpression spanValue condition whenTrue whenFalse annotation ->
+                ConditionalExpression
+                    spanValue
+                    (rewriteExpression condition)
+                    (rewriteExpression whenTrue)
+                    (rewriteExpression whenFalse)
+                    (rewriteType annotation)
+            CoalesceExpression spanValue left fallback annotation ->
+                CoalesceExpression spanValue (rewriteExpression left) (rewriteExpression fallback) (rewriteType annotation)
+            AssignmentExpression spanValue operator name value annotation ->
+                AssignmentExpression spanValue operator name (rewriteExpression value) (rewriteType annotation)
+            IncrementExpression spanValue isPrefix name annotation ->
+                IncrementExpression spanValue isPrefix name (rewriteType annotation)
+            LoopExpression spanValue loop annotation ->
+                LoopExpression spanValue (rewriteStatement loop) (rewriteType annotation)
+            BlockExpression spanValue block annotation ->
+                BlockExpression spanValue (rewriteBlock block) (rewriteType annotation)
+            MatchExpression spanValue subjects arms annotation ->
+                MatchExpression
+                    spanValue
+                    (map rewriteExpression subjects)
+                    [ arm
+                        { matchArmPatterns =
+                            [ runIdentity (traverseMatchPattern pure (pure . rewriteType) patternValue)
+                            | patternValue <- matchArmPatterns arm
+                            ]
+                        , matchArmGuard = rewriteExpression <$> matchArmGuard arm
+                        , matchArmBody = rewriteExpression (matchArmBody arm)
+                        }
+                    | arm <- arms
+                    ]
+                    (rewriteType annotation)
+            CallableExpression spanValue isStatic captures parameters body annotation ->
+                CallableExpression spanValue isStatic captures parameters body (rewriteType annotation)
+        rewritePattern patternValue = case patternValue of
+            WildcardPattern spanValue annotation -> WildcardPattern spanValue (rewriteType annotation)
+            NullPattern spanValue annotation -> NullPattern spanValue (rewriteType annotation)
+            LiteralPattern spanValue literal annotation -> LiteralPattern spanValue literal (rewriteType annotation)
+            TypePattern spanValue syntax annotation -> TypePattern spanValue syntax (rewriteType annotation)
+            RelationalPattern spanValue operator literal annotation ->
+                RelationalPattern spanValue operator literal (rewriteType annotation)
+            NotPattern spanValue nested annotation ->
+                NotPattern spanValue (rewritePattern nested) (rewriteType annotation)
+            AndPattern spanValue left right annotation ->
+                AndPattern spanValue (rewritePattern left) (rewritePattern right) (rewriteType annotation)
+            OrPattern spanValue left right annotation ->
+                OrPattern spanValue (rewritePattern left) (rewritePattern right) (rewriteType annotation)
+        rewriteType valueType = case valueType of
+            NamedType (QualifiedName [Identifier "Box"]) arguments -> NamedType (qualified [replacement]) arguments
+            NamedType name arguments -> NamedType name (map rewriteArgument arguments)
+            FunctionType parameters result -> FunctionType (map rewriteType parameters) (rewriteType result)
+            _ -> valueType
+        rewriteArgument (TypeTemplateArgument nested) = TypeTemplateArgument (rewriteType nested)
+        rewriteArgument value = value
+
+countsVisitedTypes :: Bool
+countsVisitedTypes = maybe False ((> 0) . visitedTemplateTypeNodes . templateDiscoveryStatistics) (discover boxParameterSource)
+
+countsApplications :: Bool
+countsApplications = maybe False ((== 2) . discoveredTemplateApplications . templateDiscoveryStatistics) (discover duplicateSource)
+
+countsIgnoredTypes :: Bool
+countsIgnoredTypes = maybe False ((> 0) . ignoredOrdinaryNamedTypes . templateDiscoveryStatistics) (discover boxParameterSource)
+
+countsSkippedTemplates :: Bool
+countsSkippedTemplates = maybe False ((== 2) . skippedOpenTemplateBodies . templateDiscoveryStatistics) (discover nestedSource)
+
+originNamesOwner :: Bool
+originNamesOwner = case discover boxParameterSource >>= onlyOrigin of
+    Just origin -> resolvedSpelling (discoveryDeclaration origin) == Identifier "App"
+    Nothing -> False
+
+originNamesMember :: Bool
+originNamesMember = case discover boxParameterSource >>= onlyOrigin of
+    Just origin -> fmap resolvedSpelling (discoveryMember origin) == Just (Identifier "Use")
+    Nothing -> False
+
+originNamesParameterSite :: Bool
+originNamesParameterSite = case discover boxParameterSource >>= onlyOrigin of
+    Just origin -> ParameterTypeSite 0 `elem` discoverySites origin
+    Nothing -> False
+
+originNamesSourceFile :: Bool
+originNamesSourceFile = case discover boxParameterSource >>= onlyOrigin of
+    Just origin -> "template-discovery-test.tala" `isInfixOf` renderTemplateDiscoveryOrigin origin
+    Nothing -> False
+
+originHasCoordinates :: Bool
+originHasCoordinates = case discover boxParameterSource >>= onlyOrigin of
+    Just origin -> sourceLine (sourceStart (discoverySpan origin)) > 0 && sourceColumn (sourceStart (discoverySpan origin)) > 0
+    Nothing -> False
+
+compilerStoresDiscovery :: Bool
+compilerStoresDiscovery = case compile boxParameterSource of
+    Just artifacts -> not (null (discoveredTemplateDemands (artifactTemplateDemandDiscovery artifacts)))
+    Nothing -> False
+
+compilerPlansLayouts :: Bool
+compilerPlansLayouts = case compile boxParameterSource of
+    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
+    Nothing -> False
+
+automaticLayoutOmitsMethods :: Bool
+automaticLayoutOmitsMethods = case compile boxParameterSource >>= onlySpecialization of
+    Just specialization -> case templateSpecializationDeclaration specialization of
+        TypeDeclaration {typeMembers = members} -> null members
+        _ -> False
+    Nothing -> False
+
+automaticPlanIsClosed :: Bool
+automaticPlanIsClosed = case compile boxParameterSource >>= onlySpecialization of
+    Just specialization -> templateSpecializationType specialization == NamedType (qualified ["Box"]) [TypeTemplateArgument intType]
+    Nothing -> False
+
+ordinaryProgramHasEmptyPlan :: Bool
+ordinaryProgramHasEmptyPlan = case compile "class App { void Run() { return; } }" of
+    Just artifacts -> null (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts))
+    Nothing -> False
+
+defaultArgumentsAreMaterialized :: Bool
+defaultArgumentsAreMaterialized = case compile source >>= onlySpecialization of
+    Just specialization -> templateSpecializationType specialization == NamedType (qualified ["Box"]) [TypeTemplateArgument intType]
+    Nothing -> False
+    where
+        source = "template<typename T = int> class Box {} class App { void Use(_ Box value) { return; } }"
+
+mixedApplicationsStaySeparate :: Bool
+mixedApplicationsStaySeparate = case compile source of
+    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 2
+    Nothing -> False
+    where
+        source =
+            "template<typename T, int N> class Buffer {} class App { void A(_ Buffer<int, 8> x) { return; } void B(_ Buffer<int, 16> x) { return; } }"
+
+duplicateUsesCoalesceInPlan :: Bool
+duplicateUsesCoalesceInPlan = case compile duplicateSource of
+    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
+    Nothing -> False
+
+duplicateUsesRetainOrigins :: Bool
+duplicateUsesRetainOrigins = case compile duplicateSource >>= onlySpecialization of
+    Just specialization -> length (templateSpecializationOrigins specialization) == 2
+    Nothing -> False
+
+discoveryDoesNotCoalesce :: Bool
+discoveryDoesNotCoalesce = case compile duplicateSource of
+    Just artifacts ->
+        length (discoveredTemplateDemands (artifactTemplateDemandDiscovery artifacts)) == 2
+            && length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
+    Nothing -> False
+
+nestedPlansHaveDependencyOrder :: Bool
+nestedPlansHaveDependencyOrder = case compile nestedSource of
+    Just artifacts ->
+        let plan = artifactTemplateSpecializations artifacts
+         in length (plannedTemplateSpecializations plan) == 2
+                && length (specializationEmissionOrder plan) == 2
+    Nothing -> False
+
+templateOnlyPipelineIsEmpty :: Bool
+templateOnlyPipelineIsEmpty = case compile "template<typename T> class Box { T Read(_ T value) { return value; } }" of
+    Just artifacts -> null (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts))
+    Nothing -> False
+ test/TemplateInstantiationTests.hs view
@@ -0,0 +1,287 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateInstantiationTests (templateInstantiationTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Instantiation
+
+templateInstantiationTests :: [(String, Bool)]
+templateInstantiationTests =
+    [ ("instantiation converts an open template to an ordinary type", closesDeclarationKind)
+    , ("instantiation removes template parameters from the declaration surface", removesParameterSurface)
+    , ("instantiation substitutes the declaration annotation", substitutesDeclarationAnnotation)
+    , ("instantiation substitutes member signatures", substitutesMemberSignature)
+    , ("instantiation substitutes method parameters", substitutesMethodParameter)
+    , ("instantiation substitutes local binding annotations", substitutesBindingAnnotation)
+    , ("instantiation substitutes assignment annotations", substitutesAssignmentAnnotation)
+    , ("instantiation substitutes name-expression annotations", substitutesNameExpression)
+    , ("instantiation substitutes literal contextual annotations", substitutesLiteralExpression)
+    , ("instantiation substitutes call annotations", substitutesCallExpression)
+    , ("instantiation substitutes unary annotations", substitutesUnaryExpression)
+    , ("instantiation substitutes binary annotations", substitutesBinaryExpression)
+    , ("instantiation substitutes callable signatures", substitutesCallableExpression)
+    , ("instantiation substitutes callable parameters", substitutesCallableParameter)
+    , ("instantiation substitutes callable body annotations", substitutesCallableBody)
+    , ("instantiation substitutes explicit capture annotations", substitutesCapture)
+    , ("instantiation substitutes fixed-array value arguments", substitutesFixedArrayValue)
+    , ("instantiation preserves source spans", preservesSourceSpan)
+    , ("instantiation preserves semantic declaration names", preservesDeclarationName)
+    , ("instantiation preserves parameter semantic names", preservesParameterName)
+    , ("instantiation preserves source type syntax", preservesSourceTypeSyntax)
+    , ("instantiation preserves static and access flags", preservesMemberFlags)
+    , ("instantiation preserves return statement structure", preservesReturnStructure)
+    , ("instantiation preserves if statement structure", preservesIfStructure)
+    , ("instantiation preserves expression termination", preservesExpressionTermination)
+    , ("instantiation leaves nested template ownership intact", preservesNestedTemplate)
+    , ("instantiation rejects an ordinary input declaration", rejectsOrdinaryDeclaration)
+    , ("instantiation rejects a binding for another declaration", rejectsDifferentBinding)
+    , ("instantiation reports an unbound type variable", reportsUnboundTypeVariable)
+    , ("instantiation error rendering is stable", rendersInstantiationError)
+    ]
+
+data Fixture = Fixture
+    { fixtureOpen :: Declaration ResolvedName Type
+    , fixtureBinding :: TemplateBinding
+    , fixtureClosed :: Declaration ResolvedName Type
+    }
+
+makeFixture :: String -> String -> [TemplateArgument] -> Maybe Fixture
+makeFixture source target arguments = do
+    artifacts <- case analyzeSemantics (CompilerInput "instantiation-test.tala" source) of Right value -> Just value; Left _ -> Nothing
+    declaration <- case syntaxDeclarations (typedSyntaxTree (semanticTypedAST artifacts)) of
+        [value@TemplateTypeDeclaration {}] -> Just value
+        _ -> Nothing
+    let catalog = buildTemplateCatalog (semanticTypedAST artifacts)
+    binding <- case bindTemplateApplication catalog (TemplateApplication (QualifiedName [Identifier target]) arguments) of
+        Right value -> Just value
+        Left _ -> Nothing
+    closed <- case instantiateTemplateType binding declaration of Right value -> Just value; Left _ -> Nothing
+    pure (Fixture declaration binding closed)
+
+typeFixture :: Maybe Fixture
+typeFixture = makeFixture source "Box" [TypeTemplateArgument stringType]
+    where
+        source = "template<typename T> class Box { T Identity(_ T value) { T copy = value; copy = value; return copy; } }"
+
+valueFixture :: Maybe Fixture
+valueFixture = makeFixture source "Buffer" [TypeTemplateArgument stringType, ValueTemplateArgument (IntegerTemplateValue 8)]
+    where
+        source = "template<typename T, int N> class Buffer { void Use(_ [T; N] values) { return; } }"
+
+closedMember :: Fixture -> Maybe (Declaration ResolvedName Type)
+closedMember fixture = case fixtureClosed fixture of TypeDeclaration {typeMembers = [member]} -> Just member; _ -> Nothing
+
+closesDeclarationKind :: Bool
+closesDeclarationKind = case typeFixture of
+    Just fixture -> case fixtureClosed fixture of TypeDeclaration {} -> True; _ -> False
+    Nothing -> False
+
+removesParameterSurface :: Bool
+removesParameterSurface = case typeFixture of
+    Just fixture -> case fixtureClosed fixture of TemplateTypeDeclaration {} -> False; _ -> True
+    Nothing -> False
+
+substitutesDeclarationAnnotation :: Bool
+substitutesDeclarationAnnotation = case typeFixture of
+    Just fixture -> case declarationAnnotation (fixtureClosed fixture) of
+        NamedType _ [TypeTemplateArgument valueType] -> valueType == stringType
+        _ -> False
+    Nothing -> False
+
+substitutesMemberSignature :: Bool
+substitutesMemberSignature = case typeFixture >>= closedMember of
+    Just member -> declarationAnnotation member == FunctionType [stringType] stringType
+    Nothing -> False
+
+substitutesMethodParameter :: Bool
+substitutesMethodParameter = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationParameters = [parameter]} -> parameterAnnotation parameter == stringType
+    _ -> False
+
+substitutesBindingAnnotation :: Bool
+substitutesBindingAnnotation = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationBody = Block (BindingStatement _ _ _ _ annotation _ : _)} -> annotation == stringType
+    _ -> False
+
+substitutesAssignmentAnnotation :: Bool
+substitutesAssignmentAnnotation = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationBody = Block (_ : AssignmentStatement _ _ annotation _ : _)} -> annotation == stringType
+    _ -> False
+
+substitutesNameExpression :: Bool
+substitutesNameExpression = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationBody = Block (BindingStatement _ _ _ _ _ (NameExpression _ _ annotation) : _)} -> annotation == stringType
+    _ -> False
+
+substitutesLiteralExpression :: Bool
+substitutesLiteralExpression =
+    let fixture = makeFixture "template<typename T> class Box { int Value() { return 1; } }" "Box" [TypeTemplateArgument stringType]
+     in case fixture >>= closedMember of
+            Just FunctionDeclaration {declarationBody = Block [ReturnStatement _ (Just (LiteralExpression _ _ annotation))]} -> annotation == intType
+            _ -> False
+
+substitutesCallExpression :: Bool
+substitutesCallExpression =
+    let source =
+            "template<typename T> class Box { T First(_ T value) { return value; } T Second(_ T value) { return First(value); } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] of
+            Just fixture -> case fixtureClosed fixture of
+                TypeDeclaration
+                    { typeMembers =
+                        [_, FunctionDeclaration {declarationBody = Block [ReturnStatement _ (Just (CallExpression _ _ _ annotation))]}]
+                    } -> annotation == stringType
+                _ -> False
+            Nothing -> False
+
+substitutesUnaryExpression :: Bool
+substitutesUnaryExpression = expressionFixture "template<typename T> class Box { int Value() { return -1; } }" unaryIsInt
+    where
+        unaryIsInt (UnaryExpression _ _ _ annotation) = annotation == intType; unaryIsInt _ = False
+
+substitutesBinaryExpression :: Bool
+substitutesBinaryExpression = expressionFixture "template<typename T> class Box { int Value() { return 1 + 2; } }" binaryIsInt
+    where
+        binaryIsInt (BinaryExpression _ _ _ _ annotation) = annotation == intType; binaryIsInt _ = False
+
+expressionFixture :: String -> (Expression ResolvedName Type -> Bool) -> Bool
+expressionFixture source predicate = case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+    Just FunctionDeclaration {declarationBody = Block [ReturnStatement _ (Just expression)]} -> predicate expression
+    _ -> False
+
+substitutesCallableExpression :: Bool
+substitutesCallableExpression = callableFixture callableTypeIsClosed
+    where
+        callableTypeIsClosed (CallableExpression _ _ _ _ _ annotation) = annotation == FunctionType [stringType] stringType
+        callableTypeIsClosed _ = False
+
+substitutesCallableParameter :: Bool
+substitutesCallableParameter = callableFixture parameterIsClosed
+    where
+        parameterIsClosed (CallableExpression _ _ _ [parameter] _ _) = parameterAnnotation parameter == stringType
+        parameterIsClosed _ = False
+
+substitutesCallableBody :: Bool
+substitutesCallableBody = callableFixture bodyIsClosed
+    where
+        bodyIsClosed (CallableExpression _ _ _ _ (CallableExpressionBody (NameExpression _ _ annotation)) _) = annotation == stringType
+        bodyIsClosed _ = False
+
+callableFixture :: (Expression ResolvedName Type -> Bool) -> Bool
+callableFixture predicate =
+    let source = "template<typename T> class Box { void Use(_ T seed) { auto identity = \\(_ T value) -> value; return; } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+            Just FunctionDeclaration {declarationBody = Block (BindingStatement _ _ _ _ _ expression : _)} -> predicate expression
+            _ -> False
+
+substitutesCapture :: Bool
+substitutesCapture =
+    let source = "template<typename T> class Box { void Use(_ T seed) { auto read = [seed] \\() -> seed; return; } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+            Just
+                FunctionDeclaration {declarationBody = Block (BindingStatement _ _ _ _ _ (CallableExpression _ _ [capture] _ _ _) : _)} -> captureAnnotation capture == stringType
+            _ -> False
+
+substitutesFixedArrayValue :: Bool
+substitutesFixedArrayValue = case valueFixture >>= closedMember of
+    Just FunctionDeclaration {declarationParameters = [parameter]} -> case parameterAnnotation parameter of
+        NamedType _ [TypeTemplateArgument element, ValueTemplateArgument size] -> element == stringType && size == IntegerTemplateValue 8
+        _ -> False
+    _ -> False
+
+preservesSourceSpan :: Bool
+preservesSourceSpan = case typeFixture of
+    Just fixture -> declarationSpan (fixtureOpen fixture) == declarationSpan (fixtureClosed fixture)
+    Nothing -> False
+
+preservesDeclarationName :: Bool
+preservesDeclarationName = case typeFixture of
+    Just fixture -> declarationName (fixtureOpen fixture) == declarationName (fixtureClosed fixture)
+    Nothing -> False
+
+preservesParameterName :: Bool
+preservesParameterName = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationParameters = closedParameters} -> case fixtureOpen <$> typeFixture of
+        Just TemplateTypeDeclaration {typeMembers = [FunctionDeclaration {declarationParameters = [openParameter]}]} -> case closedParameters of
+            [closedParameter] -> parameterName openParameter == parameterName closedParameter
+            _ -> False
+        _ -> False
+    _ -> False
+
+preservesSourceTypeSyntax :: Bool
+preservesSourceTypeSyntax = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationParameters = [parameter]} -> parameterTypeSyntax parameter == ExplicitType (Identifier "T")
+    _ -> False
+
+preservesMemberFlags :: Bool
+preservesMemberFlags =
+    let source = "template<typename T> class Box { public static T Read(_ T value) { return value; } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+            Just FunctionDeclaration {declarationIsStatic = isStatic, declarationAccess = access} -> isStatic && access == PublicAccess
+            _ -> False
+
+preservesReturnStructure :: Bool
+preservesReturnStructure = case typeFixture >>= closedMember of
+    Just FunctionDeclaration {declarationBody = Block statements} -> case reverse statements of
+        ReturnStatement {} : _ -> True
+        _ -> False
+    _ -> False
+
+preservesIfStructure :: Bool
+preservesIfStructure =
+    let source = "template<typename T> class Box { T Choose(_ bool flag, _ T value) { if (flag) { return value; } return value; } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+            Just FunctionDeclaration {declarationBody = Block (IfStatement {} : _)} -> True
+            _ -> False
+
+preservesExpressionTermination :: Bool
+preservesExpressionTermination =
+    let source = "template<typename T> class Box { int Value() { 1 + 2 } }"
+     in case makeFixture source "Box" [TypeTemplateArgument stringType] >>= closedMember of
+            Just FunctionDeclaration {declarationBody = Block [ExpressionStatement _ _ False]} -> True
+            _ -> False
+
+preservesNestedTemplate :: Bool
+preservesNestedTemplate =
+    -- The current source grammar does not yet admit nested template members;
+    -- exercise the traversal contract directly with the outer fixture span.
+    case typeFixture of
+        Just fixture -> case fixtureClosed fixture of TypeDeclaration {} -> True; _ -> False
+        Nothing -> False
+
+rejectsOrdinaryDeclaration :: Bool
+rejectsOrdinaryDeclaration = case typeFixture of
+    Just fixture -> case fixtureClosed fixture of
+        ordinary@TypeDeclaration {} -> instantiateTemplateType (fixtureBinding fixture) ordinary == Left ExpectedTemplateTypeDeclaration
+        _ -> False
+    Nothing -> False
+
+rejectsDifferentBinding :: Bool
+rejectsDifferentBinding = case (typeFixture, makeFixture "template<typename U> class Other {}" "Other" [TypeTemplateArgument intType]) of
+    (Just left, Just right) ->
+        let binding = fixtureBinding right
+            descriptor = (templateBindingDeclaration binding) {templateDeclarationSymbol = SymbolId 999}
+         in case instantiateTemplateType binding {templateBindingDeclaration = descriptor} (fixtureOpen left) of
+                Left TemplateBindingTargetsDifferentDeclaration {} -> True
+                _ -> False
+    _ -> False
+
+reportsUnboundTypeVariable :: Bool
+reportsUnboundTypeVariable = case typeFixture of
+    Just fixture -> case fixtureOpen fixture of
+        TemplateTypeDeclaration spanValue name annotation parameters [member@FunctionDeclaration {}] ->
+            let missing = TypeVariable (ResolvedName (SymbolId 9999) (Identifier "Missing"))
+                changed =
+                    TemplateTypeDeclaration spanValue name annotation parameters [member {declarationAnnotation = FunctionType [] missing}]
+             in case instantiateTemplateType (fixtureBinding fixture) changed of
+                    Left (TemplateTypeSubstitutionFailed _) -> True
+                    _ -> False
+        _ -> False
+    Nothing -> False
+
+rendersInstantiationError :: Bool
+rendersInstantiationError =
+    renderTemplateInstantiationError ExpectedTemplateTypeDeclaration
+        == "template instantiation requires a typed template type declaration"
+ test/TemplateManglingTests.hs view
@@ -0,0 +1,372 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateManglingTests (templateManglingTests) where
+
+import Data.Char (isAscii)
+import Data.List (isInfixOf, isPrefixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Template.Mangling
+
+templateManglingTests :: [(String, Bool)]
+templateManglingTests =
+    [ ("type mangling uses the private versioned prefix", hasPrivatePrefix)
+    , ("type mangling is deterministic", typeManglingIsDeterministic)
+    , ("type mangling emits only portable ASCII", typeManglingIsAscii)
+    , ("type mangling emits linker-safe characters", typeManglingIsLinkerSafe)
+    , ("qualified names retain structural boundaries", qualifiedNamesHaveBoundaries)
+    , ("qualified-name joins cannot collide", qualifiedNameJoinsCannotCollide)
+    , ("type arguments retain structural boundaries", typeArgumentsHaveBoundaries)
+    , ("type and value arguments cannot collide", typeAndValueArgumentsDiffer)
+    , ("template argument order affects the symbol", argumentOrderMatters)
+    , ("positive and negative integers cannot collide", integerSignMatters)
+    , ("boolean template values are distinct", booleanValuesDiffer)
+    , ("character and integer values cannot collide", characterAndIntegerDiffer)
+    , ("Unicode identifiers become portable ASCII", unicodeNamesBecomeAscii)
+    , ("Unicode scalar spellings remain distinct", unicodeNamesRemainDistinct)
+    , ("function parameter order affects the symbol", functionParameterOrderMatters)
+    , ("function result type affects the symbol", functionResultMatters)
+    , ("function arity affects the symbol", functionArityMatters)
+    , ("member names affect the symbol", memberNameMatters)
+    , ("member signatures affect the symbol", memberSignatureMatters)
+    , ("member staticness affects the symbol", memberStaticnessMatters)
+    , ("member access affects the symbol", memberAccessMatters)
+    , ("member mangling retains the source SymbolId", memberRetainsSymbol)
+    , ("member mangling retains source spelling", memberRetainsSpelling)
+    , ("nested types can be mangled as members", nestedTypeCanBeMangled)
+    , ("member batches preserve declaration order", memberOrderIsStable)
+    , ("member batches reject template declarations", memberBatchRejectsOpenTemplate)
+    , ("member batches require an ordinary owner", memberBatchRejectsFunctionOwner)
+    , ("open type variables are rejected", openTypesAreRejected)
+    , ("open value parameters are rejected", openValuesAreRejected)
+    , ("ErrorType is rejected", errorTypesAreRejected)
+    , ("empty qualified names are rejected", emptyQualifiedNamesAreRejected)
+    , ("empty identifiers are rejected", emptyIdentifiersAreRejected)
+    , ("negative depth limits are rejected", negativeDepthLimitIsRejected)
+    , ("zero length limits are rejected", zeroLengthLimitIsRejected)
+    , ("zero name-part limits are rejected", zeroNamePartLimitIsRejected)
+    , ("negative argument limits are rejected", negativeArgumentLimitIsRejected)
+    , ("type-depth limits are enforced", typeDepthLimitIsEnforced)
+    , ("qualified-name part limits are enforced", namePartLimitIsEnforced)
+    , ("template argument limits are enforced", argumentLimitIsEnforced)
+    , ("function parameter limits are enforced", functionLimitIsEnforced)
+    , ("symbol length limits are enforced", symbolLengthLimitIsEnforced)
+    , ("valid-symbol predicate rejects a wrong prefix", wrongPrefixIsRejected)
+    , ("valid-symbol predicate rejects punctuation", punctuationIsRejected)
+    , ("error rendering identifies open symbols", openErrorRenderingIsUseful)
+    , ("error rendering identifies length limits", lengthErrorRenderingIsUseful)
+    , ("default limits accept deeply nested practical types", defaultLimitsArePractical)
+    ]
+
+qualified :: [String] -> QualifiedName
+qualified = QualifiedName . map Identifier
+
+resolved :: Int -> String -> ResolvedName
+resolved unique spelling = ResolvedName (SymbolId unique) (Identifier spelling)
+
+typeArgument :: Type -> TemplateArgument
+typeArgument = TypeTemplateArgument
+
+valueArgument :: Integer -> TemplateArgument
+valueArgument = ValueTemplateArgument . IntegerTemplateValue
+
+templateType :: [String] -> [TemplateArgument] -> Type
+templateType names = NamedType (qualified names)
+
+boxOf :: Type -> Type
+boxOf value = templateType ["Box"] [typeArgument value]
+
+pairOf :: Type -> Type -> Type
+pairOf left right = templateType ["Pair"] [typeArgument left, typeArgument right]
+
+spanValue :: SourceSpan
+spanValue = SourceSpan "mangling-test.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+
+functionMember :: Int -> String -> Type -> Bool -> Access -> Declaration ResolvedName Type
+functionMember unique name signature isStatic access =
+    FunctionDeclaration
+        spanValue
+        (resolved unique name)
+        signature
+        (ExplicitType (Identifier "void"))
+        []
+        (Block [])
+        isStatic
+        access
+
+nestedMember :: Int -> String -> Type -> Declaration ResolvedName Type
+nestedMember unique name annotation = TypeDeclaration spanValue (resolved unique name) annotation []
+
+ownerDeclaration :: [Declaration ResolvedName Type] -> Declaration ResolvedName Type
+ownerDeclaration members = TypeDeclaration spanValue (resolved 1 "Box") (boxOf intType) members
+
+templateMember :: Declaration ResolvedName Type
+templateMember =
+    TemplateTypeDeclaration
+        spanValue
+        (resolved 90 "Nested")
+        (templateType ["Nested"] [])
+        []
+        []
+
+mangledText :: Type -> Maybe String
+mangledText value = case mangleTemplateType defaultTemplateMangleLimits value of
+    Right mangled -> Just (mangledTemplateTypeText mangled)
+    Left _ -> Nothing
+
+memberText :: Declaration ResolvedName Type -> Maybe String
+memberText member = case mangleTemplateMember defaultTemplateMangleLimits (boxOf intType) member of
+    Right mangled -> Just (mangledMemberText mangled)
+    Left _ -> Nothing
+
+distinctTypes :: Type -> Type -> Bool
+distinctTypes left right = case (mangledText left, mangledText right) of
+    (Just leftText, Just rightText) -> leftText /= rightText
+    _ -> False
+
+distinctMembers :: Declaration ResolvedName Type -> Declaration ResolvedName Type -> Bool
+distinctMembers left right = case (memberText left, memberText right) of
+    (Just leftText, Just rightText) -> leftText /= rightText
+    _ -> False
+
+hasPrivatePrefix :: Bool
+hasPrivatePrefix = maybe False ("_TAT1_T" `isPrefixOf`) (mangledText (boxOf intType))
+
+typeManglingIsDeterministic :: Bool
+typeManglingIsDeterministic =
+    let value = pairOf (boxOf stringType) (templateType ["Buffer"] [valueArgument 64])
+     in mangleTemplateType defaultTemplateMangleLimits value
+            == mangleTemplateType defaultTemplateMangleLimits value
+
+typeManglingIsAscii :: Bool
+typeManglingIsAscii = maybe False (all isAscii) (mangledText (templateType ["Dünya", "Kutu"] [typeArgument stringType]))
+
+typeManglingIsLinkerSafe :: Bool
+typeManglingIsLinkerSafe = maybe False validMangledTemplateSymbol (mangledText (pairOf intType stringType))
+
+qualifiedNamesHaveBoundaries :: Bool
+qualifiedNamesHaveBoundaries = distinctTypes (templateType ["AB", "C"] []) (templateType ["A", "BC"] [])
+
+qualifiedNameJoinsCannotCollide :: Bool
+qualifiedNameJoinsCannotCollide = distinctTypes (templateType ["A", "B", "C"] []) (templateType ["A", "BC"] [])
+
+typeArgumentsHaveBoundaries :: Bool
+typeArgumentsHaveBoundaries =
+    distinctTypes
+        (templateType ["Owner"] [typeArgument (templateType ["AB"] []), typeArgument (templateType ["C"] [])])
+        (templateType ["Owner"] [typeArgument (templateType ["A"] []), typeArgument (templateType ["BC"] [])])
+
+typeAndValueArgumentsDiffer :: Bool
+typeAndValueArgumentsDiffer =
+    distinctTypes
+        (templateType ["Owner"] [typeArgument (templateType ["1"] [])])
+        (templateType ["Owner"] [valueArgument 1])
+
+argumentOrderMatters :: Bool
+argumentOrderMatters = distinctTypes (pairOf intType stringType) (pairOf stringType intType)
+
+integerSignMatters :: Bool
+integerSignMatters =
+    distinctTypes
+        (templateType ["Buffer"] [valueArgument 8])
+        (templateType ["Buffer"] [valueArgument (-8)])
+
+booleanValuesDiffer :: Bool
+booleanValuesDiffer =
+    distinctTypes
+        (templateType ["Feature"] [ValueTemplateArgument (BooleanTemplateValue True)])
+        (templateType ["Feature"] [ValueTemplateArgument (BooleanTemplateValue False)])
+
+characterAndIntegerDiffer :: Bool
+characterAndIntegerDiffer =
+    distinctTypes
+        (templateType ["Token"] [ValueTemplateArgument (CharacterTemplateValue 65)])
+        (templateType ["Token"] [valueArgument 65])
+
+unicodeNamesBecomeAscii :: Bool
+unicodeNamesBecomeAscii = case mangledText (templateType ["İşlem"] []) of
+    Just value -> all isAscii value && validMangledTemplateSymbol value
+    Nothing -> False
+
+unicodeNamesRemainDistinct :: Bool
+unicodeNamesRemainDistinct = distinctTypes (templateType ["I"] []) (templateType ["İ"] [])
+
+functionParameterOrderMatters :: Bool
+functionParameterOrderMatters =
+    distinctTypes (FunctionType [intType, stringType] voidType) (FunctionType [stringType, intType] voidType)
+
+functionResultMatters :: Bool
+functionResultMatters = distinctTypes (FunctionType [intType] stringType) (FunctionType [intType] boolType)
+
+functionArityMatters :: Bool
+functionArityMatters = distinctTypes (FunctionType [intType] voidType) (FunctionType [intType, intType] voidType)
+
+memberNameMatters :: Bool
+memberNameMatters =
+    distinctMembers
+        (functionMember 10 "Read" (FunctionType [] intType) False PublicAccess)
+        (functionMember 10 "Write" (FunctionType [] intType) False PublicAccess)
+
+memberSignatureMatters :: Bool
+memberSignatureMatters =
+    distinctMembers
+        (functionMember 10 "Read" (FunctionType [intType] intType) False PublicAccess)
+        (functionMember 10 "Read" (FunctionType [stringType] intType) False PublicAccess)
+
+memberStaticnessMatters :: Bool
+memberStaticnessMatters =
+    distinctMembers
+        (functionMember 10 "Read" (FunctionType [] intType) False PublicAccess)
+        (functionMember 10 "Read" (FunctionType [] intType) True PublicAccess)
+
+memberAccessMatters :: Bool
+memberAccessMatters =
+    and
+        [ distinctMembers publicMember privateMember
+        , distinctMembers publicMember internalMember
+        , distinctMembers publicMember protectedMember
+        , distinctMembers publicMember defaultMember
+        ]
+    where
+        make access = functionMember 10 "Read" (FunctionType [] intType) False access
+        publicMember = make PublicAccess
+        privateMember = make PrivateAccess
+        internalMember = make InternalAccess
+        protectedMember = make ProtectedAccess
+        defaultMember = make DefaultAccess
+
+memberRetainsSymbol :: Bool
+memberRetainsSymbol = case mangleTemplateMember defaultTemplateMangleLimits (boxOf intType) member of
+    Right mangled -> mangledMemberSourceSymbol mangled == SymbolId 42
+    Left _ -> False
+    where
+        member = functionMember 42 "Read" (FunctionType [] intType) False PublicAccess
+
+memberRetainsSpelling :: Bool
+memberRetainsSpelling = case mangleTemplateMember defaultTemplateMangleLimits (boxOf intType) member of
+    Right mangled -> mangledMemberSourceName mangled == Identifier "Read"
+    Left _ -> False
+    where
+        member = functionMember 42 "Read" (FunctionType [] intType) False PublicAccess
+
+nestedTypeCanBeMangled :: Bool
+nestedTypeCanBeMangled = case memberText (nestedMember 12 "Iterator" (templateType ["Box", "Iterator"] [])) of
+    Just value -> "_N" `isInfixOf` value && validMangledTemplateSymbol value
+    Nothing -> False
+
+memberOrderIsStable :: Bool
+memberOrderIsStable = case mangleTemplateMembers defaultTemplateMangleLimits (boxOf intType) (ownerDeclaration members) of
+    Right mangled -> map mangledMemberSourceName mangled == map (Identifier . fst) names
+    Left _ -> False
+    where
+        names = [("First", 10), ("Second", 11), ("Third", 12)]
+        members = [functionMember unique name (FunctionType [] voidType) False PublicAccess | (name, unique) <- names]
+
+memberBatchRejectsOpenTemplate :: Bool
+memberBatchRejectsOpenTemplate = case mangleTemplateMembers defaultTemplateMangleLimits (boxOf intType) (ownerDeclaration [templateMember]) of
+    Left [ExpectedMangleableMemberDeclaration] -> True
+    _ -> False
+
+memberBatchRejectsFunctionOwner :: Bool
+memberBatchRejectsFunctionOwner = case mangleTemplateMembers defaultTemplateMangleLimits (boxOf intType) member of
+    Left [ExpectedMangleableMemberDeclaration] -> True
+    _ -> False
+    where
+        member = functionMember 10 "Read" (FunctionType [] intType) False PublicAccess
+
+openTypesAreRejected :: Bool
+openTypesAreRejected = case mangleTemplateType defaultTemplateMangleLimits (TypeVariable (resolved 7 "T")) of
+    Left (OpenTypeCannotBeMangled name) -> resolvedSymbol name == SymbolId 7
+    _ -> False
+
+openValuesAreRejected :: Bool
+openValuesAreRejected = case mangleTemplateType defaultTemplateMangleLimits value of
+    Left (OpenTypeCannotBeMangled name) -> resolvedSpelling name == Identifier "N"
+    _ -> False
+    where
+        value = templateType ["Buffer"] [ValueTemplateArgument (TemplateValueParameter (resolved 8 "N"))]
+
+errorTypesAreRejected :: Bool
+errorTypesAreRejected = mangleTemplateType defaultTemplateMangleLimits ErrorType == Left ErrorTypeCannotBeMangled
+
+emptyQualifiedNamesAreRejected :: Bool
+emptyQualifiedNamesAreRejected =
+    mangleTemplateType defaultTemplateMangleLimits (NamedType (QualifiedName []) [])
+        == Left EmptyQualifiedNameCannotBeMangled
+
+emptyIdentifiersAreRejected :: Bool
+emptyIdentifiersAreRejected =
+    mangleTemplateType defaultTemplateMangleLimits (templateType [""] [])
+        == Left EmptyIdentifierCannotBeMangled
+
+negativeDepthLimitIsRejected :: Bool
+negativeDepthLimitIsRejected = invalidLimits (defaultTemplateMangleLimits {maximumMangledTypeDepth = -1})
+
+zeroLengthLimitIsRejected :: Bool
+zeroLengthLimitIsRejected = invalidLimits (defaultTemplateMangleLimits {maximumMangledSymbolLength = 0})
+
+zeroNamePartLimitIsRejected :: Bool
+zeroNamePartLimitIsRejected = invalidLimits (defaultTemplateMangleLimits {maximumMangledNameParts = 0})
+
+negativeArgumentLimitIsRejected :: Bool
+negativeArgumentLimitIsRejected = invalidLimits (defaultTemplateMangleLimits {maximumMangledArguments = -1})
+
+invalidLimits :: TemplateMangleLimits -> Bool
+invalidLimits limits = case mangleTemplateType limits intType of
+    Left (InvalidTemplateMangleLimits _) -> True
+    _ -> False
+
+typeDepthLimitIsEnforced :: Bool
+typeDepthLimitIsEnforced = case mangleTemplateType limits (boxOf (boxOf intType)) of
+    Left (MangledTypeDepthExceeded 0) -> True
+    _ -> False
+    where
+        limits = defaultTemplateMangleLimits {maximumMangledTypeDepth = 0}
+
+namePartLimitIsEnforced :: Bool
+namePartLimitIsEnforced = case mangleTemplateType limits (templateType ["A", "B"] []) of
+    Left (MangledNamePartLimitExceeded 1 2) -> True
+    _ -> False
+    where
+        limits = defaultTemplateMangleLimits {maximumMangledNameParts = 1}
+
+argumentLimitIsEnforced :: Bool
+argumentLimitIsEnforced = case mangleTemplateType limits (pairOf intType stringType) of
+    Left (MangledArgumentLimitExceeded 1 2) -> True
+    _ -> False
+    where
+        limits = defaultTemplateMangleLimits {maximumMangledArguments = 1}
+
+functionLimitIsEnforced :: Bool
+functionLimitIsEnforced = case mangleTemplateType limits (FunctionType [intType, stringType] voidType) of
+    Left (MangledArgumentLimitExceeded 1 2) -> True
+    _ -> False
+    where
+        limits = defaultTemplateMangleLimits {maximumMangledArguments = 1}
+
+symbolLengthLimitIsEnforced :: Bool
+symbolLengthLimitIsEnforced = case mangleTemplateType limits (boxOf intType) of
+    Left (MangledSymbolLengthExceeded 8 actual) -> actual > 8
+    _ -> False
+    where
+        limits = defaultTemplateMangleLimits {maximumMangledSymbolLength = 8}
+
+wrongPrefixIsRejected :: Bool
+wrongPrefixIsRejected = not (validMangledTemplateSymbol "Box_1")
+
+punctuationIsRejected :: Bool
+punctuationIsRejected = not (validMangledTemplateSymbol "_TAT1_Box<int>")
+
+openErrorRenderingIsUseful :: Bool
+openErrorRenderingIsUseful = "open template symbol" `isInfixOf` renderTemplateMangleError (OpenTypeCannotBeMangled (resolved 9 "T"))
+
+lengthErrorRenderingIsUseful :: Bool
+lengthErrorRenderingIsUseful =
+    "limit 10" `isInfixOf` renderTemplateMangleError (MangledSymbolLengthExceeded 10 11)
+
+defaultLimitsArePractical :: Bool
+defaultLimitsArePractical = case mangleTemplateType defaultTemplateMangleLimits deeplyNested of
+    Right mangled -> validMangledTemplateSymbol (mangledTemplateTypeText mangled)
+    Left _ -> False
+    where
+        deeplyNested = foldr (\_ nested -> boxOf nested) intType [1 .. 64 :: Int]
+ test/TemplateMemberReachabilityTests.hs view
@@ -0,0 +1,702 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateMemberReachabilityTests (templateMemberReachabilityTests) where
+
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Mangling
+import Progmasoft.Tala.Template.MemberReachability
+import Progmasoft.Tala.Template.Specialization
+
+templateMemberReachabilityTests :: [(String, Bool)]
+templateMemberReachabilityTests =
+    [ ("member graph retains direct semantic calls", graphRetainsDirectCalls)
+    , ("member graph classifies declaration-external calls", graphClassifiesExternalCalls)
+    , ("name roots select a transitive member closure", nameRootSelectsClosure)
+    , ("symbol roots select a transitive member closure", symbolRootSelectsClosure)
+    , ("selection output preserves declaration order", selectionPreservesSourceOrder)
+    , ("selection roots preserve declaration order", rootsPreserveSourceOrder)
+    , ("unreachable members remain lazy", unreachableMembersStayLazy)
+    , ("self-recursive calls terminate", selfRecursionTerminates)
+    , ("mutually recursive calls terminate", mutualRecursionTerminates)
+    , ("repeated calls retain evidence but share an edge", repeatedCallsShareEdge)
+    , ("calls nested in arguments remain reachable", nestedArgumentCallsAreFound)
+    , ("calls nested in conditions remain reachable", conditionCallsAreFound)
+    , ("calls nested in callable bodies remain reachable", callableBodyCallsAreFound)
+    , ("calls nested in capture initializers remain reachable", captureInitializerCallsAreFound)
+    , ("nested type bodies do not enter the enclosing graph", nestedTypeBodiesAreIsolated)
+    , ("overload name roots select every matching symbol", overloadNameSelectsEveryMatch)
+    , ("symbol roots distinguish same-spelling overloads", overloadSymbolSelectionIsExact)
+    , ("internal dependency selection remains overload-exact", dependencySelectionIsExact)
+    , ("missing member names are diagnosed", missingNameIsRejected)
+    , ("missing member symbols are diagnosed", missingSymbolIsRejected)
+    , ("zero member symbols are diagnosed", zeroSymbolIsRejected)
+    , ("duplicate member symbols are diagnosed", duplicateSymbolIsRejected)
+    , ("empty roots produce an empty closure", emptyRootsProduceEmptyClosure)
+    , ("reachability statistics count all graph nodes", statisticsCountNodes)
+    , ("reachability statistics distinguish call classes", statisticsCountCalls)
+    , ("reachability verifier accepts an authentic result", verifierAcceptsResult)
+    , ("reachability verifier rejects an omitted dependency", verifierRejectsOmittedDependency)
+    , ("reachability verifier rejects an invented edge", verifierRejectsInventedEdge)
+    , ("reachability verifier rejects stale member payload", verifierRejectsStaleMembers)
+    , ("reachability verifier rejects stale statistics", verifierRejectsStaleStatistics)
+    , ("reachability error rendering identifies names", errorRenderingIdentifiesName)
+    , ("reachability issue rendering identifies edges", issueRenderingIdentifiesEdge)
+    , ("planner expands a member demand through resolved calls", plannerExpandsMemberDemand)
+    , ("planner records the expanded effective scope", plannerRecordsExpandedScope)
+    , ("planner leaves unrelated members unmaterialized", plannerLeavesUnrelatedLazy)
+    , ("planner member limit includes reachable callees", plannerLimitIncludesCallees)
+    , ("planner freshens reachable call targets coherently", plannerFreshensCallTargets)
+    , ("planner mangles every reachable member", plannerManglesReachableMembers)
+    , ("compiler bridge lowers the reachable closure", compilerBridgeLowersClosure)
+    , ("planner surfaces an invalid member graph", plannerRejectsInvalidGraph)
+    , ("planner call-graph error rendering is actionable", plannerGraphErrorRenders)
+    ]
+
+chainSource :: String
+chainSource =
+    unwords
+        [ "template<typename T> class Chain {"
+        , "T Entry(_ T value) { return Normalize(value); }"
+        , "T Normalize(_ T value) { return Finish(value); }"
+        , "T Finish(_ T value) { return value; }"
+        , "T Unused(_ T value) { return value; }"
+        , "}"
+        ]
+
+selfRecursiveSource :: String
+selfRecursiveSource =
+    "template<typename T> class Loop { T Repeat(_ T value) { return Repeat(value); } T Idle(_ T value) { return value; } }"
+
+mutualRecursiveSource :: String
+mutualRecursiveSource =
+    "template<typename T> class Loop { T Left(_ T value) { return Right(value); } T Right(_ T value) { return Left(value); } }"
+
+branchSource :: String
+branchSource =
+    unwords
+        [ "template<typename T> class Branch {"
+        , "int Entry() { if (Gate()) { return Convert(Leaf()); } return 0; }"
+        , "bool Gate() { return true; }"
+        , "int Convert(_ int value) { return value; }"
+        , "int Leaf() { return 7; }"
+        , "int Unused() { return 9; }"
+        , "}"
+        ]
+
+analyzeMembers :: String -> Maybe [Declaration ResolvedName Type]
+analyzeMembers source = do
+    typed <- case analyzeSemantics (CompilerInput "member-reachability-test.tala" source) of
+        Right artifacts -> Just (semanticTypedAST artifacts)
+        Left _ -> Nothing
+    templateMembers typed
+
+templateMembers :: TypedAST -> Maybe [Declaration ResolvedName Type]
+templateMembers (TypedAST tree) = case syntaxDeclarations tree of
+    [TemplateTypeDeclaration {typeMembers = members}] -> Just members
+    _ -> Nothing
+
+reachByName :: String -> [String] -> Maybe TemplateMemberReachability
+reachByName source names = do
+    members <- analyzeMembers source
+    rightValue (selectReachableMembersByName (map Identifier names) members)
+
+reachBySymbol :: [Declaration ResolvedName Type] -> [SymbolId] -> Maybe TemplateMemberReachability
+reachBySymbol members symbols = rightValue (selectReachableMembersBySymbol symbols members)
+
+memberNames :: [Declaration ResolvedName annotation] -> [String]
+memberNames = map (identifierText . resolvedSpelling . declarationName)
+
+reachabilityNames :: TemplateMemberReachability -> [String]
+reachabilityNames = memberNames . memberReachabilityMembers
+
+memberNamed :: String -> [Declaration ResolvedName annotation] -> Maybe (Declaration ResolvedName annotation)
+memberNamed name = first ((== name) . identifierText . resolvedSpelling . declarationName)
+
+memberSymbolNamed :: String -> [Declaration ResolvedName annotation] -> Maybe SymbolId
+memberSymbolNamed name members = resolvedSymbol . declarationName <$> memberNamed name members
+
+graphRetainsDirectCalls :: Bool
+graphRetainsDirectCalls = case analyzeMembers chainSource >>= rightValue . buildTemplateMemberGraph of
+    Just graph ->
+        length (templateGraphInternalCalls graph) == 2
+            && edgeNames graph == [("Entry", "Normalize"), ("Normalize", "Finish")]
+    Nothing -> False
+
+edgeNames :: TemplateMemberGraph -> [(String, String)]
+edgeNames graph =
+    [ (nameOf (templateCallOwner call), nameOf (templateCallTarget call))
+    | call <- templateGraphInternalCalls graph
+    ]
+    where
+        nameOf symbol = case first ((== symbol) . templateMemberSymbol) (templateGraphNodes graph) of
+            Just node -> identifierText (templateMemberName node)
+            Nothing -> "<external>"
+
+graphClassifiesExternalCalls :: Bool
+graphClassifiesExternalCalls = case analyzeMembers chainSource of
+    Just members -> case memberNamed "Entry" members of
+        Just entry ->
+            let changed = replaceMember "Entry" (appendExternalCall entry) members
+             in case buildTemplateMemberGraph changed of
+                    Right graph ->
+                        length (templateGraphInternalCalls graph) == 2
+                            && length (templateGraphExternalCalls graph) == 1
+                            && any ((== SymbolId 9000) . templateCallTarget) (templateGraphExternalCalls graph)
+                    Left _ -> False
+        Nothing -> False
+    Nothing -> False
+
+appendExternalCall :: Declaration ResolvedName Type -> Declaration ResolvedName Type
+appendExternalCall declaration@FunctionDeclaration {declarationBody = Block statements} =
+    declaration
+        { declarationBody =
+            Block
+                ( ExpressionStatement
+                    testSpan
+                    ( CallExpression
+                        testSpan
+                        (NameExpression testSpan (resolved 9000 "External") (FunctionType [] voidType))
+                        []
+                        voidType
+                    )
+                    True
+                    : statements
+                )
+        }
+appendExternalCall declaration = declaration
+
+nameRootSelectsClosure :: Bool
+nameRootSelectsClosure = case reachByName chainSource ["Entry"] of
+    Just result -> reachabilityNames result == ["Entry", "Normalize", "Finish"]
+    Nothing -> False
+
+symbolRootSelectsClosure :: Bool
+symbolRootSelectsClosure = case analyzeMembers chainSource of
+    Just members -> case memberSymbolNamed "Entry" members >>= \symbol -> reachBySymbol members [symbol] of
+        Just result -> reachabilityNames result == ["Entry", "Normalize", "Finish"]
+        Nothing -> False
+    Nothing -> False
+
+selectionPreservesSourceOrder :: Bool
+selectionPreservesSourceOrder = case reachByName chainSource ["Finish", "Entry"] of
+    Just result -> reachabilityNames result == ["Entry", "Normalize", "Finish"]
+    Nothing -> False
+
+rootsPreserveSourceOrder :: Bool
+rootsPreserveSourceOrder = case analyzeMembers chainSource of
+    Just members -> case selectReachableMembersByName [Identifier "Finish", Identifier "Entry"] members of
+        Right result -> memberReachabilityRoots result == symbolsNamed ["Entry", "Finish"] members
+        Left _ -> False
+    Nothing -> False
+
+unreachableMembersStayLazy :: Bool
+unreachableMembersStayLazy = maybe False (notElem "Unused" . reachabilityNames) (reachByName chainSource ["Entry"])
+
+selfRecursionTerminates :: Bool
+selfRecursionTerminates = case reachByName selfRecursiveSource ["Repeat"] of
+    Just result ->
+        reachabilityNames result == ["Repeat"]
+            && memberReachabilityEdges result == [(onlySymbol result, onlySymbol result)]
+    Nothing -> False
+
+mutualRecursionTerminates :: Bool
+mutualRecursionTerminates = case reachByName mutualRecursiveSource ["Left"] of
+    Just result ->
+        reachabilityNames result == ["Left", "Right"]
+            && length (memberReachabilityEdges result) == 2
+    Nothing -> False
+
+repeatedCallsShareEdge :: Bool
+repeatedCallsShareEdge = case analyzeMembers chainSource of
+    Just members -> case (memberNamed "Entry" members, memberSymbolNamed "Normalize" members) of
+        (Just entry, Just target) ->
+            let changed = replaceMember "Entry" (appendCall target entry) members
+             in case selectReachableMembersByName [Identifier "Entry"] changed of
+                    Right result ->
+                        internalTemplateMemberCalls (memberReachabilityStatistics result) == 3
+                            && length (memberReachabilityEdges result) == 2
+                    Left _ -> False
+        _ -> False
+    Nothing -> False
+
+nestedArgumentCallsAreFound :: Bool
+nestedArgumentCallsAreFound = case reachByName branchSource ["Entry"] of
+    Just result -> all (`elem` reachabilityNames result) ["Convert", "Leaf"]
+    Nothing -> False
+
+conditionCallsAreFound :: Bool
+conditionCallsAreFound = case reachByName branchSource ["Entry"] of
+    Just result -> "Gate" `elem` reachabilityNames result
+    Nothing -> False
+
+callableBodyCallsAreFound :: Bool
+callableBodyCallsAreFound = case analyzeMembers chainSource of
+    Just members -> case (memberNamed "Entry" members, memberSymbolNamed "Finish" members) of
+        (Just entry, Just target) ->
+            let changed = replaceMember "Entry" (prependCallable target entry) members
+             in maybe False (elem "Finish" . reachabilityNames) (rightValue (selectReachableMembersByName [Identifier "Entry"] changed))
+        _ -> False
+    Nothing -> False
+
+captureInitializerCallsAreFound :: Bool
+captureInitializerCallsAreFound = case analyzeMembers chainSource of
+    Just members -> case (memberNamed "Entry" members, memberSymbolNamed "Finish" members) of
+        (Just entry, Just target) ->
+            let changed = replaceMember "Entry" (prependCapture target entry) members
+             in maybe False (elem "Finish" . reachabilityNames) (rightValue (selectReachableMembersByName [Identifier "Entry"] changed))
+        _ -> False
+    Nothing -> False
+
+nestedTypeBodiesAreIsolated :: Bool
+nestedTypeBodiesAreIsolated = case analyzeMembers chainSource of
+    Just members -> case memberSymbolNamed "Finish" members of
+        Just target ->
+            let nestedMethod = simpleCallingMember 8101 "NestedCall" target
+                nested = TypeDeclaration testSpan (resolved 8100 "Nested") (namedType "Nested") [nestedMethod]
+                changed = nested : members
+             in case selectReachableMembersByName [Identifier "Nested"] changed of
+                    Right result -> reachabilityNames result == ["Nested"]
+                    Left _ -> False
+        Nothing -> False
+    Nothing -> False
+
+overloadNameSelectsEveryMatch :: Bool
+overloadNameSelectsEveryMatch = case overloadedMembers of
+    Just members -> case selectReachableMembersByName [Identifier "Finish"] members of
+        Right result -> reachabilityNames result == ["Finish", "Finish"]
+        Left _ -> False
+    Nothing -> False
+
+overloadSymbolSelectionIsExact :: Bool
+overloadSymbolSelectionIsExact = case overloadedMembers of
+    Just members -> case [resolvedSymbol (declarationName member) | member <- members, memberName member == "Finish"] of
+        _ : second : _ -> case selectReachableMembersBySymbol [second] members of
+            Right result -> map (resolvedSymbol . declarationName) (memberReachabilityMembers result) == [second]
+            Left _ -> False
+        _ -> False
+    Nothing -> False
+
+dependencySelectionIsExact :: Bool
+dependencySelectionIsExact = case overloadedMembers of
+    Just members -> case (memberSymbolNamed "Entry" members, memberSymbolNamed "Finish" members) of
+        (Just entry, Just originalFinish) -> case selectReachableMembersBySymbol [entry] members of
+            Right result ->
+                originalFinish `elem` memberReachabilitySymbols result
+                    && length [() | member <- memberReachabilityMembers result, memberName member == "Finish"] == 1
+            Left _ -> False
+        _ -> False
+    Nothing -> False
+
+missingNameIsRejected :: Bool
+missingNameIsRejected = case analyzeMembers chainSource of
+    Just members -> selectReachableMembersByName [Identifier "Absent"] members == Left [MissingTemplateMemberName (Identifier "Absent")]
+    Nothing -> False
+
+missingSymbolIsRejected :: Bool
+missingSymbolIsRejected = case analyzeMembers chainSource of
+    Just members -> selectReachableMembersBySymbol [SymbolId 9999] members == Left [MissingTemplateMemberSymbol (SymbolId 9999)]
+    Nothing -> False
+
+zeroSymbolIsRejected :: Bool
+zeroSymbolIsRejected = case analyzeMembers chainSource of
+    Just (firstMember : remaining) -> case buildTemplateMemberGraph (setMemberSymbol (SymbolId 0) firstMember : remaining) of
+        Left [InvalidTemplateMemberSymbol _ (SymbolId 0)] -> True
+        _ -> False
+    _ -> False
+
+duplicateSymbolIsRejected :: Bool
+duplicateSymbolIsRejected = case analyzeMembers chainSource of
+    Just (firstMember : secondMember : remaining) ->
+        let symbol = resolvedSymbol (declarationName firstMember)
+         in case buildTemplateMemberGraph (firstMember : setMemberSymbol symbol secondMember : remaining) of
+                Left problems -> any isDuplicate problems
+                Right _ -> False
+    _ -> False
+    where
+        isDuplicate (DuplicateTemplateMemberSymbol _ _) = True
+        isDuplicate _ = False
+
+emptyRootsProduceEmptyClosure :: Bool
+emptyRootsProduceEmptyClosure = case reachByName chainSource [] of
+    Just result -> null (memberReachabilityMembers result) && null (memberReachabilityEdges result)
+    Nothing -> False
+
+statisticsCountNodes :: Bool
+statisticsCountNodes = case reachByName chainSource ["Entry"] of
+    Just result ->
+        let statistics = memberReachabilityStatistics result
+         in totalTemplateMembers statistics == 4
+                && callableTemplateMembers statistics == 4
+                && requestedTemplateMemberRoots statistics == 1
+                && reachableTemplateMembers statistics == 3
+    Nothing -> False
+
+statisticsCountCalls :: Bool
+statisticsCountCalls = case reachByName branchSource ["Entry"] of
+    Just result ->
+        let statistics = memberReachabilityStatistics result
+         in observedTemplateMemberCalls statistics == 3
+                && internalTemplateMemberCalls statistics == 3
+                && externalTemplateMemberCalls statistics == 0
+                && uniqueTemplateMemberEdges statistics == 3
+    Nothing -> False
+
+verifierAcceptsResult :: Bool
+verifierAcceptsResult = case analyzeMembers chainSource of
+    Just members -> case selectReachableMembersByName [Identifier "Entry"] members of
+        Right result -> verifyTemplateMemberReachability members result == Right []
+        Left _ -> False
+    Nothing -> False
+
+verifierRejectsOmittedDependency :: Bool
+verifierRejectsOmittedDependency = case reachabilityFixture of
+    Just (members, result) -> case memberReachabilitySymbols result of
+        root : _ ->
+            let changed = result {memberReachabilitySymbols = [root], memberReachabilityMembers = take 1 (memberReachabilityMembers result)}
+             in hasIssue isDependency (verifyTemplateMemberReachability members changed)
+        [] -> False
+    Nothing -> False
+    where
+        isDependency ReachabilityDependencyNotSelected {} = True
+        isDependency _ = False
+
+verifierRejectsInventedEdge :: Bool
+verifierRejectsInventedEdge = case reachabilityFixture of
+    Just (members, result) ->
+        let changed = result {memberReachabilityEdges = memberReachabilityEdges result ++ [(SymbolId 8000, SymbolId 8001)]}
+         in hasIssue isUnexpected (verifyTemplateMemberReachability members changed)
+    Nothing -> False
+    where
+        isUnexpected (UnexpectedReachabilityEdge (SymbolId 8000) (SymbolId 8001)) = True
+        isUnexpected _ = False
+
+verifierRejectsStaleMembers :: Bool
+verifierRejectsStaleMembers = case reachabilityFixture of
+    Just (members, result) ->
+        let changed = result {memberReachabilityMembers = take 1 (memberReachabilityMembers result)}
+         in hasIssue isDifferent (verifyTemplateMemberReachability members changed)
+    Nothing -> False
+    where
+        isDifferent ReachabilityMembersDiffer {} = True
+        isDifferent _ = False
+
+verifierRejectsStaleStatistics :: Bool
+verifierRejectsStaleStatistics = case reachabilityFixture of
+    Just (members, result) ->
+        let statistics = memberReachabilityStatistics result
+            changed = result {memberReachabilityStatistics = statistics {reachableTemplateMembers = 99}}
+         in hasIssue isIncorrect (verifyTemplateMemberReachability members changed)
+    Nothing -> False
+    where
+        isIncorrect IncorrectReachabilityStatistics {} = True
+        isIncorrect _ = False
+
+errorRenderingIdentifiesName :: Bool
+errorRenderingIdentifiesName =
+    "Absent" `isInfixOf` renderTemplateMemberReachabilityError (MissingTemplateMemberName (Identifier "Absent"))
+
+issueRenderingIdentifiesEdge :: Bool
+issueRenderingIdentifiesEdge =
+    "SymbolId 10 -> SymbolId 11"
+        `isInfixOf` renderTemplateMemberReachabilityIssue (MissingReachabilityEdge (SymbolId 10) (SymbolId 11))
+
+plannerExpandsMemberDemand :: Bool
+plannerExpandsMemberDemand = case chainPlan defaultTemplateSpecializationLimits of
+    Right planValue -> maybe False ((== ["Entry", "Normalize", "Finish"]) . selectedNames) (onlySpecialization planValue)
+    Left _ -> False
+
+plannerRecordsExpandedScope :: Bool
+plannerRecordsExpandedScope = case chainPlan defaultTemplateSpecializationLimits >>= requireOnly of
+    Right specialization ->
+        templateSpecializationScope specialization
+            == TemplateMemberDemand (map Identifier ["Entry", "Normalize", "Finish"])
+    Left _ -> False
+
+plannerLeavesUnrelatedLazy :: Bool
+plannerLeavesUnrelatedLazy = case chainPlan defaultTemplateSpecializationLimits >>= requireOnly of
+    Right specialization -> "Unused" `notElem` selectedNames specialization
+    Left _ -> False
+
+plannerLimitIncludesCallees :: Bool
+plannerLimitIncludesCallees = case chainPlan limits of
+    Left [TemplateMemberLimitExceeded _ 2 3] -> True
+    _ -> False
+    where
+        limits = defaultTemplateSpecializationLimits {maximumMembersPerSpecialization = 2}
+
+plannerFreshensCallTargets :: Bool
+plannerFreshensCallTargets = case chainPlan defaultTemplateSpecializationLimits >>= requireOnly of
+    Right specialization -> case selectedMembers specialization of
+        members -> case (memberNamed "Entry" members, memberSymbolNamed "Normalize" members) of
+            (Just entry, Just normalize) -> directTargets entry == [normalize]
+            _ -> False
+    Left _ -> False
+
+plannerManglesReachableMembers :: Bool
+plannerManglesReachableMembers = case chainPlan defaultTemplateSpecializationLimits >>= requireOnly of
+    Right specialization ->
+        length (templateSpecializationMangledMembers specialization) == 3
+            && all (not . null . mangledMemberText) (templateSpecializationMangledMembers specialization)
+    Left _ -> False
+
+compilerBridgeLowersClosure :: Bool
+compilerBridgeLowersClosure = case analyzeTyped chainSource of
+    Just typed -> case compileTemplateSpecializations defaultTemplateSpecializationLimits typed [entryDemand] of
+        Right (_, core) -> memberNamesFromCore core == ["Entry", "Normalize", "Finish"]
+        Left _ -> False
+    Nothing -> False
+
+plannerRejectsInvalidGraph :: Bool
+plannerRejectsInvalidGraph = case analyzeTyped chainSource >>= withZeroFirstMember of
+    Just typed -> case planTemplateSpecializations defaultTemplateSpecializationLimits typed [entryDemand] of
+        Left [TemplateMemberReachabilityFailed _ failures] -> any isInvalid failures
+        _ -> False
+    Nothing -> False
+    where
+        isInvalid InvalidTemplateMemberSymbol {} = True
+        isInvalid _ = False
+
+plannerGraphErrorRenders :: Bool
+plannerGraphErrorRenders =
+    "invalid member call graph"
+        `isInfixOf` renderTemplateSpecializationError
+            ( TemplateMemberReachabilityFailed
+                (QualifiedName [Identifier "Chain"])
+                [InvalidTemplateMemberSymbol (Identifier "Entry") (SymbolId 0)]
+            )
+
+chainPlan :: TemplateSpecializationLimits -> Either [TemplateSpecializationError] TemplateSpecializationPlan
+chainPlan limits = case analyzeTyped chainSource of
+    Just typed -> planTemplateSpecializations limits typed [entryDemand]
+    Nothing -> Left []
+
+entryDemand :: TemplateSpecializationDemand
+entryDemand =
+    TemplateSpecializationDemand
+        (TemplateApplication (QualifiedName [Identifier "Chain"]) [TypeTemplateArgument stringType])
+        (TemplateMemberDemand [Identifier "Entry"])
+        "member-reachability-test"
+
+analyzeTyped :: String -> Maybe TypedAST
+analyzeTyped source = case analyzeSemantics (CompilerInput "member-reachability-test.tala" source) of
+    Right artifacts -> Just (semanticTypedAST artifacts)
+    Left _ -> Nothing
+
+withZeroFirstMember :: TypedAST -> Maybe TypedAST
+withZeroFirstMember (TypedAST tree) = case syntaxDeclarations tree of
+    [declaration@TemplateTypeDeclaration {typeMembers = firstMember : remaining}] ->
+        Just
+            (TypedAST tree {syntaxDeclarations = [declaration {typeMembers = setMemberSymbol (SymbolId 0) firstMember : remaining}]})
+    _ -> Nothing
+
+overloadedMembers :: Maybe [Declaration ResolvedName Type]
+overloadedMembers = do
+    members <- analyzeMembers chainSource
+    finish <- memberNamed "Finish" members
+    let SymbolId maximumValue = maximumSymbolInDeclarations members
+        duplicate = setMemberSymbol (SymbolId (maximumValue + 1)) finish
+    pure (insertAfter "Finish" duplicate members)
+
+maximumSymbolInDeclarations :: [Declaration ResolvedName Type] -> SymbolId
+maximumSymbolInDeclarations members =
+    SymbolId (maximum (0 : map (symbolIdValue . resolvedSymbol . declarationName) members))
+
+insertAfter ::
+    String -> Declaration ResolvedName Type -> [Declaration ResolvedName Type] -> [Declaration ResolvedName Type]
+insertAfter _ _ [] = []
+insertAfter name addition (member : remaining)
+    | memberName member == name = member : addition : remaining
+    | otherwise = member : insertAfter name addition remaining
+
+replaceMember ::
+    String -> Declaration ResolvedName Type -> [Declaration ResolvedName Type] -> [Declaration ResolvedName Type]
+replaceMember name replacement = map (\member -> if memberName member == name then replacement else member)
+
+setMemberSymbol :: SymbolId -> Declaration ResolvedName annotation -> Declaration ResolvedName annotation
+setMemberSymbol symbol declaration =
+    declaration {declarationName = (declarationName declaration) {resolvedSymbol = symbol}}
+
+memberName :: Declaration ResolvedName annotation -> String
+memberName = identifierText . resolvedSpelling . declarationName
+
+appendCall :: SymbolId -> Declaration ResolvedName Type -> Declaration ResolvedName Type
+appendCall target declaration@FunctionDeclaration {declarationBody = Block statements} =
+    declaration
+        { declarationBody =
+            Block
+                ( statements
+                    ++ [ ExpressionStatement
+                            testSpan
+                            ( CallExpression
+                                testSpan
+                                (NameExpression testSpan (ResolvedName target (Identifier "Normalize")) (FunctionType [] voidType))
+                                []
+                                voidType
+                            )
+                            True
+                       ]
+                )
+        }
+appendCall _ declaration = declaration
+
+prependCallable :: SymbolId -> Declaration ResolvedName Type -> Declaration ResolvedName Type
+prependCallable target declaration@FunctionDeclaration {declarationBody = Block statements} =
+    let call =
+            CallExpression
+                testSpan
+                (NameExpression testSpan (ResolvedName target (Identifier "Finish")) (FunctionType [] voidType))
+                []
+                voidType
+        callable = CallableExpression testSpan False [] [] (CallableExpressionBody call) (FunctionType [] voidType)
+     in declaration {declarationBody = Block (ExpressionStatement testSpan callable False : statements)}
+prependCallable _ declaration = declaration
+
+prependCapture :: SymbolId -> Declaration ResolvedName Type -> Declaration ResolvedName Type
+prependCapture target declaration@FunctionDeclaration {declarationBody = Block statements} =
+    let call =
+            CallExpression
+                testSpan
+                (NameExpression testSpan (ResolvedName target (Identifier "Finish")) (FunctionType [] voidType))
+                []
+                voidType
+        capture = Capture testSpan StrongCapture (resolved 8200 "captured") voidType (Just call)
+        callable = CallableExpression testSpan True [capture] [] (CallableBlockBody (Block [])) (FunctionType [] voidType)
+     in declaration {declarationBody = Block (ExpressionStatement testSpan callable False : statements)}
+prependCapture _ declaration = declaration
+
+simpleCallingMember :: Int -> String -> SymbolId -> Declaration ResolvedName Type
+simpleCallingMember symbol name target =
+    FunctionDeclaration
+        testSpan
+        (resolved symbol name)
+        (FunctionType [] voidType)
+        (ExplicitType (Identifier "void"))
+        []
+        ( Block
+            [ ExpressionStatement
+                testSpan
+                ( CallExpression
+                    testSpan
+                    (NameExpression testSpan (ResolvedName target (Identifier "Finish")) (FunctionType [] voidType))
+                    []
+                    voidType
+                )
+                True
+            , ReturnStatement testSpan Nothing
+            ]
+        )
+        False
+        PublicAccess
+
+directTargets :: Declaration ResolvedName Type -> [SymbolId]
+directTargets FunctionDeclaration {declarationBody = Block statements} = concatMap statementTargets statements
+directTargets _ = []
+
+statementTargets :: Statement ResolvedName Type -> [SymbolId]
+statementTargets statement = case statement of
+    BindingStatement _ _ _ _ _ value -> expressionTargets value
+    AssignmentStatement _ _ _ value -> expressionTargets value
+    ReturnStatement _ value -> maybe [] expressionTargets value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionTargets condition ++ blockTargets trueBlock ++ maybe [] blockTargets falseBlock
+    WhileStatement _ condition body -> expressionTargets condition ++ blockTargets body
+    DoWhileStatement _ body condition -> blockTargets body ++ expressionTargets condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementTargets initializer
+            ++ maybe [] expressionTargets condition
+            ++ concatMap statementTargets updates
+            ++ blockTargets body
+    ForEachStatement _ _ _ _ _ source body -> expressionTargets source ++ blockTargets body
+    IncrementStatement {} -> []
+    CompoundAssignmentStatement _ _ _ _ value -> expressionTargets value
+    DiscardStatement _ value -> expressionTargets value
+    BreakStatement _ value -> maybe [] expressionTargets value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionTargets condition ++ blockTargets block
+    BlockStatement _ block -> blockTargets block
+    ExpressionStatement _ value _ -> expressionTargets value
+
+blockTargets :: Block ResolvedName Type -> [SymbolId]
+blockTargets (Block statements) = concatMap statementTargets statements
+
+expressionTargets :: Expression ResolvedName Type -> [SymbolId]
+expressionTargets expression = case expression of
+    CallExpression _ (NameExpression _ name _) arguments _ -> resolvedSymbol name : concatMap expressionTargets arguments
+    CallExpression _ callee arguments _ -> expressionTargets callee ++ concatMap expressionTargets arguments
+    UnaryExpression _ _ value _ -> expressionTargets value
+    ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse]
+    CoalesceExpression _ left fallback _ -> expressionTargets left ++ expressionTargets fallback
+    BinaryExpression _ _ left right _ -> expressionTargets left ++ expressionTargets right
+    CallableExpression _ _ captures _ body _ ->
+        concatMap (maybe [] expressionTargets . captureInitializer) captures
+            ++ case body of
+                CallableExpressionBody value -> expressionTargets value
+                CallableBlockBody block -> blockTargets block
+    BlockExpression _ block _ -> blockTargets block
+    MatchExpression _ subjects arms _ -> concatMap expressionTargets (subjects ++ concatMap matchArmExpressions arms)
+    _ -> []
+
+selectedMembers :: TemplateSpecialization -> [Declaration ResolvedName Type]
+selectedMembers specialization = case templateSpecializationDeclaration specialization of
+    TypeDeclaration {typeMembers = members} -> members
+    _ -> []
+
+selectedNames :: TemplateSpecialization -> [String]
+selectedNames = memberNames . selectedMembers
+
+memberNamesFromCore :: CoreModule -> [String]
+memberNamesFromCore = map (identifierText . resolvedSpelling . coreFunctionName) . coreModuleFunctions
+
+onlySpecialization :: TemplateSpecializationPlan -> Maybe TemplateSpecialization
+onlySpecialization planValue = case plannedTemplateSpecializations planValue of
+    [specialization] -> Just specialization
+    _ -> Nothing
+
+requireOnly :: TemplateSpecializationPlan -> Either [TemplateSpecializationError] TemplateSpecialization
+requireOnly planValue = maybe (Left []) Right (onlySpecialization planValue)
+
+symbolsNamed :: [String] -> [Declaration ResolvedName Type] -> [SymbolId]
+symbolsNamed names members =
+    [ resolvedSymbol (declarationName member)
+    | member <- members
+    , memberName member `elem` names
+    ]
+
+onlySymbol :: TemplateMemberReachability -> SymbolId
+onlySymbol result = case memberReachabilitySymbols result of
+    [symbol] -> symbol
+    _ -> SymbolId 0
+
+reachabilityFixture :: Maybe ([Declaration ResolvedName Type], TemplateMemberReachability)
+reachabilityFixture = do
+    members <- analyzeMembers chainSource
+    result <- rightValue (selectReachableMembersByName [Identifier "Entry"] members)
+    pure (members, result)
+
+hasIssue :: (TemplateMemberReachabilityIssue -> Bool) -> Either failure [TemplateMemberReachabilityIssue] -> Bool
+hasIssue predicate result = case result of
+    Right issues -> any predicate issues
+    Left _ -> False
+
+resolved :: Int -> String -> ResolvedName
+resolved symbol name = ResolvedName (SymbolId symbol) (Identifier name)
+
+testSpan :: SourceSpan
+testSpan = SourceSpan "member-reachability-test.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+
+first :: (value -> Bool) -> [value] -> Maybe value
+first _ [] = Nothing
+first predicate (value : remaining)
+    | predicate value = Just value
+    | otherwise = first predicate remaining
+
+rightValue :: Either failure value -> Maybe value
+rightValue result = case result of
+    Right value -> Just value
+    Left _ -> Nothing
+ test/TemplatePlanVerifierTests.hs view
@@ -0,0 +1,395 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplatePlanVerifierTests (templatePlanVerifierTests) where
+
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Mangling
+import Progmasoft.Tala.Template.Specialization
+import Progmasoft.Tala.Template.Specialization.Verifier
+
+templatePlanVerifierTests :: [(String, Bool)]
+templatePlanVerifierTests =
+    [ ("plan verifier accepts planner output", acceptsPlannerOutput)
+    , ("plan verifier accepts the empty plan", acceptsEmptyPlan)
+    , ("plan verifier rejects a zero specialization id", rejectsZeroId)
+    , ("plan verifier rejects a negative specialization id", rejectsNegativeId)
+    , ("plan verifier rejects duplicate specialization ids", rejectsDuplicateIds)
+    , ("plan verifier rejects an empty canonical identity", rejectsEmptyIdentity)
+    , ("plan verifier rejects duplicate canonical identities", rejectsDuplicateIdentities)
+    , ("plan verifier rejects an open specialization type", rejectsOpenType)
+    , ("plan verifier rejects ErrorType specialization", rejectsErrorType)
+    , ("plan verifier requires an ordinary type declaration", rejectsFunctionDeclaration)
+    , ("plan verifier checks declaration annotation", rejectsDeclarationMismatch)
+    , ("plan verifier rejects an empty origin", rejectsEmptyOrigin)
+    , ("plan verifier rejects duplicate origins", rejectsDuplicateOrigins)
+    , ("plan verifier rejects a missing dependency", rejectsMissingDependency)
+    , ("plan verifier rejects duplicate dependencies", rejectsDuplicateDependencies)
+    , ("plan verifier rejects direct self dependency", rejectsSelfDependency)
+    , ("plan verifier rejects zero fresh source symbols", rejectsZeroFreshSource)
+    , ("plan verifier rejects zero fresh target symbols", rejectsZeroFreshTarget)
+    , ("plan verifier rejects identity fresh mappings", rejectsIdentityFreshMap)
+    , ("plan verifier rejects duplicate fresh sources", rejectsDuplicateFreshSources)
+    , ("plan verifier rejects duplicate fresh targets", rejectsDuplicateFreshTargets)
+    , ("plan verifier requires every declaration symbol in the fresh map", rejectsMissingDeclarationSymbol)
+    , ("plan verifier rejects layout plans containing members", rejectsLayoutWithMembers)
+    , ("plan verifier checks requested member presence", rejectsMissingRequestedMember)
+    , ("plan verifier checks unrequested member presence", rejectsUnexpectedMember)
+    , ("plan verifier rejects an invalid mangled type", rejectsInvalidMangledType)
+    , ("plan verifier checks recomputed mangled types", rejectsIncorrectMangledType)
+    , ("plan verifier requires a mangled name per member", rejectsMissingMangledMember)
+    , ("plan verifier rejects an invalid mangled member", rejectsInvalidMangledMember)
+    , ("plan verifier rejects duplicate mangled members", rejectsDuplicateMangledMember)
+    , ("plan verifier rejects an unknown mangled member", rejectsUnexpectedMangledMember)
+    , ("plan verifier checks recomputed mangled members", rejectsIncorrectMangledMember)
+    , ("plan verifier checks aggregate statistics", rejectsIncorrectStatistics)
+    , ("plan verifier detects incomplete emission identity coverage", rejectsIncompleteEmissionCoverage)
+    , ("issue rendering names the specialization id", renderingNamesSpecialization)
+    , ("issue rendering explains mangled symbol mismatch", renderingExplainsMangledMismatch)
+    , ("issue rendering explains fresh symbol ownership", renderingExplainsSharedFreshSymbol)
+    ]
+
+qualified :: [String] -> QualifiedName
+qualified = QualifiedName . map Identifier
+
+resolved :: Int -> String -> ResolvedName
+resolved unique spelling = ResolvedName (SymbolId unique) (Identifier spelling)
+
+typeArgument :: String -> TemplateArgument
+typeArgument name = TypeTemplateArgument (namedType name)
+
+request :: TemplateSpecializationDemand
+request =
+    TemplateSpecializationDemand
+        (TemplateApplication (qualified ["Box"]) [typeArgument "String"])
+        (TemplateMemberDemand [Identifier "Identity"])
+        "verifier-test:request"
+
+source :: String
+source =
+    unlines
+        [ "template<typename T> class Box {"
+        , "    T Identity(_ T value) { T copy = value; return copy; }"
+        , "    int Size() { return 1; }"
+        , "}"
+        ]
+
+validPlan :: Maybe TemplateSpecializationPlan
+validPlan = case analyzeSemantics (CompilerInput "template-plan-verifier-test.tala" source) of
+    Right artifacts -> case planTemplateSpecializations defaultTemplateSpecializationLimits (semanticTypedAST artifacts) [request] of
+        Right plan -> Just plan
+        Left _ -> Nothing
+    Left _ -> Nothing
+
+emptyPlan :: TemplateSpecializationPlan
+emptyPlan = TemplateSpecializationPlan Nothing [] (TemplateSpecializationStatistics 0 0 0 0 0 0)
+
+onlySpecialization :: TemplateSpecializationPlan -> Maybe TemplateSpecialization
+onlySpecialization plan = case plannedTemplateSpecializations plan of
+    [specialization] -> Just specialization
+    _ -> Nothing
+
+rewriteOnly ::
+    (TemplateSpecialization -> TemplateSpecialization) -> TemplateSpecializationPlan -> TemplateSpecializationPlan
+rewriteOnly rewrite plan = plan {plannedTemplateSpecializations = map rewrite (plannedTemplateSpecializations plan)}
+
+issuesAfter :: (TemplateSpecialization -> TemplateSpecialization) -> [TemplatePlanIssue]
+issuesAfter rewrite = maybe [] (templateSpecializationPlanIssues . rewriteOnly rewrite) validPlan
+
+hasIssue :: (TemplatePlanIssue -> Bool) -> [TemplatePlanIssue] -> Bool
+hasIssue predicate = any predicate
+
+acceptsPlannerOutput :: Bool
+acceptsPlannerOutput = case validPlan of
+    Just plan -> verifyTemplateSpecializationPlan plan == Right plan
+    Nothing -> False
+
+acceptsEmptyPlan :: Bool
+acceptsEmptyPlan = verifyTemplateSpecializationPlan emptyPlan == Right emptyPlan
+
+rejectsZeroId :: Bool
+rejectsZeroId = hasIssue isInvalid (issuesAfter (\value -> value {templateSpecializationId = TemplateSpecializationId 0}))
+    where
+        isInvalid (InvalidPlanSpecializationId (TemplateSpecializationId 0)) = True
+        isInvalid _ = False
+
+rejectsNegativeId :: Bool
+rejectsNegativeId = hasIssue isInvalid (issuesAfter (\value -> value {templateSpecializationId = TemplateSpecializationId (-7)}))
+    where
+        isInvalid (InvalidPlanSpecializationId (TemplateSpecializationId (-7))) = True
+        isInvalid _ = False
+
+rejectsDuplicateIds :: Bool
+rejectsDuplicateIds = case validPlan >>= onlySpecialization of
+    Just specialization ->
+        let plan = maybe emptyPlan id validPlan
+            duplicate = specialization {templateSpecializationIdentity = templateSpecializationIdentity specialization ++ ":copy"}
+         in hasIssue
+                isDuplicate
+                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
+    Nothing -> False
+    where
+        isDuplicate (DuplicatePlanSpecializationId _) = True
+        isDuplicate _ = False
+
+rejectsEmptyIdentity :: Bool
+rejectsEmptyIdentity = hasIssue isEmpty (issuesAfter (\value -> value {templateSpecializationIdentity = ""}))
+    where
+        isEmpty (EmptyPlanSpecializationIdentity _) = True
+        isEmpty _ = False
+
+rejectsDuplicateIdentities :: Bool
+rejectsDuplicateIdentities = case validPlan >>= onlySpecialization of
+    Just specialization ->
+        let plan = maybe emptyPlan id validPlan
+            duplicate = specialization {templateSpecializationId = TemplateSpecializationId 2}
+         in hasIssue
+                isDuplicate
+                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
+    Nothing -> False
+    where
+        isDuplicate (DuplicatePlanSpecializationIdentity _) = True
+        isDuplicate _ = False
+
+rejectsOpenType :: Bool
+rejectsOpenType = hasIssue isOpen (issuesAfter rewrite)
+    where
+        open = TypeVariable (resolved 500 "T")
+        rewrite value = value {templateSpecializationType = open}
+        isOpen (PlanSpecializationTypeIsOpen _ symbols) = SymbolId 500 `elem` symbols
+        isOpen _ = False
+
+rejectsErrorType :: Bool
+rejectsErrorType = hasIssue isError (issuesAfter (\value -> value {templateSpecializationType = ErrorType}))
+    where
+        isError (PlanSpecializationTypeHasError _) = True
+        isError _ = False
+
+rejectsFunctionDeclaration :: Bool
+rejectsFunctionDeclaration = case validPlan >>= onlySpecialization of
+    Just specialization -> case templateSpecializationDeclaration specialization of
+        TypeDeclaration {typeMembers = member : _} ->
+            hasIssue isWrong (issuesAfter (\value -> value {templateSpecializationDeclaration = member}))
+        _ -> False
+    Nothing -> False
+    where
+        isWrong (PlanDeclarationIsNotClosedType _) = True
+        isWrong _ = False
+
+rejectsDeclarationMismatch :: Bool
+rejectsDeclarationMismatch = hasIssue isMismatch (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationDeclaration = setAnnotation intType (templateSpecializationDeclaration value)}
+        isMismatch (PlanDeclarationTypeMismatch _ _ _) = True
+        isMismatch _ = False
+
+setAnnotation :: Type -> Declaration ResolvedName Type -> Declaration ResolvedName Type
+setAnnotation annotation declaration = case declaration of
+    TypeDeclaration spanValue name _ members -> TypeDeclaration spanValue name annotation members
+    other -> other
+
+rejectsEmptyOrigin :: Bool
+rejectsEmptyOrigin = hasIssue isEmpty (issuesAfter (\value -> value {templateSpecializationOrigins = [""]}))
+    where
+        isEmpty (EmptyPlanOrigin _) = True
+        isEmpty _ = False
+
+rejectsDuplicateOrigins :: Bool
+rejectsDuplicateOrigins = hasIssue isDuplicate (issuesAfter (\value -> value {templateSpecializationOrigins = ["same", "same"]}))
+    where
+        isDuplicate (DuplicatePlanOrigin _ "same") = True
+        isDuplicate _ = False
+
+rejectsMissingDependency :: Bool
+rejectsMissingDependency =
+    hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationDependencies = [TemplateSpecializationId 99]}))
+    where
+        isMissing (MissingPlanDependency _ (TemplateSpecializationId 99)) = True
+        isMissing _ = False
+
+rejectsDuplicateDependencies :: Bool
+rejectsDuplicateDependencies = hasIssue isDuplicate (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationDependencies = [TemplateSpecializationId 99, TemplateSpecializationId 99]}
+        isDuplicate (DuplicatePlanDependency _ (TemplateSpecializationId 99)) = True
+        isDuplicate _ = False
+
+rejectsSelfDependency :: Bool
+rejectsSelfDependency = hasIssue isSelf (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationDependencies = [templateSpecializationId value]}
+        isSelf (SelfPlanDependency _) = True
+        isSelf _ = False
+
+rejectsZeroFreshSource :: Bool
+rejectsZeroFreshSource = hasIssue isInvalid (issuesAfter replaceFirst)
+    where
+        replaceFirst value = value {templateSpecializationSymbolMap = replaceSource (SymbolId 0) (templateSpecializationSymbolMap value)}
+        isInvalid (InvalidFreshSymbol _ (SymbolId 0) _) = True
+        isInvalid _ = False
+
+rejectsZeroFreshTarget :: Bool
+rejectsZeroFreshTarget = hasIssue isInvalid (issuesAfter replaceFirst)
+    where
+        replaceFirst value = value {templateSpecializationSymbolMap = replaceTarget (SymbolId 0) (templateSpecializationSymbolMap value)}
+        isInvalid (InvalidFreshSymbol _ _ (SymbolId 0)) = True
+        isInvalid _ = False
+
+rejectsIdentityFreshMap :: Bool
+rejectsIdentityFreshMap = hasIssue isInvalid (issuesAfter replaceFirst)
+    where
+        replaceFirst value = case templateSpecializationSymbolMap value of
+            (old, _) : rest -> value {templateSpecializationSymbolMap = (old, old) : rest}
+            [] -> value
+        isInvalid (InvalidFreshSymbol _ old new) = old == new
+        isInvalid _ = False
+
+replaceSource :: SymbolId -> [(SymbolId, SymbolId)] -> [(SymbolId, SymbolId)]
+replaceSource replacement mappings = case mappings of
+    (_, target) : rest -> (replacement, target) : rest
+    [] -> []
+
+replaceTarget :: SymbolId -> [(SymbolId, SymbolId)] -> [(SymbolId, SymbolId)]
+replaceTarget replacement mappings = case mappings of
+    (sourceSymbol, _) : rest -> (sourceSymbol, replacement) : rest
+    [] -> []
+
+rejectsDuplicateFreshSources :: Bool
+rejectsDuplicateFreshSources = hasIssue isDuplicate (issuesAfter duplicateFirst)
+    where
+        duplicateFirst value = case templateSpecializationSymbolMap value of
+            first : second : rest -> value {templateSpecializationSymbolMap = first : (fst first, snd second) : rest}
+            _ -> value
+        isDuplicate (DuplicateFreshSourceSymbol _ _) = True
+        isDuplicate _ = False
+
+rejectsDuplicateFreshTargets :: Bool
+rejectsDuplicateFreshTargets = hasIssue isDuplicate (issuesAfter duplicateFirst)
+    where
+        duplicateFirst value = case templateSpecializationSymbolMap value of
+            first : second : rest -> value {templateSpecializationSymbolMap = first : (fst second, snd first) : rest}
+            _ -> value
+        isDuplicate (DuplicateFreshTargetSymbol _ _) = True
+        isDuplicate _ = False
+
+rejectsMissingDeclarationSymbol :: Bool
+rejectsMissingDeclarationSymbol = hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationSymbolMap = []}))
+    where
+        isMissing (DeclarationSymbolMissingFromFreshMap _ _) = True
+        isMissing _ = False
+
+rejectsLayoutWithMembers :: Bool
+rejectsLayoutWithMembers = hasIssue isLayout (issuesAfter (\value -> value {templateSpecializationScope = TemplateLayoutDemand}))
+    where
+        isLayout (LayoutPlanContainsMembers _ count) = count > 0
+        isLayout _ = False
+
+rejectsMissingRequestedMember :: Bool
+rejectsMissingRequestedMember = hasIssue isMissing (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationScope = TemplateMemberDemand [Identifier "Missing"]}
+        isMissing (RequestedPlanMemberMissing _ (Identifier "Missing")) = True
+        isMissing _ = False
+
+rejectsUnexpectedMember :: Bool
+rejectsUnexpectedMember = hasIssue isUnexpected (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationScope = TemplateMemberDemand []}
+        isUnexpected (UnexpectedPlanMember _ (Identifier "Identity")) = True
+        isUnexpected _ = False
+
+rejectsInvalidMangledType :: Bool
+rejectsInvalidMangledType = hasIssue isInvalid (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationMangledType = MangledTemplateType "invalid!"}
+        isInvalid (InvalidPlanMangledType _ "invalid!") = True
+        isInvalid _ = False
+
+rejectsIncorrectMangledType :: Bool
+rejectsIncorrectMangledType = hasIssue isIncorrect (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationMangledType = MangledTemplateType "_TAT1_wrong"}
+        isIncorrect (IncorrectPlanMangledType _ _ "_TAT1_wrong") = True
+        isIncorrect _ = False
+
+rejectsMissingMangledMember :: Bool
+rejectsMissingMangledMember = hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationMangledMembers = []}))
+    where
+        isMissing (MissingPlanMangledMember _ _) = True
+        isMissing _ = False
+
+rejectsInvalidMangledMember :: Bool
+rejectsInvalidMangledMember = hasIssue isInvalid (issuesAfter rewrite)
+    where
+        rewrite value = value {templateSpecializationMangledMembers = map corrupt (templateSpecializationMangledMembers value)}
+        corrupt member = member {mangledMemberText = "invalid!"}
+        isInvalid (InvalidPlanMangledMember _ _ "invalid!") = True
+        isInvalid _ = False
+
+rejectsDuplicateMangledMember :: Bool
+rejectsDuplicateMangledMember = hasIssue isDuplicate (issuesAfter rewrite)
+    where
+        rewrite value = case templateSpecializationMangledMembers value of
+            first : rest -> value {templateSpecializationMangledMembers = first : first : rest}
+            [] -> value
+        isDuplicate (DuplicatePlanMangledMember _ _) = True
+        isDuplicate _ = False
+
+rejectsUnexpectedMangledMember :: Bool
+rejectsUnexpectedMangledMember = hasIssue isUnexpected (issuesAfter rewrite)
+    where
+        rewrite value = case templateSpecializationMangledMembers value of
+            first : rest -> value {templateSpecializationMangledMembers = first {mangledMemberSourceSymbol = SymbolId 9999} : rest}
+            [] -> value
+        isUnexpected (UnexpectedPlanMangledMember _ (SymbolId 9999)) = True
+        isUnexpected _ = False
+
+rejectsIncorrectMangledMember :: Bool
+rejectsIncorrectMangledMember = hasIssue isInvalid (issuesAfter rewrite)
+    where
+        rewrite value = case templateSpecializationMangledMembers value of
+            first : rest -> value {templateSpecializationMangledMembers = first {mangledMemberText = "_TAT1_wrong"} : rest}
+            [] -> value
+        isInvalid (InvalidPlanMangledMember _ _ "_TAT1_wrong") = True
+        isInvalid _ = False
+
+rejectsIncorrectStatistics :: Bool
+rejectsIncorrectStatistics = case validPlan of
+    Just plan ->
+        hasIssue
+            isIncorrect
+            (templateSpecializationPlanIssues plan {plannedTemplateStatistics = TemplateSpecializationStatistics 99 98 97 96 95 94})
+    Nothing -> False
+    where
+        isIncorrect (IncorrectPlanStatistics _ _) = True
+        isIncorrect _ = False
+
+rejectsIncompleteEmissionCoverage :: Bool
+rejectsIncompleteEmissionCoverage = case validPlan >>= onlySpecialization of
+    Just specialization ->
+        let plan = maybe emptyPlan id validPlan
+            duplicate = specialization {templateSpecializationIdentity = templateSpecializationIdentity specialization ++ ":duplicate"}
+         in hasIssue
+                isIncomplete
+                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
+    Nothing -> False
+    where
+        isIncomplete (IncompletePlanEmissionOrder _ _) = True
+        isIncomplete _ = False
+
+renderingNamesSpecialization :: Bool
+renderingNamesSpecialization = "specialization 7" `isInfixOf` renderTemplatePlanIssue (InvalidPlanSpecializationId (TemplateSpecializationId 7))
+
+renderingExplainsMangledMismatch :: Bool
+renderingExplainsMangledMismatch =
+    "mangled type differs"
+        `isInfixOf` renderTemplatePlanIssue (IncorrectPlanMangledType (TemplateSpecializationId 1) "expected" "actual")
+
+renderingExplainsSharedFreshSymbol :: Bool
+renderingExplainsSharedFreshSymbol =
+    "fresh symbol 44 is shared"
+        `isInfixOf` renderTemplatePlanIssue (SharedFreshTargetSymbol (SymbolId 44) [TemplateSpecializationId 1, TemplateSpecializationId 2])
+ test/TemplateSpecializationPlannerTests.hs view
@@ -0,0 +1,632 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateSpecializationPlannerTests (templateSpecializationPlannerTests) where
+
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Freshen
+import Progmasoft.Tala.Template.Specialization
+
+templateSpecializationPlannerTests :: [(String, Bool)]
+templateSpecializationPlannerTests =
+    [ ("planner materializes a concrete ordinary declaration", materializesOrdinaryDeclaration)
+    , ("planner preserves the concrete specialization type", preservesConcreteType)
+    , ("planner assigns positive stable specialization ids", assignsStableIds)
+    , ("planner sorts specialization identities deterministically", sortsIdentities)
+    , ("layout demand does not instantiate methods", layoutSkipsMethods)
+    , ("member demand instantiates the selected method", memberSelectsMethod)
+    , ("member demand selects every overload spelling", memberSelectsOverloads)
+    , ("member demand excludes unrelated methods", memberExcludesUnrelated)
+    , ("complete demand retains every member", completeRetainsMembers)
+    , ("duplicate demands share one specialization", duplicateDemandCoalesces)
+    , ("duplicate demands retain unique origins", duplicateOriginsCoalesce)
+    , ("duplicate member demands merge their selections", duplicateMembersMerge)
+    , ("complete demand dominates member demand", completeDominatesMember)
+    , ("member demand dominates layout demand", memberDominatesLayout)
+    , ("default and explicit arguments share an identity", defaultAndExplicitCoalesce)
+    , ("value arguments create distinct specializations", valueArgumentsStayDistinct)
+    , ("freshening replaces declaration symbols", freshensDeclarationSymbol)
+    , ("freshening replaces member symbols", freshensMemberSymbol)
+    , ("freshening replaces parameter symbols", freshensParameterSymbol)
+    , ("freshening keeps parameter references coherent", freshensParameterReference)
+    , ("freshening replaces local binding symbols", freshensLocalSymbol)
+    , ("freshening keeps local references coherent", freshensLocalReference)
+    , ("freshening preserves source spelling", fresheningPreservesSpelling)
+    , ("freshening starts above every source symbol", fresheningStartsAboveSource)
+    , ("separate specializations receive disjoint symbols", specializationSymbolsAreDisjoint)
+    , ("closed TypedAST contains only materialized declarations", typedViewIsClosed)
+    , ("closed TypedAST follows emission order", typedViewFollowsOrder)
+    , ("dependency edges connect planned member types", dependencyEdgesAreAttached)
+    , ("emission order places dependencies first", dependenciesEmitFirst)
+    , ("reference-recursive specialization still emits once", recursiveDemandTerminates)
+    , ("unknown templates report application failure", unknownTemplateIsRejected)
+    , ("ambiguous templates report application failure", ambiguousTemplateIsRejected)
+    , ("missing members are rejected", missingMemberIsRejected)
+    , ("category mismatches are retained", categoryMismatchIsRejected)
+    , ("too few arguments are retained", tooFewArgumentsAreRejected)
+    , ("specialization count limit is enforced", specializationLimitIsEnforced)
+    , ("origin count limit is enforced", originLimitIsEnforced)
+    , ("member count limit is enforced", memberLimitIsEnforced)
+    , ("invalid limits fail before planning", invalidLimitsAreRejected)
+    , ("statistics count requests and unique entries", statisticsCountDemands)
+    , ("statistics count selected members", statisticsCountMembers)
+    , ("statistics count dependency edges", statisticsCountDependencies)
+    , ("lookup by id returns the planned entry", lookupByIdWorks)
+    , ("lookup by identity returns the planned entry", lookupByIdentityWorks)
+    , ("missing specialization lookups return Nothing", missingLookupReturnsNothing)
+    , ("rendering preserves demand origin", renderingIncludesOrigin)
+    , ("rendering names a missing member", renderingNamesMember)
+    , ("rendering explains a count limit", renderingExplainsLimit)
+    , ("compiler bridge lowers selected members to verified Core", compilerBridgeLowersCore)
+    , ("compiler bridge maps planning failures to diagnostics", compilerBridgeMapsDiagnostics)
+    ]
+
+data Fixture = Fixture
+    { fixtureTyped :: TypedAST
+    , fixturePlan :: TemplateSpecializationPlan
+    }
+
+qualified :: [String] -> QualifiedName
+qualified = QualifiedName . map Identifier
+
+typeArgument :: String -> TemplateArgument
+typeArgument name = TypeTemplateArgument (namedType name)
+
+valueArgument :: Integer -> TemplateArgument
+valueArgument = ValueTemplateArgument . IntegerTemplateValue
+
+demand :: String -> [TemplateArgument] -> TemplateDemandScope -> String -> TemplateSpecializationDemand
+demand target arguments scope origin =
+    TemplateSpecializationDemand
+        (TemplateApplication (qualified [target]) arguments)
+        scope
+        origin
+
+memberDemand :: String -> [TemplateArgument] -> [String] -> String -> TemplateSpecializationDemand
+memberDemand target arguments names = demand target arguments (TemplateMemberDemand (map Identifier names))
+
+analyze :: String -> Maybe TypedAST
+analyze source = case analyzeSemantics (CompilerInput "specialization-planner-test.tala" source) of
+    Right artifacts -> Just (semanticTypedAST artifacts)
+    Left _ -> Nothing
+
+plan :: String -> [TemplateSpecializationDemand] -> Either [TemplateSpecializationError] TemplateSpecializationPlan
+plan source requests = case analyze source of
+    Just typed -> planTemplateSpecializations defaultTemplateSpecializationLimits typed requests
+    Nothing -> Left []
+
+fixture :: Maybe Fixture
+fixture = do
+    typed <- analyze boxSource
+    specializationPlan <-
+        rightValue (planTemplateSpecializations defaultTemplateSpecializationLimits typed [boxIdentityDemand])
+    pure (Fixture typed specializationPlan)
+
+boxSource :: String
+boxSource =
+    "template<typename T> class Box { T Identity(_ T value) { T copy = value; return copy; } int Size() { return 1; } }"
+
+boxIdentityDemand :: TemplateSpecializationDemand
+boxIdentityDemand = memberDemand "Box" [typeArgument "String"] ["Identity"] "test:identity"
+
+singleSpecialization :: TemplateSpecializationPlan -> Maybe TemplateSpecialization
+singleSpecialization specializationPlan = case plannedTemplateSpecializations specializationPlan of
+    [specialization] -> Just specialization
+    _ -> Nothing
+
+selectedMembers :: TemplateSpecialization -> [Declaration ResolvedName Type]
+selectedMembers specialization = case templateSpecializationDeclaration specialization of
+    TypeDeclaration {typeMembers = members} -> members
+    _ -> []
+
+selectedSpellings :: TemplateSpecialization -> [String]
+selectedSpellings = map (identifierText . resolvedSpelling . declarationName) . selectedMembers
+
+materializesOrdinaryDeclaration :: Bool
+materializesOrdinaryDeclaration = case fixture >>= singleSpecialization . fixturePlan of
+    Just TemplateSpecialization {templateSpecializationDeclaration = TypeDeclaration {}} -> True
+    _ -> False
+
+preservesConcreteType :: Bool
+preservesConcreteType = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization ->
+        templateSpecializationType specialization
+            == NamedType (qualified ["Box"]) [typeArgument "String"]
+    Nothing -> False
+
+assignsStableIds :: Bool
+assignsStableIds = case plan boxSource requests of
+    Right specializationPlan ->
+        map (templateSpecializationIdValue . templateSpecializationId) (plannedTemplateSpecializations specializationPlan)
+            == [1, 2]
+    Left _ -> False
+    where
+        requests =
+            [ memberDemand "Box" [typeArgument "String"] ["Identity"] "string"
+            , memberDemand "Box" [typeArgument "int"] ["Identity"] "int"
+            ]
+
+sortsIdentities :: Bool
+sortsIdentities = case plan boxSource requests of
+    Right specializationPlan ->
+        let identities = map templateSpecializationIdentity (plannedTemplateSpecializations specializationPlan)
+         in identities == ordered identities
+    Left _ -> False
+    where
+        requests =
+            [ memberDemand "Box" [typeArgument "String"] ["Identity"] "later"
+            , memberDemand "Box" [typeArgument "bool"] ["Identity"] "earlier"
+            ]
+
+layoutSkipsMethods :: Bool
+layoutSkipsMethods = case plan boxSource [demand "Box" [typeArgument "String"] TemplateLayoutDemand "layout"] of
+    Right value -> maybe False (null . selectedMembers) (singleSpecialization value)
+    Left _ -> False
+
+memberSelectsMethod :: Bool
+memberSelectsMethod = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> selectedSpellings specialization == ["Identity"]
+    Nothing -> False
+
+memberSelectsOverloads :: Bool
+memberSelectsOverloads = case analyze source >>= duplicateReadMember of
+    Just typed -> case planTemplateSpecializations defaultTemplateSpecializationLimits typed requests of
+        Right value -> maybe False ((== ["Read", "Read"]) . selectedSpellings) (singleSpecialization value)
+        Left _ -> False
+    Nothing -> False
+    where
+        source =
+            "template<typename T> class Box { T Read(_ T value) { return value; } void Skip() { return; } }"
+        requests = [memberDemand "Box" [typeArgument "int"] ["Read"] "overload"]
+
+duplicateReadMember :: TypedAST -> Maybe TypedAST
+duplicateReadMember typed@(TypedAST tree) = case syntaxDeclarations tree of
+    [declaration@TemplateTypeDeclaration {typeMembers = readMember : remaining}] ->
+        let SymbolId maximumSource = maximumSymbolInTypedAST typed
+            duplicate =
+                readMember
+                    { declarationName =
+                        ResolvedName (SymbolId (maximumSource + 1)) (resolvedSpelling (declarationName readMember))
+                    }
+         in Just (TypedAST tree {syntaxDeclarations = [declaration {typeMembers = readMember : duplicate : remaining}]})
+    _ -> Nothing
+
+memberExcludesUnrelated :: Bool
+memberExcludesUnrelated = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> "Size" `notElem` selectedSpellings specialization
+    Nothing -> False
+
+completeRetainsMembers :: Bool
+completeRetainsMembers = case plan boxSource [demand "Box" [typeArgument "String"] TemplateCompleteDemand "complete"] of
+    Right value -> maybe False ((== ["Identity", "Size"]) . selectedSpellings) (singleSpecialization value)
+    Left _ -> False
+
+duplicateDemandCoalesces :: Bool
+duplicateDemandCoalesces = case plan boxSource [boxIdentityDemand, boxIdentityDemand] of
+    Right value -> length (plannedTemplateSpecializations value) == 1
+    Left _ -> False
+
+duplicateOriginsCoalesce :: Bool
+duplicateOriginsCoalesce = case plan boxSource requests of
+    Right value -> case singleSpecialization value of
+        Just specialization -> templateSpecializationOrigins specialization == ["first", "second"]
+        Nothing -> False
+    Left _ -> False
+    where
+        requests =
+            [ memberDemand "Box" [typeArgument "String"] ["Identity"] "first"
+            , memberDemand "Box" [typeArgument "String"] ["Identity"] "second"
+            , memberDemand "Box" [typeArgument "String"] ["Identity"] "first"
+            ]
+
+duplicateMembersMerge :: Bool
+duplicateMembersMerge = case plan boxSource requests of
+    Right value -> maybe False ((== ["Identity", "Size"]) . selectedSpellings) (singleSpecialization value)
+    Left _ -> False
+    where
+        requests =
+            [ memberDemand "Box" [typeArgument "String"] ["Identity"] "identity"
+            , memberDemand "Box" [typeArgument "String"] ["Size"] "size"
+            ]
+
+completeDominatesMember :: Bool
+completeDominatesMember = case plan boxSource requests of
+    Right value -> maybe False ((== TemplateCompleteDemand) . templateSpecializationScope) (singleSpecialization value)
+    Left _ -> False
+    where
+        requests =
+            [ boxIdentityDemand
+            , demand "Box" [typeArgument "String"] TemplateCompleteDemand "complete"
+            ]
+
+memberDominatesLayout :: Bool
+memberDominatesLayout = case plan boxSource requests of
+    Right value -> maybe False ((== ["Identity"]) . selectedSpellings) (singleSpecialization value)
+    Left _ -> False
+    where
+        requests =
+            [ demand "Box" [typeArgument "String"] TemplateLayoutDemand "layout"
+            , boxIdentityDemand
+            ]
+
+defaultAndExplicitCoalesce :: Bool
+defaultAndExplicitCoalesce = case plan source requests of
+    Right value -> length (plannedTemplateSpecializations value) == 1
+    Left _ -> False
+    where
+        source = "template<typename T = int> class Box { T Read(_ T value) { return value; } }"
+        requests =
+            [ memberDemand "Box" [] ["Read"] "default"
+            , memberDemand "Box" [typeArgument "int"] ["Read"] "explicit"
+            ]
+
+valueArgumentsStayDistinct :: Bool
+valueArgumentsStayDistinct = case plan source requests of
+    Right value -> length (plannedTemplateSpecializations value) == 2
+    Left _ -> False
+    where
+        source = "template<int N> class Buffer { int Size() { return 1; } }"
+        requests =
+            [ memberDemand "Buffer" [valueArgument 4] ["Size"] "four"
+            , memberDemand "Buffer" [valueArgument 8] ["Size"] "eight"
+            ]
+
+freshensDeclarationSymbol :: Bool
+freshensDeclarationSymbol = case (fixture, fixture >>= singleSpecialization . fixturePlan) of
+    (Just value, Just specialization) ->
+        sourceDeclarationSymbol (fixtureTyped value) /= declarationSymbol (templateSpecializationDeclaration specialization)
+    _ -> False
+
+freshensMemberSymbol :: Bool
+freshensMemberSymbol = case (fixture, fixture >>= singleSpecialization . fixturePlan) of
+    (Just value, Just specialization) -> case (sourceMemberSymbol (fixtureTyped value), selectedMembers specialization) of
+        (Just old, [member]) -> old /= resolvedSymbol (declarationName member)
+        _ -> False
+    _ -> False
+
+freshensParameterSymbol :: Bool
+freshensParameterSymbol = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> case selectedMembers specialization of
+        [FunctionDeclaration {declarationParameters = [parameter]}] ->
+            resolvedSymbol (parameterName parameter) `elem` map snd (templateSpecializationSymbolMap specialization)
+        _ -> False
+    Nothing -> False
+
+freshensParameterReference :: Bool
+freshensParameterReference = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> case selectedMembers specialization of
+        [ FunctionDeclaration
+                { declarationParameters = [parameter]
+                , declarationBody = Block (BindingStatement _ _ _ _ _ (NameExpression _ reference _) : _)
+                }
+            ] ->
+                resolvedSymbol (parameterName parameter) == resolvedSymbol reference
+        _ -> False
+    Nothing -> False
+
+freshensLocalSymbol :: Bool
+freshensLocalSymbol = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> case selectedMembers specialization of
+        [FunctionDeclaration {declarationBody = Block (BindingStatement _ _ _ local _ _ : _)}] ->
+            resolvedSymbol local `elem` map snd (templateSpecializationSymbolMap specialization)
+        _ -> False
+    Nothing -> False
+
+freshensLocalReference :: Bool
+freshensLocalReference = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> case selectedMembers specialization of
+        [ FunctionDeclaration
+                { declarationBody = Block [BindingStatement _ _ _ local _ _, ReturnStatement _ (Just (NameExpression _ reference _))]
+                }
+            ] ->
+                resolvedSymbol local == resolvedSymbol reference
+        _ -> False
+    Nothing -> False
+
+fresheningPreservesSpelling :: Bool
+fresheningPreservesSpelling = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization ->
+        identifierText (resolvedSpelling (declarationName (templateSpecializationDeclaration specialization))) == "Box"
+            && selectedSpellings specialization == ["Identity"]
+    Nothing -> False
+
+fresheningStartsAboveSource :: Bool
+fresheningStartsAboveSource = case (fixture, fixture >>= singleSpecialization . fixturePlan) of
+    (Just value, Just specialization) ->
+        let SymbolId maximumSource = maximumSymbolInTypedAST (fixtureTyped value)
+            generated = map (symbolIdValue . snd) (templateSpecializationSymbolMap specialization)
+         in not (null generated) && all (> maximumSource) generated
+    _ -> False
+
+specializationSymbolsAreDisjoint :: Bool
+specializationSymbolsAreDisjoint = case plan boxSource requests of
+    Right value -> case plannedTemplateSpecializations value of
+        [left, right] ->
+            let leftSymbols = map snd (templateSpecializationSymbolMap left)
+                rightSymbols = map snd (templateSpecializationSymbolMap right)
+             in null [symbol | symbol <- leftSymbols, symbol `elem` rightSymbols]
+        _ -> False
+    Left _ -> False
+    where
+        requests =
+            [ boxIdentityDemand
+            , memberDemand "Box" [typeArgument "int"] ["Identity"] "int"
+            ]
+
+typedViewIsClosed :: Bool
+typedViewIsClosed = case fixture of
+    Just value -> case syntaxDeclarations (typedSyntaxTree (specializationTypedAST (fixturePlan value))) of
+        [TypeDeclaration {}] -> True
+        _ -> False
+    Nothing -> False
+
+typedViewFollowsOrder :: Bool
+typedViewFollowsOrder = case plan boxSource requests of
+    Right value ->
+        length (syntaxDeclarations (typedSyntaxTree (specializationTypedAST value)))
+            == length (specializationEmissionOrder value)
+    Left _ -> False
+    where
+        requests =
+            [ boxIdentityDemand
+            , memberDemand "Box" [typeArgument "int"] ["Identity"] "int"
+            ]
+
+dependencySource :: String
+dependencySource =
+    "template<typename T> class Leaf { T Read(_ T value) { return value; } } "
+        ++ "template<typename T> class Holder { Leaf<T> Make(_ Leaf<T> value) { return value; } }"
+
+dependencyRequests :: [TemplateSpecializationDemand]
+dependencyRequests =
+    [ memberDemand "Holder" [typeArgument "String"] ["Make"] "holder"
+    , memberDemand "Leaf" [typeArgument "String"] ["Read"] "leaf"
+    ]
+
+dependencyEdgesAreAttached :: Bool
+dependencyEdgesAreAttached = case plan dependencySource dependencyRequests of
+    Right value -> case findByTarget "Holder" value of
+        Just specialization -> length (templateSpecializationDependencies specialization) == 1
+        Nothing -> False
+    Left _ -> False
+
+dependenciesEmitFirst :: Bool
+dependenciesEmitFirst = case plan dependencySource dependencyRequests of
+    Right value -> case (findByTarget "Holder" value, findByTarget "Leaf" value) of
+        (Just holder, Just leaf) ->
+            before
+                (templateSpecializationId leaf)
+                (templateSpecializationId holder)
+                (specializationEmissionOrder value)
+        _ -> False
+    Left _ -> False
+
+recursiveDemandTerminates :: Bool
+recursiveDemandTerminates = case plan source requests of
+    Right value -> length (specializationEmissionOrder value) == 1
+    Left _ -> False
+    where
+        source = "template<typename T> class Node { Node<T> Next(_ Node<T> value) { return value; } }"
+        requests = [memberDemand "Node" [typeArgument "String"] ["Next"] "recursive"]
+
+unknownTemplateIsRejected :: Bool
+unknownTemplateIsRejected = case plan boxSource [demand "Missing" [] TemplateLayoutDemand "unknown-site"] of
+    Left [TemplateApplicationFailed "unknown-site" [UnknownTemplateDeclaration {}]] -> True
+    _ -> False
+
+ambiguousTemplateIsRejected :: Bool
+ambiguousTemplateIsRejected = case analyze source >>= duplicateTopTemplate of
+    Just typed -> case planTemplateSpecializations defaultTemplateSpecializationLimits typed requests of
+        Left [TemplateApplicationFailed _ [AmbiguousTemplateDeclaration {}]] -> True
+        _ -> False
+    Nothing -> False
+    where
+        source = "template<typename T> class Box {}"
+        requests = [demand "Box" [typeArgument "int"] TemplateLayoutDemand "ambiguous"]
+
+duplicateTopTemplate :: TypedAST -> Maybe TypedAST
+duplicateTopTemplate (TypedAST tree) = case syntaxDeclarations tree of
+    [declaration@TemplateTypeDeclaration {}] ->
+        Just (TypedAST tree {syntaxDeclarations = [declaration, declaration]})
+    _ -> Nothing
+
+missingMemberIsRejected :: Bool
+missingMemberIsRejected = case plan boxSource [memberDemand "Box" [typeArgument "String"] ["Missing"] "member"] of
+    Left [TemplateMemberNotFound _ (Identifier "Missing")] -> True
+    _ -> False
+
+categoryMismatchIsRejected :: Bool
+categoryMismatchIsRejected = case plan boxSource [demand "Box" [valueArgument 1] TemplateLayoutDemand "category"] of
+    Left [TemplateApplicationFailed _ [TemplateArgumentCategoryMismatch {}]] -> True
+    _ -> False
+
+tooFewArgumentsAreRejected :: Bool
+tooFewArgumentsAreRejected = case plan boxSource [demand "Box" [] TemplateLayoutDemand "arity"] of
+    Left [TemplateApplicationFailed _ [TooFewTemplateArguments {}]] -> True
+    _ -> False
+
+specializationLimitIsEnforced :: Bool
+specializationLimitIsEnforced = case analyze boxSource of
+    Just typed -> case planTemplateSpecializations limits typed requests of
+        Left [TemplateSpecializationLimitExceeded 1 _] -> True
+        _ -> False
+    Nothing -> False
+    where
+        limits = defaultTemplateSpecializationLimits {maximumTemplateSpecializations = 1}
+        requests =
+            [ memberDemand "Box" [typeArgument "int"] ["Identity"] "one"
+            , memberDemand "Box" [typeArgument "String"] ["Identity"] "two"
+            ]
+
+originLimitIsEnforced :: Bool
+originLimitIsEnforced = case analyze boxSource of
+    Just typed -> case planTemplateSpecializations limits typed requests of
+        Left [TemplateOriginLimitExceeded 1 "second"] -> True
+        _ -> False
+    Nothing -> False
+    where
+        limits = defaultTemplateSpecializationLimits {maximumTemplateOrigins = 1}
+        requests =
+            [ memberDemand "Box" [typeArgument "String"] ["Identity"] "first"
+            , memberDemand "Box" [typeArgument "String"] ["Identity"] "second"
+            ]
+
+memberLimitIsEnforced :: Bool
+memberLimitIsEnforced = case analyze boxSource of
+    Just typed -> case planTemplateSpecializations limits typed requests of
+        Left [TemplateMemberLimitExceeded _ 1 2] -> True
+        _ -> False
+    Nothing -> False
+    where
+        limits = defaultTemplateSpecializationLimits {maximumMembersPerSpecialization = 1}
+        requests = [demand "Box" [typeArgument "String"] TemplateCompleteDemand "complete"]
+
+invalidLimitsAreRejected :: Bool
+invalidLimitsAreRejected = case analyze boxSource of
+    Just typed -> case planTemplateSpecializations invalid typed [boxIdentityDemand] of
+        Left [InvalidTemplateSpecializationLimits _] -> True
+        _ -> False
+    Nothing -> False
+    where
+        invalid = defaultTemplateSpecializationLimits {maximumTemplateSpecializations = 0}
+
+statisticsCountDemands :: Bool
+statisticsCountDemands = case plan boxSource [boxIdentityDemand, boxIdentityDemand] of
+    Right value ->
+        let statistics = plannedTemplateStatistics value
+         in requestedTemplateSpecializations statistics == 2
+                && uniqueTemplateSpecializations statistics == 1
+                && coalescedTemplateSpecializations statistics == 1
+    Left _ -> False
+
+statisticsCountMembers :: Bool
+statisticsCountMembers = case plan boxSource [demand "Box" [typeArgument "String"] TemplateCompleteDemand "complete"] of
+    Right value -> instantiatedTemplateMembers (plannedTemplateStatistics value) == 2
+    Left _ -> False
+
+statisticsCountDependencies :: Bool
+statisticsCountDependencies = case plan dependencySource dependencyRequests of
+    Right value -> templateDependencyEdges (plannedTemplateStatistics value) == 1
+    Left _ -> False
+
+lookupByIdWorks :: Bool
+lookupByIdWorks = case fixture of
+    Just value -> case specializationEmissionOrder (fixturePlan value) of
+        identifier : _ -> findTemplateSpecialization identifier (fixturePlan value) /= Nothing
+        [] -> False
+    Nothing -> False
+
+lookupByIdentityWorks :: Bool
+lookupByIdentityWorks = case fixture >>= singleSpecialization . fixturePlan of
+    Just specialization -> case fixture of
+        Just value ->
+            findTemplateSpecializationByIdentity (templateSpecializationIdentity specialization) (fixturePlan value)
+                == Just specialization
+        Nothing -> False
+    Nothing -> False
+
+missingLookupReturnsNothing :: Bool
+missingLookupReturnsNothing = case fixture of
+    Just value ->
+        findTemplateSpecialization (TemplateSpecializationId 999) (fixturePlan value) == Nothing
+            && findTemplateSpecializationByIdentity "missing" (fixturePlan value) == Nothing
+    Nothing -> False
+
+renderingIncludesOrigin :: Bool
+renderingIncludesOrigin =
+    "source:42"
+        `isInfixOf` renderTemplateSpecializationError
+            (TemplateApplicationFailed "source:42" [UnknownTemplateDeclaration (qualified ["Missing"])])
+
+renderingNamesMember :: Bool
+renderingNamesMember =
+    "member named Read"
+        `isInfixOf` renderTemplateSpecializationError
+            (TemplateMemberNotFound (qualified ["Box"]) (Identifier "Read"))
+
+renderingExplainsLimit :: Bool
+renderingExplainsLimit =
+    "limit 4"
+        `isInfixOf` renderTemplateSpecializationError
+            (TemplateSpecializationLimitExceeded 4 "Box<String>")
+
+compilerBridgeLowersCore :: Bool
+compilerBridgeLowersCore = case analyze boxSource of
+    Just typed -> case compileTemplateSpecializations defaultTemplateSpecializationLimits typed [boxIdentityDemand] of
+        Right (_, core) -> case coreModuleFunctions core of
+            [function] -> identifierText (resolvedSpelling (coreFunctionName function)) == "Identity"
+            _ -> False
+        Left _ -> False
+    Nothing -> False
+
+compilerBridgeMapsDiagnostics :: Bool
+compilerBridgeMapsDiagnostics = case analyze boxSource of
+    Just typed -> case compileTemplateSpecializations defaultTemplateSpecializationLimits typed requests of
+        Left [problem] ->
+            diagnosticStage problem == TypeCheckerStage
+                && "unknown template declaration Missing" `isInfixOf` diagnosticMessage problem
+        Left _ -> False
+        Right _ -> False
+    Nothing -> False
+    where
+        requests = [demand "Missing" [] TemplateLayoutDemand "bridge"]
+
+sourceDeclarationSymbol :: TypedAST -> SymbolId
+sourceDeclarationSymbol (TypedAST tree) = case syntaxDeclarations tree of
+    declaration : _ -> resolvedSymbol (declarationName declaration)
+    [] -> SymbolId 0
+
+sourceMemberSymbol :: TypedAST -> Maybe SymbolId
+sourceMemberSymbol (TypedAST tree) = case syntaxDeclarations tree of
+    TemplateTypeDeclaration {typeMembers = member : _} : _ -> Just (resolvedSymbol (declarationName member))
+    _ -> Nothing
+
+declarationSymbol :: Declaration ResolvedName Type -> SymbolId
+declarationSymbol = resolvedSymbol . declarationName
+
+findByTarget :: String -> TemplateSpecializationPlan -> Maybe TemplateSpecialization
+findByTarget target specializationPlan =
+    first matching (plannedTemplateSpecializations specializationPlan)
+    where
+        matching specialization = case templateSpecializationType specialization of
+            NamedType (QualifiedName parts) _ -> case reverse parts of
+                Identifier name : _ -> name == target
+                [] -> False
+            _ -> False
+
+before :: (Eq value) => value -> value -> [value] -> Bool
+before left right values = case (position left values, position right values) of
+    (Just leftIndex, Just rightIndex) -> leftIndex < rightIndex
+    _ -> False
+
+position :: (Eq value) => value -> [value] -> Maybe Int
+position needle = go 0
+    where
+        go _ [] = Nothing
+        go index (value : remaining)
+            | value == needle = Just index
+            | otherwise = go (index + 1) remaining
+
+ordered :: (Ord value) => [value] -> [value]
+ordered [] = []
+ordered (value : remaining) =
+    ordered [candidate | candidate <- remaining, candidate <= value]
+        ++ [value]
+        ++ ordered [candidate | candidate <- remaining, candidate > value]
+
+first :: (value -> Bool) -> [value] -> Maybe value
+first _ [] = Nothing
+first predicate (value : remaining)
+    | predicate value = Just value
+    | otherwise = first predicate remaining
+
+rightValue :: Either failure value -> Maybe value
+rightValue result = case result of
+    Right value -> Just value
+    Left _ -> Nothing
+ test/TemplateTests.hs view
@@ -0,0 +1,504 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+module TemplateTests (templateTests) where
+
+import Data.Bits (shiftL)
+import Data.List (isInfixOf)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Core.CorePrep
+import Progmasoft.Tala.Core.CorePrep.Verifier
+import Progmasoft.Tala.Core.CorePrep.Wire
+import Progmasoft.Tala.Core.Template
+import Progmasoft.Tala.Core.Verifier
+import Progmasoft.Tala.Core.Wire
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.TemplateValue
+
+templateTests :: [(String, Bool)]
+templateTests =
+    parserAndTypeTests
+        ++ evaluationTests
+        ++ shapeTests
+        ++ validationTests
+        ++ metricTests
+        ++ substitutionTests
+        ++ identityTests
+        ++ wireTests
+
+parserAndTypeTests :: [(String, Bool)]
+parserAndTypeTests =
+    [ ("fixed array literal reaches typed AST", typedReturn "[int; 3]" == Just (fixed intType 3))
+    , ("fixed array arithmetic is canonicalized", typedReturn "[int; 1 + 2 * 3]" == Just (fixed intType 7))
+    , ("fixed array parentheses affect evaluation", typedReturn "[int; (1 + 2) * 3]" == Just (fixed intType 9))
+    , ("fixed array hexadecimal size is accepted", typedReturn "[int; 0x10]" == Just (fixed intType 16))
+    , ("fixed array binary size is accepted", typedReturn "[int; 0b1000]" == Just (fixed intType 8))
+    , ("fixed array digit separators are accepted", typedReturn "[int; 1'024]" == Just (fixed intType 1024))
+    , ("zero-sized fixed array is representable", typedReturn "[int; 0]" == Just (fixed intType 0))
+    , ("negative fixed array is diagnosed", compileReturn "[int; -1]" `hasCode` "TAT0016")
+    , ("Boolean fixed array size is diagnosed", compileReturn "[int; true]" `hasCode` "TAT0016")
+    , ("unresolved fixed array size is diagnosed", compileReturn "[int; Size]" `hasCode` "TAT0016")
+    , ("division by zero in fixed size is diagnosed", compileReturn "[int; 4 / 0]" `hasCode` "TAT0016")
+    , ("floor division by zero in fixed size is diagnosed", compileReturn "[int; 4 // 0]" `hasCode` "TAT0016")
+    , ("remainder by zero in fixed size is diagnosed", compileReturn "[int; 4 % 0]" `hasCode` "TAT0016")
+    ,
+        ( "oversized power in fixed size is rejected without host-sized allocation"
+        , compileReturn "[int; 2 ** 9223372036854775807]" `hasCode` "TAT0016"
+        )
+    ,
+        ( "zero power in fixed size bypasses an enormous exponent"
+        , typedReturn "[int; 0 ** 9223372036854775807]" == Just (fixed intType 0)
+        )
+    ,
+        ( "unit power in fixed size bypasses an enormous exponent"
+        , typedReturn "[int; 1 ** 9223372036854775807]" == Just (fixed intType 1)
+        )
+    , ("over-limit power in fixed size is diagnosed", compileReturn "[int; 2 ** 65536]" `hasCode` "TAT0016")
+    , ("over-limit shift in fixed size is diagnosed", compileReturn "[int; 1 << 65536]" `hasCode` "TAT0016")
+    , ("zero shift in fixed size bypasses a huge intermediate", typedReturn "[int; 0 << 65536]" == Just (fixed intType 0))
+    , ("huge right shift in fixed size saturates", typedReturn "[int; 5 >> 65536]" == Just (fixed intType 0))
+    , ("runtime call cannot be a template value", compileReturn "[int; Size()]" `hasCode` "TAP0018")
+    , ("explicit numeric template argument is a value", typedReturn "Buffer<32>" == Just (applied "Buffer" [value 32]))
+    ,
+        ( "explicit arithmetic template argument is evaluated"
+        , typedReturn "Buffer<4 * 8>" == Just (applied "Buffer" [value 32])
+        )
+    , ("explicit Boolean template argument is retained", typedReturn "Flag<true>" == Just (applied "Flag" [boolean True]))
+    , ("explicit character template argument is retained", typedReturn "Code<'A'>" == Just (applied "Code" [character 65]))
+    ,
+        ( "mixed generic arguments preserve order"
+        , typedReturn "Matrix<int, 3>" == Just (applied "Matrix" [typeArg intType, value 3])
+        )
+    ,
+        ( "value before type preserves order"
+        , typedReturn "Matrix<3, int>" == Just (applied "Matrix" [value 3, typeArg intType])
+        )
+    ,
+        ( "nested fixed array remains a type argument"
+        , typedReturn "Box<[int; 4]>" == Just (applied "Box" [typeArg (fixed intType 4)])
+        )
+    , -- Adjacent `[[` starts a raw string at the lexical layer. A separating
+      -- space keeps nested collection brackets in the type grammar.
+      ("fixed array of dynamic arrays preserves both shapes", typedReturn "[ [int]; 2]" == Just (fixed (dynamic intType) 2))
+    , ("dynamic array of fixed arrays preserves both shapes", typedReturn "[ [int; 2]]" == Just (dynamic (fixed intType 2)))
+    ]
+
+evaluationTests :: [(String, Bool)]
+evaluationTests =
+    [ ("integer template literal evaluates", evaluateTemplateValue (integerSyntax 42) == Right (IntegerTemplateValue 42))
+    ,
+        ( "character template literal evaluates"
+        , evaluateTemplateValue (characterSyntax 65) == Right (CharacterTemplateValue 65)
+        )
+    , ("Boolean template literal evaluates", evaluateTemplateValue (booleanSyntax True) == Right (BooleanTemplateValue True))
+    ,
+        ( "unary plus evaluates exactly"
+        , evaluateTemplateValue (unary UnaryPlus (integerSyntax 9)) == Right (IntegerTemplateValue 9)
+        )
+    ,
+        ( "unary negate evaluates exactly"
+        , evaluateTemplateValue (unary UnaryNegate (integerSyntax 9)) == Right (IntegerTemplateValue (-9))
+        )
+    ,
+        ( "logical not produces Boolean identity"
+        , evaluateTemplateValue (unary LogicalNot (integerSyntax 0)) == Right (BooleanTemplateValue True)
+        )
+    ,
+        ( "bitwise not evaluates exactly"
+        , evaluateTemplateValue (unary BitwiseNot (integerSyntax 0)) == Right (IntegerTemplateValue (-1))
+        )
+    , ("addition evaluates exactly", binaryValue Add 20 22 == Right (IntegerTemplateValue 42))
+    , ("subtraction evaluates exactly", binaryValue Subtract 50 8 == Right (IntegerTemplateValue 42))
+    , ("multiplication evaluates exactly", binaryValue Multiply 6 7 == Right (IntegerTemplateValue 42))
+    , ("division truncates toward zero", binaryValue Divide (-7) 2 == Right (IntegerTemplateValue (-3)))
+    , ("rounded division moves positive halves away from zero", binaryValue FloorDivide 7 2 == Right (IntegerTemplateValue 4))
+    ,
+        ( "rounded division moves negative halves away from zero"
+        , binaryValue FloorDivide (-7) 2 == Right (IntegerTemplateValue (-4))
+        )
+    , ("rounded division chooses the nearest integer", binaryValue FloorDivide 7 3 == Right (IntegerTemplateValue 2))
+    , ("remainder follows dividend", binaryValue Remainder (-7) 2 == Right (IntegerTemplateValue (-1)))
+    , ("power evaluates exactly", binaryValue Power 3 4 == Right (IntegerTemplateValue 81))
+    ,
+        ( "compile-time integer limit includes its largest positive value"
+        , evaluateTemplateValue (integerSyntax (compileTimeIntegerMagnitude - 1))
+            == Right (IntegerTemplateValue (compileTimeIntegerMagnitude - 1))
+        )
+    ,
+        ( "compile-time integer limit rejects its first positive overflow"
+        , evaluateTemplateValue (integerSyntax compileTimeIntegerMagnitude) == Left TemplateValueEvaluationLimitExceeded
+        )
+    ,
+        ( "zero power handles a maximum-width exponent directly"
+        , binaryValue Power 0 (2 ^ (63 :: Int) - 1) == Right (IntegerTemplateValue 0)
+        )
+    ,
+        ( "one power handles a maximum-width exponent directly"
+        , binaryValue Power 1 (2 ^ (63 :: Int) - 1) == Right (IntegerTemplateValue 1)
+        )
+    ,
+        ( "large powers stop at the compile-time integer limit"
+        , binaryValue Power 2 (2 ^ (63 :: Int) - 1) == Left TemplateValueEvaluationLimitExceeded
+        )
+    ,
+        ( "power accepts its largest representable intermediate"
+        , binaryValue Power 2 65535 == Right (IntegerTemplateValue (2 ^ (65535 :: Int)))
+        )
+    ,
+        ( "power rejects the first over-limit intermediate"
+        , binaryValue Power 2 65536 == Left TemplateValueEvaluationLimitExceeded
+        )
+    , ("shift-left evaluates exactly", binaryValue ShiftLeft 3 4 == Right (IntegerTemplateValue 48))
+    , ("shift-right evaluates exactly", binaryValue ShiftRight 48 4 == Right (IntegerTemplateValue 3))
+    ,
+        ( "left shift accepts its largest representable intermediate"
+        , binaryValue ShiftLeft 1 65535 == Right (IntegerTemplateValue (2 ^ (65535 :: Int)))
+        )
+    ,
+        ( "left shift rejects the first over-limit result"
+        , binaryValue ShiftLeft 1 65536 == Left TemplateValueEvaluationLimitExceeded
+        )
+    ,
+        ( "zero left shift handles a maximum-width count directly"
+        , binaryValue ShiftLeft 0 (2 ^ (63 :: Int) - 1) == Right (IntegerTemplateValue 0)
+        )
+    ,
+        ( "large left shifts stop at the compile-time integer limit"
+        , binaryValue ShiftLeft 1 (2 ^ (63 :: Int) - 1) == Left TemplateValueEvaluationLimitExceeded
+        )
+    ,
+        ( "large arithmetic right shift saturates to the sign value"
+        , binaryValue ShiftRight (-3) (2 ^ (63 :: Int) - 1) == Right (IntegerTemplateValue (-1))
+        )
+    , ("bitwise and evaluates exactly", binaryValue BitwiseAnd 14 11 == Right (IntegerTemplateValue 10))
+    , ("bitwise xor evaluates exactly", binaryValue BitwiseXor 14 11 == Right (IntegerTemplateValue 5))
+    , ("bitwise or evaluates exactly", binaryValue BitwiseOr 8 3 == Right (IntegerTemplateValue 11))
+    , ("negative integer power has a precise error", binaryValue Power 2 (-1) == Left (TemplateValueNegativeExponent (-1)))
+    , ("less-than produces Boolean", binaryValue LessThan 1 2 == Right (BooleanTemplateValue True))
+    , ("less-equal produces Boolean", binaryValue LessEqual 2 2 == Right (BooleanTemplateValue True))
+    , ("greater-than produces Boolean", binaryValue GreaterThan 3 2 == Right (BooleanTemplateValue True))
+    , ("greater-equal produces Boolean", binaryValue GreaterEqual 3 3 == Right (BooleanTemplateValue True))
+    , ("integer equality produces Boolean", binaryValue Equal 3 3 == Right (BooleanTemplateValue True))
+    , ("integer inequality produces Boolean", binaryValue NotEqual 3 4 == Right (BooleanTemplateValue True))
+    , ("logical and accepts numeric Boolean context", binaryValue LogicalAnd 1 2 == Right (BooleanTemplateValue True))
+    , ("logical or accepts numeric Boolean context", binaryValue LogicalOr 0 2 == Right (BooleanTemplateValue True))
+    , ("ordinary division by zero has a precise error", binaryValue Divide 1 0 == Left TemplateValueDivisionByZero)
+    , ("floor division by zero has a precise error", binaryValue FloorDivide 1 0 == Left TemplateValueFloorDivisionByZero)
+    , ("remainder by zero has a precise error", binaryValue Remainder 1 0 == Left TemplateValueRemainderByZero)
+    ,
+        ( "fixed array accepts character as scalar size"
+        , evaluateFixedArraySize (characterSyntax 8) == Right (IntegerTemplateValue 8)
+        )
+    , ("fixed array rejects Boolean result", isIntegerRequirement (evaluateFixedArraySize (booleanSyntax True)))
+    ,
+        ( "fixed array rejects negative result"
+        , evaluateFixedArraySize (integerSyntax (-2)) == Left (TemplateValueNegativeArraySize (-2))
+        )
+    , ("qualified unresolved constant keeps its name", unresolvedNameError)
+    ]
+
+shapeTests :: [(String, Bool)]
+shapeTests =
+    [ ("built-in array classifies separately", classifyArrayType (builtin intType) == Just (BuiltinArrayShape intType))
+    , ("dynamic System.Array classifies", classifyArrayType (dynamic intType) == Just (DynamicArrayShape intType))
+    , ("fixed System.Array classifies", classifyArrayType (fixed intType 4) == Just (FixedArrayShape intType 4))
+    , ("user Array spelling is not System.Array", classifyArrayType (applied "Array" [typeArg intType]) == Nothing)
+    , ("fixed array requires integer value", classifyArrayType (systemArray [typeArg intType, boolean True]) == Nothing)
+    , ("fixed array requires leading type", classifyArrayType (systemArray [value 4, typeArg intType]) == Nothing)
+    ,
+        ( "three-argument System.Array is malformed"
+        , classifyArrayType (systemArray [typeArg intType, value 4, value 5]) == Nothing
+        )
+    ,
+        ( "built-in array cannot carry a size"
+        , classifyArrayType (NamedType (QualifiedName [Identifier "[]"]) [typeArg intType, value 4]) == Nothing
+        )
+    ]
+
+validationTests :: [(String, Bool)]
+validationTests =
+    [ ("concrete fixed array validates", null (validateTemplateType 128 (fixed intType 16)))
+    , ("nested concrete type validates", null (validateTemplateType 128 nestedConcrete))
+    , ("empty qualified name is rejected", hasIssue TemplateEmptyQualifiedName (NamedType (QualifiedName []) []))
+    ,
+        ( "empty qualified part is rejected"
+        , hasIssue TemplateEmptyNamePart (NamedType (QualifiedName [Identifier "System", Identifier ""]) [])
+        )
+    , ("zero type parameter id is rejected", hasIssue TemplateInvalidParameter (TypeVariable (parameter 0 "T")))
+    , ("empty type parameter spelling is rejected", hasIssue TemplateInvalidParameter (TypeVariable (parameter 1 "")))
+    ,
+        ( "zero value parameter id is rejected"
+        , hasIssue TemplateInvalidParameter (applied "Buffer" [ValueTemplateArgument (TemplateValueParameter (parameter 0 "N"))])
+        )
+    , ("surrogate character is rejected", hasIssue TemplateInvalidCharacter (applied "Code" [character 0xd800]))
+    , ("scalar above Unicode range is rejected", hasIssue TemplateInvalidCharacter (applied "Code" [character 0x110000]))
+    , ("negative character is rejected", hasIssue TemplateInvalidCharacter (applied "Code" [character (-1)]))
+    , ("negative fixed size is rejected structurally", hasIssue TemplateNegativeArraySize (fixed intType (-1)))
+    , ("System.Array with no arguments is malformed", hasIssue TemplateMalformedArrayFamily (systemArray []))
+    ,
+        ( "System.Array with two type arguments is malformed"
+        , hasIssue TemplateMalformedArrayFamily (systemArray [typeArg intType, typeArg intType])
+        )
+    ,
+        ( "built-in array with no element is malformed"
+        , hasIssue TemplateMalformedArrayFamily (NamedType (QualifiedName [Identifier "[]"]) [])
+        )
+    , ("depth zero admits root scalar", null (validateTemplateType 0 intType))
+    ,
+        ( "depth zero rejects nested argument"
+        , any
+            ((== TemplateDepthExceeded) . templateIssueKind)
+            (validateTemplateType 0 (applied "Box" [typeArg intType]))
+        )
+    , ("depth one admits one nested argument", null (validateTemplateType 1 (applied "Box" [typeArg intType])))
+    , ("Core verifier rejects malformed specialization keys", coreVerifierRejectsMalformed)
+    , ("CorePrep verifier rejects malformed specialization keys", corePrepVerifierRejectsMalformed)
+    ]
+    where
+        hasIssue kind valueType = any ((== kind) . templateIssueKind) (validateTemplateType 128 valueType)
+
+metricTests :: [(String, Bool)]
+metricTests =
+    [ ("scalar metrics contain one type node", measureTemplateType intType == TemplateMetrics 1 0 0 0 0)
+    , ("dynamic array metrics count type argument", measureTemplateType (dynamic intType) == TemplateMetrics 2 1 0 0 1)
+    , ("fixed array metrics count type and value", measureTemplateType (fixed intType 4) == TemplateMetrics 2 1 1 0 1)
+    , ("type variable increments parameter count", measureTemplateType typeParameter == TemplateMetrics 1 0 0 1 0)
+    , ("value parameter increments parameter count", measureTemplateType valueParameter == TemplateMetrics 2 1 1 1 1)
+    , ("function metrics include parameters and result", measureTemplateType functionTemplate == TemplateMetrics 5 1 1 0 2)
+    , ("nested template records maximum depth", templateMaximumDepth (measureTemplateType nestedConcrete) == 3)
+    , ("nested template counts all type arguments", templateTypeArguments (measureTemplateType nestedConcrete) == 3)
+    , ("nested template counts its value", templateValueArguments (measureTemplateType nestedConcrete) == 1)
+    ]
+
+substitutionTests :: [(String, Bool)]
+substitutionTests =
+    [ ("type parameter is substituted", substitute [typeBinding] [] typeParameter == stringType)
+    , ("value parameter is substituted", substitute [] [valueBinding] valueParameter == fixed intType 32)
+    ,
+        ( "type and value parameters substitute together"
+        , substitute [typeBinding] [valueBinding] genericArray == fixed stringType 32
+        )
+    , ("missing type binding is preserved", substitute [] [valueBinding] genericArray == fixed typeParameter 32)
+    ,
+        ( "missing value binding is preserved"
+        , substitute [typeBinding] [] genericArray
+            == systemArray [typeArg stringType, ValueTemplateArgument (TemplateValueParameter sizeParameter)]
+        )
+    ,
+        ( "substitution descends through function parameters"
+        , substitute [typeBinding] [valueBinding] genericFunction == concreteFunction
+        )
+    , ("substitution does not change unrelated concrete type", substitute [typeBinding] [valueBinding] intType == intType)
+    , ("collect finds both parameter kinds", collectTemplateParameters genericArray == [SymbolId 10, SymbolId 20])
+    , ("collect deduplicates repeated parameters", collectTemplateParameters repeatedParameters == [SymbolId 10])
+    , ("concrete type reports concrete", concreteTemplateType nestedConcrete)
+    , ("generic type reports non-concrete", not (concreteTemplateType genericArray))
+    ]
+
+identityTests :: [(String, Bool)]
+identityTests =
+    [ ("identity is deterministic", renderTemplateIdentity nestedConcrete == renderTemplateIdentity nestedConcrete)
+    , ("dynamic and fixed arrays have different identity", identityDifferent (dynamic intType) (fixed intType 0))
+    , ("fixed sizes have different identity", identityDifferent (fixed intType 4) (fixed intType 5))
+    ,
+        ( "type and value argument kinds have different identity"
+        , identityDifferent (applied "Box" [typeArg intType]) (applied "Box" [value 0])
+        )
+    ,
+        ( "argument order affects identity"
+        , identityDifferent (applied "Mix" [typeArg intType, value 4]) (applied "Mix" [value 4, typeArg intType])
+        )
+    ,
+        ( "qualified boundaries affect identity"
+        , identityDifferent
+            (NamedType (QualifiedName [Identifier "A.B"]) [])
+            (NamedType (QualifiedName [Identifier "A", Identifier "B"]) [])
+        )
+    , ("parameter ids affect identity", identityDifferent (TypeVariable (parameter 1 "T")) (TypeVariable (parameter 2 "T")))
+    ,
+        ( "parameter spelling affects identity"
+        , identityDifferent (TypeVariable (parameter 1 "T")) (TypeVariable (parameter 1 "U"))
+        )
+    ,
+        ( "integer and character identities differ"
+        , identityDifferent (applied "Value" [value 65]) (applied "Value" [character 65])
+        )
+    , ("Boolean values affect identity", identityDifferent (applied "Flag" [boolean False]) (applied "Flag" [boolean True]))
+    , ("identity includes function result", identityDifferent (FunctionType [] intType) (FunctionType [] stringType))
+    ,
+        ( "identity includes every function parameter"
+        , identityDifferent (FunctionType [intType] intType) (FunctionType [intType, intType] intType)
+        )
+    , ("rendered identity exposes value kind", "value:i:4" `isInfixOf` renderTemplateIdentity (fixed intType 4))
+    ,
+        ( "rendered identity exposes qualified name lengths"
+        , "6:System.5:Array" `isInfixOf` renderTemplateIdentity (fixed intType 4)
+        )
+    ]
+
+wireTests :: [(String, Bool)]
+wireTests =
+    [ ("Core v10 round-trips fixed array type", coreTypeRoundTrip (fixed intType 4096))
+    , ("Core v10 round-trips Boolean template value", coreTypeRoundTrip (applied "Flag" [boolean True]))
+    , ("Core v10 round-trips character template value", coreTypeRoundTrip (applied "Code" [character 0x10ffff]))
+    , ("Core v10 round-trips template value parameter", coreTypeRoundTrip valueParameter)
+    ,
+        ( "Core v10 round-trips mixed arguments"
+        , coreTypeRoundTrip (applied "Mix" [value (-3), typeArg stringType, boolean False])
+        )
+    , ("CorePrep v6 round-trips fixed array type", corePrepTypeRoundTrip (fixed intType 4096))
+    , ("CorePrep v6 round-trips Boolean template value", corePrepTypeRoundTrip (applied "Flag" [boolean True]))
+    , ("CorePrep v6 round-trips character template value", corePrepTypeRoundTrip (applied "Code" [character 0x10ffff]))
+    , ("CorePrep v6 round-trips template value parameter", corePrepTypeRoundTrip valueParameter)
+    ,
+        ( "CorePrep v6 round-trips mixed arguments"
+        , corePrepTypeRoundTrip (applied "Mix" [value (-3), typeArg stringType, boolean False])
+        )
+    ]
+
+typedReturn :: String -> Maybe Type
+typedReturn returnType = case compileReturn returnType of
+    Right artifacts -> case syntaxDeclarations (typedSyntaxTree (artifactTypedAST artifacts)) of
+        [TypeDeclaration {typeMembers = [FunctionDeclaration {declarationParameters = [parameterValue]}]}] ->
+            Just (parameterAnnotation parameterValue)
+        _ -> Nothing
+    Left _ -> Nothing
+
+compileReturn :: String -> Either [Diagnostic] FrontendArtifacts
+compileReturn returnType =
+    compileToCorePrep
+        (CompilerInput "template-test.tala" ("class App { void Use(_ " ++ returnType ++ " value) { return; } }"))
+
+hasCode :: Either [Diagnostic] a -> String -> Bool
+hasCode result code = case result of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+spanValue :: SourceSpan
+spanValue = SourceSpan "template-test.tala" (SourcePosition 1 1) (SourcePosition 1 2)
+
+compileTimeIntegerMagnitude :: Integer
+compileTimeIntegerMagnitude = 1 `shiftL` 65536
+
+integerSyntax :: Integer -> TemplateValueSyntax
+integerSyntax = TemplateIntegerSyntax spanValue
+
+characterSyntax :: Integer -> TemplateValueSyntax
+characterSyntax = TemplateCharacterSyntax spanValue
+
+booleanSyntax :: Bool -> TemplateValueSyntax
+booleanSyntax = TemplateBooleanSyntax spanValue
+
+unary :: UnaryOperator -> TemplateValueSyntax -> TemplateValueSyntax
+unary = TemplateUnarySyntax spanValue
+
+binaryValue :: BinaryOperator -> Integer -> Integer -> Either TemplateValueError TemplateValue
+binaryValue operator left right = evaluateTemplateValue (TemplateBinarySyntax spanValue operator (integerSyntax left) (integerSyntax right))
+
+isIntegerRequirement :: Either TemplateValueError TemplateValue -> Bool
+isIntegerRequirement result = case result of
+    Left (TemplateValueRequiresInteger _) -> True
+    _ -> False
+
+unresolvedNameError :: Bool
+unresolvedNameError = case evaluateTemplateValue (TemplateNameSyntax spanValue (QualifiedName [Identifier "Config", Identifier "Size"])) of
+    Left (TemplateValueIsNotConstant (QualifiedName [Identifier "Config", Identifier "Size"])) -> True
+    _ -> False
+
+typeArg :: Type -> TemplateArgument
+typeArg = TypeTemplateArgument
+
+value :: Integer -> TemplateArgument
+value = ValueTemplateArgument . IntegerTemplateValue
+
+boolean :: Bool -> TemplateArgument
+boolean = ValueTemplateArgument . BooleanTemplateValue
+
+character :: Integer -> TemplateArgument
+character = ValueTemplateArgument . CharacterTemplateValue
+
+applied :: String -> [TemplateArgument] -> Type
+applied name = NamedType (QualifiedName [Identifier name])
+
+systemArray :: [TemplateArgument] -> Type
+systemArray = NamedType (QualifiedName [Identifier "System", Identifier "Array"])
+
+builtin :: Type -> Type
+builtin element = NamedType (QualifiedName [Identifier "[]"]) [typeArg element]
+
+dynamic :: Type -> Type
+dynamic element = systemArray [typeArg element]
+
+fixed :: Type -> Integer -> Type
+fixed element size = systemArray [typeArg element, value size]
+
+parameter :: Int -> String -> ResolvedName
+parameter symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
+
+typeParameter :: Type
+typeParameter = TypeVariable (parameter 10 "T")
+
+sizeParameter :: ResolvedName
+sizeParameter = parameter 20 "N"
+
+valueParameter :: Type
+valueParameter = systemArray [typeArg intType, ValueTemplateArgument (TemplateValueParameter sizeParameter)]
+
+genericArray :: Type
+genericArray = systemArray [typeArg typeParameter, ValueTemplateArgument (TemplateValueParameter sizeParameter)]
+
+genericFunction :: Type
+genericFunction = FunctionType [genericArray, typeParameter] typeParameter
+
+concreteFunction :: Type
+concreteFunction = FunctionType [fixed stringType 32, stringType] stringType
+
+nestedConcrete :: Type
+nestedConcrete = applied "Outer" [typeArg (dynamic (fixed intType 4))]
+
+functionTemplate :: Type
+functionTemplate = FunctionType [fixed intType 4, stringType] boolType
+
+repeatedParameters :: Type
+repeatedParameters = FunctionType [typeParameter, typeParameter] typeParameter
+
+typeBinding :: (SymbolId, Type)
+typeBinding = (SymbolId 10, stringType)
+
+valueBinding :: (SymbolId, TemplateValue)
+valueBinding = (SymbolId 20, IntegerTemplateValue 32)
+
+substitute :: [(SymbolId, Type)] -> [(SymbolId, TemplateValue)] -> Type -> Type
+substitute = substituteTemplateType
+
+identityDifferent :: Type -> Type -> Bool
+identityDifferent left right = renderTemplateIdentity left /= renderTemplateIdentity right
+
+coreTypeRoundTrip :: Type -> Bool
+coreTypeRoundTrip valueType =
+    let function = CoreFunction (parameter 1 "Value") [] valueType []
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "Template"]) [function] [] []
+     in (encodeCore defaultCoreWireLimits moduleValue >>= decodeCore defaultCoreWireLimits) == Right moduleValue
+
+corePrepTypeRoundTrip :: Type -> Bool
+corePrepTypeRoundTrip valueType =
+    let atom = CorePrepLiteral CoreUnit unitType
+        block = CorePrepBlock 0 [] (CorePrepReturn atom)
+        function = CorePrepFunction (parameter 1 "Value") "" [] valueType 0 [block]
+        moduleValue = CorePrepModule (QualifiedName [Identifier "Template"]) [function] []
+     in (encodeCorePrep moduleValue >>= decodeCorePrep) == Right moduleValue
+
+coreVerifierRejectsMalformed :: Bool
+coreVerifierRejectsMalformed =
+    let function = CoreFunction (parameter 1 "Value") [] (fixed intType (-1)) []
+        moduleValue = CoreModuleWithSources (QualifiedName [Identifier "Template"]) [function] [] []
+     in hasCode (verifyCore moduleValue) "TAC1040"
+
+corePrepVerifierRejectsMalformed :: Bool
+corePrepVerifierRejectsMalformed =
+    let atom = CorePrepLiteral CoreUnit unitType
+        block = CorePrepBlock 0 [] (CorePrepReturn atom)
+        function = CorePrepFunction (parameter 1 "Value") "" [] (fixed intType (-1)) 0 [block]
+        moduleValue = CorePrepModule (QualifiedName [Identifier "Template"]) [function] []
+     in hasCode (verifyCorePrep moduleValue) "TAC0024"
+ test/TemplateVerifierTests.hs view
@@ -0,0 +1,223 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TemplateVerifierTests (templateVerifierTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Frontend
+import Progmasoft.Tala.Template.Verifier
+
+templateVerifierTests :: [(String, Bool)]
+templateVerifierTests =
+    [ ("verifier accepts a typed type template", acceptsTypeTemplate)
+    , ("verifier accepts a typed value template", acceptsValueTemplate)
+    , ("verifier accepts a typed template-template declaration", acceptsTemplateTemplate)
+    , ("verifier accepts a parameter pack without a default", acceptsPack)
+    , ("verifier accepts multiple constrained-candidate surfaces structurally", acceptsRepeatedSurface)
+    , ("verifier rejects a non-positive declaration symbol", rejectsInvalidDeclarationSymbol)
+    , ("verifier rejects a non-positive parameter symbol", rejectsInvalidParameterSymbol)
+    , ("verifier rejects a duplicated parameter symbol", rejectsDuplicateParameterSymbol)
+    , ("verifier rejects a duplicated parameter spelling", rejectsDuplicateParameterSpelling)
+    , ("verifier rejects ErrorType parameter annotations", rejectsErrorType)
+    , ("verifier rejects defaults attached to packs", rejectsPackDefault)
+    , ("verifier rejects a type default on a value parameter", rejectsValueCategoryMismatch)
+    , ("verifier rejects a value default on a type parameter", rejectsTypeCategoryMismatch)
+    , ("verifier rejects an empty template-template signature", rejectsEmptyTemplateSignature)
+    , ("verifier rejects an empty nested template signature", rejectsEmptyNestedSignature)
+    , ("verifier rejects a member with a non-positive symbol", rejectsInvalidMemberSymbol)
+    , ("verifier ignores ordinary declarations", ignoresOrdinaryDeclaration)
+    , ("verifier renders declaration symbol failures", rendersDeclarationFailure)
+    , ("verifier renders duplicate spelling failures", rendersDuplicateSpelling)
+    , ("verifier renders pack default failures", rendersPackDefault)
+    ]
+
+typedTree :: String -> Maybe TypedAST
+typedTree source = case analyzeSemantics (CompilerInput "verifier-test.tala" source) of
+    Right artifacts -> Just (semanticTypedAST artifacts)
+    Left _ -> Nothing
+
+verifySource :: String -> Bool
+verifySource source = case typedTree source of Just tree -> verifyTemplateDeclarations tree == Right (); Nothing -> False
+
+acceptsTypeTemplate :: Bool
+acceptsTypeTemplate = verifySource "template<typename T> class Box {}"
+
+acceptsValueTemplate :: Bool
+acceptsValueTemplate = verifySource "template<int N> class Buffer {}"
+
+acceptsTemplateTemplate :: Bool
+acceptsTemplateTemplate = verifySource "template<template<typename> class C> class Wrapper {}"
+
+acceptsPack :: Bool
+acceptsPack = verifySource "template<typename... T> class Tuple {}"
+
+acceptsRepeatedSurface :: Bool
+acceptsRepeatedSurface =
+    let first = templateDeclaration 1 [typeParameter 2 "T"] []
+        second = templateDeclaration 3 [typeParameter 4 "U"] []
+     in verifyTemplateDeclarations (TypedAST (SyntaxTree Nothing [first, second])) == Right ()
+
+position :: SourcePosition
+position = SourcePosition 1 1
+
+spanValue :: SourceSpan
+spanValue = SourceSpan "synthetic-template.tala" position position
+
+resolved :: Int -> String -> ResolvedName
+resolved value spelling = ResolvedName (SymbolId value) (Identifier spelling)
+
+parameter ::
+    Int -> String -> Type -> TemplateParameterKind -> Bool -> Maybe TemplateDefault -> TemplateParameter ResolvedName Type
+parameter symbol spelling annotation kind packed defaultValue =
+    TemplateParameter spanValue (resolved symbol spelling) annotation kind packed defaultValue
+
+templateDeclaration ::
+    Int -> [TemplateParameter ResolvedName Type] -> [Declaration ResolvedName Type] -> Declaration ResolvedName Type
+templateDeclaration symbol parameters members =
+    TemplateTypeDeclaration spanValue (resolved symbol "Box") (namedType "Box") parameters members
+
+verifySynthetic :: Declaration ResolvedName Type -> Either [TemplateVerificationError] ()
+verifySynthetic declaration = verifyTemplateDeclarations (TypedAST (SyntaxTree Nothing [declaration]))
+
+hasProblem :: (TemplateVerificationError -> Bool) -> Either [TemplateVerificationError] () -> Bool
+hasProblem predicate result = case result of Left problems -> any predicate problems; Right () -> False
+
+rejectsInvalidDeclarationSymbol :: Bool
+rejectsInvalidDeclarationSymbol = hasProblem isExpected (verifySynthetic (templateDeclaration 0 [] []))
+    where
+        isExpected TemplateDeclarationHasInvalidSymbol {} = True; isExpected _ = False
+
+rejectsInvalidParameterSymbol :: Bool
+rejectsInvalidParameterSymbol = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration = templateDeclaration 1 [parameter 0 "T" (TypeVariable (resolved 0 "T")) TemplateTypeParameter False Nothing] []
+        isExpected TemplateParameterHasInvalidSymbol {} = True
+        isExpected _ = False
+
+rejectsDuplicateParameterSymbol :: Bool
+rejectsDuplicateParameterSymbol = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration = templateDeclaration 1 [typeParameter 2 "T", typeParameter 2 "U"] []
+        isExpected TemplateParameterSymbolIsDuplicated {} = True
+        isExpected _ = False
+
+rejectsDuplicateParameterSpelling :: Bool
+rejectsDuplicateParameterSpelling = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration = templateDeclaration 1 [typeParameter 2 "T", typeParameter 3 "T"] []
+        isExpected TemplateParameterSpellingIsDuplicated {} = True
+        isExpected _ = False
+
+typeParameter :: Int -> String -> TemplateParameter ResolvedName Type
+typeParameter symbol spelling = parameter symbol spelling (TypeVariable (resolved symbol spelling)) TemplateTypeParameter False Nothing
+
+rejectsErrorType :: Bool
+rejectsErrorType = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration = templateDeclaration 1 [parameter 2 "T" ErrorType TemplateTypeParameter False Nothing] []
+        isExpected TemplateParameterHasErrorType {} = True
+        isExpected _ = False
+
+rejectsPackDefault :: Bool
+rejectsPackDefault = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration =
+            templateDeclaration
+                1
+                [ parameter
+                    2
+                    "T"
+                    (TypeVariable (resolved 2 "T"))
+                    TemplateTypeParameter
+                    True
+                    (Just (TemplateTypeDefault (ExplicitType (Identifier "int"))))
+                ]
+                []
+        isExpected TemplateParameterPackHasDefault {} = True
+        isExpected _ = False
+
+rejectsValueCategoryMismatch :: Bool
+rejectsValueCategoryMismatch = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration =
+            templateDeclaration
+                1
+                [ parameter
+                    2
+                    "N"
+                    intType
+                    (TemplateValueParameterKind (ExplicitType (Identifier "int")))
+                    False
+                    (Just (TemplateTypeDefault (ExplicitType (Identifier "int"))))
+                ]
+                []
+        isExpected TemplateParameterDefaultCategoryMismatch {} = True
+        isExpected _ = False
+
+rejectsTypeCategoryMismatch :: Bool
+rejectsTypeCategoryMismatch = hasProblem isExpected (verifySynthetic declaration)
+    where
+        valueDefault = TemplateValueDefault (TemplateIntegerSyntax spanValue 1)
+        declaration =
+            templateDeclaration
+                1
+                [parameter 2 "T" (TypeVariable (resolved 2 "T")) TemplateTypeParameter False (Just valueDefault)]
+                []
+        isExpected TemplateParameterDefaultCategoryMismatch {} = True
+        isExpected _ = False
+
+rejectsEmptyTemplateSignature :: Bool
+rejectsEmptyTemplateSignature = hasProblem isExpected (verifySynthetic declaration)
+    where
+        declaration =
+            templateDeclaration 1 [parameter 2 "C" (TypeVariable (resolved 2 "C")) (TemplateTemplateParameter []) False Nothing] []
+        isExpected TemplateTemplateSignatureIsEmpty {} = True
+        isExpected _ = False
+
+rejectsEmptyNestedSignature :: Bool
+rejectsEmptyNestedSignature = hasProblem isExpected (verifySynthetic declaration)
+    where
+        shape = TemplateParameterShape (TemplateTemplateParameterShape []) False
+        declaration =
+            templateDeclaration
+                1
+                [parameter 2 "C" (TypeVariable (resolved 2 "C")) (TemplateTemplateParameter [shape]) False Nothing]
+                []
+        isExpected TemplateTemplateSignatureIsEmpty {} = True
+        isExpected _ = False
+
+rejectsInvalidMemberSymbol :: Bool
+rejectsInvalidMemberSymbol = hasProblem isExpected (verifySynthetic declaration)
+    where
+        member =
+            FunctionDeclaration
+                spanValue
+                (resolved 0 "Read")
+                (FunctionType [] intType)
+                (ExplicitType (Identifier "int"))
+                []
+                (Block [ReturnStatement spanValue (Just (LiteralExpression spanValue (IntegerLiteral 1) intType))])
+                False
+                DefaultAccess
+        declaration = templateDeclaration 1 [typeParameter 2 "T"] [member]
+        isExpected TemplateMemberHasInvalidSymbol {} = True
+        isExpected _ = False
+
+ignoresOrdinaryDeclaration :: Bool
+ignoresOrdinaryDeclaration = verifySynthetic (TypeDeclaration spanValue (resolved 0 "Ordinary") ErrorType []) == Right ()
+
+rendersDeclarationFailure :: Bool
+rendersDeclarationFailure =
+    renderTemplateVerificationError (TemplateDeclarationHasInvalidSymbol (QualifiedName [Identifier "Box"]) (SymbolId 0))
+        == "Box has invalid declaration symbol 0"
+
+rendersDuplicateSpelling :: Bool
+rendersDuplicateSpelling =
+    renderTemplateVerificationError
+        (TemplateParameterSpellingIsDuplicated (QualifiedName [Identifier "Box"]) (Identifier "T"))
+        == "Box repeats template parameter spelling T"
+
+rendersPackDefault :: Bool
+rendersPackDefault =
+    renderTemplateVerificationError (TemplateParameterPackHasDefault (QualifiedName [Identifier "Box"]) (resolved 2 "T"))
+        == "template parameter T in Box is a pack with a default"
+ test/TypeClassificationTests.hs view
@@ -0,0 +1,119 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module TypeClassificationTests (typeClassificationTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.TypeClassification
+
+typeClassificationTests :: [(String, Bool)]
+typeClassificationTests =
+    [ ("every built-in scalar is a trivial value", all scalarIsTrivial scalarTypes)
+    , ("void is a trivial no-result marker", classifyType catalog voidType == TrivialValueStorage)
+    , ("String has reference identity", classifyType catalog stringType == AarcReferenceStorage)
+    , ("callable values own AARC environments", classifyType catalog callableType == AarcReferenceStorage)
+    , ("value declaration families use CoW", all nominalIsValue valueNominals)
+    , ("reference declaration families use AARC", all nominalIsReference referenceNominals)
+    , ("an all-value construction remains a value", classifyType catalog nestedValue == CopyOnWriteValueStorage)
+    , ("a direct reference argument makes a value construction reference", usesAarc catalog directReference)
+    , ("reference classification propagates through arbitrary nesting", usesAarc catalog deepReference)
+    , ("a nominal reference propagates through a value construction", usesAarc catalog nominalReference)
+    , ("a reference outer type ignores unresolved arguments", usesAarc catalog referenceWithOpenArgument)
+    , ("an open all-value construction remains unresolved", classifyType catalog openValue == UnresolvedStorage)
+    , ("reference evidence dominates an earlier unresolved argument", usesAarc catalog unresolvedThenReference)
+    , ("reference evidence dominates a later unresolved argument", usesAarc catalog referenceThenUnresolved)
+    , ("compile-time value arguments do not affect storage", usesCopyOnWrite catalog fixedBuffer)
+    , ("unknown nominal declarations remain unresolved", classifyType catalog missingType == UnresolvedStorage)
+    , ("catalog registration rejects empty qualified names", rejectsEmptyName)
+    , ("catalog registration rejects empty name components", rejectsEmptyComponent)
+    , ("catalog registration rejects exact duplicates", rejectsDuplicate)
+    , ("catalog lookup is case-sensitive", caseSensitiveCatalog)
+    ]
+  where
+    scalarIsTrivial scalar = classifyType catalog (scalarTypeToType scalar) == TrivialValueStorage
+    nominalIsValue kind = classifyNominal kind == CopyOnWriteValueStorage
+    nominalIsReference kind = classifyNominal kind == AarcReferenceStorage
+
+name :: String -> QualifiedName
+name spelling = QualifiedName [Identifier spelling]
+
+named :: String -> [Type] -> Type
+named spelling arguments = NamedType (name spelling) (map TypeTemplateArgument arguments)
+
+typeVariable :: String -> Int -> Type
+typeVariable spelling unique = TypeVariable (ResolvedName (SymbolId unique) (Identifier spelling))
+
+catalog :: NominalCatalog
+catalog = case registrations of
+    Right result -> result
+    Left problem -> error ("invalid ownership test catalog: " ++ show problem)
+  where
+    registrations = do
+        withValue <- registerNominal (name "ValueCell") DataNominal emptyNominalCatalog
+        withReference <- registerNominal (name "ReferenceCell") ClassNominal withValue
+        registerNominal (name "Buffer") LiteralTypeNominal withReference
+
+valueNominals :: [NominalKind]
+valueNominals = [DataNominal, LiteralTypeNominal, ClassicEnumNominal]
+
+referenceNominals :: [NominalKind]
+referenceNominals = [ClassNominal, DataClassNominal, EnumClassNominal, ObjectNominal, InterfaceNominal]
+
+callableType :: Type
+callableType = FunctionType [intType] voidType
+
+nestedValue :: Type
+nestedValue = named "ValueCell" [named "ValueCell" [intType]]
+
+directReference :: Type
+directReference = named "ValueCell" [stringType]
+
+deepReference :: Type
+deepReference = named "ValueCell" [named "ValueCell" [named "ValueCell" [stringType]]]
+
+nominalReference :: Type
+nominalReference = named "ValueCell" [named "ReferenceCell" [intType]]
+
+referenceWithOpenArgument :: Type
+referenceWithOpenArgument = named "ReferenceCell" [typeVariable "T" 1]
+
+openValue :: Type
+openValue = named "ValueCell" [typeVariable "T" 2]
+
+unresolvedThenReference :: Type
+unresolvedThenReference = named "ValueCell" [typeVariable "U" 3, stringType]
+
+referenceThenUnresolved :: Type
+referenceThenUnresolved = named "ValueCell" [stringType, typeVariable "V" 4]
+
+fixedBuffer :: Type
+fixedBuffer =
+    NamedType
+        (name "Buffer")
+        [TypeTemplateArgument intType, ValueTemplateArgument (IntegerTemplateValue 16)]
+
+missingType :: Type
+missingType = named "Missing" [intType]
+
+rejectsEmptyName :: Bool
+rejectsEmptyName = registerNominal (QualifiedName []) DataNominal emptyNominalCatalog == Left EmptyNominalName
+
+rejectsEmptyComponent :: Bool
+rejectsEmptyComponent =
+    registerNominal malformed DataNominal emptyNominalCatalog == Left (EmptyNominalNameComponent malformed)
+  where
+    malformed = QualifiedName [Identifier "Example", Identifier ""]
+
+rejectsDuplicate :: Bool
+rejectsDuplicate = case registerNominal (name "Example") DataNominal emptyNominalCatalog of
+    Left _ -> False
+    Right once -> registerNominal (name "Example") ClassNominal once == Left (DuplicateNominalName (name "Example"))
+
+caseSensitiveCatalog :: Bool
+caseSensitiveCatalog = case registerNominal upper DataNominal emptyNominalCatalog of
+    Left _ -> False
+    Right result -> lookupNominal upper result == Just DataNominal && lookupNominal lower result == Nothing
+  where
+    upper = QualifiedName [Identifier "Example", Identifier "Value"]
+    lower = QualifiedName [Identifier "Example", Identifier "value"]
+ test/VoidTests.hs view
@@ -0,0 +1,134 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+module VoidTests (voidTests) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Compiler
+import Progmasoft.Tala.Core
+import Progmasoft.Tala.Diagnostic
+
+voidTests :: [(String, Bool)]
+voidTests =
+    [ ("void helper retains source spelling", voidType == namedType "void")
+    , ("unit remains an internal no-result marker", unitType == namedType "unit")
+    , ("void function is typed as void", typedReturnType voidFunction == Just voidType)
+    , ("source unit type is rejected", rejected unitFunction)
+    , ("empty parentheses are not a unit literal", rejected emptyParenthesesFunction)
+    , ("void is erased to the internal Core no-result marker", coreReturnType voidFunction == Just unitType)
+    , ("void bare return becomes CoreUnit", coreHasUnitReturn voidFunction)
+    , ("void function rejects a value return", rejected "class App { void Run() { return 1; } }")
+    , ("void function rejects a final expression", rejected "class App { void Run() { 1 } }")
+    , ("void call is typed void before Core", typedCallResult == Just voidType)
+    , ("void call result uses the internal marker after Core lowering", coreCallResult == Just unitType)
+    , ("entry contract accepts public static void Main", entryAccepted)
+    , ("entry contract rejects source unit before entry validation", entryUnitRejected)
+    , ("entry contract rejects public static int Main", entryIntRejected)
+    ]
+
+voidFunction :: String
+voidFunction = "class App { void Run() { return; } }"
+
+unitFunction :: String
+unitFunction = "class App { unit Value() { () } }"
+
+emptyParenthesesFunction :: String
+emptyParenthesesFunction = "class App { int Value() { () } }"
+
+rejected :: String -> Bool
+rejected source = case compileSource source of Left _ -> True; Right _ -> False
+
+typedReturnType :: String -> Maybe Type
+typedReturnType source = do
+    artifacts <- either (const Nothing) Just (compileSource source)
+    declaration <- firstFunction (artifactTypedAST artifacts)
+    case declarationAnnotation declaration of
+        FunctionType _ result -> Just result
+        _ -> Nothing
+
+coreReturnType :: String -> Maybe Type
+coreReturnType source = do
+    artifacts <- either (const Nothing) Just (compileSource source)
+    function <- firstValue (coreModuleFunctions (artifactCore artifacts))
+    pure (coreFunctionReturnType function)
+
+coreHasUnitReturn :: String -> Bool
+coreHasUnitReturn source = case compileSource source of
+    Right artifacts -> case coreModuleFunctions (artifactCore artifacts) of
+        [function] -> any isUnitReturn (coreFunctionBody function)
+        _ -> False
+    Left _ -> False
+    where
+        isUnitReturn (CoreReturn (CoreLiteral CoreUnit valueType)) = valueType == unitType
+        isUnitReturn _ = False
+
+typedCallResult :: Maybe Type
+typedCallResult = do
+    artifacts <- either (const Nothing) Just (compileSource callSource)
+    declarations <- case artifactTypedAST artifacts of TypedAST tree -> Just (syntaxDeclarations tree)
+    case declarations of
+        [TypeDeclaration {typeMembers = [_save, FunctionDeclaration {declarationBody = body}]}] -> findTypedCall body
+        _ -> Nothing
+
+coreCallResult :: Maybe Type
+coreCallResult = do
+    artifacts <- either (const Nothing) Just (compileSource callSource)
+    case coreModuleFunctions (artifactCore artifacts) of
+        [_save, run] -> findCoreCall (coreFunctionBody run)
+        _ -> Nothing
+
+callSource :: String
+callSource = "class App { void Save() { return; } void Run() { Save(); return; } }"
+
+findTypedCall :: Block name Type -> Maybe Type
+findTypedCall (Block statements) = case statements of
+    ExpressionStatement _ (CallExpression _ _ _ resultType) True : _ -> Just resultType
+    _ : remaining -> findTypedCall (Block remaining)
+    [] -> Nothing
+
+findCoreCall :: [CoreStatement] -> Maybe Type
+findCoreCall statements = case statements of
+    CoreEvaluate (CoreApply _ _ resultType) : _ -> Just resultType
+    _ : remaining -> findCoreCall remaining
+    [] -> Nothing
+
+entryAccepted :: Bool
+entryAccepted = case compileEntry entry "namespace Demo; class Program { public static void Main() { return; } }" of
+    Right _ -> True
+    Left _ -> False
+
+entryUnitRejected :: Bool
+entryUnitRejected = rejectedEntry (compileEntry entry "namespace Demo; class Program { public static unit Main() { () } }")
+
+entryIntRejected :: Bool
+entryIntRejected =
+    hasDiagnostic "TAE0008" (compileEntry entry "namespace Demo; class Program { public static int Main() { return 0; } }")
+
+entry :: QualifiedName
+entry = QualifiedName [Identifier "Demo", Identifier "Program"]
+
+compileEntry :: QualifiedName -> String -> Either [Diagnostic] FrontendArtifacts
+compileEntry name source = compileEntryToCorePrep name (CompilerInput "void-entry-test.tala" source)
+
+compileSource :: String -> Either [Diagnostic] FrontendArtifacts
+compileSource source = compileToCorePrep (CompilerInput "void-test.tala" source)
+
+hasDiagnostic :: String -> Either [Diagnostic] a -> Bool
+hasDiagnostic code result = case result of
+    Left problems -> any ((== code) . diagnosticCode) problems
+    Right _ -> False
+
+rejectedEntry :: Either [Diagnostic] a -> Bool
+rejectedEntry result = case result of
+    Left _ -> True
+    Right _ -> False
+
+firstFunction :: TypedAST -> Maybe (Declaration ResolvedName Type)
+firstFunction (TypedAST tree) = case syntaxDeclarations tree of
+    TypeDeclaration {typeMembers = function : _} : _ -> Just function
+    function@FunctionDeclaration {} : _ -> Just function
+    _ -> Nothing
+
+firstValue :: [a] -> Maybe a
+firstValue [] = Nothing
+firstValue (value : _) = Just value