packages feed

tala-frontend (empty) → 0.1.0

raw patch · 32 files changed

+10711/−0 lines, 32 filesdep +basedep +bytestringdep +containers

Dependencies added: base, bytestring, containers, directory, filepath, 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/BuiltinTypes.hs view
@@ -0,0 +1,270 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Canonical built-in scalar descriptions shared by type checking and Core.
+Widths here are language widths, never host-machine widths.
+-}
+module Progmasoft.Tala.BuiltinTypes
+    ( ScalarFamily (..)
+    , ScalarType (..)
+    , scalarTypes
+    , scalarTypeName
+    , scalarTypeWidth
+    , scalarTypeFamily
+    , scalarTypeSigned
+    , scalarWrapperName
+    , wrapperNameToScalarType
+    , scalarTypeRank
+    , widerScalarType
+    , scalarTypeToType
+    , typeToScalarType
+    , isIntegerType
+    , isSignedIntegerType
+    , isUnsignedIntegerType
+    , isFloatingType
+    , isNumericType
+    , integerMinimum
+    , integerMaximum
+    , integerFits
+    , defaultIntegerScalar
+    , defaultFloatingScalar
+    ) where
+
+import Progmasoft.Tala.AST
+
+-- | Semantic family used to group built-in scalar types for operator rules.
+data ScalarFamily = CharacterFamily | BooleanFamily | SignedIntegerFamily | UnsignedIntegerFamily | FloatingFamily
+    deriving (Bounded, Enum, Eq, Ord, Read, Show)
+
+-- | Closed inventory of Tala built-in scalar types.
+data ScalarType
+    = -- | Unicode scalar-value character type.
+      CharacterScalar
+    | -- | Boolean type with an 8-bit storage width.
+      BooleanScalar
+    | -- | 8-bit signed integer.
+      ByteScalar
+    | -- | 16-bit signed integer.
+      ShortScalar
+    | -- | 32-bit signed integer.
+      LongScalar
+    | -- | 64-bit signed integer and default integer type.
+      IntScalar
+    | -- | 128-bit signed integer.
+      LongIntScalar
+    | -- | 8-bit unsigned integer.
+      UByteScalar
+    | -- | 16-bit unsigned integer.
+      UShortScalar
+    | -- | 32-bit unsigned integer.
+      ULongScalar
+    | -- | 64-bit unsigned integer.
+      UIntScalar
+    | -- | 128-bit unsigned integer.
+      ULongIntScalar
+    | -- | 16-bit floating-point type.
+      SFloatScalar
+    | -- | 32-bit floating-point type.
+      LFloatScalar
+    | -- | 64-bit floating-point type and default float.
+      FloatScalar
+    | -- | 128-bit floating-point type.
+      DoubleScalar
+    deriving (Bounded, Enum, Eq, Ord, Read, Show)
+
+-- | All built-in scalar types in canonical declaration/rank order.
+scalarTypes :: [ScalarType]
+scalarTypes = [minBound .. maxBound]
+
+-- | Return the lowercase source spelling of a scalar type.
+scalarTypeName :: ScalarType -> String
+scalarTypeName scalar = case scalar of
+    CharacterScalar -> "char"
+    BooleanScalar -> "bool"
+    ByteScalar -> "byte"
+    ShortScalar -> "short"
+    LongScalar -> "long"
+    IntScalar -> "int"
+    LongIntScalar -> "longint"
+    UByteScalar -> "ubyte"
+    UShortScalar -> "ushort"
+    ULongScalar -> "ulong"
+    UIntScalar -> "uint"
+    ULongIntScalar -> "ulongint"
+    SFloatScalar -> "sfloat"
+    LFloatScalar -> "lfloat"
+    FloatScalar -> "float"
+    DoubleScalar -> "double"
+
+-- | Return the language-defined storage width, independent of host architecture.
+scalarTypeWidth :: ScalarType -> Int
+scalarTypeWidth scalar = case scalar of
+    CharacterScalar -> 32
+    BooleanScalar -> 8
+    ByteScalar -> 8
+    ShortScalar -> 16
+    LongScalar -> 32
+    IntScalar -> 64
+    LongIntScalar -> 128
+    UByteScalar -> 8
+    UShortScalar -> 16
+    ULongScalar -> 32
+    UIntScalar -> 64
+    ULongIntScalar -> 128
+    SFloatScalar -> 16
+    LFloatScalar -> 32
+    FloatScalar -> 64
+    DoubleScalar -> 128
+
+-- | Classify a scalar by its semantic arithmetic family.
+scalarTypeFamily :: ScalarType -> ScalarFamily
+scalarTypeFamily scalar = case scalar of
+    CharacterScalar -> CharacterFamily
+    BooleanScalar -> BooleanFamily
+    ByteScalar -> SignedIntegerFamily
+    ShortScalar -> SignedIntegerFamily
+    LongScalar -> SignedIntegerFamily
+    IntScalar -> SignedIntegerFamily
+    LongIntScalar -> SignedIntegerFamily
+    UByteScalar -> UnsignedIntegerFamily
+    UShortScalar -> UnsignedIntegerFamily
+    ULongScalar -> UnsignedIntegerFamily
+    UIntScalar -> UnsignedIntegerFamily
+    ULongIntScalar -> UnsignedIntegerFamily
+    SFloatScalar -> FloatingFamily
+    LFloatScalar -> FloatingFamily
+    FloatScalar -> FloatingFamily
+    DoubleScalar -> FloatingFamily
+
+-- | Test whether a scalar uses signed integer arithmetic.
+scalarTypeSigned :: ScalarType -> Bool
+scalarTypeSigned scalar = scalarTypeFamily scalar == SignedIntegerFamily
+
+-- Wrapper names are canonical system types used by contextual Of(value: ...)
+-- insertion. Keeping the table beside scalar widths prevents wrapper and
+-- primitive catalogs from drifting as the standard library grows.
+
+-- | Resolve the canonical @System.*@ wrapper name for contextual conversions.
+scalarWrapperName :: ScalarType -> QualifiedName
+scalarWrapperName scalar = QualifiedName [Identifier "System", Identifier (wrapperLeaf scalar)]
+    where
+        wrapperLeaf value = case value of
+            CharacterScalar -> "Character"
+            BooleanScalar -> "Boolean"
+            ByteScalar -> "Byte"
+            ShortScalar -> "Short"
+            LongScalar -> "Long"
+            IntScalar -> "Integer"
+            LongIntScalar -> "LongInteger"
+            UByteScalar -> "UByte"
+            UShortScalar -> "UShort"
+            ULongScalar -> "ULong"
+            UIntScalar -> "UInteger"
+            ULongIntScalar -> "ULongInteger"
+            SFloatScalar -> "SFloat"
+            LFloatScalar -> "LFloat"
+            FloatScalar -> "Float"
+            DoubleScalar -> "Double"
+
+-- | Resolve a canonical wrapper qualified name to its scalar type.
+wrapperNameToScalarType :: QualifiedName -> Maybe ScalarType
+wrapperNameToScalarType name = lookup name [(scalarWrapperName scalar, scalar) | scalar <- scalarTypes]
+
+-- Rank is meaningful only inside one family. It is not an implicit-conversion
+-- rule; it exists for range analysis and explicit conversion diagnostics.
+
+{- | Return widening rank within the scalar's own arithmetic family.
+The rank is metadata for range analysis, not an implicit-conversion rule.
+-}
+scalarTypeRank :: ScalarType -> Int
+scalarTypeRank scalar = case scalar of
+    CharacterScalar -> 0
+    BooleanScalar -> 0
+    ByteScalar -> 0
+    ShortScalar -> 1
+    LongScalar -> 2
+    IntScalar -> 3
+    LongIntScalar -> 4
+    UByteScalar -> 0
+    UShortScalar -> 1
+    ULongScalar -> 2
+    UIntScalar -> 3
+    ULongIntScalar -> 4
+    SFloatScalar -> 0
+    LFloatScalar -> 1
+    FloatScalar -> 2
+    DoubleScalar -> 3
+
+{- | Select the wider scalar when both operands belong to the same family.
+@Nothing@ means the families differ and no widening relation is defined.
+-}
+widerScalarType :: ScalarType -> ScalarType -> Maybe ScalarType
+widerScalarType left right
+    | scalarTypeFamily left /= scalarTypeFamily right = Nothing
+    | scalarTypeRank left >= scalarTypeRank right = Just left
+    | otherwise = Just right
+
+-- | Convert a scalar descriptor to its canonical AST named type.
+scalarTypeToType :: ScalarType -> Type
+scalarTypeToType = named . scalarTypeName
+    where
+        named value = NamedType (QualifiedName [Identifier value]) []
+
+-- | Recognize a non-generic built-in scalar AST type.
+typeToScalarType :: Type -> Maybe ScalarType
+typeToScalarType (NamedType (QualifiedName [Identifier name]) []) = lookup name table
+    where
+        table = [(scalarTypeName scalar, scalar) | scalar <- scalarTypes]
+typeToScalarType _ = Nothing
+
+-- | Test whether an AST type denotes a signed or unsigned integer scalar.
+isIntegerType :: Type -> Bool
+isIntegerType valueType = maybe False integerScalar (typeToScalarType valueType)
+    where
+        integerScalar scalar = scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily]
+
+-- | Test whether an AST type denotes a signed integer scalar.
+isSignedIntegerType :: Type -> Bool
+isSignedIntegerType valueType = maybe False ((== SignedIntegerFamily) . scalarTypeFamily) (typeToScalarType valueType)
+
+-- | Test whether an AST type denotes an unsigned integer scalar.
+isUnsignedIntegerType :: Type -> Bool
+isUnsignedIntegerType valueType = maybe False ((== UnsignedIntegerFamily) . scalarTypeFamily) (typeToScalarType valueType)
+
+-- | Test whether an AST type denotes a floating-point scalar.
+isFloatingType :: Type -> Bool
+isFloatingType valueType = maybe False ((== FloatingFamily) . scalarTypeFamily) (typeToScalarType valueType)
+
+-- | Test whether an AST type denotes an integer or floating-point scalar.
+isNumericType :: Type -> Bool
+isNumericType valueType = isIntegerType valueType || isFloatingType valueType
+
+{- | Return the inclusive minimum mathematical value for an integer scalar.
+Character, Boolean, and floating-point types have no integer range.
+-}
+integerMinimum :: ScalarType -> Maybe Integer
+integerMinimum scalar = case scalarTypeFamily scalar of
+    SignedIntegerFamily -> Just (negate (2 ^ (scalarTypeWidth scalar - 1)))
+    UnsignedIntegerFamily -> Just 0
+    _ -> Nothing
+
+-- | Return the inclusive maximum mathematical value for an integer scalar.
+integerMaximum :: ScalarType -> Maybe Integer
+integerMaximum scalar = case scalarTypeFamily scalar of
+    SignedIntegerFamily -> Just (2 ^ (scalarTypeWidth scalar - 1) - 1)
+    UnsignedIntegerFamily -> Just (2 ^ scalarTypeWidth scalar - 1)
+    _ -> Nothing
+
+-- | Test whether an arbitrary-precision integer fits a scalar's exact range.
+integerFits :: ScalarType -> Integer -> Bool
+integerFits scalar value = case (integerMinimum scalar, integerMaximum scalar) of
+    (Just minimumValue, Just maximumValue) -> value >= minimumValue && value <= maximumValue
+    _ -> False
+
+-- | Default contextual type for an otherwise-unconstrained integer literal.
+defaultIntegerScalar :: ScalarType
+defaultIntegerScalar = IntScalar
+
+-- | Default contextual type for an otherwise-unconstrained float literal.
+defaultFloatingScalar :: ScalarType
+defaultFloatingScalar = FloatScalar
+ src/Progmasoft/Tala/Closure/Analysis.hs view
@@ -0,0 +1,474 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Structural closure facts shared by diagnostics, tooling, and lowering.
+
+This module deliberately does not choose a heap layout.  It describes the
+semantic facts which survive into CorePrep: source order, ownership mode,
+mutation, nesting, and whether a captured value is read by a nested closure.
+-}
+module Progmasoft.Tala.Closure.Analysis
+    ( ClosureId (..)
+    , CaptureUse (..)
+    , ClosureSummary (..)
+    , ClosureCatalog (..)
+    , analyzeClosures
+    , closureById
+    , closuresCapturing
+    , stronglyCapturedSymbols
+    , nonOwningCapturedSymbols
+    ) where
+
+import Data.List (nubBy)
+import Progmasoft.Tala.AST
+
+-- | Stable preorder identity of a closure within one analyzed typed AST.
+newtype ClosureId = ClosureId {closureIdValue :: Int}
+    deriving (Eq, Ord, Read, Show)
+
+-- | Use and ownership facts for one closure capture.
+data CaptureUse = CaptureUse
+    { captureUseName :: ResolvedName
+    -- ^ Resolved symbol captured by the closure.
+    , captureUseType :: Type
+    -- ^ Type of the captured binding.
+    , captureUseMode :: CaptureMode
+    -- ^ Explicit or inferred ownership mode.
+    , captureUseOrder :: Int
+    -- ^ Source-order position in the capture environment.
+    , captureUseExplicit :: Bool
+    -- ^ Whether source syntax explicitly listed it.
+    , captureUseAlias :: Bool
+    -- ^ Whether the capture has an explicit alias.
+    , captureUseRead :: Bool
+    -- ^ Whether this closure reads the captured value.
+    , captureUseWritten :: Bool
+    -- ^ Whether this closure writes the captured binding.
+    , captureUseReadByNestedClosure :: Bool
+    -- ^ Whether a descendant closure reads it.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Structural properties needed to analyze or lower one closure.
+data ClosureSummary = ClosureSummary
+    { closureSummaryId :: ClosureId
+    -- ^ Unique identity assigned in source traversal order.
+    , closureSummaryParent :: Maybe ClosureId
+    -- ^ Lexically enclosing closure, if nested.
+    , closureSummarySpan :: SourceSpan
+    -- ^ Full source span of the closure expression.
+    , closureSummaryType :: Type
+    -- ^ Resolved callable type.
+    , closureSummaryExplicitCaptureMode :: Bool
+    -- ^ Whether capture policy was explicit.
+    , closureSummaryParameters :: [(ResolvedName, Type)]
+    -- ^ Resolved parameter names and types.
+    , closureSummaryCaptures :: [CaptureUse]
+    -- ^ Captures in source order.
+    , closureSummaryChildren :: [ClosureId]
+    -- ^ Directly nested closure identities.
+    , closureSummaryContainsReturn :: Bool
+    -- ^ Whether the body contains a return statement.
+    , closureSummaryContainsCall :: Bool
+    -- ^ Whether the body contains a call expression.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Closure summaries for all closures in one typed syntax tree.
+newtype ClosureCatalog = ClosureCatalog {closureSummaries :: [ClosureSummary]}
+    deriving (Eq, Ord, Read, Show)
+
+data WalkState = WalkState
+    { walkNextId :: Int
+    , walkSummaries :: [ClosureSummary]
+    }
+
+-- | Walk a typed AST and collect lexical nesting, captures, and use facts.
+analyzeClosures :: TypedAST -> ClosureCatalog
+analyzeClosures (TypedAST tree) =
+    let final = walkTree (WalkState 1 []) tree
+     in ClosureCatalog (walkSummaries final)
+
+-- | Look up a closure summary by its traversal-assigned identity.
+closureById :: ClosureId -> ClosureCatalog -> Maybe ClosureSummary
+closureById identifier (ClosureCatalog summaries) =
+    findFirst ((== identifier) . closureSummaryId) summaries
+
+-- | Find closures whose capture environments contain the given symbol.
+closuresCapturing :: SymbolId -> ClosureCatalog -> [ClosureSummary]
+closuresCapturing symbol (ClosureCatalog summaries) =
+    [ summary
+    | summary <- summaries
+    , any ((== symbol) . resolvedSymbol . captureUseName) (closureSummaryCaptures summary)
+    ]
+
+-- | Return unique symbols captured with strong ownership.
+stronglyCapturedSymbols :: ClosureCatalog -> [SymbolId]
+stronglyCapturedSymbols = capturedSymbolsByMode (== StrongCapture)
+
+-- | Return unique symbols captured without strong ownership.
+nonOwningCapturedSymbols :: ClosureCatalog -> [SymbolId]
+nonOwningCapturedSymbols = capturedSymbolsByMode (/= StrongCapture)
+
+capturedSymbolsByMode :: (CaptureMode -> Bool) -> ClosureCatalog -> [SymbolId]
+capturedSymbolsByMode predicate (ClosureCatalog summaries) =
+    unique
+        [ resolvedSymbol (captureUseName capture)
+        | summary <- summaries
+        , capture <- closureSummaryCaptures summary
+        , predicate (captureUseMode capture)
+        ]
+
+walkTree :: WalkState -> SyntaxTree ResolvedName Type -> WalkState
+walkTree state tree = foldDeclarations (syntaxDeclarations tree) state
+
+foldDeclarations :: [Declaration ResolvedName Type] -> WalkState -> WalkState
+foldDeclarations declarations initial = foldl walkDeclaration initial declarations
+
+walkDeclaration :: WalkState -> Declaration ResolvedName Type -> WalkState
+walkDeclaration state declaration = case declaration of
+    TypeDeclaration {typeMembers = members} -> foldl walkDeclaration state members
+    -- Open template members can contain closures even though they do not lower
+    -- before specialization. Analyze them now so tooling sees the same capture
+    -- relationships before and after concrete declaration instantiation.
+    TemplateTypeDeclaration {typeMembers = members} -> foldl walkDeclaration state members
+    FunctionDeclaration {declarationBody = body} -> walkBlock Nothing state body
+    EnumDeclaration {} -> state
+
+walkBlock :: Maybe ClosureId -> WalkState -> Block ResolvedName Type -> WalkState
+walkBlock parent state block = foldStatements parent (blockStatements block) state
+
+foldStatements :: Maybe ClosureId -> [Statement ResolvedName Type] -> WalkState -> WalkState
+foldStatements parent statements initial = foldl (walkStatement parent) initial statements
+
+walkStatement :: Maybe ClosureId -> WalkState -> Statement ResolvedName Type -> WalkState
+walkStatement parent state statement = case statement of
+    BindingStatement _ _ _ _ _ value -> walkExpression parent state value
+    AssignmentStatement _ _ _ value -> walkExpression parent state value
+    ReturnStatement _ value -> maybe state (walkExpression parent state) value
+    IfStatement _ condition trueBlock falseBlock ->
+        let afterCondition = walkExpression parent state condition
+            afterTrue = walkBlock parent afterCondition trueBlock
+         in maybe afterTrue (walkBlock parent afterTrue) falseBlock
+    WhileStatement _ condition body ->
+        walkBlock parent (walkExpression parent state condition) body
+    DoWhileStatement _ body condition ->
+        walkExpression parent (walkBlock parent state body) condition
+    ForStatement _ initializer condition updates body ->
+        let afterInitializer = maybe state (walkStatement parent state) initializer
+            afterCondition = maybe afterInitializer (walkExpression parent afterInitializer) condition
+            afterUpdates = foldl (walkStatement parent) afterCondition updates
+         in walkBlock parent afterUpdates body
+    ForEachStatement _ _ _ _ _ source body ->
+        walkBlock parent (walkExpression parent state source) body
+    IncrementStatement {} -> state
+    CompoundAssignmentStatement _ _ _ _ value -> walkExpression parent state value
+    DiscardStatement _ value -> walkExpression parent state value
+    BreakStatement _ value -> maybe state (walkExpression parent state) value
+    ContinueStatement {} -> state
+    GuardStatement _ condition block -> walkBlock parent (walkExpression parent state condition) block
+    BlockStatement _ block -> walkBlock parent state block
+    ExpressionStatement _ value _ -> walkExpression parent state value
+
+walkExpression :: Maybe ClosureId -> WalkState -> Expression ResolvedName Type -> WalkState
+walkExpression parent state expression = case expression of
+    NameExpression {} -> state
+    LiteralExpression {} -> state
+    MemberAccessExpression _ receiver _ _ -> walkExpression parent state receiver
+    MethodReferenceExpression _ receiver _ _ -> walkExpression parent state receiver
+    CallExpression _ callee arguments _ ->
+        foldl (walkExpression parent) (walkExpression parent state callee) arguments
+    UnaryExpression _ _ value _ -> walkExpression parent state value
+    BinaryExpression _ _ left right _ ->
+        walkExpression parent (walkExpression parent state left) right
+    IsPatternExpression _ subject _ _ -> walkExpression parent state subject
+    ConditionalExpression _ condition first second _ ->
+        foldl (walkExpression parent) state [condition, first, second]
+    CoalesceExpression _ left fallback _ ->
+        walkExpression parent (walkExpression parent state left) fallback
+    AssignmentExpression _ _ _ value _ -> walkExpression parent state value
+    IncrementExpression {} -> state
+    LoopExpression _ loop _ -> walkStatement parent state loop
+    BlockExpression _ block _ -> walkBlock parent state block
+    MatchExpression _ subjects arms _ ->
+        foldl (walkExpression parent) state (subjects ++ concatMap matchArmExpressions arms)
+    callable@CallableExpression {} -> walkCallable parent state callable
+
+walkCallable :: Maybe ClosureId -> WalkState -> Expression ResolvedName Type -> WalkState
+walkCallable parent state (CallableExpression spanValue explicit captures parameters body valueType) =
+    let identifier = ClosureId (walkNextId state)
+        bodyFacts = inspectBody body
+        explicitUses = zipWith (captureUse bodyFacts explicit) [0 ..] captures
+        implicitUses =
+            if explicit
+                then []
+                else inferImplicitUses parameters bodyFacts
+        beforeChildren = state {walkNextId = walkNextId state + 1}
+        afterChildren = walkCallableBody (Just identifier) beforeChildren body
+        children =
+            [ closureSummaryId candidateSummary
+            | candidateSummary <- walkSummaries afterChildren
+            , closureSummaryParent candidateSummary == Just identifier
+            ]
+        nestedReads =
+            concatMap
+                (map (resolvedSymbol . captureUseName) . closureSummaryCaptures)
+                [ nestedSummary
+                | nestedSummary <- walkSummaries afterChildren
+                , closureSummaryId nestedSummary `elem` descendantsOf children (ClosureCatalog (walkSummaries afterChildren))
+                ]
+        markNested use =
+            use {captureUseReadByNestedClosure = resolvedSymbol (captureUseName use) `elem` nestedReads}
+        summary =
+            ClosureSummary
+                { closureSummaryId = identifier
+                , closureSummaryParent = parent
+                , closureSummarySpan = spanValue
+                , closureSummaryType = valueType
+                , closureSummaryExplicitCaptureMode = explicit
+                , closureSummaryParameters =
+                    [(parameterName parameter, parameterAnnotation parameter) | parameter <- parameters]
+                , closureSummaryCaptures = map markNested (explicitUses ++ implicitUses)
+                , closureSummaryChildren = children
+                , closureSummaryContainsReturn = bodyContainsReturn body
+                , closureSummaryContainsCall = bodyContainsCall body
+                }
+     in afterChildren {walkSummaries = walkSummaries afterChildren ++ [summary]}
+walkCallable _ state _ = state
+
+walkCallableBody :: Maybe ClosureId -> WalkState -> CallableBody ResolvedName Type -> WalkState
+walkCallableBody parent state body = case body of
+    CallableExpressionBody expression -> walkExpression parent state expression
+    CallableBlockBody block -> walkBlock parent state block
+
+data BodyFacts = BodyFacts
+    { factReads :: [(ResolvedName, Type)]
+    , factWrites :: [ResolvedName]
+    , factLocals :: [ResolvedName]
+    }
+
+inspectBody :: CallableBody ResolvedName Type -> BodyFacts
+inspectBody body = case body of
+    CallableExpressionBody expression -> expressionFacts expression
+    CallableBlockBody block -> blockFacts block
+
+emptyFacts :: BodyFacts
+emptyFacts = BodyFacts [] [] []
+
+appendFacts :: BodyFacts -> BodyFacts -> BodyFacts
+appendFacts left right =
+    BodyFacts
+        (factReads left ++ factReads right)
+        (factWrites left ++ factWrites right)
+        (factLocals left ++ factLocals right)
+
+blockFacts :: Block ResolvedName Type -> BodyFacts
+blockFacts = foldl appendFacts emptyFacts . map statementFacts . blockStatements
+
+statementFacts :: Statement ResolvedName Type -> BodyFacts
+statementFacts statement = case statement of
+    BindingStatement _ _ _ name _ value ->
+        (expressionFacts value) {factLocals = name : factLocals (expressionFacts value)}
+    AssignmentStatement _ name _ value ->
+        (expressionFacts value) {factWrites = name : factWrites (expressionFacts value)}
+    ReturnStatement _ value -> maybe emptyFacts expressionFacts value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionFacts condition
+            `appendFacts` blockFacts trueBlock
+            `appendFacts` maybe emptyFacts blockFacts falseBlock
+    WhileStatement _ condition body -> expressionFacts condition `appendFacts` blockFacts body
+    DoWhileStatement _ body condition -> blockFacts body `appendFacts` expressionFacts condition
+    ForStatement _ initializer condition updates body ->
+        maybe emptyFacts statementFacts initializer
+            `appendFacts` maybe emptyFacts expressionFacts condition
+            `appendFacts` foldl appendFacts emptyFacts (map statementFacts updates)
+            `appendFacts` blockFacts body
+    ForEachStatement _ _ _ name _ source body ->
+        let nested = expressionFacts source `appendFacts` blockFacts body
+         in nested {factLocals = name : factLocals nested, factWrites = name : factWrites nested}
+    IncrementStatement _ name annotation -> emptyFacts {factReads = [(name, annotation)], factWrites = [name]}
+    -- A compound assignment reads its target before storing the result.
+    CompoundAssignmentStatement _ _ name annotation value ->
+        let nested = expressionFacts value
+         in nested {factReads = (name, annotation) : factReads nested, factWrites = name : factWrites nested}
+    DiscardStatement _ value -> expressionFacts value
+    BreakStatement _ value -> maybe emptyFacts expressionFacts value
+    ContinueStatement {} -> emptyFacts
+    GuardStatement _ condition block -> expressionFacts condition `appendFacts` blockFacts block
+    BlockStatement _ block -> blockFacts block
+    ExpressionStatement _ value _ -> expressionFacts value
+
+expressionFacts :: Expression ResolvedName Type -> BodyFacts
+expressionFacts expression = case expression of
+    NameExpression _ name valueType -> BodyFacts [(name, valueType)] [] []
+    LiteralExpression {} -> emptyFacts
+    MemberAccessExpression _ receiver _ _ -> expressionFacts receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionFacts receiver
+    CallExpression _ callee arguments _ ->
+        foldl appendFacts (expressionFacts callee) (map expressionFacts arguments)
+    UnaryExpression _ _ value _ -> expressionFacts value
+    BinaryExpression _ _ left right _ -> expressionFacts left `appendFacts` expressionFacts right
+    IsPatternExpression _ subject _ _ -> expressionFacts subject
+    ConditionalExpression _ condition first second _ ->
+        foldl appendFacts (expressionFacts condition) (map expressionFacts [first, second])
+    CoalesceExpression _ left fallback _ -> expressionFacts left `appendFacts` expressionFacts fallback
+    -- A simple assignment only writes its target; a compound one reads it
+    -- first, exactly as the statement forms do.
+    AssignmentExpression _ operator name value annotation ->
+        let nested = expressionFacts value
+            targetReads = maybe [] (const [(name, annotation)]) operator
+         in nested {factReads = targetReads ++ factReads nested, factWrites = name : factWrites nested}
+    IncrementExpression _ _ name annotation -> emptyFacts {factReads = [(name, annotation)], factWrites = [name]}
+    LoopExpression _ loop _ -> statementFacts loop
+    BlockExpression _ block _ -> blockFacts block
+    -- A pattern binding is a local of the match, written once when its arm
+    -- is entered.
+    MatchExpression _ subjects arms _ ->
+        let nested = foldl appendFacts emptyFacts (map expressionFacts (subjects ++ concatMap matchArmExpressions arms))
+            bound = [name | arm <- arms, Just name <- map matchPatternBinding (matchArmPatterns arm)]
+         in nested {factLocals = bound ++ factLocals nested, factWrites = bound ++ factWrites nested}
+    CallableExpression {} -> emptyFacts
+
+captureUse :: BodyFacts -> Bool -> Int -> Capture ResolvedName Type -> CaptureUse
+captureUse facts explicit order capture =
+    let symbol = resolvedSymbol (captureName capture)
+        isRead = any ((== symbol) . resolvedSymbol . fst) (factReads facts)
+        writes = any ((== symbol) . resolvedSymbol) (factWrites facts)
+     in CaptureUse
+            (captureName capture)
+            (captureAnnotation capture)
+            (captureMode capture)
+            order
+            explicit
+            (captureAlias capture)
+            isRead
+            writes
+            False
+
+captureAlias :: Capture ResolvedName Type -> Bool
+captureAlias capture = case captureInitializer capture of
+    Just (NameExpression _ source _) -> resolvedSymbol source /= resolvedSymbol (captureName capture)
+    Just _ -> True
+    Nothing -> False
+
+inferImplicitUses :: [Parameter ResolvedName Type] -> BodyFacts -> [CaptureUse]
+inferImplicitUses parameters facts =
+    let localSymbols = map (resolvedSymbol . parameterName) parameters ++ map resolvedSymbol (factLocals facts)
+        freeReads =
+            uniqueBy
+                (resolvedSymbol . fst)
+                [(name, valueType) | (name, valueType) <- factReads facts, resolvedSymbol name `notElem` localSymbols]
+     in [ CaptureUse
+            name
+            valueType
+            StrongCapture
+            order
+            False
+            False
+            True
+            (any ((== resolvedSymbol name) . resolvedSymbol) (factWrites facts))
+            False
+        | (order, (name, valueType)) <- zip [0 ..] freeReads
+        ]
+
+bodyContainsReturn :: CallableBody name annotation -> Bool
+bodyContainsReturn (CallableExpressionBody _) = True
+bodyContainsReturn (CallableBlockBody block) = any statementContainsReturn (blockStatements block)
+
+statementContainsReturn :: Statement name annotation -> Bool
+statementContainsReturn statement = case statement of
+    ReturnStatement {} -> True
+    IfStatement _ _ yes no ->
+        any statementContainsReturn (blockStatements yes)
+            || maybe False (any statementContainsReturn . blockStatements) no
+    WhileStatement _ _ body -> any statementContainsReturn (blockStatements body)
+    DoWhileStatement _ body _ -> any statementContainsReturn (blockStatements body)
+    ForStatement _ initializer _ updates body ->
+        maybe False statementContainsReturn initializer
+            || any statementContainsReturn updates
+            || any statementContainsReturn (blockStatements body)
+    ForEachStatement _ _ _ _ _ _ body -> any statementContainsReturn (blockStatements body)
+    BreakStatement _ value -> maybe False (const False) value
+    GuardStatement _ _ block -> any statementContainsReturn (blockStatements block)
+    BlockStatement _ block -> any statementContainsReturn (blockStatements block)
+    -- The arms of a statement match are statements, so a return in a block
+    -- body returns from the enclosing callable.
+    ExpressionStatement _ (MatchExpression _ _ arms _) _ ->
+        or
+            [ any statementContainsReturn (blockStatements block)
+            | BlockExpression _ block _ <- map matchArmBody arms
+            ]
+    _ -> False
+
+bodyContainsCall :: CallableBody name annotation -> Bool
+bodyContainsCall (CallableExpressionBody expression) = expressionContainsCall expression
+bodyContainsCall (CallableBlockBody block) = any statementContainsCall (blockStatements block)
+
+statementContainsCall :: Statement name annotation -> Bool
+statementContainsCall statement = case statement of
+    BindingStatement _ _ _ _ _ value -> expressionContainsCall value
+    AssignmentStatement _ _ _ value -> expressionContainsCall value
+    ReturnStatement _ value -> maybe False expressionContainsCall value
+    IfStatement _ condition yes no ->
+        expressionContainsCall condition
+            || any statementContainsCall (blockStatements yes)
+            || maybe False (any statementContainsCall . blockStatements) no
+    WhileStatement _ condition body ->
+        expressionContainsCall condition || any statementContainsCall (blockStatements body)
+    DoWhileStatement _ body condition ->
+        any statementContainsCall (blockStatements body) || expressionContainsCall condition
+    ForStatement _ initializer condition updates body ->
+        maybe False statementContainsCall initializer
+            || maybe False expressionContainsCall condition
+            || any statementContainsCall updates
+            || any statementContainsCall (blockStatements body)
+    ForEachStatement _ _ _ _ _ source body ->
+        expressionContainsCall source || any statementContainsCall (blockStatements body)
+    IncrementStatement {} -> False
+    CompoundAssignmentStatement _ _ _ _ value -> expressionContainsCall value
+    DiscardStatement _ value -> expressionContainsCall value
+    BreakStatement _ value -> maybe False expressionContainsCall value
+    ContinueStatement {} -> False
+    GuardStatement _ condition block ->
+        expressionContainsCall condition || any statementContainsCall (blockStatements block)
+    BlockStatement _ block -> any statementContainsCall (blockStatements block)
+    ExpressionStatement _ value _ -> expressionContainsCall value
+
+expressionContainsCall :: Expression name annotation -> Bool
+expressionContainsCall expression = case expression of
+    CallExpression {} -> True
+    MemberAccessExpression _ receiver _ _ -> expressionContainsCall receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionContainsCall receiver
+    UnaryExpression _ _ value _ -> expressionContainsCall value
+    BinaryExpression _ _ left right _ -> expressionContainsCall left || expressionContainsCall right
+    IsPatternExpression _ subject _ _ -> expressionContainsCall subject
+    ConditionalExpression _ condition first second _ -> any expressionContainsCall [condition, first, second]
+    CoalesceExpression _ left fallback _ -> expressionContainsCall left || expressionContainsCall fallback
+    AssignmentExpression _ _ _ value _ -> expressionContainsCall value
+    IncrementExpression {} -> False
+    LoopExpression _ loop _ -> statementContainsCall loop
+    BlockExpression _ block _ -> any statementContainsCall (blockStatements block)
+    MatchExpression _ subjects arms _ ->
+        any expressionContainsCall (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression {} -> False
+    NameExpression {} -> False
+    LiteralExpression {} -> False
+
+descendantsOf :: [ClosureId] -> ClosureCatalog -> [ClosureId]
+descendantsOf roots catalog = roots ++ concatMap children roots
+    where
+        children identifier = case closureById identifier catalog of
+            Nothing -> []
+            Just summary -> descendantsOf (closureSummaryChildren summary) catalog
+
+findFirst :: (value -> Bool) -> [value] -> Maybe value
+findFirst _ [] = Nothing
+findFirst predicate (value : remaining)
+    | predicate value = Just value
+    | otherwise = findFirst predicate remaining
+
+unique :: (Eq value) => [value] -> [value]
+unique = nubBy (==)
+
+uniqueBy :: (Eq key) => (value -> key) -> [value] -> [value]
+uniqueBy select = nubBy (\left right -> select left == select right)
+ src/Progmasoft/Tala/ConstantEvaluation.hs view
@@ -0,0 +1,157 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+-- | Side-effect-free constant evaluation used for target-range diagnostics.
+module Progmasoft.Tala.ConstantEvaluation
+    ( ConstantIntegerError (..)
+    , evaluateConstantInteger
+    , renderConstantIntegerError
+    ) where
+
+import Data.Bits (complement, shiftL, xor, (.&.), (.|.))
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+    ( ScalarFamily (UnsignedIntegerFamily)
+    , scalarTypeFamily
+    , scalarTypeWidth
+    , typeToScalarType
+    )
+import Progmasoft.Tala.IntegerEvaluation
+    ( CompileTimeIntegerError (..)
+    , checkCompileTimeInteger
+    , evaluateCompileTimePower
+    , evaluateCompileTimeShiftLeft
+    , evaluateCompileTimeShiftRight
+    , multiplyCompileTimeIntegers
+    )
+
+-- | Definite arithmetic failures detected during integer constant evaluation.
+data ConstantIntegerError
+    = -- | Division used a zero divisor.
+      ConstantDivisionByZero
+    | -- | Floor division used a zero divisor.
+      ConstantFloorDivisionByZero
+    | -- | Remainder used a zero divisor.
+      ConstantRemainderByZero
+    | -- | Integer power used a negative exponent.
+      ConstantNegativeExponent
+    | -- | Exact evaluation exceeded its resource bound.
+      ConstantEvaluationLimitExceeded
+    deriving (Eq, Ord, Read, Show)
+
+-- Non-constant expressions return Nothing. A definite arithmetic failure is
+-- retained as Left so callers can diagnose it without pretending the entire
+-- expression ceased to be constant.
+-- The resource error is likewise retained: treating an oversized expression
+-- as merely nonconstant would let it bypass the frontend's compile-time bound.
+
+{- | Evaluate a constant integer expression without executing user code.
+@Right Nothing@ means non-constant; @Left@ preserves a definite arithmetic
+error so callers can report it at the originating expression.
+-}
+evaluateConstantInteger :: Expression name Type -> Either ConstantIntegerError (Maybe Integer)
+evaluateConstantInteger expression = case expression of
+    LiteralExpression _ literal _ -> case literal of
+        IntegerLiteral value -> checkedValue value
+        CharacterLiteral value -> checkedValue value
+        BooleanLiteral value -> checkedValue (if value then 1 else 0)
+        _ -> pure Nothing
+    UnaryExpression _ operator value resultType -> do
+        operand <- evaluateConstantInteger value
+        case (operator, operand) of
+            (UnaryPlus, Just number) -> checkedMaybe number
+            (UnaryNegate, Just number) -> checkedMaybe (-number)
+            (LogicalNot, Just number) -> checkedMaybe (if number == 0 then 1 else 0)
+            (BitwiseNot, Just number) -> checkedMaybe (typedBitwiseComplement resultType number)
+            _ -> pure Nothing
+    BinaryExpression _ operator left right _ -> do
+        leftValue <- evaluateConstantInteger left
+        rightValue <- evaluateConstantInteger right
+        evaluateBinary operator leftValue rightValue
+    -- Only the selected operand is evaluated, so a definite arithmetic error
+    -- in the other one is not a property of this expression.
+    ConditionalExpression _ condition first second _ -> do
+        test <- evaluateConstantInteger condition
+        case test of
+            Just number -> evaluateConstantInteger (if number /= 0 then first else second)
+            Nothing -> pure Nothing
+    CoalesceExpression _ left fallback _ -> do
+        leftValue <- evaluateConstantInteger left
+        case leftValue of
+            Just 0 -> evaluateConstantInteger fallback
+            other -> pure other
+    _ -> pure Nothing
+
+typedBitwiseComplement :: Type -> Integer -> Integer
+typedBitwiseComplement resultType value = case typeToScalarType resultType of
+    Just scalar
+        | scalarTypeFamily scalar == UnsignedIntegerFamily ->
+            complement value .&. ((1 `shiftL` scalarTypeWidth scalar) - 1)
+    _ -> complement value
+
+checkedValue :: Integer -> Either ConstantIntegerError (Maybe Integer)
+checkedValue value = Just <$> mapLimitError (checkCompileTimeInteger value)
+
+checkedMaybe :: Integer -> Either ConstantIntegerError (Maybe Integer)
+checkedMaybe value = Just <$> mapLimitError (checkCompileTimeInteger value)
+
+mapLimitError :: Either CompileTimeIntegerError value -> Either ConstantIntegerError value
+mapLimitError result = case result of
+    Right value -> Right value
+    Left CompileTimeNegativeExponent -> Left ConstantNegativeExponent
+    Left CompileTimeIntegerLimitExceeded -> Left ConstantEvaluationLimitExceeded
+
+evaluateBinary :: BinaryOperator -> Maybe Integer -> Maybe Integer -> Either ConstantIntegerError (Maybe Integer)
+evaluateBinary _ Nothing _ = pure Nothing
+evaluateBinary _ _ Nothing = pure Nothing
+evaluateBinary operator (Just left) (Just right) = case operator of
+    Add -> value (left + right)
+    Subtract -> value (left - right)
+    Multiply -> Just <$> mapLimitError (multiplyCompileTimeIntegers left right)
+    Divide
+        | right == 0 -> Left ConstantDivisionByZero
+        | otherwise -> value (left `quot` right)
+    FloorDivide
+        | right == 0 -> Left ConstantFloorDivisionByZero
+        | otherwise -> value (roundedIntegerDivision left right)
+    Remainder
+        | right == 0 -> Left ConstantRemainderByZero
+        | otherwise -> value (left `rem` right)
+    Power
+        | right < 0 -> Left ConstantNegativeExponent
+        | otherwise -> Just <$> mapLimitError (evaluateCompileTimePower left right)
+    ShiftLeft -> Just <$> mapLimitError (evaluateCompileTimeShiftLeft left right)
+    ShiftRight -> Just <$> mapLimitError (evaluateCompileTimeShiftRight left right)
+    BitwiseAnd -> value (left .&. right)
+    BitwiseXor -> value (xor left right)
+    BitwiseOr -> value (left .|. right)
+    LessThan -> boolean (left < right)
+    LessEqual -> boolean (left <= right)
+    GreaterThan -> boolean (left > right)
+    GreaterEqual -> boolean (left >= right)
+    Equal -> boolean (left == right)
+    NotEqual -> boolean (left /= right)
+    LogicalAnd -> boolean (left /= 0 && right /= 0)
+    LogicalOr -> boolean (left /= 0 || right /= 0)
+    where
+        value number = Just <$> mapLimitError (checkCompileTimeInteger number)
+        boolean result = value (if result then 1 else 0)
+
+-- `//` is nearest-integer division with exact halves moving away from zero.
+-- quotRem gives a truncating quotient; comparing twice the remainder magnitude
+-- avoids floating conversion and therefore remains exact for arbitrary-width
+-- compile-time integers.
+roundedIntegerDivision :: Integer -> Integer -> Integer
+roundedIntegerDivision 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
+
+-- | Render a constant arithmetic failure for diagnostic presentation.
+renderConstantIntegerError :: ConstantIntegerError -> String
+renderConstantIntegerError issue = case issue of
+    ConstantDivisionByZero -> "constant division by zero"
+    ConstantFloorDivisionByZero -> "constant floor division by zero"
+    ConstantRemainderByZero -> "constant remainder by zero"
+    ConstantNegativeExponent -> "constant integer exponent cannot be negative"
+    ConstantEvaluationLimitExceeded -> "constant integer expression exceeds the compile-time evaluation limit"
+ src/Progmasoft/Tala/Frontend.hs view
@@ -0,0 +1,83 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Reusable, target-independent frontend analysis boundaries.
+
+Compiler-adjacent tools must observe exactly the same tokens, parsed tree, and
+semantic names as a build. Keeping these boundaries in the compiler package
+prevents an analyzer, formatter, or linter from growing a second language
+implementation merely to stop before Core lowering.
+-}
+module Progmasoft.Tala.Frontend
+    ( CompilerInput (..)
+    , SyntaxArtifacts (..)
+    , SemanticArtifacts (..)
+    , analyzeSyntax
+    , analyzeSemantics
+    , analyzeParsedSemantics
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.Lexer
+import Progmasoft.Tala.NestingLimits
+import Progmasoft.Tala.Parser
+import Progmasoft.Tala.Resolver.NameResolution
+import Progmasoft.Tala.Resolver.Renamer
+import Progmasoft.Tala.TypeChecker
+
+-- | Source identity and decoded text supplied to the frontend.
+data CompilerInput = CompilerInput
+    { compilerSourceFile :: FilePath
+    -- ^ Path used in diagnostics and source spans.
+    , compilerSourceText :: String
+    -- ^ Source characters after caller-owned decoding.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Shared lexer and parser products, before name or type analysis.
+data SyntaxArtifacts = SyntaxArtifacts
+    { syntaxTokens :: [Token]
+    -- ^ Tokens, including trivia policy from the lexer.
+    , syntaxParsedAST :: ParsedAST
+    -- ^ Parsed source tree with unresolved names.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Intermediate ASTs produced by the ordered semantic frontend passes.
+data SemanticArtifacts = SemanticArtifacts
+    { semanticParsedAST :: ParsedAST
+    -- ^ Original parser output.
+    , semanticRenamedAST :: RenamedAST
+    -- ^ AST with unique local/declaration IDs.
+    , semanticResolvedAST :: ResolvedAST
+    -- ^ AST with references bound to symbols.
+    , semanticTypedAST :: TypedAST
+    -- ^ Type-checked AST consumed by later tools.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Lex and parse source without invoking semantic analysis or Core lowering.
+analyzeSyntax :: CompilerInput -> Either [Diagnostic] SyntaxArtifacts
+analyzeSyntax input = do
+    tokens <- runLexer defaultLexer (LexerInput (compilerSourceFile input) (compilerSourceText input))
+    parsed <- runParser defaultParser (ParserInput (compilerSourceFile input) tokens)
+    pure (SyntaxArtifacts tokens parsed)
+
+-- | Run syntax analysis followed by renaming, resolution, and type checking.
+analyzeSemantics :: CompilerInput -> Either [Diagnostic] SemanticArtifacts
+analyzeSemantics input = syntaxParsedAST <$> analyzeSyntax input >>= analyzeParsedSemantics
+
+-- | Run semantic passes on an existing parsed AST without lexing or parsing.
+analyzeParsedSemantics :: ParsedAST -> Either [Diagnostic] SemanticArtifacts
+analyzeParsedSemantics parsed = do
+    -- Every later pass, and every stage after the frontend, recurses along
+    -- the nesting of the tree. A tree that nests beyond the limits is
+    -- rejected here, with the place where it becomes too deep.
+    case nestingProblems parsed of
+        [] -> pure ()
+        problems -> Left problems
+    renamed <- runRenamer defaultRenamer parsed
+    resolved <- runNameResolution defaultNameResolution renamed
+    typed <- runTypeChecker defaultTypeChecker resolved
+    pure (SemanticArtifacts parsed renamed resolved typed)
+ src/Progmasoft/Tala/IntegerEvaluation.hs view
@@ -0,0 +1,146 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Bounded, exact integer operations used while evaluating compile-time
+expressions. These limits protect compiler availability; they do not narrow the
+runtime integer types or the accepted integer-literal grammar.
+
+Every public operation in this module either returns an exact value strictly
+inside the magnitude bound or reports the same resource-limit error. Callers
+translate that error into a diagnostic appropriate to their source context.
+The helpers deliberately accept `Integer` operands so no conversion to a host
+word can happen before validation.
+-}
+module Progmasoft.Tala.IntegerEvaluation
+    ( CompileTimeIntegerError (..)
+    , checkCompileTimeInteger
+    , multiplyCompileTimeIntegers
+    , evaluateCompileTimePower
+    , evaluateCompileTimeShiftLeft
+    , evaluateCompileTimeShiftRight
+    ) where
+
+import Data.Bits (countLeadingZeros, shiftL, shiftR)
+import Data.Word (Word64)
+
+-- | Failures returned by bounded compile-time integer operations.
+data CompileTimeIntegerError
+    = -- | The exact result exceeds the 65,536-bit bound.
+      CompileTimeIntegerLimitExceeded
+    | -- | Integer exponentiation was given a negative exponent.
+      CompileTimeNegativeExponent
+    deriving (Eq, Ord, Read, Show)
+
+-- This is intentionally much wider than any built-in scalar (128 bits). A
+-- fixed ceiling also bounds intermediate products before they can exhaust the
+-- host while processing untrusted source or a project artifact. The lower edge
+-- is exclusive, matching the positive bound and avoiding an asymmetric domain.
+maximumCompileTimeIntegerBits :: Int
+maximumCompileTimeIntegerBits = 65536
+
+maximumCompileTimeMagnitude :: Integer
+maximumCompileTimeMagnitude = 1 `shiftL` maximumCompileTimeIntegerBits
+
+-- | Accept an integer strictly inside the module's symmetric magnitude bound.
+checkCompileTimeInteger :: Integer -> Either CompileTimeIntegerError Integer
+checkCompileTimeInteger value
+    | value <= negate maximumCompileTimeMagnitude || value >= maximumCompileTimeMagnitude =
+        Left CompileTimeIntegerLimitExceeded
+    | otherwise = Right value
+
+{- | Reject products that cannot fit before constructing a potentially twice-
+as-wide intermediate. The one-bit lower bound is exact for powers of two;
+borderline products are still checked after multiplication. Taking absolute
+values makes the same proof valid for all four sign combinations. If the lower
+bound does not prove overflow, the exact product is constructed once and then
+checked against the half-open limit; this keeps the fast rejection conservative.
+-}
+
+-- | Multiply exact integers after checking operand and result resource bounds.
+multiplyCompileTimeIntegers :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+multiplyCompileTimeIntegers left right = do
+    boundedLeft <- checkCompileTimeInteger left
+    boundedRight <- checkCompileTimeInteger right
+    if boundedLeft == 0 || boundedRight == 0
+        then Right 0
+        else
+            if integerBitLength (abs boundedLeft) + integerBitLength (abs boundedRight) - 1 > maximumCompileTimeIntegerBits
+                then Left CompileTimeIntegerLimitExceeded
+                else checkCompileTimeInteger (boundedLeft * boundedRight)
+
+-- | Compute an exact non-negative integer power by bounded repeated squaring.
+evaluateCompileTimePower :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+evaluateCompileTimePower base exponentValue
+    | exponentValue < 0 = Left CompileTimeNegativeExponent
+    | otherwise = do
+        boundedBase <- checkCompileTimeInteger base
+        case boundedBase of
+            0 -> Right (if exponentValue == 0 then 1 else 0)
+            1 -> Right 1
+            -1 -> Right (if even exponentValue then 1 else -1)
+            _ -> powerLoop 1 boundedBase exponentValue
+    where
+        -- Repeated squaring makes the number of iterations logarithmic in the
+        -- exponent. Multiplication itself performs the resource preflight, so
+        -- an enormous exponent cannot force construction of its final power.
+        powerLoop accumulated factor remaining
+            | remaining == 0 = checkCompileTimeInteger accumulated
+            | otherwise = do
+                nextAccumulated <-
+                    if odd remaining
+                        then multiplyCompileTimeIntegers accumulated factor
+                        else Right accumulated
+                let nextRemaining = remaining `quot` 2
+                if nextRemaining == 0
+                    then checkCompileTimeInteger nextAccumulated
+                    else do
+                        squaredFactor <- multiplyCompileTimeIntegers factor factor
+                        powerLoop nextAccumulated squaredFactor nextRemaining
+
+-- | Shift an exact integer left, interpreting a negative count as right shift.
+evaluateCompileTimeShiftLeft :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+evaluateCompileTimeShiftLeft value amount = do
+    boundedValue <- checkCompileTimeInteger value
+    if amount < 0
+        then evaluateCompileTimeShiftRight boundedValue (negate amount)
+        else
+            if boundedValue == 0
+                then Right 0
+                else do
+                    -- Check the resulting bit width before asking the bignum
+                    -- backend to allocate the shifted representation.
+                    if toInteger (integerBitLength (abs boundedValue)) + amount > toInteger maximumCompileTimeIntegerBits
+                        then Left CompileTimeIntegerLimitExceeded
+                        else pure ()
+                    boundedAmount <- checkedShiftAmount amount
+                    checkCompileTimeInteger (shiftL boundedValue boundedAmount)
+
+-- | Arithmetic-shift an exact integer right, interpreting a negative count as left shift.
+evaluateCompileTimeShiftRight :: Integer -> Integer -> Either CompileTimeIntegerError Integer
+evaluateCompileTimeShiftRight value amount = do
+    boundedValue <- checkCompileTimeInteger value
+    if amount < 0
+        then evaluateCompileTimeShiftLeft boundedValue (negate amount)
+        else
+            if amount >= toInteger maximumCompileTimeIntegerBits
+                -- Past the largest possible magnitude, arithmetic right shift
+                -- contains only the sign fill and needs no large temporary.
+                then Right (if boundedValue < 0 then -1 else 0)
+                else do
+                    boundedAmount <- checkedShiftAmount amount
+                    checkCompileTimeInteger (shiftR boundedValue boundedAmount)
+
+checkedShiftAmount :: Integer -> Either CompileTimeIntegerError Int
+checkedShiftAmount amount
+    | amount < 0 || amount > toInteger maximumCompileTimeIntegerBits = Left CompileTimeIntegerLimitExceeded
+    | otherwise = Right (fromInteger amount)
+
+-- Callers validate magnitudes before asking for their width, so this recursion
+-- visits at most 1,024 fixed-size limbs at the 65,536-bit ceiling. Using
+-- Word64 as the limb definition keeps the result independent of host Int width
+-- and avoids narrowing the complete arbitrary-precision value in one cast.
+integerBitLength :: Integer -> Int
+integerBitLength value
+    | value <= 0 = 0
+    | value <= toInteger (maxBound :: Word64) = 64 - countLeadingZeros (fromInteger value :: Word64)
+    | otherwise = 64 + integerBitLength (value `shiftR` 64)
+ src/Progmasoft/Tala/NumericSemantics.hs view
@@ -0,0 +1,174 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Central numeric operator and contextual-literal policy.
+
+Lexer and parser establish literal identity. This module answers semantic
+questions without constructing diagnostics, which keeps it usable by the
+TypeChecker, Analyzer, and future constant evaluator.
+-}
+module Progmasoft.Tala.NumericSemantics
+    ( NumericContext (..)
+    , NumericRuleError (..)
+    , NumericRuleResult (..)
+    , integerLiteralRule
+    , floatingLiteralRule
+    , unaryNumericRule
+    , binaryNumericRule
+    , acceptsBooleanContext
+    , renderNumericRuleError
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+
+-- | Type context available while applying a literal or operator rule.
+data NumericContext
+    = -- | No destination type constrains the expression.
+      NoNumericContext
+    | -- | A concrete expected scalar type is available.
+      TargetNumericType Type
+    | -- | The expression is consumed as a condition.
+      BooleanNumericContext
+    deriving (Eq, Ord, Read, Show)
+
+-- | Specific reason that a contextual literal or numeric operator is invalid.
+data NumericRuleError
+    = -- | Integer does not fit its selected scalar type.
+      IntegerLiteralOutsideTarget ScalarType Integer
+    | -- | Uncontextualized literal exceeds the default @int@ range.
+      UntargetedIntegerOutsideInt Integer
+    | -- | Expected target is not a floating-point type.
+      FloatingLiteralRequiresFloatingTarget Type
+    | -- | Unary operator received a non-numeric operand.
+      UnaryRequiresNumeric UnaryOperator Type
+    | -- | Bitwise operator received a non-integer operand.
+      UnaryRequiresInteger UnaryOperator Type
+    | -- | Negation received an unsigned or non-numeric operand.
+      NegationRequiresSignedNumeric Type
+    | -- | Logical negation operand is not condition-compatible.
+      LogicalRequiresBooleanContext UnaryOperator Type
+    | -- | Binary operands have different types.
+      BinaryRequiresMatchingTypes BinaryOperator Type Type
+    | -- | Arithmetic operator received a non-numeric type.
+      BinaryRequiresNumericType BinaryOperator Type
+    | -- | Integer-only operator received a non-integer type.
+      BinaryRequiresIntegerType BinaryOperator Type
+    | -- | Logical operands are not condition-compatible.
+      BinaryRequiresBooleanContext BinaryOperator Type Type
+    deriving (Eq, Ord, Read, Show)
+
+-- | Result type and optional validation error produced by a numeric rule.
+data NumericRuleResult = NumericRuleResult
+    { numericRuleType :: Type
+    -- ^ Type to assign even when the rule reports an error.
+    , numericRuleError :: Maybe NumericRuleError
+    -- ^ Failure details, or @Nothing@ on success.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+{- | Select and range-check the type of an integer literal from its context.
+Boolean conditions retain the language's documented numeric condition rule;
+an unconstrained literal defaults to @int@ and must fit that exact range.
+-}
+integerLiteralRule :: NumericContext -> Integer -> NumericRuleResult
+integerLiteralRule context value = case context of
+    BooleanNumericContext -> success boolType
+    TargetNumericType target -> case typeToScalarType target of
+        Just BooleanScalar -> success boolType
+        Just scalar
+            | scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily] ->
+                if integerFits scalar value
+                    then success (scalarTypeToType scalar)
+                    else failure (scalarTypeToType scalar) (IntegerLiteralOutsideTarget scalar value)
+        _ -> defaultInteger
+    NoNumericContext -> defaultInteger
+    where
+        defaultInteger
+            | integerFits defaultIntegerScalar value = success (scalarTypeToType defaultIntegerScalar)
+            | otherwise = failure (scalarTypeToType defaultIntegerScalar) (UntargetedIntegerOutsideInt value)
+
+-- | Select a floating literal type, requiring any explicit target to be float.
+floatingLiteralRule :: NumericContext -> NumericRuleResult
+floatingLiteralRule context = case context of
+    TargetNumericType target -> case typeToScalarType target of
+        Just scalar | scalarTypeFamily scalar == FloatingFamily -> success (scalarTypeToType scalar)
+        _ -> failure (scalarTypeToType defaultFloatingScalar) (FloatingLiteralRequiresFloatingTarget target)
+    _ -> success (scalarTypeToType defaultFloatingScalar)
+
+-- | Apply the type rule for a unary numeric or logical operator.
+unaryNumericRule :: UnaryOperator -> Type -> NumericRuleResult
+unaryNumericRule operator operandType = case operator of
+    LogicalNot
+        | acceptsBooleanContext operandType -> success boolType
+        | otherwise -> failure boolType (LogicalRequiresBooleanContext operator operandType)
+    BitwiseNot
+        | isIntegerType operandType -> success operandType
+        | otherwise -> failure operandType (UnaryRequiresInteger operator operandType)
+    UnaryPlus
+        | isNumericType operandType -> success operandType
+        | otherwise -> failure operandType (UnaryRequiresNumeric operator operandType)
+    UnaryNegate
+        | isSignedIntegerType operandType || isFloatingType operandType -> success operandType
+        | otherwise -> failure operandType (NegationRequiresSignedNumeric operandType)
+
+-- | Apply operand compatibility and result-type rules for a binary operator.
+binaryNumericRule :: BinaryOperator -> Type -> Type -> NumericRuleResult
+binaryNumericRule operator leftType rightType
+    | operator `elem` [LogicalAnd, LogicalOr] =
+        if acceptsBooleanContext leftType && acceptsBooleanContext rightType
+            then success boolType
+            else failure boolType (BinaryRequiresBooleanContext operator leftType rightType)
+    | operator `elem` [Equal, NotEqual] =
+        if leftType == rightType
+            then success boolType
+            else failure boolType (BinaryRequiresMatchingTypes operator leftType rightType)
+    | leftType /= rightType =
+        failure (resultFor operator leftType) (BinaryRequiresMatchingTypes operator leftType rightType)
+    | operator `elem` [ShiftLeft, ShiftRight, BitwiseAnd, BitwiseXor, BitwiseOr] && not (isIntegerType leftType) =
+        failure leftType (BinaryRequiresIntegerType operator leftType)
+    | not (isNumericType leftType) =
+        failure (resultFor operator leftType) (BinaryRequiresNumericType operator leftType)
+    | otherwise = success (resultFor operator leftType)
+
+-- | Test whether a type is accepted as a condition by the current language rules.
+acceptsBooleanContext :: Type -> Bool
+acceptsBooleanContext valueType = valueType == boolType || isNumericType valueType
+
+resultFor :: BinaryOperator -> Type -> Type
+resultFor operator operandType
+    | operator `elem` [LessThan, LessEqual, GreaterThan, GreaterEqual, Equal, NotEqual] = boolType
+    | operator == FloorDivide && isFloatingType operandType = intType
+    | otherwise = operandType
+
+success :: Type -> NumericRuleResult
+success valueType = NumericRuleResult valueType Nothing
+
+failure :: Type -> NumericRuleError -> NumericRuleResult
+failure valueType issue = NumericRuleResult valueType (Just issue)
+
+-- | Render a numeric rule failure as concise user-facing diagnostic text.
+renderNumericRuleError :: NumericRuleError -> String
+renderNumericRuleError issue = case issue of
+    IntegerLiteralOutsideTarget scalar value ->
+        "integer literal " ++ show value ++ " does not fit target type " ++ scalarTypeName scalar
+    UntargetedIntegerOutsideInt value ->
+        "un-targeted integer literal " ++ show value ++ " does not fit int; provide an explicit wider target"
+    FloatingLiteralRequiresFloatingTarget target ->
+        "floating-point literal cannot use non-floating target " ++ show target
+    UnaryRequiresNumeric operator operand ->
+        show operator ++ " requires a numeric operand, found " ++ show operand
+    UnaryRequiresInteger operator operand ->
+        show operator ++ " requires an integer operand, found " ++ show operand
+    NegationRequiresSignedNumeric operand ->
+        "unary negation requires a signed integer or floating-point operand, found " ++ show operand
+    LogicalRequiresBooleanContext operator operand ->
+        show operator ++ " requires bool or numeric context, found " ++ show operand
+    BinaryRequiresMatchingTypes operator left right ->
+        show operator ++ " requires matching operand types, found " ++ show left ++ " and " ++ show right
+    BinaryRequiresNumericType operator operand ->
+        show operator ++ " requires numeric operands, found " ++ show operand
+    BinaryRequiresIntegerType operator operand ->
+        show operator ++ " requires integer operands, found " ++ show operand
+    BinaryRequiresBooleanContext operator left right ->
+        show operator ++ " requires bool or numeric operands, found " ++ show left ++ " and " ++ show right
+ src/Progmasoft/Tala/Resolver/NameResolution.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
+
+{- | Convert renamed identifiers into stable symbol identities and diagnose
+unresolved or reserved names before type checking.
+-}
+module Progmasoft.Tala.Resolver.NameResolution (NameResolution (..), defaultNameResolution, runNameResolution) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.RuntimeCall (builtinConsoleSymbol, builtinSystemSymbol)
+
+-- | Pluggable name-resolution pass over a fully renamed syntax tree.
+newtype NameResolution = NameResolution
+    { resolveRenamedAST :: RenamedAST -> Either [Diagnostic] ResolvedAST
+    -- ^ Resolve every identifier or return all name-resolution diagnostics.
+    }
+
+-- | Execute a supplied name-resolution implementation.
+runNameResolution :: NameResolution -> RenamedAST -> Either [Diagnostic] ResolvedAST
+runNameResolution = resolveRenamedAST
+
+-- | Default compiler name resolver using the AST's assigned symbol identities.
+defaultNameResolution :: NameResolution
+defaultNameResolution = NameResolution resolveTree
+
+resolveTree :: RenamedAST -> Either [Diagnostic] ResolvedAST
+resolveTree (RenamedAST tree) = case traverseTree tree of
+    (resolved, []) -> Right (ResolvedAST resolved)
+    (_, problems) -> Left problems
+
+traverseTree :: SyntaxTree RenamedName () -> (SyntaxTree ResolvedName (), [Diagnostic])
+traverseTree (SyntaxTree namespace declarations) =
+    let values = map resolveDeclaration declarations in (SyntaxTree namespace (map fst values), concatMap snd values)
+
+resolveDeclaration :: Declaration RenamedName () -> (Declaration ResolvedName (), [Diagnostic])
+resolveDeclaration declaration = case declaration of
+    TypeDeclaration spanValue sourceName _ members ->
+        let name = resolveName spanValue sourceName; resolvedMembers = map resolveDeclaration members
+         in (TypeDeclaration spanValue (fst name) () (map fst resolvedMembers), snd name ++ concatMap snd resolvedMembers)
+    TemplateTypeDeclaration spanValue sourceName _ sourceParameters members ->
+        let name = resolveName spanValue sourceName
+            parameters = map resolveTemplateParameter sourceParameters
+            resolvedMembers = map resolveDeclaration members
+         in ( TemplateTypeDeclaration
+                spanValue
+                (fst name)
+                ()
+                (map fst parameters)
+                (map fst resolvedMembers)
+            , snd name ++ concatMap snd parameters ++ concatMap snd resolvedMembers
+            )
+    EnumDeclaration spanValue sourceName _ underlying cases ->
+        let name = resolveName spanValue sourceName
+         in (EnumDeclaration spanValue (fst name) () underlying cases, snd name)
+    FunctionDeclaration spanValue sourceName _ returnSyntax sourceParameters sourceBody isStatic access ->
+        let name = resolveName spanValue sourceName
+            parameters = map resolveParameter sourceParameters
+            (body, bodyProblems) = resolveBlock sourceBody
+         in ( FunctionDeclaration spanValue (fst name) () returnSyntax (map fst parameters) body isStatic access
+            , snd name ++ concatMap snd parameters ++ bodyProblems
+            )
+
+resolveParameter :: Parameter RenamedName () -> (Parameter ResolvedName (), [Diagnostic])
+resolveParameter (Parameter spanValue name _ syntax) = let (resolved, problems) = resolveName spanValue name in (Parameter spanValue resolved () syntax, problems)
+
+resolveTemplateParameter ::
+    TemplateParameter RenamedName () ->
+    (TemplateParameter ResolvedName (), [Diagnostic])
+resolveTemplateParameter parameter =
+    let (resolved, problems) = resolveName (templateParameterSpan parameter) (templateParameterName parameter)
+     in ( TemplateParameter
+            (templateParameterSpan parameter)
+            resolved
+            ()
+            (templateParameterKind parameter)
+            (templateParameterIsPack parameter)
+            (templateParameterDefault parameter)
+        , problems
+        )
+
+resolveBlock :: Block RenamedName () -> (Block ResolvedName (), [Diagnostic])
+resolveBlock (Block statements) = let values = map resolveStatement statements in (Block (map fst values), concatMap snd values)
+
+resolveStatement :: Statement RenamedName () -> (Statement ResolvedName (), [Diagnostic])
+resolveStatement statement = case statement of
+    BindingStatement spanValue kind syntax name _ value ->
+        let (resolvedName, nameProblems) = resolveName spanValue name; (resolvedValue, valueProblems) = resolveExpression value
+         in (BindingStatement spanValue kind syntax resolvedName () resolvedValue, nameProblems ++ valueProblems)
+    AssignmentStatement spanValue name _ value ->
+        let (resolvedName, nameProblems) = resolveName spanValue name; (resolvedValue, valueProblems) = resolveExpression value
+         in (AssignmentStatement spanValue resolvedName () resolvedValue, nameProblems ++ valueProblems)
+    ReturnStatement spanValue value -> let (resolved, problems) = resolveOptional value in (ReturnStatement spanValue resolved, problems)
+    IfStatement spanValue condition trueBlock falseBlock ->
+        let (resolvedCondition, conditionProblems) = resolveExpression condition
+            (resolvedTrue, trueProblems) = resolveBlock trueBlock
+            (resolvedFalse, falseProblems) = case falseBlock of
+                Nothing -> (Nothing, [])
+                Just value -> let (block, problems) = resolveBlock value in (Just block, problems)
+         in (IfStatement spanValue resolvedCondition resolvedTrue resolvedFalse, conditionProblems ++ trueProblems ++ falseProblems)
+    WhileStatement spanValue condition body ->
+        let (resolvedCondition, conditionProblems) = resolveExpression condition
+            (resolvedBody, bodyProblems) = resolveBlock body
+         in (WhileStatement spanValue resolvedCondition resolvedBody, conditionProblems ++ bodyProblems)
+    DoWhileStatement spanValue body condition ->
+        let (resolvedBody, bodyProblems) = resolveBlock body
+            (resolvedCondition, conditionProblems) = resolveExpression condition
+         in (DoWhileStatement spanValue resolvedBody resolvedCondition, bodyProblems ++ conditionProblems)
+    ForStatement spanValue initializer condition updates body ->
+        let (resolvedInitializer, initializerProblems) = case initializer of
+                Nothing -> (Nothing, [])
+                Just value -> let (resolved, problems) = resolveStatement value in (Just resolved, problems)
+            (resolvedCondition, conditionProblems) = resolveOptional condition
+            resolvedUpdates = map resolveStatement updates
+            (resolvedBody, bodyProblems) = resolveBlock body
+         in ( ForStatement spanValue resolvedInitializer resolvedCondition (map fst resolvedUpdates) resolvedBody
+            , initializerProblems ++ conditionProblems ++ concatMap snd resolvedUpdates ++ bodyProblems
+            )
+    ForEachStatement spanValue kind syntax name annotation source body ->
+        let (resolvedName, nameProblems) = resolveName spanValue name
+            (resolvedSource, sourceProblems) = resolveExpression source
+            (resolvedBody, bodyProblems) = resolveBlock body
+         in ( ForEachStatement spanValue kind syntax resolvedName annotation resolvedSource resolvedBody
+            , nameProblems ++ sourceProblems ++ bodyProblems
+            )
+    IncrementStatement spanValue name annotation ->
+        let (resolvedName, problems) = resolveName spanValue name
+         in (IncrementStatement spanValue resolvedName annotation, problems)
+    CompoundAssignmentStatement spanValue operator name _ value ->
+        let (resolvedName, nameProblems) = resolveName spanValue name
+            (resolvedValue, valueProblems) = resolveExpression value
+         in (CompoundAssignmentStatement spanValue operator resolvedName () resolvedValue, nameProblems ++ valueProblems)
+    DiscardStatement spanValue value ->
+        let (resolved, problems) = resolveExpression value in (DiscardStatement spanValue resolved, problems)
+    BreakStatement spanValue value ->
+        let (resolvedValue, problems) = resolveOptional value
+         in (BreakStatement spanValue resolvedValue, problems)
+    ContinueStatement spanValue -> (ContinueStatement spanValue, [])
+    GuardStatement spanValue condition block ->
+        let (resolvedCondition, conditionProblems) = resolveExpression condition
+            (resolvedBlock, blockProblems) = resolveBlock block
+         in (GuardStatement spanValue resolvedCondition resolvedBlock, conditionProblems ++ blockProblems)
+    BlockStatement spanValue block ->
+        let (resolvedBlock, problems) = resolveBlock block
+         in (BlockStatement spanValue resolvedBlock, problems)
+    ExpressionStatement spanValue value terminated -> let (resolved, problems) = resolveExpression value in (ExpressionStatement spanValue resolved terminated, problems)
+
+resolveOptional :: Maybe (Expression RenamedName ()) -> (Maybe (Expression ResolvedName ()), [Diagnostic])
+resolveOptional Nothing = (Nothing, [])
+resolveOptional (Just value) = let (resolved, problems) = resolveExpression value in (Just resolved, problems)
+
+resolveExpression :: Expression RenamedName () -> (Expression ResolvedName (), [Diagnostic])
+resolveExpression expression = case expression of
+    NameExpression spanValue name _ -> let (resolved, problems) = resolveName spanValue name in (NameExpression spanValue resolved (), problems)
+    LiteralExpression spanValue literal _ -> (LiteralExpression spanValue literal (), [])
+    MemberAccessExpression spanValue receiver member _ ->
+        let (resolvedReceiver, problems) = resolveExpression receiver
+         in (MemberAccessExpression spanValue resolvedReceiver member (), problems)
+    MethodReferenceExpression spanValue receiver member _ ->
+        let (resolvedReceiver, problems) = resolveExpression receiver
+         in (MethodReferenceExpression spanValue resolvedReceiver member (), problems)
+    CallExpression spanValue callee arguments _ ->
+        let (resolvedCallee, firstProblems) = resolveExpression callee; values = map resolveExpression arguments
+         in (CallExpression spanValue resolvedCallee (map fst values) (), firstProblems ++ concatMap snd values)
+    UnaryExpression spanValue operator value _ -> let (resolved, problems) = resolveExpression value in (UnaryExpression spanValue operator resolved (), problems)
+    BinaryExpression spanValue operator left right _ ->
+        let (resolvedLeft, leftProblems) = resolveExpression left; (resolvedRight, rightProblems) = resolveExpression right
+         in (BinaryExpression spanValue operator resolvedLeft resolvedRight (), leftProblems ++ rightProblems)
+    IsPatternExpression spanValue subject patternValue _ ->
+        let (resolvedSubject, subjectProblems) = resolveExpression subject
+            (resolvedPattern, patternProblems) = resolvePattern patternValue
+         in (IsPatternExpression spanValue resolvedSubject resolvedPattern (), subjectProblems ++ patternProblems)
+    ConditionalExpression spanValue condition first second _ ->
+        let (resolvedCondition, conditionProblems) = resolveExpression condition
+            (resolvedFirst, firstProblems) = resolveExpression first
+            (resolvedSecond, secondProblems) = resolveExpression second
+         in ( ConditionalExpression spanValue resolvedCondition resolvedFirst resolvedSecond ()
+            , conditionProblems ++ firstProblems ++ secondProblems
+            )
+    CoalesceExpression spanValue left fallback _ ->
+        let (resolvedLeft, leftProblems) = resolveExpression left
+            (resolvedFallback, fallbackProblems) = resolveExpression fallback
+         in (CoalesceExpression spanValue resolvedLeft resolvedFallback (), leftProblems ++ fallbackProblems)
+    AssignmentExpression spanValue operator name value _ ->
+        let (resolvedName, nameProblems) = resolveName spanValue name
+            (resolvedValue, valueProblems) = resolveExpression value
+         in (AssignmentExpression spanValue operator resolvedName resolvedValue (), nameProblems ++ valueProblems)
+    IncrementExpression spanValue isPrefix name _ ->
+        let (resolvedName, problems) = resolveName spanValue name
+         in (IncrementExpression spanValue isPrefix resolvedName (), problems)
+    LoopExpression spanValue loop _ ->
+        let (resolvedLoop, problems) = resolveStatement loop
+         in (LoopExpression spanValue resolvedLoop (), problems)
+    BlockExpression spanValue block _ ->
+        let (resolvedBlock, problems) = resolveBlock block
+         in (BlockExpression spanValue resolvedBlock (), problems)
+    MatchExpression spanValue subjects arms _ ->
+        let resolvedSubjects = map resolveExpression subjects
+            resolvedArms = map resolveMatchArm arms
+         in ( MatchExpression spanValue (map fst resolvedSubjects) (map fst resolvedArms) ()
+            , concatMap snd resolvedSubjects ++ concatMap snd resolvedArms
+            )
+    CallableExpression spanValue explicit captures parameters body _ ->
+        let resolvedCaptures = map resolveCapture captures
+            resolvedParameters = map resolveParameter parameters
+            (resolvedBody, bodyProblems) = resolveCallableBody body
+         in ( CallableExpression
+                spanValue
+                explicit
+                (map fst resolvedCaptures)
+                (map fst resolvedParameters)
+                resolvedBody
+                ()
+            , concatMap snd resolvedCaptures ++ concatMap snd resolvedParameters ++ bodyProblems
+            )
+
+resolveMatchArm :: MatchArm RenamedName () -> (MatchArm ResolvedName (), [Diagnostic])
+resolveMatchArm (MatchArm spanValue patterns guard body) =
+    let resolvedPatterns = map resolveMatchPattern patterns
+        (resolvedGuard, guardProblems) = resolveOptional guard
+        (resolvedBody, bodyProblems) = resolveExpression body
+     in ( MatchArm spanValue (map fst resolvedPatterns) resolvedGuard resolvedBody
+        , concatMap snd resolvedPatterns ++ guardProblems ++ bodyProblems
+        )
+
+resolveMatchPattern :: MatchPattern RenamedName () -> (MatchPattern ResolvedName (), [Diagnostic])
+resolveMatchPattern patternValue = case patternValue of
+    MatchWildcardPattern spanValue _ -> (MatchWildcardPattern spanValue (), [])
+    MatchLiteralPattern spanValue literal _ -> (MatchLiteralPattern spanValue literal (), [])
+    MatchNullPattern spanValue _ -> (MatchNullPattern spanValue (), [])
+    MatchCasePattern spanValue name _ -> (MatchCasePattern spanValue name (), [])
+    MatchTypePattern spanValue syntax Nothing _ -> (MatchTypePattern spanValue syntax Nothing (), [])
+    MatchTypePattern spanValue syntax (Just name) _ ->
+        let (resolvedName, problems) = resolveName spanValue name
+         in (MatchTypePattern spanValue syntax (Just resolvedName) (), problems)
+
+resolvePattern :: Pattern RenamedName () -> (Pattern ResolvedName (), [Diagnostic])
+resolvePattern patternValue = case patternValue of
+    WildcardPattern spanValue _ -> (WildcardPattern spanValue (), [])
+    NullPattern spanValue _ -> (NullPattern spanValue (), [])
+    LiteralPattern spanValue literal _ -> (LiteralPattern spanValue literal (), [])
+    TypePattern spanValue syntax _ -> (TypePattern spanValue syntax (), [])
+    RelationalPattern spanValue operator literal _ -> (RelationalPattern spanValue operator literal (), [])
+    NotPattern spanValue nested _ ->
+        let (resolved, problems) = resolvePattern nested
+         in (NotPattern spanValue resolved (), problems)
+    AndPattern spanValue left right _ -> resolvePair AndPattern spanValue left right
+    OrPattern spanValue left right _ -> resolvePair OrPattern spanValue left right
+    where
+        resolvePair constructor spanValue left right =
+            let (resolvedLeft, leftProblems) = resolvePattern left
+                (resolvedRight, rightProblems) = resolvePattern right
+             in (constructor spanValue resolvedLeft resolvedRight (), leftProblems ++ rightProblems)
+
+resolveCapture :: Capture RenamedName () -> (Capture ResolvedName (), [Diagnostic])
+resolveCapture (Capture spanValue mode name _ initializer) =
+    let (resolvedName, nameProblems) = resolveName spanValue name
+        (resolvedInitializer, initializerProblems) = resolveOptional initializer
+     in ( Capture spanValue mode resolvedName () resolvedInitializer
+        , nameProblems ++ initializerProblems
+        )
+
+resolveCallableBody ::
+    CallableBody RenamedName () ->
+    (CallableBody ResolvedName (), [Diagnostic])
+resolveCallableBody body = case body of
+    CallableExpressionBody expression ->
+        let (resolved, problems) = resolveExpression expression
+         in (CallableExpressionBody resolved, problems)
+    CallableBlockBody block ->
+        let (resolved, problems) = resolveBlock block
+         in (CallableBlockBody resolved, problems)
+
+resolveName :: SourceSpan -> RenamedName -> (ResolvedName, [Diagnostic])
+resolveName spanValue name
+    | renamedUnique name > 0 = (ResolvedName (SymbolId (renamedUnique name)) (renamedSpelling name), [])
+    -- The names the language declares for every program.
+    | SymbolId (renamedUnique name) `elem` [builtinSystemSymbol, builtinConsoleSymbol] =
+        (ResolvedName (SymbolId (renamedUnique name)) (renamedSpelling name), [])
+    | renamedUnique name == 0 =
+        ( ResolvedName (SymbolId 0) (renamedSpelling name)
+        ,
+            [ Diagnostic
+                NameResolutionStage
+                Error
+                "TAN0002"
+                (Just spanValue)
+                "reserved symbol id zero reached name resolution"
+            ]
+        )
+    | otherwise =
+        ( ResolvedName (SymbolId (-1)) (renamedSpelling name)
+        ,
+            [ Diagnostic
+                NameResolutionStage
+                Error
+                "TAN0001"
+                (Just spanValue)
+                ("unknown name " ++ identifierText (renamedSpelling name))
+            ]
+        )
+ src/Progmasoft/Tala/Resolver/Renamer.hs view
@@ -0,0 +1,587 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Assign stable symbol identities to declarations and references, detecting
+duplicate declarations while preserving overload-family semantics.
+-}
+module Progmasoft.Tala.Resolver.Renamer (Renamer (..), defaultRenamer, runRenamer) where
+
+import Data.List (intercalate)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.RuntimeCall (builtinConsoleSymbol, builtinSystemSymbol)
+
+-- | Pluggable pass that assigns declaration and local-binding identities.
+newtype Renamer = Renamer
+    { renameParsedAST :: ParsedAST -> Either [Diagnostic] RenamedAST
+    -- ^ Rename a parsed tree or report declaration and binding errors.
+    }
+
+-- | Execute a supplied renaming implementation.
+runRenamer :: Renamer -> ParsedAST -> Either [Diagnostic] RenamedAST
+runRenamer = renameParsedAST
+
+-- | Default renamer with deterministic positive symbol IDs.
+defaultRenamer :: Renamer
+defaultRenamer = Renamer renameTree
+
+{- | The names in scope, each with the binding a use of it refers to. A
+binding made later replaces an earlier one of the same spelling, which is
+how an inner scope shadows an outer one.
+
+It is a map and not a list of pairs: a type with thousands of members made
+every lookup and every duplicate check walk all of them, and renaming grew
+with the square of the member count.
+-}
+type Environment = Map.Map Identifier RenamedName
+
+-- | The scope with one more binding, which shadows an earlier one.
+bind :: Identifier -> RenamedName -> Environment -> Environment
+bind = Map.insert
+
+-- | The inner scope laid over the outer one; inner bindings shadow outer ones.
+over :: Environment -> Environment -> Environment
+over = Map.union
+
+renameTree :: ParsedAST -> Either [Diagnostic] RenamedAST
+renameTree (ParsedAST (SyntaxTree namespace declarations)) =
+    let (globals, next, duplicateProblems) =
+            declareMany
+                RenamerStage
+                "TAR0001"
+                1
+                Map.empty
+                [(declarationName declaration, declarationSpan declaration) | declaration <- declarations]
+        -- What the language declares for every program stands outside
+        -- the program's own names, which shadow it.
+        (renamed, _, problems) =
+            renameDeclarations (globals `over` qualified namespace globals `over` predeclared) next declarations
+        allProblems = duplicateProblems ++ problems
+     in if null allProblems then Right (RenamedAST (SyntaxTree namespace renamed)) else Left allProblems
+
+{- | The names every program may use without declaring them: @System@, and
+@Console@, because @System@ is imported implicitly. Each has a reserved
+symbol, which no declaration of a program can have.
+-}
+predeclared :: Environment
+predeclared =
+    Map.fromList
+        [ (Identifier "System", RenamedName (Identifier "System") (symbolIdValue builtinSystemSymbol))
+        , (Identifier "Console", RenamedName (Identifier "Console") (symbolIdValue builtinConsoleSymbol))
+        ]
+
+{- | The declarations of a namespace under their qualified names:
+@Demo.Program@ beside @Program@ in the namespace @Demo@. A qualified name
+is kept as one spelling with its dots, which no identifier of a program
+has, so nothing a program declares can replace it.
+-}
+qualified :: Maybe QualifiedName -> Environment -> Environment
+qualified namespace globals = case namespace of
+    Just (QualifiedName parts@(_ : _)) ->
+        Map.mapKeys (\name -> Identifier (intercalate "." (map identifierText parts ++ [identifierText name]))) globals
+    _ -> Map.empty
+
+{- | The qualified name a selector spells, when it spells one that is
+declared: @Demo.Program@ for the selector of @Program@ on @Demo@. A name of
+the program that is spelled like the first part hides the namespace, as an
+inner name hides an outer one.
+-}
+qualifiedSelector :: Environment -> Expression Identifier () -> Maybe RenamedName
+qualifiedSelector environment expression = do
+    parts@(first : _ : _) <- path expression
+    case Map.lookup first environment of
+        Just _ -> Nothing
+        Nothing -> Map.lookup (Identifier (intercalate "." (map identifierText parts))) environment
+    where
+        path value = case value of
+            NameExpression _ name _ -> Just [name]
+            MemberAccessExpression _ receiver member _ -> (++ [member]) <$> path receiver
+            _ -> Nothing
+
+declareMany ::
+    DiagnosticStage -> String -> Int -> Environment -> [(Identifier, SourceSpan)] -> (Environment, Int, [Diagnostic])
+declareMany _ _ next environment [] = (environment, next, [])
+declareMany stage code next environment ((name, spanValue) : remaining) =
+    let duplicate = case Map.lookup name environment of
+            Just _ -> [Diagnostic stage Error code (Just spanValue) ("duplicate declaration " ++ identifierText name)]
+            Nothing -> []
+        renamed = RenamedName name next
+        (final, after, problems) = declareMany stage code (next + 1) (bind name renamed environment) remaining
+     in (final, after, duplicate ++ problems)
+
+-- Methods with one spelling are a single overload family. Other declarations
+-- still occupy that shared member namespace, so a method cannot coexist with
+-- a field-like or nested declaration of the same name. Each overload receives
+-- its own SymbolId; the type checker later validates signature uniqueness.
+declareMembers :: Int -> [Declaration Identifier ()] -> (Environment, [RenamedName], Int, [Diagnostic])
+declareMembers next declarations = go Map.empty Map.empty next declarations
+    where
+        go environment _ current [] = (environment, [], current, [])
+        go environment seen current (declaration : remaining) =
+            let name = declarationName declaration
+                isMethod = case declaration of FunctionDeclaration {} -> True; _ -> False
+                collision = case Map.lookup name seen of
+                    Nothing -> []
+                    Just previousWasMethod
+                        | previousWasMethod && isMethod -> []
+                        | otherwise ->
+                            [ Diagnostic
+                                RenamerStage
+                                Error
+                                "TAR0004"
+                                (Just (declarationSpan declaration))
+                                ("duplicate declaration " ++ identifierText name ++ " in one type")
+                            ]
+                renamed = RenamedName name current
+                (finalEnvironment, laterNames, finalNext, laterProblems) =
+                    go (bind name renamed environment) (Map.insert name isMethod seen) (current + 1) remaining
+             in (finalEnvironment, renamed : laterNames, finalNext, collision ++ laterProblems)
+
+renameDeclarations ::
+    Environment -> Int -> [Declaration Identifier ()] -> ([Declaration RenamedName ()], Int, [Diagnostic])
+renameDeclarations globals next declarations =
+    renameDeclarationsWithBindings
+        globals
+        next
+        declarations
+        (map (\declaration -> valueOrMissing (declarationName declaration) globals) declarations)
+
+-- Declaration nodes need their own binding, not a name-only lookup. In an
+-- overload family every source spelling is equal, while each declared method
+-- must retain the SymbolId allocated for its exact source position.
+renameDeclarationsWithBindings ::
+    Environment ->
+    Int ->
+    [Declaration Identifier ()] ->
+    [RenamedName] ->
+    ([Declaration RenamedName ()], Int, [Diagnostic])
+renameDeclarationsWithBindings _ next [] [] = ([], next, [])
+renameDeclarationsWithBindings globals next (declaration : remaining) (assignedName : assignedRemaining) =
+    case declaration of
+        TypeDeclaration spanValue _ _ members ->
+            let name = assignedName
+                (declaredMembers, memberNames, afterMembers, duplicateProblems) = declareMembers next members
+                (renamedMembers, afterBody, memberProblems) =
+                    renameDeclarationsWithBindings (declaredMembers `over` globals) afterMembers members memberNames
+                renamed = TypeDeclaration spanValue name () renamedMembers
+                (rest, final, restProblems) = renameDeclarationsWithBindings globals afterBody remaining assignedRemaining
+             in (renamed : rest, final, duplicateProblems ++ memberProblems ++ restProblems)
+        TemplateTypeDeclaration spanValue _ _ sourceTemplateParameters members ->
+            let name = assignedName
+                (templateParameters, templateEnvironment, afterTemplateParameters, templateProblems) =
+                    renameTemplateParameters globals next sourceTemplateParameters
+                (declaredMembers, memberNames, afterMembers, duplicateProblems) = declareMembers afterTemplateParameters members
+                memberEnvironment = declaredMembers `over` templateEnvironment
+                (renamedMembers, afterBody, memberProblems) =
+                    renameDeclarationsWithBindings memberEnvironment afterMembers members memberNames
+                renamed = TemplateTypeDeclaration spanValue name () templateParameters renamedMembers
+                (rest, final, restProblems) = renameDeclarationsWithBindings globals afterBody remaining assignedRemaining
+             in ( renamed : rest
+                , final
+                , templateProblems ++ duplicateProblems ++ memberProblems ++ restProblems
+                )
+        -- The members of an enum are named by spelling under their enum
+        -- and take no symbols of their own.
+        EnumDeclaration spanValue _ _ underlying cases ->
+            let (rest, final, restProblems) = renameDeclarationsWithBindings globals next remaining assignedRemaining
+             in (EnumDeclaration spanValue assignedName () underlying cases : rest, final, restProblems)
+        FunctionDeclaration spanValue _ _ returnSyntax sourceParameters sourceBody isStatic access ->
+            let name = assignedName
+                (parameters, parameterEnvironment, afterParameters, parameterProblems) = renameParameters globals next sourceParameters
+                (body, afterBody, bodyProblems) = renameBlock parameterEnvironment afterParameters sourceBody
+                renamed = FunctionDeclaration spanValue name () returnSyntax parameters body isStatic access
+                (rest, final, restProblems) = renameDeclarationsWithBindings globals afterBody remaining assignedRemaining
+             in (renamed : rest, final, parameterProblems ++ bodyProblems ++ restProblems)
+renameDeclarationsWithBindings _ next declarations _ =
+    ( []
+    , next
+    ,
+        [ Diagnostic
+            RenamerStage
+            Error
+            "TAR0007"
+            (case declarations of declaration : _ -> Just (declarationSpan declaration); [] -> Nothing)
+            "declaration binding sequence does not match its syntax sequence"
+        ]
+    )
+
+-- All template parameters enter scope together.  This permits the documented
+-- default `T = U, U = int` while keeping source-order SymbolIds deterministic.
+-- Defaults remain syntax trees; their spelling is resolved by the type checker
+-- against the semantic parameter catalog created here.
+renameTemplateParameters ::
+    Environment ->
+    Int ->
+    [TemplateParameter Identifier ()] ->
+    ([TemplateParameter RenamedName ()], Environment, Int, [Diagnostic])
+renameTemplateParameters outer next parameters =
+    let declarations = [(templateParameterName parameter, templateParameterSpan parameter) | parameter <- parameters]
+        (environment, after, problems) = declareMany RenamerStage "TAR0006" next outer declarations
+        renamed =
+            [ TemplateParameter
+                (templateParameterSpan parameter)
+                (valueOrMissing (templateParameterName parameter) environment)
+                ()
+                (templateParameterKind parameter)
+                (templateParameterIsPack parameter)
+                (templateParameterDefault parameter)
+            | parameter <- parameters
+            ]
+     in (renamed, environment, after, problems)
+
+renameParameters ::
+    Environment -> Int -> [Parameter Identifier ()] -> ([Parameter RenamedName ()], Environment, Int, [Diagnostic])
+renameParameters environment next parameters = go environment next parameters [] []
+    where
+        go env current [] output problems = (reverse output, env, current, reverse problems)
+        go env current (Parameter spanValue name _ syntax : rest) output problems =
+            let duplicate =
+                    if any (\(Parameter _ earlier _ _) -> renamedSpelling earlier == name) output
+                        then Diagnostic RenamerStage Error "TAR0002" (Just spanValue) ("duplicate parameter " ++ identifierText name) : problems
+                        else problems
+                renamed = RenamedName name current
+             in go (bind name renamed env) (current + 1) rest (Parameter spanValue renamed () syntax : output) duplicate
+
+renameBlock :: Environment -> Int -> Block Identifier () -> (Block RenamedName (), Int, [Diagnostic])
+renameBlock environment next (Block statements) = let (values, _, final, problems) = go environment next statements in (Block values, final, problems)
+    where
+        go env current [] = ([], env, current, [])
+        go env current (statement : rest) =
+            let (renamed, nextEnv, after, firstProblems) = renameStatement env current statement
+                (remaining, finalEnv, final, restProblems) = go nextEnv after rest
+             in (renamed : remaining, finalEnv, final, firstProblems ++ restProblems)
+
+renameStatement ::
+    Environment -> Int -> Statement Identifier () -> (Statement RenamedName (), Environment, Int, [Diagnostic])
+renameStatement environment next statement = case statement of
+    BindingStatement spanValue kind syntax name _ value ->
+        let (renamedValue, afterValue, problems) = renameExpression environment next value
+            duplicate = Map.member name environment
+            renamed = RenamedName name afterValue
+            duplicateProblems =
+                if duplicate
+                    then [Diagnostic RenamerStage Error "TAR0003" (Just spanValue) ("duplicate local " ++ identifierText name)]
+                    else []
+         in ( BindingStatement spanValue kind syntax renamed () renamedValue
+            , bind name renamed environment
+            , afterValue + 1
+            , problems ++ duplicateProblems
+            )
+    AssignmentStatement spanValue name _ value ->
+        let (renamedValue, after, problems) = renameExpression environment next value
+         in (AssignmentStatement spanValue (valueOrMissing name environment) () renamedValue, environment, after, problems)
+    ReturnStatement spanValue value ->
+        let (renamedValue, after, problems) = renameOptional environment next value
+         in (ReturnStatement spanValue renamedValue, environment, after, problems)
+    IfStatement spanValue condition trueBlock falseBlock ->
+        let (renamedCondition, afterCondition, conditionProblems) = renameExpression environment next condition
+            (renamedTrue, afterTrue, trueProblems) = renameBlock environment afterCondition trueBlock
+            (renamedFalse, afterFalse, falseProblems) = case falseBlock of
+                Nothing -> (Nothing, afterTrue, [])
+                Just value -> let (block, after, problems) = renameBlock environment afterTrue value in (Just block, after, problems)
+         in ( IfStatement spanValue renamedCondition renamedTrue renamedFalse
+            , environment
+            , afterFalse
+            , conditionProblems ++ trueProblems ++ falseProblems
+            )
+    WhileStatement spanValue condition body ->
+        let (renamedCondition, afterCondition, conditionProblems) = renameExpression environment next condition
+            (renamedBody, afterBody, bodyProblems) = renameBlock environment afterCondition body
+         in (WhileStatement spanValue renamedCondition renamedBody, environment, afterBody, conditionProblems ++ bodyProblems)
+    DoWhileStatement spanValue body condition ->
+        let (renamedBody, afterBody, bodyProblems) = renameBlock environment next body
+            (renamedCondition, afterCondition, conditionProblems) = renameExpression environment afterBody condition
+         in ( DoWhileStatement spanValue renamedBody renamedCondition
+            , environment
+            , afterCondition
+            , bodyProblems ++ conditionProblems
+            )
+    ForStatement spanValue initializer condition updates body ->
+        let (renamedInitializer, loopEnvironment, afterInitializer, initializerProblems) = case initializer of
+                Nothing -> (Nothing, environment, next, [])
+                Just value ->
+                    let (renamed, nested, after, problems) = renameStatement environment next value
+                     in (Just renamed, nested, after, problems)
+            (renamedCondition, afterCondition, conditionProblems) = case condition of
+                Nothing -> (Nothing, afterInitializer, [])
+                Just value ->
+                    let (renamed, after, problems) = renameExpression loopEnvironment afterInitializer value
+                     in (Just renamed, after, problems)
+            (renamedUpdates, afterUpdates, updateProblems) = renameStatementList loopEnvironment afterCondition updates
+            (renamedBody, afterBody, bodyProblems) = renameBlock loopEnvironment afterUpdates body
+         in ( ForStatement spanValue renamedInitializer renamedCondition renamedUpdates renamedBody
+            , environment
+            , afterBody
+            , initializerProblems ++ conditionProblems ++ updateProblems ++ bodyProblems
+            )
+    ForEachStatement spanValue kind syntax sourceName _ source body ->
+        let (renamedSource, afterSource, sourceProblems) = renameExpression environment next source
+            duplicate = Map.member sourceName environment
+            renamedName = RenamedName sourceName afterSource
+            duplicateProblems =
+                if duplicate
+                    then [Diagnostic RenamerStage Error "TAR0003" (Just spanValue) ("duplicate loop binding " ++ identifierText sourceName)]
+                    else []
+            (renamedBody, afterBody, bodyProblems) =
+                renameBlock (bind sourceName renamedName environment) (afterSource + 1) body
+         in ( ForEachStatement spanValue kind syntax renamedName () renamedSource renamedBody
+            , environment
+            , afterBody
+            , sourceProblems ++ duplicateProblems ++ bodyProblems
+            )
+    IncrementStatement spanValue name _ ->
+        (IncrementStatement spanValue (valueOrMissing name environment) (), environment, next, [])
+    CompoundAssignmentStatement spanValue operator name _ value ->
+        let (renamedValue, after, problems) = renameExpression environment next value
+         in ( CompoundAssignmentStatement spanValue operator (valueOrMissing name environment) () renamedValue
+            , environment
+            , after
+            , problems
+            )
+    DiscardStatement spanValue value ->
+        let (renamedValue, after, problems) = renameExpression environment next value
+         in (DiscardStatement spanValue renamedValue, environment, after, problems)
+    BreakStatement spanValue value ->
+        let (renamed, after, problems) = renameOptional environment next value
+         in (BreakStatement spanValue renamed, environment, after, problems)
+    ContinueStatement spanValue -> (ContinueStatement spanValue, environment, next, [])
+    GuardStatement spanValue condition block ->
+        let (renamedCondition, afterCondition, conditionProblems) = renameExpression environment next condition
+            (renamedBlock, afterBlock, blockProblems) = renameBlock environment afterCondition block
+         in (GuardStatement spanValue renamedCondition renamedBlock, environment, afterBlock, conditionProblems ++ blockProblems)
+    -- The names a nested block declares end with the block.
+    BlockStatement spanValue block ->
+        let (renamedBlock, afterBlock, problems) = renameBlock environment next block
+         in (BlockStatement spanValue renamedBlock, environment, afterBlock, problems)
+    ExpressionStatement spanValue value terminated ->
+        let (renamedValue, after, problems) = renameExpression environment next value
+         in (ExpressionStatement spanValue renamedValue terminated, environment, after, problems)
+
+renameStatementList ::
+    Environment -> Int -> [Statement Identifier ()] -> ([Statement RenamedName ()], Int, [Diagnostic])
+renameStatementList _ next [] = ([], next, [])
+renameStatementList environment next (statement : remaining) =
+    let (renamed, nested, after, firstProblems) = renameStatement environment next statement
+        (later, final, laterProblems) = renameStatementList nested after remaining
+     in (renamed : later, final, firstProblems ++ laterProblems)
+
+renameOptional ::
+    Environment -> Int -> Maybe (Expression Identifier ()) -> (Maybe (Expression RenamedName ()), Int, [Diagnostic])
+renameOptional _ next Nothing = (Nothing, next, [])
+renameOptional environment next (Just value) = let (renamed, after, problems) = renameExpression environment next value in (Just renamed, after, problems)
+
+renameExpression :: Environment -> Int -> Expression Identifier () -> (Expression RenamedName (), Int, [Diagnostic])
+renameExpression environment next expression = case expression of
+    NameExpression spanValue name _ -> (NameExpression spanValue (valueOrMissing name environment) (), next, [])
+    LiteralExpression spanValue literal _ -> (LiteralExpression spanValue literal (), next, [])
+    -- A declaration named through its namespace is that declaration.
+    MemberAccessExpression spanValue _ _ _
+        | Just name <- qualifiedSelector environment expression -> (NameExpression spanValue name (), next, [])
+    MemberAccessExpression spanValue receiver member _ ->
+        let (renamedReceiver, afterReceiver, problems) = renameExpression environment next receiver
+         in (MemberAccessExpression spanValue renamedReceiver member (), afterReceiver, problems)
+    MethodReferenceExpression spanValue receiver member _ ->
+        let (renamedReceiver, afterReceiver, problems) = renameExpression environment next receiver
+         in (MethodReferenceExpression spanValue renamedReceiver member (), afterReceiver, problems)
+    CallExpression spanValue callee arguments _ ->
+        let (renamedCallee, afterCallee, firstProblems) = renameExpression environment next callee
+            (renamedArguments, after, problems) = renameExpressions environment afterCallee arguments
+         in (CallExpression spanValue renamedCallee renamedArguments (), after, firstProblems ++ problems)
+    UnaryExpression spanValue operator value _ ->
+        let (renamed, after, problems) = renameExpression environment next value
+         in (UnaryExpression spanValue operator renamed (), after, problems)
+    BinaryExpression spanValue operator left right _ ->
+        let (renamedLeft, afterLeft, leftProblems) = renameExpression environment next left
+            (renamedRight, afterRight, rightProblems) = renameExpression environment afterLeft right
+         in (BinaryExpression spanValue operator renamedLeft renamedRight (), afterRight, leftProblems ++ rightProblems)
+    IsPatternExpression spanValue subject patternValue _ ->
+        let (renamedSubject, afterSubject, subjectProblems) = renameExpression environment next subject
+            (renamedPattern, afterPattern, patternProblems) = renamePattern environment afterSubject patternValue
+         in ( IsPatternExpression spanValue renamedSubject renamedPattern ()
+            , afterPattern
+            , subjectProblems ++ patternProblems
+            )
+    ConditionalExpression spanValue condition first second _ ->
+        let (renamedCondition, afterCondition, conditionProblems) = renameExpression environment next condition
+            (renamedFirst, afterFirst, firstProblems) = renameExpression environment afterCondition first
+            (renamedSecond, afterSecond, secondProblems) = renameExpression environment afterFirst second
+         in ( ConditionalExpression spanValue renamedCondition renamedFirst renamedSecond ()
+            , afterSecond
+            , conditionProblems ++ firstProblems ++ secondProblems
+            )
+    CoalesceExpression spanValue left fallback _ ->
+        let (renamedLeft, afterLeft, leftProblems) = renameExpression environment next left
+            (renamedFallback, afterFallback, fallbackProblems) = renameExpression environment afterLeft fallback
+         in (CoalesceExpression spanValue renamedLeft renamedFallback (), afterFallback, leftProblems ++ fallbackProblems)
+    AssignmentExpression spanValue operator name value _ ->
+        let (renamedValue, after, problems) = renameExpression environment next value
+         in ( AssignmentExpression spanValue operator (valueOrMissing name environment) renamedValue ()
+            , after
+            , problems
+            )
+    IncrementExpression spanValue isPrefix name _ ->
+        (IncrementExpression spanValue isPrefix (valueOrMissing name environment) (), next, [])
+    -- Names a loop introduces are scoped to the loop, as for the statement.
+    LoopExpression spanValue loop _ ->
+        let (renamedLoop, _, after, problems) = renameStatement environment next loop
+         in (LoopExpression spanValue renamedLoop (), after, problems)
+    -- Names a value block introduces are scoped to the block.
+    BlockExpression spanValue block _ ->
+        let (renamedBlock, after, problems) = renameBlock environment next block
+         in (BlockExpression spanValue renamedBlock (), after, problems)
+    MatchExpression spanValue subjects arms _ ->
+        let (renamedSubjects, afterSubjects, subjectProblems) = renameExpressions environment next subjects
+            (renamedArms, afterArms, armProblems) = renameMatchArms environment afterSubjects arms
+         in (MatchExpression spanValue renamedSubjects renamedArms (), afterArms, subjectProblems ++ armProblems)
+    CallableExpression spanValue explicit sourceCaptures sourceParameters sourceBody _ ->
+        let (captures, captureEnvironment, afterCaptures, captureProblems) =
+                renameCaptures environment next sourceCaptures
+            bodyOuterEnvironment =
+                if explicit
+                    then captureEnvironment
+                    else captureEnvironment `over` environment
+            (parameters, parameterEnvironment, afterParameters, parameterProblems) =
+                renameParameters bodyOuterEnvironment afterCaptures sourceParameters
+            (body, afterBody, bodyProblems) =
+                renameCallableBody parameterEnvironment afterParameters sourceBody
+         in ( CallableExpression spanValue explicit captures parameters body ()
+            , afterBody
+            , captureProblems ++ parameterProblems ++ bodyProblems
+            )
+
+renameMatchArms ::
+    Environment -> Int -> [MatchArm Identifier ()] -> ([MatchArm RenamedName ()], Int, [Diagnostic])
+renameMatchArms _ next [] = ([], next, [])
+renameMatchArms environment next (arm : remaining) =
+    let (renamed, after, problems) = renameMatchArm environment next arm
+        (later, final, laterProblems) = renameMatchArms environment after remaining
+     in (renamed : later, final, problems ++ laterProblems)
+
+{- | Rename one arm. The names its patterns bind are in scope in its guard
+and its body and nowhere else, so every arm starts from the environment of
+the match itself.
+-}
+renameMatchArm :: Environment -> Int -> MatchArm Identifier () -> (MatchArm RenamedName (), Int, [Diagnostic])
+renameMatchArm environment next (MatchArm spanValue patterns guard body) =
+    let (renamedPatterns, armEnvironment, afterPatterns, patternProblems) = bindPatterns environment next patterns
+        (renamedGuard, afterGuard, guardProblems) = renameOptional armEnvironment afterPatterns guard
+        (renamedBody, afterBody, bodyProblems) = renameExpression armEnvironment afterGuard body
+     in ( MatchArm spanValue renamedPatterns renamedGuard renamedBody
+        , afterBody
+        , patternProblems ++ guardProblems ++ bodyProblems
+        )
+    where
+        bindPatterns env current [] = ([], env, current, [])
+        bindPatterns env current (patternValue : rest) =
+            let (renamed, nextEnv, after, problems) = renameMatchPattern env current patternValue
+                (later, finalEnv, final, laterProblems) = bindPatterns nextEnv after rest
+             in (renamed : later, finalEnv, final, problems ++ laterProblems)
+
+-- A binding pattern declares a local like any other: it receives a fresh
+-- identity and may not reuse a name that is already in scope, which includes
+-- a name bound by an earlier pattern of the same arm.
+renameMatchPattern ::
+    Environment ->
+    Int ->
+    MatchPattern Identifier () ->
+    (MatchPattern RenamedName (), Environment, Int, [Diagnostic])
+renameMatchPattern environment next patternValue = case patternValue of
+    MatchWildcardPattern spanValue _ -> (MatchWildcardPattern spanValue (), environment, next, [])
+    MatchLiteralPattern spanValue literal _ -> (MatchLiteralPattern spanValue literal (), environment, next, [])
+    MatchNullPattern spanValue _ -> (MatchNullPattern spanValue (), environment, next, [])
+    MatchCasePattern spanValue name _ -> (MatchCasePattern spanValue name (), environment, next, [])
+    MatchTypePattern spanValue syntax Nothing _ -> (MatchTypePattern spanValue syntax Nothing (), environment, next, [])
+    MatchTypePattern spanValue syntax (Just name) _ ->
+        let renamed = RenamedName name next
+            duplicateProblems =
+                [ Diagnostic RenamerStage Error "TAR0008" (Just spanValue) ("duplicate match binding " ++ identifierText name)
+                | Map.member name environment
+                ]
+         in ( MatchTypePattern spanValue syntax (Just renamed) ()
+            , bind name renamed environment
+            , next + 1
+            , duplicateProblems
+            )
+
+renamePattern :: Environment -> Int -> Pattern Identifier () -> (Pattern RenamedName (), Int, [Diagnostic])
+renamePattern environment next patternValue = case patternValue of
+    WildcardPattern spanValue _ -> (WildcardPattern spanValue (), next, [])
+    NullPattern spanValue _ -> (NullPattern spanValue (), next, [])
+    LiteralPattern spanValue literal _ -> (LiteralPattern spanValue literal (), next, [])
+    TypePattern spanValue syntax _ -> (TypePattern spanValue syntax (), next, [])
+    RelationalPattern spanValue operator literal _ ->
+        (RelationalPattern spanValue operator literal (), next, [])
+    NotPattern spanValue nested _ ->
+        let (renamed, after, problems) = renamePattern environment next nested
+         in (NotPattern spanValue renamed (), after, problems)
+    AndPattern spanValue left right _ -> renamePair AndPattern spanValue left right
+    OrPattern spanValue left right _ -> renamePair OrPattern spanValue left right
+    where
+        renamePair constructor spanValue left right =
+            let (renamedLeft, afterLeft, leftProblems) = renamePattern environment next left
+                (renamedRight, afterRight, rightProblems) = renamePattern environment afterLeft right
+             in (constructor spanValue renamedLeft renamedRight (), afterRight, leftProblems ++ rightProblems)
+
+-- Capture initializers are renamed in the surrounding scope and in source
+-- order.  Each captured binding receives a fresh identity, which is how the
+-- AST records that assigning inside a closure does not alias the outer binding.
+renameCaptures ::
+    Environment ->
+    Int ->
+    [Capture Identifier ()] ->
+    ([Capture RenamedName ()], Environment, Int, [Diagnostic])
+renameCaptures environment next captures = go next captures [] Map.empty []
+    where
+        go current [] output localEnvironment problems =
+            (reverse output, localEnvironment, current, reverse problems)
+        go current (Capture spanValue mode name _ initializer : remaining) output localEnvironment problems =
+            let sourceExpression = case initializer of
+                    Just value -> value
+                    Nothing -> NameExpression spanValue name ()
+                (renamedInitializer, afterInitializer, initializerProblems) =
+                    renameExpression environment current sourceExpression
+                duplicate = Map.member name localEnvironment
+                duplicateProblems =
+                    if duplicate
+                        then
+                            Diagnostic
+                                RenamerStage
+                                Error
+                                "TAR0005"
+                                (Just spanValue)
+                                ("duplicate capture " ++ identifierText name)
+                                : problems
+                        else problems
+                renamedName = RenamedName name afterInitializer
+                renamedCapture = Capture spanValue mode renamedName () (Just renamedInitializer)
+             in go
+                    (afterInitializer + 1)
+                    remaining
+                    (renamedCapture : output)
+                    (bind name renamedName localEnvironment)
+                    (reverse initializerProblems ++ duplicateProblems)
+
+renameCallableBody ::
+    Environment ->
+    Int ->
+    CallableBody Identifier () ->
+    (CallableBody RenamedName (), Int, [Diagnostic])
+renameCallableBody environment next body = case body of
+    CallableExpressionBody expression ->
+        let (renamed, after, problems) = renameExpression environment next expression
+         in (CallableExpressionBody renamed, after, problems)
+    CallableBlockBody block ->
+        let (renamed, after, problems) = renameBlock environment next block
+         in (CallableBlockBody renamed, after, problems)
+
+renameExpressions ::
+    Environment -> Int -> [Expression Identifier ()] -> ([Expression RenamedName ()], Int, [Diagnostic])
+renameExpressions _ next [] = ([], next, [])
+renameExpressions environment next (value : rest) =
+    let (renamed, after, problems) = renameExpression environment next value
+        (remaining, final, restProblems) = renameExpressions environment after rest
+     in (renamed : remaining, final, problems ++ restProblems)
+
+valueOrMissing :: Identifier -> Environment -> RenamedName
+valueOrMissing name environment = maybe (RenamedName name (-1)) id (Map.lookup name environment)
+ src/Progmasoft/Tala/SourceSet.hs view
@@ -0,0 +1,394 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Filesystem ownership for Tala source compilation.
+
+The Kotlin project evaluator deliberately emits roots and exclusion policy,
+not a list of source files. This module is the compiler-owned boundary that
+turns that policy into deterministic, UTF-8 decoded compilation units. It is
+kept outside the lexer so filesystem failures cannot masquerade as lexical
+diagnostics and outside the native driver so namespace discovery remains a
+Haskell frontend responsibility.
+-}
+module Progmasoft.Tala.SourceSet
+    ( SourceSetRequest (..)
+    , LoadedSource (..)
+    , discoverSourceSet
+    , loadSourceFile
+    , loadSourceSet
+    , matchesGlob
+    , normalizeSourcePath
+    ) where
+
+import Control.Exception (IOException, try)
+import Control.Monad (foldM)
+import Data.ByteString qualified as ByteString
+import Data.Char (toLower)
+import Data.List (nubBy, sortOn)
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Text
+import System.Directory
+    ( canonicalizePath
+    , doesDirectoryExist
+    , doesFileExist
+    , listDirectory
+    , pathIsSymbolicLink
+    )
+import System.FilePath
+    ( isAbsolute
+    , makeRelative
+    , normalise
+    , splitDirectories
+    , takeExtension
+    , takeFileName
+    , (</>)
+    )
+import System.Info (os)
+import Progmasoft.Tala.Diagnostic
+
+{- | All paths crossing this record may be absolute or relative. Relative roots
+are interpreted from 'sourceSetProjectRoot', never from an incidental process
+directory after discovery has started.
+-}
+data SourceSetRequest = SourceSetRequest
+    { sourceSetProjectRoot :: FilePath
+    , sourceSetRoots :: [FilePath]
+    , sourceSetExcludes :: [FilePath]
+    }
+    deriving (Eq, Ord, Read, Show)
+
+{- | The canonical path is used for diagnostics and de-duplication. The
+project-relative path is the stable ordering/exclusion identity. Source text
+is decoded strictly as UTF-8 before the lexer sees it.
+-}
+data LoadedSource = LoadedSource
+    { loadedSourcePath :: FilePath
+    , loadedSourceRelativePath :: FilePath
+    , loadedSourceText :: String
+    }
+    deriving (Eq, Ord, Read, Show)
+
+data DiscoveredFile = DiscoveredFile
+    { discoveredCanonicalPath :: FilePath
+    , discoveredRelativePath :: FilePath
+    }
+    deriving (Eq, Ord, Read, Show)
+
+data WalkState = WalkState
+    { walkVisitedDirectories :: [FilePath]
+    , walkFiles :: [DiscoveredFile]
+    , walkProblems :: [Diagnostic]
+    }
+
+emptyWalkState :: WalkState
+emptyWalkState = WalkState [] [] []
+
+{- | Load one explicit @-File@ input through the same strict decoder used by
+project compilation. File mode does not invent a project root or apply glob
+policy, but it still rejects directories, wrong extensions, and malformed
+UTF-8 at the source boundary.
+-}
+loadSourceFile :: FilePath -> IO (Either [Diagnostic] LoadedSource)
+loadSourceFile path = do
+    canonical <- tryCanonical path
+    case canonical of
+        Left issue -> pure (Left [sourceProblem "TAS0001" ("cannot resolve source file '" ++ path ++ "': " ++ issue)])
+        Right canonicalPath -> do
+            exists <- safeDoesFileExist canonicalPath
+            if not exists
+                then pure (Left [sourceProblem "TAS0002" ("source file does not exist: " ++ canonicalPath)])
+                else
+                    if takeExtension canonicalPath /= ".tala"
+                        then pure (Left [sourceProblem "TAS0003" ("source file must use the case-sensitive .tala extension: " ++ canonicalPath)])
+                        else do
+                            workingDirectory <- tryCanonical "."
+                            let sourceIdentity = case workingDirectory of
+                                    Right root
+                                        | isContainedBy root canonicalPath -> relativeTo root canonicalPath
+                                    _ -> takeFileName canonicalPath
+                            decodeSource canonicalPath sourceIdentity
+
+{- | Discover, validate, de-duplicate, sort, and decode a project source set.
+A failure in any configured root makes the whole source set invalid; silently
+compiling a partial project would make diagnostics dependent on permissions,
+junction state, or traversal order.
+-}
+discoverSourceSet :: SourceSetRequest -> IO (Either [Diagnostic] [FilePath])
+discoverSourceSet request = fmap (fmap (map discoveredCanonicalPath)) (discoverSourceFiles request)
+
+{- | Discover project sources and strictly decode each file as UTF-8.
+Any discovery or decoding failure rejects the complete set rather than
+returning a partial compilation input.
+-}
+loadSourceSet :: SourceSetRequest -> IO (Either [Diagnostic] [LoadedSource])
+loadSourceSet request = do
+    discovered <- discoverSourceFiles request
+    case discovered of
+        Left problems -> pure (Left problems)
+        Right files -> do
+            decoded <- mapM decodeDiscovered files
+            let decodeProblems = concat [problems | Left problems <- decoded]
+                sources = [source | Right source <- decoded]
+            pure $ if null decodeProblems then Right sources else Left decodeProblems
+    where
+        decodeDiscovered file =
+            decodeSource (discoveredCanonicalPath file) (discoveredRelativePath file)
+
+{- | Discover source identities without opening their contents. Formatting and
+linting own their input encoding, so project-wide tool dispatch must not make
+the compiler's UTF-8 source decoder an accidental gate before the tool runs.
+Compilation still calls 'loadSourceSet' and therefore remains strict UTF-8.
+-}
+discoverSourceFiles :: SourceSetRequest -> IO (Either [Diagnostic] [DiscoveredFile])
+discoverSourceFiles request = do
+    canonicalProject <- tryCanonical (sourceSetProjectRoot request)
+    case canonicalProject of
+        Left problem -> pure (Left [sourceProblem "TAS0004" ("cannot resolve project root: " ++ problem)])
+        Right projectRoot -> do
+            projectExists <- safeDoesDirectoryExist projectRoot
+            if not projectExists
+                then pure (Left [sourceProblem "TAS0005" ("project root is not a directory: " ++ projectRoot)])
+                else discoverFromRoots projectRoot request
+
+discoverFromRoots :: FilePath -> SourceSetRequest -> IO (Either [Diagnostic] [DiscoveredFile])
+discoverFromRoots projectRoot request
+    | null (sourceSetRoots request) = pure (Left [sourceProblem "TAS0006" "project has no configured source roots"])
+    | otherwise = do
+        walked <- foldM (walkConfiguredRoot projectRoot patterns) emptyWalkState (sourceSetRoots request)
+        let unique = uniqueFiles (walkFiles walked)
+            ordered = sortOn (pathOrderingKey . discoveredRelativePath) unique
+            discoveryProblems = reverse (walkProblems walked)
+        pure $
+            if not (null discoveryProblems)
+                then Left discoveryProblems
+                else
+                    if null ordered
+                        then Left [sourceProblem "TAS0007" "configured source roots contain no non-excluded .tala files"]
+                        else Right ordered
+    where
+        patterns = map normalizePattern (sourceSetExcludes request)
+
+walkConfiguredRoot :: FilePath -> [String] -> WalkState -> FilePath -> IO WalkState
+walkConfiguredRoot projectRoot patterns state configuredRoot = do
+    let rooted = if isAbsolute configuredRoot then configuredRoot else projectRoot </> configuredRoot
+    canonical <- tryCanonical rooted
+    case canonical of
+        Left problem -> pure (addProblem state "TAS0008" ("cannot resolve source root '" ++ configuredRoot ++ "': " ++ problem))
+        Right sourceRoot ->
+            if not (isContainedBy projectRoot sourceRoot)
+                then pure (addProblem state "TAS0009" ("source root escapes the project root: " ++ sourceRoot))
+                else do
+                    exists <- safeDoesDirectoryExist sourceRoot
+                    if not exists
+                        then pure (addProblem state "TAS0010" ("source root is not a directory: " ++ sourceRoot))
+                        else walkDirectory projectRoot patterns state sourceRoot
+
+walkDirectory :: FilePath -> [String] -> WalkState -> FilePath -> IO WalkState
+walkDirectory projectRoot patterns state directory = do
+    canonical <- tryCanonical directory
+    case canonical of
+        Left problem -> pure (addProblem state "TAS0011" ("cannot resolve directory '" ++ directory ++ "': " ++ problem))
+        Right canonicalDirectory
+            | not (isContainedBy projectRoot canonicalDirectory) ->
+                pure (addProblem state "TAS0012" ("source traversal escapes the project root: " ++ canonicalDirectory))
+            | pathMember canonicalDirectory (walkVisitedDirectories state) ->
+                pure state
+            | excluded patterns (relativeTo projectRoot canonicalDirectory) True ->
+                pure state
+            | otherwise -> do
+                entries <- tryListDirectory canonicalDirectory
+                case entries of
+                    Left problem -> pure (addProblem state "TAS0013" ("cannot enumerate source directory '" ++ canonicalDirectory ++ "': " ++ problem))
+                    Right names -> do
+                        let entered = state {walkVisitedDirectories = canonicalDirectory : walkVisitedDirectories state}
+                        foldM (walkEntry projectRoot patterns canonicalDirectory) entered (sortOn pathOrderingKey names)
+
+walkEntry :: FilePath -> [String] -> FilePath -> WalkState -> FilePath -> IO WalkState
+walkEntry projectRoot patterns parent state name = do
+    let candidate = parent </> name
+    directory <- safeDoesDirectoryExist candidate
+    file <- safeDoesFileExist candidate
+    symbolic <- safePathIsSymbolicLink candidate
+    if directory
+        then do
+            canonical <- tryCanonical candidate
+            case canonical of
+                Left problem -> pure (addProblem state "TAS0014" ("cannot resolve source directory '" ++ candidate ++ "': " ++ problem))
+                Right target
+                    | symbolic && not (isContainedBy projectRoot target) ->
+                        pure (addProblem state "TAS0015" ("source directory link escapes the project root: " ++ candidate))
+                    | otherwise -> walkDirectory projectRoot patterns state target
+        else
+            if file
+                then considerFile projectRoot patterns state candidate symbolic
+                else pure state
+
+considerFile :: FilePath -> [String] -> WalkState -> FilePath -> Bool -> IO WalkState
+considerFile projectRoot patterns state candidate symbolic = do
+    canonical <- tryCanonical candidate
+    case canonical of
+        Left problem -> pure (addProblem state "TAS0016" ("cannot resolve source file '" ++ candidate ++ "': " ++ problem))
+        Right canonicalFile
+            | symbolic && not (isContainedBy projectRoot canonicalFile) ->
+                pure (addProblem state "TAS0017" ("source file link escapes the project root: " ++ candidate))
+            | not (isContainedBy projectRoot canonicalFile) ->
+                pure (addProblem state "TAS0018" ("source file escapes the project root: " ++ canonicalFile))
+            | takeExtension candidate /= ".tala" -> pure state
+            | excluded patterns relative False -> pure state
+            | otherwise ->
+                pure state {walkFiles = DiscoveredFile canonicalFile relative : walkFiles state}
+            where
+                relative = relativeTo projectRoot canonicalFile
+
+decodeSource :: FilePath -> FilePath -> IO (Either [Diagnostic] LoadedSource)
+decodeSource canonicalPath relativePath = do
+    bytes <- try (ByteString.readFile canonicalPath) :: IO (Either IOException ByteString.ByteString)
+    pure $ case bytes of
+        Left problem -> Left [sourceProblem "TAS0019" ("cannot read source file '" ++ canonicalPath ++ "': " ++ show problem)]
+        Right content -> case Text.decodeUtf8' content of
+            Left problem -> Left [sourceProblem "TAS0020" ("source file is not valid UTF-8: " ++ canonicalPath ++ " (" ++ show problem ++ ")")]
+            Right text -> Right (LoadedSource canonicalPath (normalizeSourcePath relativePath) (Text.unpack text))
+
+{- | Match a normalized project-relative path against a glob. @*@ and @?@ stay
+within one path segment; a segment consisting of @**@ consumes zero or more
+complete segments. This deliberately small grammar is deterministic across
+Windows and Unix and is sufficient for the DSL's source exclusion policy.
+-}
+matchesGlob :: FilePath -> FilePath -> Bool
+matchesGlob rawPattern rawPath = matchSegments (segments (normalizePattern rawPattern)) (segments (normalizeSourcePath rawPath))
+
+matchSegments :: [String] -> [String] -> Bool
+matchSegments [] [] = True
+matchSegments [] _ = False
+matchSegments ("**" : remaining) values =
+    matchSegments remaining values
+        || case values of
+            [] -> False
+            _ : rest -> matchSegments ("**" : remaining) rest
+matchSegments (_ : _) [] = False
+matchSegments (patternSegment : remainingPatterns) (pathSegment : remainingPaths) =
+    matchSegment patternSegment pathSegment
+        && matchSegments remainingPatterns remainingPaths
+
+matchSegment :: String -> String -> Bool
+matchSegment [] [] = True
+matchSegment [] _ = False
+matchSegment ('*' : patternRest) value =
+    matchSegment patternRest value
+        || case value of
+            [] -> False
+            _ : valueRest -> matchSegment ('*' : patternRest) valueRest
+matchSegment ('?' : patternRest) (_ : valueRest) = matchSegment patternRest valueRest
+matchSegment ('?' : _) [] = False
+matchSegment (expected : patternRest) (actual : valueRest) =
+    pathCharacterEqual expected actual && matchSegment patternRest valueRest
+matchSegment (_ : _) [] = False
+
+excluded :: [String] -> FilePath -> Bool -> Bool
+excluded patterns relative directory =
+    any (matchesCandidate normalized) patterns
+    where
+        normalized = normalizeSourcePath relative
+        -- A directory pattern without an explicit descendant wildcard owns the
+        -- directory itself and therefore prunes its complete subtree.
+        candidates = if directory then [normalized, normalized ++ "/"] else [normalized]
+        matchesCandidate path pattern =
+            any (matchesGlob pattern) candidates
+                || (directory && matchesGlob (trimTrailingSlash pattern ++ "/**") path)
+
+normalizePattern :: FilePath -> String
+normalizePattern value =
+    let normalized = dropCurrentPrefix (normalizeSourcePath value)
+     in trimTrailingSlash normalized
+
+{- | Normalize separators and redundant path syntax to a stable slash form.
+Path comparison applies platform-specific case handling separately.
+-}
+normalizeSourcePath :: FilePath -> String
+normalizeSourcePath = collapseSlashes . map slash . normalise
+    where
+        slash '\\' = '/'
+        slash value = value
+
+collapseSlashes :: String -> String
+collapseSlashes [] = []
+collapseSlashes ('/' : '/' : remaining) = collapseSlashes ('/' : remaining)
+collapseSlashes (value : remaining) = value : collapseSlashes remaining
+
+dropCurrentPrefix :: String -> String
+dropCurrentPrefix ('.' : '/' : remaining) = dropCurrentPrefix remaining
+dropCurrentPrefix value = dropWhile (== '/') value
+
+trimTrailingSlash :: String -> String
+trimTrailingSlash = reverse . dropWhile (== '/') . reverse
+
+segments :: String -> [String]
+segments value = filter (not . null) (splitOnSlash value)
+
+splitOnSlash :: String -> [String]
+splitOnSlash [] = [[]]
+splitOnSlash ('/' : remaining) = [] : splitOnSlash remaining
+splitOnSlash (value : remaining) = case splitOnSlash remaining of
+    [] -> [[value]]
+    first : rest -> (value : first) : rest
+
+relativeTo :: FilePath -> FilePath -> FilePath
+relativeTo root path = normalizeSourcePath (makeRelative root path)
+
+isContainedBy :: FilePath -> FilePath -> Bool
+isContainedBy root path =
+    let relative = normalise (makeRelative root path)
+        parts = splitDirectories relative
+     in not (isAbsolute relative) && case parts of
+            ".." : _ -> False
+            _ -> relative /= ".."
+
+uniqueFiles :: [DiscoveredFile] -> [DiscoveredFile]
+uniqueFiles = nubBy (\left right -> pathEqual (discoveredCanonicalPath left) (discoveredCanonicalPath right))
+
+pathMember :: FilePath -> [FilePath] -> Bool
+pathMember value = any (pathEqual value)
+
+pathEqual :: FilePath -> FilePath -> Bool
+pathEqual left right = pathOrderingKey left == pathOrderingKey right
+
+pathOrderingKey :: FilePath -> String
+pathOrderingKey value
+    | os == "mingw32" = map toLower (normalizeSourcePath value)
+    | otherwise = normalizeSourcePath value
+
+pathCharacterEqual :: Char -> Char -> Bool
+pathCharacterEqual left right
+    | os == "mingw32" = toLower left == toLower right
+    | otherwise = left == right
+
+tryCanonical :: FilePath -> IO (Either String FilePath)
+tryCanonical path = do
+    result <- try (canonicalizePath path) :: IO (Either IOException FilePath)
+    pure (either (Left . show) (Right . normalise) result)
+
+tryListDirectory :: FilePath -> IO (Either String [FilePath])
+tryListDirectory path = do
+    result <- try (listDirectory path) :: IO (Either IOException [FilePath])
+    pure (either (Left . show) Right result)
+
+safeDoesDirectoryExist :: FilePath -> IO Bool
+safeDoesDirectoryExist path = do
+    result <- try (doesDirectoryExist path) :: IO (Either IOException Bool)
+    pure (either (const False) id result)
+
+safeDoesFileExist :: FilePath -> IO Bool
+safeDoesFileExist path = do
+    result <- try (doesFileExist path) :: IO (Either IOException Bool)
+    pure (either (const False) id result)
+
+safePathIsSymbolicLink :: FilePath -> IO Bool
+safePathIsSymbolicLink path = do
+    result <- try (pathIsSymbolicLink path) :: IO (Either IOException Bool)
+    pure (either (const False) id result)
+
+addProblem :: WalkState -> String -> String -> WalkState
+addProblem state code message = state {walkProblems = sourceProblem code message : walkProblems state}
+
+sourceProblem :: String -> String -> Diagnostic
+sourceProblem code message = Diagnostic SourceLoaderStage Error code Nothing message
+ src/Progmasoft/Tala/Template/Application.hs view
@@ -0,0 +1,550 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Typed template declaration catalog and application binding.
+
+The parser and semantic passes preserve open declarations.  This module owns
+the next boundary: matching a concrete ordered argument list to one template
+declaration without cloning its body yet.  Keeping binding independent from
+Core is important because defaults and packs are source declaration semantics,
+whereas Core may only contain a fully closed specialization.
+-}
+module Progmasoft.Tala.Template.Application
+    ( TemplateCatalog (..)
+    , TemplateDeclarationDescriptor (..)
+    , TemplateParameterDescriptor (..)
+    , TemplateParameterCategory (..)
+    , TemplateApplication (..)
+    , TemplateBinding (..)
+    , BoundTemplateArgument (..)
+    , TemplateApplicationError (..)
+    , buildTemplateCatalog
+    , lookupTemplateDeclaration
+    , bindTemplateApplication
+    , minimumTemplateArity
+    , maximumTemplateArity
+    , substituteType
+    , substituteTemplateArgument
+    , substituteTemplateValue
+    , renderTemplateApplicationError
+    ) where
+
+import Progmasoft.Tala.AST
+
+-- | Index of all typed template declarations in one compilation unit.
+newtype TemplateCatalog = TemplateCatalog
+    { templateCatalogDeclarations :: [TemplateDeclarationDescriptor]
+    -- ^ Declarations retained in deterministic source order.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Resolved template declaration identity, parameters, and member signatures.
+data TemplateDeclarationDescriptor = TemplateDeclarationDescriptor
+    { templateDeclarationName :: QualifiedName
+    -- ^ Fully qualified source name.
+    , templateDeclarationSymbol :: SymbolId
+    -- ^ Unique symbol assigned by the renamer.
+    , templateDeclarationParameters :: [TemplateParameterDescriptor]
+    -- ^ Parameters in declaration order.
+    , templateDeclarationMembers :: [(ResolvedName, Type)]
+    -- ^ Direct members and their resolved types.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Semantic matching requirements for one template parameter.
+data TemplateParameterDescriptor = TemplateParameterDescriptor
+    { templateDescriptorName :: ResolvedName
+    -- ^ Resolved parameter identity.
+    , templateDescriptorCategory :: TemplateParameterCategory
+    -- ^ Accepted argument category and shape.
+    , templateDescriptorIsPack :: Bool
+    -- ^ Whether the parameter captures remaining arguments.
+    , templateDescriptorDefault :: Maybe TemplateDefault
+    -- ^ Source default, if declared.
+    , templateDescriptorAnnotation :: Type
+    -- ^ Resolved value/type annotation.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Category expected for an explicit template argument.
+data TemplateParameterCategory
+    = -- | A type-valued template parameter.
+      TypeParameterCategory
+    | -- | A compile-time value constrained by this type.
+      ValueParameterCategory Type
+    | -- | A template-template parameter with accepted shape.
+      TemplateParameterCategory [TemplateParameterShape]
+    deriving (Eq, Ord, Read, Show)
+
+-- | One application request with an ordered list of supplied arguments.
+data TemplateApplication = TemplateApplication
+    { templateApplicationTarget :: QualifiedName
+    -- ^ Fully qualified declaration to instantiate.
+    , templateApplicationArguments :: [TemplateArgument]
+    -- ^ Explicit arguments in source order.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Complete parameter-to-argument environment after defaults and packs bind.
+data TemplateBinding = TemplateBinding
+    { templateBindingDeclaration :: TemplateDeclarationDescriptor
+    -- ^ Declaration selected by name lookup.
+    , templateBindingArguments :: [(ResolvedName, [BoundTemplateArgument])]
+    -- ^ Bound values grouped by parameter identity.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Argument together with whether it was supplied or came from a default.
+data BoundTemplateArgument
+    = -- | Written by the source application.
+      ExplicitTemplateArgument TemplateArgument
+    | -- | Filled from the parameter declaration.
+      DefaultTemplateArgument TemplateArgument
+    deriving (Eq, Ord, Read, Show)
+
+-- | Binding, lookup, or substitution failure for a template application.
+data TemplateApplicationError
+    = -- | No declaration has this qualified name.
+      UnknownTemplateDeclaration QualifiedName
+    | -- | Required arity and supplied arity.
+      TooFewTemplateArguments QualifiedName Int Int
+    | -- | Maximum arity and supplied arity.
+      TooManyTemplateArguments QualifiedName Int Int
+    | -- | An argument does not match its parameter category.
+      TemplateArgumentCategoryMismatch QualifiedName ResolvedName Int TemplateParameterCategory TemplateArgument
+    | -- | A default references a parameter without a bound value.
+      UnresolvedTemplateDefault QualifiedName ResolvedName TemplateDefault
+    | -- | A parameter pack default cannot be represented.
+      UnsupportedTemplatePackDefault QualifiedName ResolvedName
+    | -- | Multiple declarations share the requested name.
+      AmbiguousTemplateDeclaration QualifiedName [SymbolId]
+    deriving (Eq, Ord, Read, Show)
+
+-- | Build a source-ordered catalog from template declarations in a typed AST.
+buildTemplateCatalog :: TypedAST -> TemplateCatalog
+buildTemplateCatalog (TypedAST (SyntaxTree namespace declarations)) =
+    TemplateCatalog (concatMap (describe namespace) declarations)
+    where
+        describe owner declaration = case declaration of
+            TemplateTypeDeclaration _ name _ parameters members ->
+                [ TemplateDeclarationDescriptor
+                    (qualify owner (resolvedSpelling name))
+                    (resolvedSymbol name)
+                    (map describeParameter parameters)
+                    [(declarationName member, declarationAnnotation member) | member <- members]
+                ]
+            _ -> []
+
+        qualify Nothing name = QualifiedName [name]
+        qualify (Just (QualifiedName parts)) name = QualifiedName (parts ++ [name])
+
+describeParameter :: TemplateParameter ResolvedName Type -> TemplateParameterDescriptor
+describeParameter parameter =
+    TemplateParameterDescriptor
+        (templateParameterName parameter)
+        category
+        (templateParameterIsPack parameter)
+        (templateParameterDefault parameter)
+        (templateParameterAnnotation parameter)
+    where
+        category = case templateParameterKind parameter of
+            TemplateTypeParameter -> TypeParameterCategory
+            TemplateValueParameterKind _ -> ValueParameterCategory (templateParameterAnnotation parameter)
+            TemplateTemplateParameter shapes -> TemplateParameterCategory shapes
+
+-- | Resolve a qualified template name, rejecting missing or ambiguous matches.
+lookupTemplateDeclaration ::
+    QualifiedName ->
+    TemplateCatalog ->
+    Either TemplateApplicationError TemplateDeclarationDescriptor
+lookupTemplateDeclaration name catalog = case matching of
+    [] -> Left (UnknownTemplateDeclaration name)
+    [declaration] -> Right declaration
+    declarations -> Left (AmbiguousTemplateDeclaration name (map templateDeclarationSymbol declarations))
+    where
+        matching = filter ((== name) . templateDeclarationName) (templateCatalogDeclarations catalog)
+
+-- | Count required explicit arguments after defaults and packs are considered.
+minimumTemplateArity :: TemplateDeclarationDescriptor -> Int
+minimumTemplateArity = length . filter required . templateDeclarationParameters
+    where
+        required parameter =
+            not (templateDescriptorIsPack parameter)
+                && templateDescriptorDefault parameter == Nothing
+
+-- | Return the finite maximum argument count, or Nothing for a parameter pack.
+maximumTemplateArity :: TemplateDeclarationDescriptor -> Maybe Int
+maximumTemplateArity declaration
+    | any templateDescriptorIsPack parameters = Nothing
+    | otherwise = Just (length parameters)
+    where
+        parameters = templateDeclarationParameters declaration
+
+-- | Bind explicit arguments, defaults, and packs to the selected declaration.
+bindTemplateApplication ::
+    TemplateCatalog ->
+    TemplateApplication ->
+    Either [TemplateApplicationError] TemplateBinding
+bindTemplateApplication catalog application = case lookupTemplateDeclaration target catalog of
+    Left issue -> Left [issue]
+    Right declaration ->
+        let supplied = templateApplicationArguments application
+            minimumArity = minimumTemplateArity declaration
+            maximumArity = maximumTemplateArity declaration
+         in if length supplied < minimumArity
+                then Left [TooFewTemplateArguments target minimumArity (length supplied)]
+                else case maximumArity of
+                    Just limit
+                        | length supplied > limit ->
+                            Left [TooManyTemplateArguments target limit (length supplied)]
+                    _ -> bindKnownDeclaration declaration supplied
+    where
+        target = templateApplicationTarget application
+
+bindKnownDeclaration ::
+    TemplateDeclarationDescriptor ->
+    [TemplateArgument] ->
+    Either [TemplateApplicationError] TemplateBinding
+bindKnownDeclaration declaration supplied =
+    case bindParameters declaration [] (templateDeclarationParameters declaration) supplied of
+        (bindings, [], []) -> Right (TemplateBinding declaration bindings)
+        (_, leftovers, []) ->
+            Left
+                [ TooManyTemplateArguments
+                    (templateDeclarationName declaration)
+                    (length supplied - length leftovers)
+                    (length supplied)
+                ]
+        (_, _, problems) -> Left problems
+
+type PartialBindings = [(ResolvedName, [BoundTemplateArgument])]
+
+bindParameters ::
+    TemplateDeclarationDescriptor ->
+    PartialBindings ->
+    [TemplateParameterDescriptor] ->
+    [TemplateArgument] ->
+    (PartialBindings, [TemplateArgument], [TemplateApplicationError])
+bindParameters _ bindings [] supplied = (bindings, supplied, [])
+bindParameters declaration bindings (parameter : remaining) supplied
+    | templateDescriptorIsPack parameter =
+        let reserve = minimumRequired remaining
+            count = max 0 (length supplied - reserve)
+            (packArguments, rest) = splitAt count supplied
+            checked = zipWith (validateCategory declaration parameter) [bindingOffset bindings ..] packArguments
+            problems = concatMap snd checked
+            values = map (ExplicitTemplateArgument . fst) checked
+            nextBindings = bindings ++ [(templateDescriptorName parameter, values)]
+            (finalBindings, leftovers, laterProblems) =
+                bindParameters declaration nextBindings remaining rest
+         in (finalBindings, leftovers, problems ++ laterProblems)
+    | otherwise = case supplied of
+        argument : rest ->
+            let (checked, problems) = validateCategory declaration parameter (bindingOffset bindings) argument
+                nextBindings = bindings ++ [(templateDescriptorName parameter, [ExplicitTemplateArgument checked])]
+                (finalBindings, leftovers, laterProblems) =
+                    bindParameters declaration nextBindings remaining rest
+             in (finalBindings, leftovers, problems ++ laterProblems)
+        [] -> case templateDescriptorDefault parameter of
+            Nothing ->
+                ( bindings
+                , []
+                ,
+                    [ TooFewTemplateArguments
+                        (templateDeclarationName declaration)
+                        (minimumTemplateArity declaration)
+                        (bindingOffset bindings)
+                    ]
+                )
+            Just defaultSyntax -> case resolveDefaultFor declaration bindings [resolvedSymbol (templateDescriptorName parameter)] defaultSyntax of
+                Nothing ->
+                    ( bindings
+                    , []
+                    ,
+                        [ UnresolvedTemplateDefault
+                            (templateDeclarationName declaration)
+                            (templateDescriptorName parameter)
+                            defaultSyntax
+                        ]
+                    )
+                Just argument ->
+                    let (checked, problems) = validateCategory declaration parameter (bindingOffset bindings) argument
+                        nextBindings = bindings ++ [(templateDescriptorName parameter, [DefaultTemplateArgument checked])]
+                        (finalBindings, leftovers, laterProblems) =
+                            bindParameters declaration nextBindings remaining []
+                     in (finalBindings, leftovers, problems ++ laterProblems)
+
+minimumRequired :: [TemplateParameterDescriptor] -> Int
+minimumRequired = length . filter required
+    where
+        required parameter =
+            not (templateDescriptorIsPack parameter)
+                && templateDescriptorDefault parameter == Nothing
+
+bindingOffset :: PartialBindings -> Int
+bindingOffset = sum . map (length . snd)
+
+validateCategory ::
+    TemplateDeclarationDescriptor ->
+    TemplateParameterDescriptor ->
+    Int ->
+    TemplateArgument ->
+    (TemplateArgument, [TemplateApplicationError])
+validateCategory declaration parameter index argument
+    | acceptsCategory (templateDescriptorCategory parameter) argument = (argument, [])
+    | otherwise =
+        ( argument
+        ,
+            [ TemplateArgumentCategoryMismatch
+                (templateDeclarationName declaration)
+                (templateDescriptorName parameter)
+                index
+                (templateDescriptorCategory parameter)
+                argument
+            ]
+        )
+
+acceptsCategory :: TemplateParameterCategory -> TemplateArgument -> Bool
+acceptsCategory category argument = case (category, argument) of
+    (TypeParameterCategory, TypeTemplateArgument _) -> True
+    (ValueParameterCategory _, ValueTemplateArgument _) -> True
+    -- A template-template argument is represented by its named type until the
+    -- declaration catalog grows first-class template references.  Requiring a
+    -- zero-argument named type prevents values and structural function types
+    -- from being mistaken for declaration identities.
+    (TemplateParameterCategory _, TypeTemplateArgument (NamedType _ [])) -> True
+    _ -> False
+
+resolveDefault :: PartialBindings -> TemplateDefault -> Maybe TemplateArgument
+resolveDefault bindings defaultSyntax = case defaultSyntax of
+    TemplateTypeDefault valueType -> TypeTemplateArgument <$> resolveDefaultType bindings valueType
+    TemplateValueDefault value -> ValueTemplateArgument <$> resolveDefaultValue bindings value
+
+-- Defaults may name a parameter declared later in the list.  Resolution is
+-- therefore graph-shaped rather than a left fold.  The visited SymbolId set
+-- rejects a default cycle without relying on source order or recursion depth.
+resolveDefaultFor ::
+    TemplateDeclarationDescriptor ->
+    PartialBindings ->
+    [SymbolId] ->
+    TemplateDefault ->
+    Maybe TemplateArgument
+resolveDefaultFor declaration bindings visited defaultSyntax = case defaultSyntax of
+    TemplateTypeDefault (ExplicitType identifier) ->
+        case lookupBoundType identifier bindings of
+            Just valueType -> Just (TypeTemplateArgument valueType)
+            Nothing -> case findParameter identifier declaration of
+                Nothing -> resolveDefault bindings defaultSyntax
+                Just parameter ->
+                    let symbol = resolvedSymbol (templateDescriptorName parameter)
+                     in if symbol `elem` visited
+                            then Nothing
+                            else do
+                                nestedDefault <- templateDescriptorDefault parameter
+                                resolveDefaultFor declaration bindings (symbol : visited) nestedDefault
+    TemplateValueDefault (TemplateNameSyntax _ (QualifiedName [identifier])) ->
+        case lookupBoundValue identifier bindings of
+            Just value -> Just (ValueTemplateArgument value)
+            Nothing -> do
+                parameter <- findParameter identifier declaration
+                let symbol = resolvedSymbol (templateDescriptorName parameter)
+                if symbol `elem` visited
+                    then Nothing
+                    else do
+                        nestedDefault <- templateDescriptorDefault parameter
+                        resolveDefaultFor declaration bindings (symbol : visited) nestedDefault
+    _ -> resolveDefault bindings defaultSyntax
+
+findParameter :: Identifier -> TemplateDeclarationDescriptor -> Maybe TemplateParameterDescriptor
+findParameter identifier declaration = first matching (templateDeclarationParameters declaration)
+    where
+        matching parameter = resolvedSpelling (templateDescriptorName parameter) == identifier
+        first _ [] = Nothing
+        first predicate (value : remaining)
+            | predicate value = Just value
+            | otherwise = first predicate remaining
+
+resolveDefaultType :: PartialBindings -> TypeSyntax -> Maybe Type
+resolveDefaultType bindings syntax = case syntax of
+    AutoType -> Nothing
+    ExplicitType identifier ->
+        lookupBoundType identifier bindings
+            `orElse` Just (NamedType (QualifiedName [identifier]) [])
+    QualifiedTypeSyntax name arguments ->
+        NamedType name <$> traverse (resolveDefaultArgument bindings) arguments
+    BuiltinArrayTypeSyntax element ->
+        named "[]" . (: []) . TypeTemplateArgument <$> resolveDefaultType bindings element
+    ArrayTypeSyntax element ->
+        system "Array" . (: []) . TypeTemplateArgument <$> resolveDefaultType bindings element
+    FixedArrayTypeSyntax element size -> do
+        elementType <- resolveDefaultType bindings element
+        sizeValue <- resolveDefaultValue bindings size
+        pure (system "Array" [TypeTemplateArgument elementType, ValueTemplateArgument sizeValue])
+    DictionaryTypeSyntax key value -> do
+        keyType <- resolveDefaultType bindings key
+        valueType <- resolveDefaultType bindings value
+        pure (system "Dictionary" [TypeTemplateArgument keyType, TypeTemplateArgument valueType])
+    CallableTypeSyntax parameters result ->
+        FunctionType <$> traverse (resolveDefaultType bindings) parameters <*> resolveDefaultType bindings result
+    where
+        named spelling arguments = NamedType (QualifiedName [Identifier spelling]) arguments
+        system spelling arguments = NamedType (QualifiedName [Identifier "System", Identifier spelling]) arguments
+
+resolveDefaultArgument :: PartialBindings -> TemplateArgumentSyntax -> Maybe TemplateArgument
+resolveDefaultArgument bindings argument = case argument of
+    TemplateTypeSyntax valueType -> TypeTemplateArgument <$> resolveDefaultType bindings valueType
+    TemplateValueArgumentSyntax value -> ValueTemplateArgument <$> resolveDefaultValue bindings value
+
+resolveDefaultValue :: PartialBindings -> TemplateValueSyntax -> Maybe TemplateValue
+resolveDefaultValue bindings syntax = case syntax of
+    TemplateIntegerSyntax _ value -> Just (IntegerTemplateValue value)
+    TemplateCharacterSyntax _ value -> Just (CharacterTemplateValue value)
+    TemplateBooleanSyntax _ value -> Just (BooleanTemplateValue value)
+    TemplateNameSyntax _ (QualifiedName [identifier]) -> lookupBoundValue identifier bindings
+    -- Arithmetic defaults are deliberately not evaluated here.  The type
+    -- checker already validates them, and the specialization evaluator will
+    -- own substitution-aware constant folding in the cloning stage.
+    TemplateUnarySyntax {} -> Nothing
+    TemplateBinarySyntax {} -> Nothing
+    TemplateNameSyntax {} -> Nothing
+
+lookupBoundType :: Identifier -> PartialBindings -> Maybe Type
+lookupBoundType identifier bindings = do
+    arguments <- lookupBySpelling identifier bindings
+    case arguments of
+        [ExplicitTemplateArgument (TypeTemplateArgument valueType)] -> Just valueType
+        [DefaultTemplateArgument (TypeTemplateArgument valueType)] -> Just valueType
+        _ -> Nothing
+
+lookupBoundValue :: Identifier -> PartialBindings -> Maybe TemplateValue
+lookupBoundValue identifier bindings = do
+    arguments <- lookupBySpelling identifier bindings
+    case arguments of
+        [ExplicitTemplateArgument (ValueTemplateArgument value)] -> Just value
+        [DefaultTemplateArgument (ValueTemplateArgument value)] -> Just value
+        _ -> Nothing
+
+lookupBySpelling :: Identifier -> PartialBindings -> Maybe [BoundTemplateArgument]
+lookupBySpelling identifier = lookupFirst . filter ((== identifier) . resolvedSpelling . fst)
+    where
+        lookupFirst [] = Nothing
+        lookupFirst ((_, values) : _) = Just values
+
+orElse :: Maybe a -> Maybe a -> Maybe a
+orElse (Just value) _ = Just value
+orElse Nothing fallback = fallback
+
+-- | Replace bound type variables recursively in a resolved type expression.
+substituteType :: TemplateBinding -> Type -> Either TemplateApplicationError Type
+substituteType binding valueType = case valueType of
+    TypeVariable name -> case lookup (resolvedSymbol name) substitutions of
+        Just [TypeTemplateArgument replacement] -> Right replacement
+        _ -> Left (missing name (TemplateTypeDefault (ExplicitType (resolvedSpelling name))))
+    NamedType name arguments -> NamedType name <$> traverse (substituteTemplateArgument binding) arguments
+    FunctionType parameters result -> FunctionType <$> traverse (substituteType binding) parameters <*> substituteType binding result
+    ErrorType -> Right ErrorType
+    where
+        substitutions = flattenedBindings binding
+        missing name defaultValue =
+            UnresolvedTemplateDefault
+                (templateDeclarationName (templateBindingDeclaration binding))
+                name
+                defaultValue
+
+-- | Substitute type or compile-time value variables inside one argument.
+substituteTemplateArgument :: TemplateBinding -> TemplateArgument -> Either TemplateApplicationError TemplateArgument
+substituteTemplateArgument binding argument = case argument of
+    TypeTemplateArgument valueType -> TypeTemplateArgument <$> substituteType binding valueType
+    ValueTemplateArgument value -> ValueTemplateArgument <$> substituteTemplateValue binding value
+
+-- | Substitute one value-parameter reference using the completed binding.
+substituteTemplateValue :: TemplateBinding -> TemplateValue -> Either TemplateApplicationError TemplateValue
+substituteTemplateValue binding value = case value of
+    TemplateValueParameter name -> case lookup (resolvedSymbol name) (flattenedBindings binding) of
+        Just [ValueTemplateArgument replacement] -> Right replacement
+        _ ->
+            Left
+                ( UnresolvedTemplateDefault
+                    (templateDeclarationName (templateBindingDeclaration binding))
+                    name
+                    (TemplateValueDefault (TemplateNameSyntax syntheticSpan (QualifiedName [resolvedSpelling name])))
+                )
+    _ -> Right value
+
+flattenedBindings :: TemplateBinding -> [(SymbolId, [TemplateArgument])]
+flattenedBindings binding =
+    [ (resolvedSymbol name, map unwrap values)
+    | (name, values) <- templateBindingArguments binding
+    ]
+    where
+        unwrap (ExplicitTemplateArgument argument) = argument
+        unwrap (DefaultTemplateArgument argument) = argument
+
+syntheticSpan :: SourceSpan
+syntheticSpan = SourceSpan "<template>" (SourcePosition 1 1) (SourcePosition 1 1)
+
+-- | Render a template lookup, arity, category, or substitution failure.
+renderTemplateApplicationError :: TemplateApplicationError -> String
+renderTemplateApplicationError issue = case issue of
+    UnknownTemplateDeclaration name -> "unknown template declaration " ++ renderQualifiedName name
+    TooFewTemplateArguments name expected actual ->
+        renderQualifiedName name
+            ++ " requires at least "
+            ++ show expected
+            ++ " template arguments, but received "
+            ++ show actual
+    TooManyTemplateArguments name expected actual ->
+        renderQualifiedName name
+            ++ " accepts at most "
+            ++ show expected
+            ++ " template arguments, but received "
+            ++ show actual
+    TemplateArgumentCategoryMismatch name parameter index category argument ->
+        "template argument "
+            ++ show (index + 1)
+            ++ " for "
+            ++ renderQualifiedName name
+            ++ " does not match parameter "
+            ++ identifierText (resolvedSpelling parameter)
+            ++ " (expected "
+            ++ renderCategory category
+            ++ ", received "
+            ++ renderArgumentCategory argument
+            ++ ")"
+    UnresolvedTemplateDefault name parameter _ ->
+        "default for template parameter "
+            ++ identifierText (resolvedSpelling parameter)
+            ++ " of "
+            ++ renderQualifiedName name
+            ++ " could not be resolved"
+    UnsupportedTemplatePackDefault name parameter ->
+        "template parameter pack "
+            ++ identifierText (resolvedSpelling parameter)
+            ++ " of "
+            ++ renderQualifiedName name
+            ++ " cannot have a default"
+    AmbiguousTemplateDeclaration name symbols ->
+        "template declaration "
+            ++ renderQualifiedName name
+            ++ " is ambiguous across symbols "
+            ++ show (map symbolIdValue symbols)
+
+renderQualifiedName :: QualifiedName -> String
+renderQualifiedName (QualifiedName parts) = joinWith "." (map identifierText parts)
+
+renderCategory :: TemplateParameterCategory -> String
+renderCategory category = case category of
+    TypeParameterCategory -> "type"
+    ValueParameterCategory _ -> "value"
+    TemplateParameterCategory _ -> "template"
+
+renderArgumentCategory :: TemplateArgument -> String
+renderArgumentCategory argument = case argument of
+    TypeTemplateArgument _ -> "type"
+    ValueTemplateArgument _ -> "value"
+
+joinWith :: String -> [String] -> String
+joinWith _ [] = ""
+joinWith _ [value] = value
+joinWith separator (value : remaining) = value ++ separator ++ joinWith separator remaining
+ src/Progmasoft/Tala/Template/Discovery.hs view
@@ -0,0 +1,520 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Typed-AST discovery of concrete template layout demands.
+
+Discovery is intentionally conservative. A concrete template type appearing
+in an ordinary checked declaration requests layout, but never requests method
+bodies. Open template declarations are definitions, not uses, so this pass
+does not walk their parameter-dependent bodies. Member/call resolution will
+add narrower body demands through the same planner API in a later stage.
+-}
+module Progmasoft.Tala.Template.Discovery
+    ( TemplateTypeSite (..)
+    , TemplateDiscoveryOrigin (..)
+    , TemplateDiscoveryStatistics (..)
+    , TemplateDemandDiscovery (..)
+    , discoverTemplateDemands
+    , renderTemplateDiscoveryOrigin
+    ) where
+
+import Data.List (intercalate)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Specialization
+
+-- | Syntactic/semantic position at which a concrete template application occurs.
+data TemplateTypeSite
+    = -- | Type declaration annotation.
+      DeclarationTypeSite
+    | -- | Function's complete callable signature.
+      FunctionSignatureSite
+    | -- | Parameter index in a function signature.
+      ParameterTypeSite Int
+    | -- | Local binding annotation at statement index.
+      BindingTypeSite Int
+    | -- | Assignment target annotation at statement index.
+      AssignmentTypeSite Int
+    | -- | Return expression at statement index.
+      ReturnTypeSite Int
+    | -- | Conditional or loop condition at statement index.
+      ConditionTypeSite Int
+    | -- | General expression node at traversal index.
+      ExpressionTypeSite Int
+    | -- | Callable expression type at traversal index.
+      CallableTypeSite Int
+    | -- | Capture index nested in a callable expression.
+      CaptureTypeSite Int Int
+    | -- | Type argument nested at the indexed depth.
+      NestedTypeArgumentSite Int
+    | -- | Type inside a function-type parameter list.
+      FunctionParameterTypeSite Int
+    | -- | Result type nested inside a function type.
+      FunctionResultTypeSite
+    deriving (Eq, Ord, Read, Show)
+
+-- | Source provenance explaining why one template application was discovered.
+data TemplateDiscoveryOrigin = TemplateDiscoveryOrigin
+    { discoveryDeclaration :: ResolvedName
+    -- ^ Enclosing declaration that contains the use.
+    , discoveryMember :: Maybe ResolvedName
+    -- ^ Enclosing member, when the use is within a type.
+    , discoverySites :: [TemplateTypeSite]
+    -- ^ Nested sites traversed to reach the application.
+    , discoverySpan :: SourceSpan
+    -- ^ Source location associated with the use.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Counters describing conservative demand discovery work.
+data TemplateDiscoveryStatistics = TemplateDiscoveryStatistics
+    { visitedTemplateTypeNodes :: Int
+    -- ^ Type nodes visited during traversal.
+    , discoveredTemplateApplications :: Int
+    -- ^ Concrete applications converted to demands.
+    , ignoredOrdinaryNamedTypes :: Int
+    -- ^ Non-template named types skipped.
+    , skippedOpenTemplateBodies :: Int
+    -- ^ Open declaration bodies deliberately not walked.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Explicit specialization demands, their provenance, and work counters.
+data TemplateDemandDiscovery = TemplateDemandDiscovery
+    { discoveredTemplateDemands :: [TemplateSpecializationDemand]
+    -- ^ Layout-only demands found in checked declarations.
+    , discoveredTemplateOrigins :: [TemplateDiscoveryOrigin]
+    -- ^ Source sites corresponding to discovered uses.
+    , templateDiscoveryStatistics :: TemplateDiscoveryStatistics
+    -- ^ Aggregate traversal counts.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+data DiscoveryState = DiscoveryState
+    { stateDemands :: [TemplateSpecializationDemand]
+    , stateOrigins :: [TemplateDiscoveryOrigin]
+    , stateVisitedTypes :: Int
+    , stateIgnoredTypes :: Int
+    , stateSkippedTemplates :: Int
+    }
+
+emptyState :: DiscoveryState
+emptyState = DiscoveryState [] [] 0 0 0
+
+-- | Find concrete template type uses without implicitly requesting method bodies.
+discoverTemplateDemands :: TypedAST -> TemplateDemandDiscovery
+discoverTemplateDemands typed@(TypedAST tree) =
+    let catalog = buildTemplateCatalog typed
+        final = foldl' (discoverTop catalog (syntaxNamespace tree)) emptyState (syntaxDeclarations tree)
+        demands = reverse (stateDemands final)
+        origins = reverse (stateOrigins final)
+     in TemplateDemandDiscovery
+            demands
+            origins
+            ( TemplateDiscoveryStatistics
+                (stateVisitedTypes final)
+                (length demands)
+                (stateIgnoredTypes final)
+                (stateSkippedTemplates final)
+            )
+
+discoverTop ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    DiscoveryState ->
+    Declaration ResolvedName Type ->
+    DiscoveryState
+discoverTop catalog namespace state declaration = case declaration of
+    TemplateTypeDeclaration {} -> state {stateSkippedTemplates = stateSkippedTemplates state + 1}
+    TypeDeclaration spanValue name annotation members ->
+        let origin = TemplateDiscoveryOrigin name Nothing [DeclarationTypeSite] spanValue
+            afterType = discoverType catalog namespace origin annotation state
+         in foldl' (discoverMember catalog namespace name) afterType members
+    FunctionDeclaration {} ->
+        -- Top-level runtime functions are not part of the renewed source
+        -- grammar, but keeping this traversal total makes hand-built TypedAST
+        -- fixtures and future declaration categories deterministic.
+        discoverFunction catalog namespace (declarationName declaration) Nothing declaration state
+    -- An enum uses no template: its underlying type is a scalar.
+    EnumDeclaration {} -> state
+
+discoverMember ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    ResolvedName ->
+    DiscoveryState ->
+    Declaration ResolvedName Type ->
+    DiscoveryState
+discoverMember catalog namespace owner state member = case member of
+    FunctionDeclaration {} -> discoverFunction catalog namespace owner (Just (declarationName member)) member state
+    TypeDeclaration spanValue name annotation members ->
+        let origin = TemplateDiscoveryOrigin owner (Just name) [DeclarationTypeSite] spanValue
+            afterType = discoverType catalog namespace origin annotation state
+         in foldl' (discoverMember catalog namespace owner) afterType members
+    TemplateTypeDeclaration {} -> state {stateSkippedTemplates = stateSkippedTemplates state + 1}
+    EnumDeclaration {} -> state
+
+discoverFunction ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    ResolvedName ->
+    Maybe ResolvedName ->
+    Declaration ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverFunction catalog namespace owner member declaration state = case declaration of
+    FunctionDeclaration spanValue _ annotation _ parameters body _ _ ->
+        let root = TemplateDiscoveryOrigin owner member [FunctionSignatureSite] spanValue
+            -- Function annotations repeat parameter types in a callable shape.
+            -- Walk the result here and the declared parameters below so each
+            -- source type use produces exactly one demand and one origin.
+            resultType = case annotation of
+                FunctionType _ result -> result
+                other -> other
+            afterSignature = discoverType catalog namespace root resultType state
+            afterParameters =
+                foldl'
+                    ( \current (index, parameter) ->
+                        discoverType
+                            catalog
+                            namespace
+                            (root {discoverySites = [ParameterTypeSite index]})
+                            (parameterAnnotation parameter)
+                            current
+                    )
+                    afterSignature
+                    (zip [0 ..] parameters)
+         in discoverBlock catalog namespace root body afterParameters
+    _ -> state
+
+discoverBlock ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Block ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverBlock catalog namespace root (Block statements) state =
+    foldl'
+        (\current (index, statement) -> discoverStatement catalog namespace root index statement current)
+        state
+        (zip [0 ..] statements)
+
+discoverStatement ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Int ->
+    Statement ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverStatement catalog namespace root index statement state = case statement of
+    BindingStatement spanValue _ _ _ annotation value ->
+        let origin = root {discoverySites = [BindingTypeSite index], discoverySpan = spanValue}
+         in discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state)
+    AssignmentStatement spanValue _ annotation value ->
+        let origin = root {discoverySites = [AssignmentTypeSite index], discoverySpan = spanValue}
+         in discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state)
+    ReturnStatement spanValue value ->
+        let origin = root {discoverySites = [ReturnTypeSite index], discoverySpan = spanValue}
+         in maybe state (\expression -> discoverExpression catalog namespace origin expression state) value
+    IfStatement spanValue condition trueBlock falseBlock ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+            afterCondition = discoverExpression catalog namespace origin condition state
+            afterTrue = discoverBlock catalog namespace origin trueBlock afterCondition
+         in maybe afterTrue (\block -> discoverBlock catalog namespace origin block afterTrue) falseBlock
+    WhileStatement spanValue condition body ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+         in discoverBlock catalog namespace origin body (discoverExpression catalog namespace origin condition state)
+    DoWhileStatement spanValue body condition ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+            afterBody = discoverBlock catalog namespace origin body state
+         in discoverExpression catalog namespace origin condition afterBody
+    ForStatement spanValue initializer condition updates body ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+            afterInitializer = maybe state (\value -> discoverStatement catalog namespace origin index value state) initializer
+            afterCondition = maybe afterInitializer (\value -> discoverExpression catalog namespace origin value afterInitializer) condition
+            afterUpdates = foldl' (\current value -> discoverStatement catalog namespace origin index value current) afterCondition updates
+         in discoverBlock catalog namespace origin body afterUpdates
+    ForEachStatement spanValue _ _ _ annotation source body ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+            afterAnnotation = discoverType catalog namespace origin annotation state
+            afterSource = discoverExpression catalog namespace origin source afterAnnotation
+         in discoverBlock catalog namespace origin body afterSource
+    IncrementStatement _ _ annotation -> discoverType catalog namespace root annotation state
+    CompoundAssignmentStatement spanValue _ _ annotation value ->
+        let origin = root {discoverySites = [AssignmentTypeSite index], discoverySpan = spanValue}
+         in discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state)
+    DiscardStatement spanValue expression ->
+        discoverExpression
+            catalog
+            namespace
+            (root {discoverySites = [ExpressionTypeSite index], discoverySpan = spanValue})
+            expression
+            state
+    BreakStatement _ value -> maybe state (\expression -> discoverExpression catalog namespace root expression state) value
+    ContinueStatement {} -> state
+    GuardStatement spanValue condition block ->
+        let origin = root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}
+         in discoverBlock catalog namespace origin block (discoverExpression catalog namespace origin condition state)
+    BlockStatement spanValue block ->
+        discoverBlock catalog namespace (root {discoverySites = [ConditionTypeSite index], discoverySpan = spanValue}) block state
+    ExpressionStatement spanValue expression _ ->
+        discoverExpression
+            catalog
+            namespace
+            (root {discoverySites = [ExpressionTypeSite index], discoverySpan = spanValue})
+            expression
+            state
+
+discoverExpression ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Expression ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverExpression catalog namespace origin expression state = case expression of
+    NameExpression _ _ annotation -> discoverType catalog namespace origin annotation state
+    LiteralExpression _ _ annotation -> discoverType catalog namespace origin annotation state
+    MemberAccessExpression _ receiver _ annotation ->
+        discoverExpression catalog namespace origin receiver (discoverType catalog namespace origin annotation state)
+    MethodReferenceExpression _ receiver _ annotation ->
+        discoverExpression catalog namespace origin receiver (discoverType catalog namespace origin annotation state)
+    CallExpression _ callee arguments annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+            afterCallee = discoverExpression catalog namespace origin callee afterType
+         in foldl' (\current value -> discoverExpression catalog namespace origin value current) afterCallee arguments
+    UnaryExpression _ _ value annotation ->
+        discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state)
+    BinaryExpression _ _ left right annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+            afterLeft = discoverExpression catalog namespace origin left afterType
+         in discoverExpression catalog namespace origin right afterLeft
+    IsPatternExpression _ subject patternValue annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+            afterSubject = discoverExpression catalog namespace origin subject afterType
+         in discoverPattern catalog namespace origin patternValue afterSubject
+    ConditionalExpression _ condition first second annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+         in foldl'
+                (\current value -> discoverExpression catalog namespace origin value current)
+                afterType
+                [condition, first, second]
+    CoalesceExpression _ left fallback annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+            afterLeft = discoverExpression catalog namespace origin left afterType
+         in discoverExpression catalog namespace origin fallback afterLeft
+    AssignmentExpression _ _ _ value annotation ->
+        discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state)
+    IncrementExpression _ _ _ annotation -> discoverType catalog namespace origin annotation state
+    LoopExpression _ loop annotation ->
+        discoverStatement catalog namespace origin 0 loop (discoverType catalog namespace origin annotation state)
+    BlockExpression _ block annotation ->
+        discoverBlock catalog namespace origin block (discoverType catalog namespace origin annotation state)
+    MatchExpression _ subjects arms annotation ->
+        let afterType = discoverType catalog namespace origin annotation state
+            afterPatterns =
+                foldl'
+                    (\current patternValue -> discoverType catalog namespace origin (matchPatternAnnotation patternValue) current)
+                    afterType
+                    (concatMap matchArmPatterns arms)
+         in foldl'
+                (\current value -> discoverExpression catalog namespace origin value current)
+                afterPatterns
+                (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression spanValue _ captures parameters body annotation ->
+        let callableOrigin =
+                origin
+                    { discoverySites = discoverySites origin ++ [CallableTypeSite (sourceColumn (sourceStart spanValue))]
+                    , discoverySpan = spanValue
+                    }
+            afterType = discoverType catalog namespace callableOrigin annotation state
+            afterCaptures =
+                foldl'
+                    (discoverCapture catalog namespace callableOrigin)
+                    afterType
+                    (zip [0 ..] captures)
+            afterParameters =
+                foldl'
+                    ( \current (index, parameter) ->
+                        discoverType
+                            catalog
+                            namespace
+                            (callableOrigin {discoverySites = discoverySites callableOrigin ++ [ParameterTypeSite index]})
+                            (parameterAnnotation parameter)
+                            current
+                    )
+                    afterCaptures
+                    (zip [0 ..] parameters)
+         in discoverCallableBody catalog namespace callableOrigin body afterParameters
+
+discoverPattern ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Pattern ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverPattern catalog namespace origin patternValue state = case patternValue of
+    WildcardPattern _ annotation -> discoverAnnotation annotation state
+    NullPattern _ annotation -> discoverAnnotation annotation state
+    LiteralPattern _ _ annotation -> discoverAnnotation annotation state
+    TypePattern _ _ annotation -> discoverAnnotation annotation state
+    RelationalPattern _ _ _ annotation -> discoverAnnotation annotation state
+    NotPattern _ nested annotation ->
+        discoverPattern catalog namespace origin nested (discoverAnnotation annotation state)
+    AndPattern _ left right annotation -> discoverPair annotation left right
+    OrPattern _ left right annotation -> discoverPair annotation left right
+    where
+        discoverAnnotation = discoverType catalog namespace origin
+        discoverPair annotation left right =
+            let afterType = discoverAnnotation annotation state
+                afterLeft = discoverPattern catalog namespace origin left afterType
+             in discoverPattern catalog namespace origin right afterLeft
+
+discoverCapture ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    DiscoveryState ->
+    (Int, Capture ResolvedName Type) ->
+    DiscoveryState
+discoverCapture catalog namespace root state (index, capture) =
+    let origin = root {discoverySites = discoverySites root ++ [CaptureTypeSite index 0], discoverySpan = captureSpan capture}
+        afterType = discoverType catalog namespace origin (captureAnnotation capture) state
+     in maybe afterType (\value -> discoverExpression catalog namespace origin value afterType) (captureInitializer capture)
+
+discoverCallableBody ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    CallableBody ResolvedName Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverCallableBody catalog namespace origin body state = case body of
+    CallableExpressionBody expression -> discoverExpression catalog namespace origin expression state
+    CallableBlockBody block -> discoverBlock catalog namespace origin block state
+
+discoverType ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Type ->
+    DiscoveryState ->
+    DiscoveryState
+discoverType catalog namespace origin valueType state =
+    let visited = state {stateVisitedTypes = stateVisitedTypes state + 1}
+     in case valueType of
+            NamedType name arguments ->
+                let afterRoot = case resolveTemplateTarget namespace name catalog of
+                        Just target
+                            | isClosedType valueType ->
+                                visited
+                                    { stateDemands =
+                                        TemplateSpecializationDemand
+                                            (TemplateApplication target arguments)
+                                            TemplateLayoutDemand
+                                            (renderTemplateDiscoveryOrigin origin)
+                                            : stateDemands visited
+                                    , stateOrigins = origin : stateOrigins visited
+                                    }
+                        _ -> visited {stateIgnoredTypes = stateIgnoredTypes visited + 1}
+                 in foldl'
+                        (\current (index, argument) -> discoverArgument catalog namespace origin index argument current)
+                        afterRoot
+                        (zip [0 ..] arguments)
+            FunctionType parameters result ->
+                let afterParameters =
+                        foldl'
+                            ( \current (index, parameter) ->
+                                discoverType
+                                    catalog
+                                    namespace
+                                    (origin {discoverySites = discoverySites origin ++ [FunctionParameterTypeSite index]})
+                                    parameter
+                                    current
+                            )
+                            visited
+                            (zip [0 ..] parameters)
+                 in discoverType
+                        catalog
+                        namespace
+                        (origin {discoverySites = discoverySites origin ++ [FunctionResultTypeSite]})
+                        result
+                        afterParameters
+            TypeVariable _ -> visited
+            ErrorType -> visited
+
+discoverArgument ::
+    TemplateCatalog ->
+    Maybe QualifiedName ->
+    TemplateDiscoveryOrigin ->
+    Int ->
+    TemplateArgument ->
+    DiscoveryState ->
+    DiscoveryState
+discoverArgument catalog namespace origin index argument state = case argument of
+    TypeTemplateArgument nested ->
+        discoverType
+            catalog
+            namespace
+            (origin {discoverySites = discoverySites origin ++ [NestedTypeArgumentSite index]})
+            nested
+            state
+    ValueTemplateArgument _ -> state
+
+resolveTemplateTarget :: Maybe QualifiedName -> QualifiedName -> TemplateCatalog -> Maybe QualifiedName
+resolveTemplateTarget namespace name (TemplateCatalog declarations)
+    | any ((== name) . templateDeclarationName) declarations = Just name
+    | otherwise = case (namespace, qualifiedNameParts name) of
+        (Just (QualifiedName owner), [_]) ->
+            let qualified = QualifiedName (owner ++ qualifiedNameParts name)
+             in if any ((== qualified) . templateDeclarationName) declarations then Just qualified else Nothing
+        _ -> Nothing
+
+isClosedType :: Type -> Bool
+isClosedType valueType = case valueType of
+    NamedType _ arguments -> all closedArgument arguments
+    FunctionType parameters result -> all isClosedType parameters && isClosedType result
+    TypeVariable _ -> False
+    ErrorType -> False
+    where
+        closedArgument (TypeTemplateArgument nested) = isClosedType nested
+        closedArgument (ValueTemplateArgument (TemplateValueParameter _)) = False
+        closedArgument (ValueTemplateArgument _) = True
+
+-- | Render a stable human-readable provenance path for one discovered demand.
+renderTemplateDiscoveryOrigin :: TemplateDiscoveryOrigin -> String
+renderTemplateDiscoveryOrigin origin =
+    sourceFile (discoverySpan origin)
+        ++ ":"
+        ++ show (sourceLine (sourceStart (discoverySpan origin)))
+        ++ ":"
+        ++ show (sourceColumn (sourceStart (discoverySpan origin)))
+        ++ " in "
+        ++ renderResolved (discoveryDeclaration origin)
+        ++ maybe "" (("." ++) . renderResolved) (discoveryMember origin)
+        ++ " ["
+        ++ intercalate "/" (map renderSite (discoverySites origin))
+        ++ "]"
+
+renderResolved :: ResolvedName -> String
+renderResolved = identifierText . resolvedSpelling
+
+renderSite :: TemplateTypeSite -> String
+renderSite site = case site of
+    DeclarationTypeSite -> "declaration"
+    FunctionSignatureSite -> "signature"
+    ParameterTypeSite index -> "parameter:" ++ show index
+    BindingTypeSite index -> "binding:" ++ show index
+    AssignmentTypeSite index -> "assignment:" ++ show index
+    ReturnTypeSite index -> "return:" ++ show index
+    ConditionTypeSite index -> "condition:" ++ show index
+    ExpressionTypeSite index -> "expression:" ++ show index
+    CallableTypeSite index -> "callable:" ++ show index
+    CaptureTypeSite index initializer -> "capture:" ++ show index ++ ":" ++ show initializer
+    NestedTypeArgumentSite index -> "argument:" ++ show index
+    FunctionParameterTypeSite index -> "function-parameter:" ++ show index
+    FunctionResultTypeSite -> "function-result"
+ src/Progmasoft/Tala/Template/Freshen.hs view
@@ -0,0 +1,442 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Deterministic semantic-name freshening for cloned declarations.
+
+Template specialization copies a typed declaration, but a copy must not keep
+the source declaration's SymbolIds.  Core identifies definitions by SymbolId;
+sharing those ids would make two otherwise distinct specializations collide.
+This module performs the alpha-renaming after substitution and before Core
+lowering.  Spelling and source locations remain unchanged for diagnostics.
+-}
+module Progmasoft.Tala.Template.Freshen
+    ( FreshenResult (..)
+    , maximumSymbolInTypedAST
+    , freshenDeclaration
+    ) where
+
+import Data.Foldable (traverse_)
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+
+-- | Freshened declaration and the old-to-new identities allocated for it.
+data FreshenResult = FreshenResult
+    { freshenedDeclaration :: Declaration ResolvedName Type
+    -- ^ Cloned tree with definitions and references updated.
+    , freshenedSymbols :: [(SymbolId, SymbolId)]
+    -- ^ Identity mapping in ascending old-symbol order.
+    , nextFreshSymbol :: SymbolId
+    -- ^ First identity available to the next clone.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+data FreshState = FreshState
+    { freshNext :: Int
+    , freshMap :: Map SymbolId ResolvedName
+    }
+
+-- | Find the greatest symbol identity present anywhere in a typed AST.
+maximumSymbolInTypedAST :: TypedAST -> SymbolId
+maximumSymbolInTypedAST (TypedAST tree) =
+    SymbolId (maximum (0 : concatMap declarationSymbols (syntaxDeclarations tree)))
+
+-- | Alpha-rename one cloned declaration, preserving spelling and source spans.
+freshenDeclaration :: SymbolId -> Declaration ResolvedName Type -> FreshenResult
+freshenDeclaration (SymbolId first) declaration =
+    let (closed, finalState) = runFresh (freshDeclaration declaration) (FreshState (max 1 first) Map.empty)
+        pairs = [(old, resolvedSymbol replacement) | (old, replacement) <- Map.toAscList (freshMap finalState)]
+     in FreshenResult closed pairs (SymbolId (freshNext finalState))
+
+newtype Fresh a = Fresh {runFresh :: FreshState -> (a, FreshState)}
+
+instance Functor Fresh where
+    fmap transform action = Fresh $ \state ->
+        let (value, next) = runFresh action state
+         in (transform value, next)
+
+instance Applicative Fresh where
+    pure value = Fresh (value,)
+    functionAction <*> valueAction = Fresh $ \state ->
+        let (function, next) = runFresh functionAction state
+            (value, final) = runFresh valueAction next
+         in (function value, final)
+
+instance Monad Fresh where
+    action >>= continuation = Fresh $ \state ->
+        let (value, next) = runFresh action state
+         in runFresh (continuation value) next
+
+freshDefinition :: ResolvedName -> Fresh ResolvedName
+freshDefinition original = Fresh $ \state ->
+    case Map.lookup (resolvedSymbol original) (freshMap state) of
+        Just existing -> (existing, state)
+        Nothing ->
+            let replacement = ResolvedName (SymbolId (freshNext state)) (resolvedSpelling original)
+                nextState =
+                    state
+                        { freshNext = freshNext state + 1
+                        , freshMap = Map.insert (resolvedSymbol original) replacement (freshMap state)
+                        }
+             in (replacement, nextState)
+
+freshReference :: ResolvedName -> Fresh ResolvedName
+freshReference original = Fresh $ \state ->
+    (Map.findWithDefault original (resolvedSymbol original) (freshMap state), state)
+
+freshDeclaration :: Declaration ResolvedName Type -> Fresh (Declaration ResolvedName Type)
+freshDeclaration declaration = case declaration of
+    TypeDeclaration spanValue name annotation members -> do
+        closedName <- freshDefinition name
+        closedAnnotation <- freshType annotation
+        -- A type scope is recursive: a member body may call a member declared
+        -- later in source order. Reserve every immediate member identity before
+        -- rewriting any body so forward calls and mutually recursive methods
+        -- cannot retain the template declaration's old SymbolIds.
+        traverse_ reserveDeclarationName members
+        closedMembers <- traverse freshDeclaration members
+        pure (TypeDeclaration spanValue closedName closedAnnotation closedMembers)
+    FunctionDeclaration spanValue name annotation returnSyntax parameters body isStatic access -> do
+        closedName <- freshDefinition name
+        -- Parameters are definitions and must be allocated before the body is
+        -- walked, otherwise references in the body retain the template copy's
+        -- old semantic identity.
+        closedParameters <- traverse freshParameterDefinition parameters
+        closedAnnotation <- freshType annotation
+        closedBody <- freshBlock body
+        pure
+            ( FunctionDeclaration
+                spanValue
+                closedName
+                closedAnnotation
+                returnSyntax
+                closedParameters
+                closedBody
+                isStatic
+                access
+            )
+    TemplateTypeDeclaration {} ->
+        -- A nested template introduces a separate substitution/freshening
+        -- environment. It will be selected independently when demanded.
+        pure declaration
+    -- An enum has no body and no template parameter in it.
+    EnumDeclaration {} -> pure declaration
+
+reserveDeclarationName :: Declaration ResolvedName Type -> Fresh ()
+reserveDeclarationName declaration = case declaration of
+    TypeDeclaration _ name _ _ -> freshDefinition name >> pure ()
+    FunctionDeclaration _ name _ _ _ _ _ _ -> freshDefinition name >> pure ()
+    TemplateTypeDeclaration {} ->
+        -- Nested templates own a separate specialization environment and must
+        -- not leak definitions into the enclosing concrete type's map.
+        pure ()
+    EnumDeclaration {} -> pure ()
+
+freshParameterDefinition :: Parameter ResolvedName Type -> Fresh (Parameter ResolvedName Type)
+freshParameterDefinition parameter = do
+    name <- freshDefinition (parameterName parameter)
+    annotation <- freshType (parameterAnnotation parameter)
+    pure
+        ( Parameter
+            (parameterSpan parameter)
+            name
+            annotation
+            (parameterTypeSyntax parameter)
+        )
+
+freshBlock :: Block ResolvedName Type -> Fresh (Block ResolvedName Type)
+freshBlock (Block statements) = Block <$> traverse freshStatement statements
+
+freshStatement :: Statement ResolvedName Type -> Fresh (Statement ResolvedName Type)
+freshStatement statement = case statement of
+    BindingStatement spanValue kind syntax name annotation value -> do
+        -- Initializers are evaluated in the surrounding scope. Allocate the
+        -- local only after rewriting its initializer so self-shadowing does
+        -- not accidentally rewrite an outer reference.
+        closedValue <- freshExpression value
+        closedName <- freshDefinition name
+        closedAnnotation <- freshType annotation
+        pure (BindingStatement spanValue kind syntax closedName closedAnnotation closedValue)
+    AssignmentStatement spanValue name annotation value ->
+        AssignmentStatement spanValue
+            <$> freshReference name
+            <*> freshType annotation
+            <*> freshExpression value
+    ReturnStatement spanValue value -> ReturnStatement spanValue <$> traverse freshExpression value
+    IfStatement spanValue condition trueBlock falseBlock ->
+        IfStatement spanValue
+            <$> freshExpression condition
+            <*> freshBlock trueBlock
+            <*> traverse freshBlock falseBlock
+    WhileStatement spanValue condition body ->
+        WhileStatement spanValue <$> freshExpression condition <*> freshBlock body
+    DoWhileStatement spanValue body condition ->
+        DoWhileStatement spanValue <$> freshBlock body <*> freshExpression condition
+    ForStatement spanValue initializer condition updates body -> do
+        closedInitializer <- traverse freshStatement initializer
+        closedCondition <- traverse freshExpression condition
+        closedUpdates <- traverse freshStatement updates
+        closedBody <- freshBlock body
+        pure (ForStatement spanValue closedInitializer closedCondition closedUpdates closedBody)
+    ForEachStatement spanValue kind syntax name annotation source body -> do
+        closedSource <- freshExpression source
+        closedName <- freshDefinition name
+        closedAnnotation <- freshType annotation
+        closedBody <- freshBlock body
+        pure (ForEachStatement spanValue kind syntax closedName closedAnnotation closedSource closedBody)
+    IncrementStatement spanValue name annotation ->
+        IncrementStatement spanValue <$> freshReference name <*> freshType annotation
+    CompoundAssignmentStatement spanValue operator name annotation value ->
+        CompoundAssignmentStatement spanValue operator
+            <$> freshReference name
+            <*> freshType annotation
+            <*> freshExpression value
+    DiscardStatement spanValue value -> DiscardStatement spanValue <$> freshExpression value
+    BreakStatement spanValue value -> BreakStatement spanValue <$> traverse freshExpression value
+    ContinueStatement spanValue -> pure (ContinueStatement spanValue)
+    GuardStatement spanValue condition block ->
+        GuardStatement spanValue <$> freshExpression condition <*> freshBlock block
+    BlockStatement spanValue block -> BlockStatement spanValue <$> freshBlock block
+    ExpressionStatement spanValue value terminated ->
+        ExpressionStatement spanValue <$> freshExpression value <*> pure terminated
+
+freshExpression :: Expression ResolvedName Type -> Fresh (Expression ResolvedName Type)
+freshExpression expression = case expression of
+    NameExpression spanValue name annotation ->
+        NameExpression spanValue <$> freshReference name <*> freshType annotation
+    LiteralExpression spanValue literal annotation ->
+        LiteralExpression spanValue literal <$> freshType annotation
+    MemberAccessExpression spanValue receiver member annotation ->
+        MemberAccessExpression spanValue <$> freshExpression receiver <*> pure member <*> freshType annotation
+    MethodReferenceExpression spanValue receiver member annotation ->
+        MethodReferenceExpression spanValue <$> freshExpression receiver <*> pure member <*> freshType annotation
+    CallExpression spanValue callee arguments annotation ->
+        CallExpression spanValue
+            <$> freshExpression callee
+            <*> traverse freshExpression arguments
+            <*> freshType annotation
+    UnaryExpression spanValue operator value annotation ->
+        UnaryExpression spanValue operator <$> freshExpression value <*> freshType annotation
+    BinaryExpression spanValue operator left right annotation ->
+        BinaryExpression spanValue operator
+            <$> freshExpression left
+            <*> freshExpression right
+            <*> freshType annotation
+    IsPatternExpression spanValue subject patternValue annotation ->
+        IsPatternExpression spanValue
+            <$> freshExpression subject
+            <*> freshPattern patternValue
+            <*> freshType annotation
+    ConditionalExpression spanValue condition first second annotation ->
+        ConditionalExpression spanValue
+            <$> freshExpression condition
+            <*> freshExpression first
+            <*> freshExpression second
+            <*> freshType annotation
+    CoalesceExpression spanValue left fallback annotation ->
+        CoalesceExpression spanValue
+            <$> freshExpression left
+            <*> freshExpression fallback
+            <*> freshType annotation
+    AssignmentExpression spanValue operator name value annotation ->
+        AssignmentExpression spanValue operator
+            <$> freshReference name
+            <*> freshExpression value
+            <*> freshType annotation
+    IncrementExpression spanValue isPrefix name annotation ->
+        IncrementExpression spanValue isPrefix <$> freshReference name <*> freshType annotation
+    LoopExpression spanValue loop annotation ->
+        LoopExpression spanValue <$> freshStatement loop <*> freshType annotation
+    BlockExpression spanValue block annotation ->
+        BlockExpression spanValue <$> freshBlock block <*> freshType annotation
+    -- The subjects are evaluated in the surrounding scope. Each arm then
+    -- allocates the locals its patterns bind before its guard and its body
+    -- refer to them.
+    MatchExpression spanValue subjects arms annotation ->
+        MatchExpression spanValue
+            <$> traverse freshExpression subjects
+            <*> traverse (traverseMatchArm (traverseMatchPattern freshDefinition freshType) freshExpression) arms
+            <*> freshType annotation
+    CallableExpression spanValue explicit captures parameters body annotation -> do
+        closedCaptures <- traverse freshCaptureDefinition captures
+        closedParameters <- traverse freshParameterDefinition parameters
+        closedBody <- freshCallableBody body
+        closedAnnotation <- freshType annotation
+        pure (CallableExpression spanValue explicit closedCaptures closedParameters closedBody closedAnnotation)
+
+freshPattern :: Pattern ResolvedName Type -> Fresh (Pattern ResolvedName Type)
+freshPattern patternValue = case patternValue of
+    WildcardPattern spanValue annotation -> WildcardPattern spanValue <$> freshType annotation
+    NullPattern spanValue annotation -> NullPattern spanValue <$> freshType annotation
+    LiteralPattern spanValue literal annotation ->
+        LiteralPattern spanValue literal <$> freshType annotation
+    TypePattern spanValue syntax annotation -> TypePattern spanValue syntax <$> freshType annotation
+    RelationalPattern spanValue operator literal annotation ->
+        RelationalPattern spanValue operator literal <$> freshType annotation
+    NotPattern spanValue nested annotation ->
+        NotPattern spanValue <$> freshPattern nested <*> freshType annotation
+    AndPattern spanValue left right annotation ->
+        AndPattern spanValue <$> freshPattern left <*> freshPattern right <*> freshType annotation
+    OrPattern spanValue left right annotation ->
+        OrPattern spanValue <$> freshPattern left <*> freshPattern right <*> freshType annotation
+
+freshCaptureDefinition :: Capture ResolvedName Type -> Fresh (Capture ResolvedName Type)
+freshCaptureDefinition capture = do
+    -- The initializer belongs to the outer scope, just like a local binding
+    -- initializer. The capture name becomes visible only in the callable.
+    initializer <- traverse freshExpression (captureInitializer capture)
+    name <- freshDefinition (captureName capture)
+    annotation <- freshType (captureAnnotation capture)
+    pure (Capture (captureSpan capture) (captureMode capture) name annotation initializer)
+
+freshCallableBody :: CallableBody ResolvedName Type -> Fresh (CallableBody ResolvedName Type)
+freshCallableBody body = case body of
+    CallableExpressionBody expression -> CallableExpressionBody <$> freshExpression expression
+    CallableBlockBody block -> CallableBlockBody <$> freshBlock block
+
+freshType :: Type -> Fresh Type
+freshType valueType = case valueType of
+    NamedType name arguments -> NamedType name <$> traverse freshArgument arguments
+    FunctionType parameters result -> FunctionType <$> traverse freshType parameters <*> freshType result
+    TypeVariable name -> TypeVariable <$> freshReference name
+    ErrorType -> pure ErrorType
+
+freshArgument :: TemplateArgument -> Fresh TemplateArgument
+freshArgument argument = case argument of
+    TypeTemplateArgument valueType -> TypeTemplateArgument <$> freshType valueType
+    ValueTemplateArgument value -> ValueTemplateArgument <$> freshValue value
+
+freshValue :: TemplateValue -> Fresh TemplateValue
+freshValue value = case value of
+    TemplateValueParameter name -> TemplateValueParameter <$> freshReference name
+    _ -> pure value
+
+declarationSymbols :: Declaration ResolvedName Type -> [Int]
+declarationSymbols declaration = case declaration of
+    TypeDeclaration _ name annotation members ->
+        nameSymbol name : typeSymbols annotation ++ concatMap declarationSymbols members
+    FunctionDeclaration _ name annotation _ parameters body _ _ ->
+        nameSymbol name
+            : typeSymbols annotation
+            ++ concatMap parameterSymbols parameters
+            ++ blockSymbols body
+    TemplateTypeDeclaration _ name annotation parameters members ->
+        nameSymbol name
+            : typeSymbols annotation
+            ++ concatMap templateParameterSymbols parameters
+            ++ concatMap declarationSymbols members
+    EnumDeclaration _ name annotation _ _ -> nameSymbol name : typeSymbols annotation
+
+templateParameterSymbols :: TemplateParameter ResolvedName Type -> [Int]
+templateParameterSymbols parameter =
+    nameSymbol (templateParameterName parameter) : typeSymbols (templateParameterAnnotation parameter)
+
+parameterSymbols :: Parameter ResolvedName Type -> [Int]
+parameterSymbols parameter = nameSymbol (parameterName parameter) : typeSymbols (parameterAnnotation parameter)
+
+blockSymbols :: Block ResolvedName Type -> [Int]
+blockSymbols (Block statements) = concatMap statementSymbols statements
+
+statementSymbols :: Statement ResolvedName Type -> [Int]
+statementSymbols statement = case statement of
+    BindingStatement _ _ _ name annotation value -> nameSymbol name : typeSymbols annotation ++ expressionSymbols value
+    AssignmentStatement _ name annotation value -> nameSymbol name : typeSymbols annotation ++ expressionSymbols value
+    ReturnStatement _ value -> maybe [] expressionSymbols value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionSymbols condition ++ blockSymbols trueBlock ++ maybe [] blockSymbols falseBlock
+    WhileStatement _ condition body -> expressionSymbols condition ++ blockSymbols body
+    DoWhileStatement _ body condition -> blockSymbols body ++ expressionSymbols condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementSymbols initializer
+            ++ maybe [] expressionSymbols condition
+            ++ concatMap statementSymbols updates
+            ++ blockSymbols body
+    ForEachStatement _ _ _ name annotation source body ->
+        nameSymbol name : typeSymbols annotation ++ expressionSymbols source ++ blockSymbols body
+    IncrementStatement _ name annotation -> nameSymbol name : typeSymbols annotation
+    CompoundAssignmentStatement _ _ name annotation value ->
+        nameSymbol name : typeSymbols annotation ++ expressionSymbols value
+    DiscardStatement _ value -> expressionSymbols value
+    BreakStatement _ value -> maybe [] expressionSymbols value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionSymbols condition ++ blockSymbols block
+    BlockStatement _ block -> blockSymbols block
+    ExpressionStatement _ value _ -> expressionSymbols value
+
+expressionSymbols :: Expression ResolvedName Type -> [Int]
+expressionSymbols expression = case expression of
+    NameExpression _ name annotation -> nameSymbol name : typeSymbols annotation
+    LiteralExpression _ _ annotation -> typeSymbols annotation
+    MemberAccessExpression _ receiver _ annotation -> expressionSymbols receiver ++ typeSymbols annotation
+    MethodReferenceExpression _ receiver _ annotation -> expressionSymbols receiver ++ typeSymbols annotation
+    CallExpression _ callee arguments annotation ->
+        expressionSymbols callee ++ concatMap expressionSymbols arguments ++ typeSymbols annotation
+    UnaryExpression _ _ value annotation -> expressionSymbols value ++ typeSymbols annotation
+    BinaryExpression _ _ left right annotation ->
+        expressionSymbols left ++ expressionSymbols right ++ typeSymbols annotation
+    IsPatternExpression _ subject patternValue annotation ->
+        expressionSymbols subject ++ patternSymbols patternValue ++ typeSymbols annotation
+    ConditionalExpression _ condition first second annotation ->
+        concatMap expressionSymbols [condition, first, second] ++ typeSymbols annotation
+    CoalesceExpression _ left fallback annotation ->
+        expressionSymbols left ++ expressionSymbols fallback ++ typeSymbols annotation
+    AssignmentExpression _ _ name value annotation ->
+        nameSymbol name : expressionSymbols value ++ typeSymbols annotation
+    IncrementExpression _ _ name annotation -> nameSymbol name : typeSymbols annotation
+    LoopExpression _ loop annotation -> statementSymbols loop ++ typeSymbols annotation
+    BlockExpression _ block annotation -> blockSymbols block ++ typeSymbols annotation
+    MatchExpression _ subjects arms annotation ->
+        concatMap expressionSymbols (subjects ++ concatMap matchArmExpressions arms)
+            ++ concat
+                [ maybe [] ((: []) . nameSymbol) (matchPatternBinding patternValue)
+                    ++ typeSymbols (matchPatternAnnotation patternValue)
+                | arm <- arms
+                , patternValue <- matchArmPatterns arm
+                ]
+            ++ typeSymbols annotation
+    CallableExpression _ _ captures parameters body annotation ->
+        concatMap captureSymbols captures
+            ++ concatMap parameterSymbols parameters
+            ++ callableBodySymbols body
+            ++ typeSymbols annotation
+
+patternSymbols :: Pattern ResolvedName Type -> [Int]
+patternSymbols patternValue = case patternValue of
+    WildcardPattern _ annotation -> typeSymbols annotation
+    NullPattern _ annotation -> typeSymbols annotation
+    LiteralPattern _ _ annotation -> typeSymbols annotation
+    TypePattern _ _ annotation -> typeSymbols annotation
+    RelationalPattern _ _ _ annotation -> typeSymbols annotation
+    NotPattern _ nested annotation -> patternSymbols nested ++ typeSymbols annotation
+    AndPattern _ left right annotation ->
+        patternSymbols left ++ patternSymbols right ++ typeSymbols annotation
+    OrPattern _ left right annotation ->
+        patternSymbols left ++ patternSymbols right ++ typeSymbols annotation
+
+captureSymbols :: Capture ResolvedName Type -> [Int]
+captureSymbols capture =
+    nameSymbol (captureName capture)
+        : typeSymbols (captureAnnotation capture)
+        ++ maybe [] expressionSymbols (captureInitializer capture)
+
+callableBodySymbols :: CallableBody ResolvedName Type -> [Int]
+callableBodySymbols body = case body of
+    CallableExpressionBody expression -> expressionSymbols expression
+    CallableBlockBody block -> blockSymbols block
+
+typeSymbols :: Type -> [Int]
+typeSymbols valueType = case valueType of
+    NamedType _ arguments -> concatMap argumentSymbols arguments
+    FunctionType parameters result -> concatMap typeSymbols (result : parameters)
+    TypeVariable name -> [nameSymbol name]
+    ErrorType -> []
+
+argumentSymbols :: TemplateArgument -> [Int]
+argumentSymbols argument = case argument of
+    TypeTemplateArgument valueType -> typeSymbols valueType
+    ValueTemplateArgument (TemplateValueParameter name) -> [nameSymbol name]
+    ValueTemplateArgument _ -> []
+
+nameSymbol :: ResolvedName -> Int
+nameSymbol = symbolIdValue . resolvedSymbol
+ src/Progmasoft/Tala/Template/Instantiation.hs view
@@ -0,0 +1,318 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Typed declaration instantiation after template application binding.
+
+This pass does not choose overloads, constraints, or a native symbol spelling.
+It has one narrow responsibility: replace the semantic template variables in
+an already typed declaration tree.  The output is an ordinary type declaration
+that the existing Desugarer can lower without learning template syntax.
+-}
+module Progmasoft.Tala.Template.Instantiation
+    ( TemplateInstantiationError (..)
+    , instantiateTemplateType
+    , instantiateMember
+    , instantiateParameter
+    , instantiateBlock
+    , instantiateStatement
+    , instantiateExpression
+    , instantiateCallableBody
+    , renderTemplateInstantiationError
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Template.Application
+
+-- | Failure to instantiate a typed template declaration or one of its nodes.
+data TemplateInstantiationError
+    = -- | The supplied declaration is not a template type.
+      ExpectedTemplateTypeDeclaration
+    | -- | Binding and declaration identities differ.
+      TemplateBindingTargetsDifferentDeclaration SymbolId SymbolId
+    | -- | A bound type or value could not be substituted.
+      TemplateTypeSubstitutionFailed TemplateApplicationError
+    deriving (Eq, Ord, Read, Show)
+
+-- | Close a template type declaration using its previously validated binding.
+instantiateTemplateType ::
+    TemplateBinding ->
+    Declaration ResolvedName Type ->
+    Either TemplateInstantiationError (Declaration ResolvedName Type)
+instantiateTemplateType binding declaration = case declaration of
+    TemplateTypeDeclaration spanValue name annotation _ members
+        | resolvedSymbol name /= templateDeclarationSymbol descriptor ->
+            Left
+                ( TemplateBindingTargetsDifferentDeclaration
+                    (templateDeclarationSymbol descriptor)
+                    (resolvedSymbol name)
+                )
+        | otherwise -> do
+            closedAnnotation <- instantiateType binding annotation
+            closedMembers <- traverse (instantiateMember binding) members
+            pure (TypeDeclaration spanValue name closedAnnotation closedMembers)
+    _ -> Left ExpectedTemplateTypeDeclaration
+    where
+        descriptor = templateBindingDeclaration binding
+
+-- | Substitute template variables in one member declaration recursively.
+instantiateMember ::
+    TemplateBinding ->
+    Declaration ResolvedName Type ->
+    Either TemplateInstantiationError (Declaration ResolvedName Type)
+instantiateMember binding declaration = case declaration of
+    FunctionDeclaration spanValue name annotation returnSyntax parameters body isStatic access -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedParameters <- traverse (instantiateParameter binding) parameters
+        closedBody <- instantiateBlock binding body
+        pure
+            ( FunctionDeclaration
+                spanValue
+                name
+                closedAnnotation
+                returnSyntax
+                closedParameters
+                closedBody
+                isStatic
+                access
+            )
+    TypeDeclaration spanValue name annotation members -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedMembers <- traverse (instantiateMember binding) members
+        pure (TypeDeclaration spanValue name closedAnnotation closedMembers)
+    TemplateTypeDeclaration {} ->
+        -- Nested templates own a distinct parameter environment. Applying the
+        -- outer binding blindly would capture same-spelled inner parameters.
+        -- Nested instantiation will be selected independently by the planner.
+        pure declaration
+    EnumDeclaration {} -> pure declaration
+
+-- | Instantiate one parameter annotation while preserving its source syntax.
+instantiateParameter ::
+    TemplateBinding ->
+    Parameter ResolvedName Type ->
+    Either TemplateInstantiationError (Parameter ResolvedName Type)
+instantiateParameter binding parameter = do
+    closedType <- instantiateType binding (parameterAnnotation parameter)
+    pure
+        ( Parameter
+            (parameterSpan parameter)
+            (parameterName parameter)
+            closedType
+            (parameterTypeSyntax parameter)
+        )
+
+-- | Instantiate every statement in a block without changing statement order.
+instantiateBlock ::
+    TemplateBinding ->
+    Block ResolvedName Type ->
+    Either TemplateInstantiationError (Block ResolvedName Type)
+instantiateBlock binding (Block statements) = Block <$> traverse (instantiateStatement binding) statements
+
+-- | Substitute all types nested in one statement and its expressions.
+instantiateStatement ::
+    TemplateBinding ->
+    Statement ResolvedName Type ->
+    Either TemplateInstantiationError (Statement ResolvedName Type)
+instantiateStatement binding statement = case statement of
+    BindingStatement spanValue kind syntax name annotation value -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedValue <- instantiateExpression binding value
+        pure (BindingStatement spanValue kind syntax name closedAnnotation closedValue)
+    AssignmentStatement spanValue name annotation value -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedValue <- instantiateExpression binding value
+        pure (AssignmentStatement spanValue name closedAnnotation closedValue)
+    ReturnStatement spanValue value ->
+        ReturnStatement spanValue <$> traverse (instantiateExpression binding) value
+    IfStatement spanValue condition trueBlock falseBlock -> do
+        closedCondition <- instantiateExpression binding condition
+        closedTrue <- instantiateBlock binding trueBlock
+        closedFalse <- traverse (instantiateBlock binding) falseBlock
+        pure (IfStatement spanValue closedCondition closedTrue closedFalse)
+    WhileStatement spanValue condition body -> do
+        closedCondition <- instantiateExpression binding condition
+        closedBody <- instantiateBlock binding body
+        pure (WhileStatement spanValue closedCondition closedBody)
+    DoWhileStatement spanValue body condition -> do
+        closedBody <- instantiateBlock binding body
+        closedCondition <- instantiateExpression binding condition
+        pure (DoWhileStatement spanValue closedBody closedCondition)
+    ForStatement spanValue initializer condition updates body -> do
+        closedInitializer <- traverse (instantiateStatement binding) initializer
+        closedCondition <- traverse (instantiateExpression binding) condition
+        closedUpdates <- traverse (instantiateStatement binding) updates
+        closedBody <- instantiateBlock binding body
+        pure (ForStatement spanValue closedInitializer closedCondition closedUpdates closedBody)
+    ForEachStatement spanValue kind syntax name annotation source body -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedSource <- instantiateExpression binding source
+        closedBody <- instantiateBlock binding body
+        pure (ForEachStatement spanValue kind syntax name closedAnnotation closedSource closedBody)
+    IncrementStatement spanValue name annotation ->
+        IncrementStatement spanValue name <$> instantiateType binding annotation
+    CompoundAssignmentStatement spanValue operator name annotation value -> do
+        closedAnnotation <- instantiateType binding annotation
+        closedValue <- instantiateExpression binding value
+        pure (CompoundAssignmentStatement spanValue operator name closedAnnotation closedValue)
+    DiscardStatement spanValue value ->
+        DiscardStatement spanValue <$> instantiateExpression binding value
+    BreakStatement spanValue value ->
+        BreakStatement spanValue <$> traverse (instantiateExpression binding) value
+    ContinueStatement spanValue -> pure (ContinueStatement spanValue)
+    GuardStatement spanValue condition block ->
+        GuardStatement spanValue <$> instantiateExpression binding condition <*> instantiateBlock binding block
+    BlockStatement spanValue block -> BlockStatement spanValue <$> instantiateBlock binding block
+    ExpressionStatement spanValue value terminated ->
+        ExpressionStatement spanValue <$> instantiateExpression binding value <*> pure terminated
+
+-- | Substitute every type annotation in an expression tree.
+instantiateExpression ::
+    TemplateBinding ->
+    Expression ResolvedName Type ->
+    Either TemplateInstantiationError (Expression ResolvedName Type)
+instantiateExpression binding expression = case expression of
+    NameExpression spanValue name annotation ->
+        NameExpression spanValue name <$> instantiateType binding annotation
+    LiteralExpression spanValue literal annotation ->
+        LiteralExpression spanValue literal <$> instantiateType binding annotation
+    MemberAccessExpression spanValue receiver member annotation -> do
+        closedReceiver <- instantiateExpression binding receiver
+        closedAnnotation <- instantiateType binding annotation
+        pure (MemberAccessExpression spanValue closedReceiver member closedAnnotation)
+    MethodReferenceExpression spanValue receiver member annotation -> do
+        closedReceiver <- instantiateExpression binding receiver
+        closedAnnotation <- instantiateType binding annotation
+        pure (MethodReferenceExpression spanValue closedReceiver member closedAnnotation)
+    CallExpression spanValue callee arguments annotation -> do
+        closedCallee <- instantiateExpression binding callee
+        closedArguments <- traverse (instantiateExpression binding) arguments
+        closedAnnotation <- instantiateType binding annotation
+        pure (CallExpression spanValue closedCallee closedArguments closedAnnotation)
+    UnaryExpression spanValue operator value annotation -> do
+        closedValue <- instantiateExpression binding value
+        closedAnnotation <- instantiateType binding annotation
+        pure (UnaryExpression spanValue operator closedValue closedAnnotation)
+    BinaryExpression spanValue operator left right annotation -> do
+        closedLeft <- instantiateExpression binding left
+        closedRight <- instantiateExpression binding right
+        closedAnnotation <- instantiateType binding annotation
+        pure (BinaryExpression spanValue operator closedLeft closedRight closedAnnotation)
+    IsPatternExpression spanValue subject patternValue annotation -> do
+        closedSubject <- instantiateExpression binding subject
+        closedPattern <- instantiatePattern binding patternValue
+        closedAnnotation <- instantiateType binding annotation
+        pure (IsPatternExpression spanValue closedSubject closedPattern closedAnnotation)
+    ConditionalExpression spanValue condition first second annotation -> do
+        closedCondition <- instantiateExpression binding condition
+        closedFirst <- instantiateExpression binding first
+        closedSecond <- instantiateExpression binding second
+        closedAnnotation <- instantiateType binding annotation
+        pure (ConditionalExpression spanValue closedCondition closedFirst closedSecond closedAnnotation)
+    CoalesceExpression spanValue left fallback annotation -> do
+        closedLeft <- instantiateExpression binding left
+        closedFallback <- instantiateExpression binding fallback
+        closedAnnotation <- instantiateType binding annotation
+        pure (CoalesceExpression spanValue closedLeft closedFallback closedAnnotation)
+    AssignmentExpression spanValue operator name value annotation -> do
+        closedValue <- instantiateExpression binding value
+        closedAnnotation <- instantiateType binding annotation
+        pure (AssignmentExpression spanValue operator name closedValue closedAnnotation)
+    IncrementExpression spanValue isPrefix name annotation ->
+        IncrementExpression spanValue isPrefix name <$> instantiateType binding annotation
+    LoopExpression spanValue loop annotation ->
+        LoopExpression spanValue <$> instantiateStatement binding loop <*> instantiateType binding annotation
+    BlockExpression spanValue block annotation ->
+        BlockExpression spanValue <$> instantiateBlock binding block <*> instantiateType binding annotation
+    MatchExpression spanValue subjects arms annotation ->
+        MatchExpression spanValue
+            <$> traverse (instantiateExpression binding) subjects
+            <*> traverse
+                (traverseMatchArm (traverseMatchPattern pure (instantiateType binding)) (instantiateExpression binding))
+                arms
+            <*> instantiateType binding annotation
+    CallableExpression spanValue explicit captures parameters body annotation -> do
+        closedCaptures <- traverse (instantiateCapture binding) captures
+        closedParameters <- traverse (instantiateParameter binding) parameters
+        closedBody <- instantiateCallableBody binding body
+        closedAnnotation <- instantiateType binding annotation
+        pure
+            ( CallableExpression
+                spanValue
+                explicit
+                closedCaptures
+                closedParameters
+                closedBody
+                closedAnnotation
+            )
+
+instantiatePattern ::
+    TemplateBinding ->
+    Pattern ResolvedName Type ->
+    Either TemplateInstantiationError (Pattern ResolvedName Type)
+instantiatePattern binding patternValue = case patternValue of
+    WildcardPattern spanValue annotation ->
+        WildcardPattern spanValue <$> instantiateType binding annotation
+    NullPattern spanValue annotation -> NullPattern spanValue <$> instantiateType binding annotation
+    LiteralPattern spanValue literal annotation ->
+        LiteralPattern spanValue literal <$> instantiateType binding annotation
+    TypePattern spanValue syntax annotation ->
+        TypePattern spanValue syntax <$> instantiateType binding annotation
+    RelationalPattern spanValue operator literal annotation ->
+        RelationalPattern spanValue operator literal <$> instantiateType binding annotation
+    NotPattern spanValue nested annotation ->
+        NotPattern spanValue
+            <$> instantiatePattern binding nested
+            <*> instantiateType binding annotation
+    AndPattern spanValue left right annotation ->
+        AndPattern spanValue
+            <$> instantiatePattern binding left
+            <*> instantiatePattern binding right
+            <*> instantiateType binding annotation
+    OrPattern spanValue left right annotation ->
+        OrPattern spanValue
+            <$> instantiatePattern binding left
+            <*> instantiatePattern binding right
+            <*> instantiateType binding annotation
+
+instantiateCapture ::
+    TemplateBinding ->
+    Capture ResolvedName Type ->
+    Either TemplateInstantiationError (Capture ResolvedName Type)
+instantiateCapture binding capture = do
+    closedAnnotation <- instantiateType binding (captureAnnotation capture)
+    closedInitializer <- traverse (instantiateExpression binding) (captureInitializer capture)
+    pure
+        ( Capture
+            (captureSpan capture)
+            (captureMode capture)
+            (captureName capture)
+            closedAnnotation
+            closedInitializer
+        )
+
+-- | Instantiate the expression or block body of a callable.
+instantiateCallableBody ::
+    TemplateBinding ->
+    CallableBody ResolvedName Type ->
+    Either TemplateInstantiationError (CallableBody ResolvedName Type)
+instantiateCallableBody binding body = case body of
+    CallableExpressionBody expression ->
+        CallableExpressionBody <$> instantiateExpression binding expression
+    CallableBlockBody block -> CallableBlockBody <$> instantiateBlock binding block
+
+instantiateType :: TemplateBinding -> Type -> Either TemplateInstantiationError Type
+instantiateType binding valueType = case substituteType binding valueType of
+    Right closed -> Right closed
+    Left issue -> Left (TemplateTypeSubstitutionFailed issue)
+
+-- | Render a declaration mismatch or template substitution failure.
+renderTemplateInstantiationError :: TemplateInstantiationError -> String
+renderTemplateInstantiationError issue = case issue of
+    ExpectedTemplateTypeDeclaration -> "template instantiation requires a typed template type declaration"
+    TemplateBindingTargetsDifferentDeclaration expected actual ->
+        "template binding targets symbol "
+            ++ show (symbolIdValue expected)
+            ++ ", but the declaration has symbol "
+            ++ show (symbolIdValue actual)
+    TemplateTypeSubstitutionFailed problem -> renderTemplateApplicationError problem
+ src/Progmasoft/Tala/Template/Mangling.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
+
+{- |
+Reversible-by-construction ASCII names for internal template emission.
+
+This is not a public ABI promise. It gives Core/native handoff a deterministic,
+collision-free spelling while the public ABI and linker compatibility policy
+are still being designed. Every structural boundary is tagged and counted;
+Unicode identifiers are encoded as fixed-width scalar values.
+-}
+module Progmasoft.Tala.Template.Mangling
+    ( TemplateMangleLimits (..)
+    , defaultTemplateMangleLimits
+    , TemplateMangleError (..)
+    , MangledTemplateType (..)
+    , MangledTemplateMember (..)
+    , mangleTemplateType
+    , mangleTemplateMember
+    , mangleTemplateMembers
+    , validMangledTemplateSymbol
+    , renderTemplateMangleError
+    ) where
+
+import Data.Char (isAsciiLower, isAsciiUpper, isDigit, ord)
+import Numeric (showHex)
+import Progmasoft.Tala.AST
+
+-- | Resource bounds applied while producing deterministic internal names.
+data TemplateMangleLimits = TemplateMangleLimits
+    { maximumMangledTypeDepth :: Int
+    -- ^ Maximum nested type-argument depth.
+    , maximumMangledSymbolLength :: Int
+    -- ^ Maximum emitted ASCII symbol length.
+    , maximumMangledNameParts :: Int
+    -- ^ Maximum qualified-name segment count.
+    , maximumMangledArguments :: Int
+    -- ^ Maximum arguments or function parameters.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Conservative finite limits used by normal compiler specialization.
+defaultTemplateMangleLimits :: TemplateMangleLimits
+defaultTemplateMangleLimits = TemplateMangleLimits 128 65535 1024 4096
+
+-- | Failure while converting a closed template type or member to an ASCII name.
+data TemplateMangleError
+    = -- | One or more configured bounds are invalid.
+      InvalidTemplateMangleLimits String
+    | -- | An unresolved template variable remains.
+      OpenTypeCannotBeMangled ResolvedName
+    | -- | ErrorType has no stable concrete identity.
+      ErrorTypeCannotBeMangled
+    | -- | A nominal type has no name segments.
+      EmptyQualifiedNameCannotBeMangled
+    | -- | A name segment has no Unicode scalars.
+      EmptyIdentifierCannotBeMangled
+    | -- | A name contains a non-scalar code point.
+      InvalidIdentifierScalar Integer
+    | -- | A character argument is not a Unicode scalar.
+      InvalidCharacterTemplateValue Integer
+    | -- | Nested type structure exceeded its configured bound.
+      MangledTypeDepthExceeded Int
+    | -- | Qualified name exceeded its segment bound.
+      MangledNamePartLimitExceeded Int Int
+    | -- | Type argument or parameter list exceeded its bound.
+      MangledArgumentLimitExceeded Int Int
+    | -- | Final encoded name exceeded its length bound.
+      MangledSymbolLengthExceeded Int Int
+    | -- | The input is not a supported member declaration.
+      ExpectedMangleableMemberDeclaration
+    deriving (Eq, Ord, Read, Show)
+
+-- | Encoded nominal template type identity used only at internal compiler boundaries.
+newtype MangledTemplateType = MangledTemplateType
+    { mangledTemplateTypeText :: String
+    -- ^ Reversible, deterministic ASCII symbol spelling.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Encoded member identity paired with its original semantic identity.
+data MangledTemplateMember = MangledTemplateMember
+    { mangledMemberSourceSymbol :: SymbolId
+    -- ^ Resolved source symbol for diagnostics and mapping.
+    , mangledMemberSourceName :: Identifier
+    -- ^ Original source spelling.
+    , mangledMemberText :: String
+    -- ^ Reversible internal ASCII name.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Mangle a closed type under explicit recursion and output-size limits.
+mangleTemplateType :: TemplateMangleLimits -> Type -> Either TemplateMangleError MangledTemplateType
+mangleTemplateType limits valueType = do
+    validateLimits limits
+    payload <- encodeType limits 0 valueType
+    let symbol = "_TAT1_T" ++ payload
+    enforceLength limits symbol
+    pure (MangledTemplateType symbol)
+
+-- | Mangle one method or nested type using its owner type as the namespace.
+mangleTemplateMember ::
+    TemplateMangleLimits ->
+    Type ->
+    Declaration ResolvedName Type ->
+    Either TemplateMangleError MangledTemplateMember
+mangleTemplateMember limits owner member = do
+    MangledTemplateType ownerName <- mangleTemplateType limits owner
+    case member of
+        FunctionDeclaration {} -> do
+            name <- encodeIdentifier (resolvedSpelling (declarationName member))
+            signature <- encodeType limits 0 (declarationAnnotation member)
+            let staticTag = if declarationIsStatic member then "S1" else "S0"
+                accessTag = case declarationAccess member of
+                    DefaultAccess -> "A0"
+                    PublicAccess -> "A1"
+                    PrivateAccess -> "A2"
+                    InternalAccess -> "A3"
+                    ProtectedAccess -> "A4"
+                symbol = ownerName ++ "_M" ++ name ++ "_" ++ staticTag ++ accessTag ++ "_Y" ++ signature
+            enforceLength limits symbol
+            pure
+                ( MangledTemplateMember
+                    (resolvedSymbol (declarationName member))
+                    (resolvedSpelling (declarationName member))
+                    symbol
+                )
+        TypeDeclaration {} -> nestedTypeMember ownerName member
+        TemplateTypeDeclaration {} -> Left ExpectedMangleableMemberDeclaration
+        EnumDeclaration {} -> Left ExpectedMangleableMemberDeclaration
+    where
+        nestedTypeMember ownerName declaration = do
+            name <- encodeIdentifier (resolvedSpelling (declarationName declaration))
+            annotation <- encodeType limits 0 (declarationAnnotation declaration)
+            let symbol = ownerName ++ "_N" ++ name ++ "_Y" ++ annotation
+            enforceLength limits symbol
+            pure
+                ( MangledTemplateMember
+                    (resolvedSymbol (declarationName declaration))
+                    (resolvedSpelling (declarationName declaration))
+                    symbol
+                )
+
+-- | Mangle every direct member of a type declaration in source order.
+mangleTemplateMembers ::
+    TemplateMangleLimits ->
+    Type ->
+    Declaration ResolvedName Type ->
+    Either [TemplateMangleError] [MangledTemplateMember]
+mangleTemplateMembers limits owner declaration = case declaration of
+    TypeDeclaration {typeMembers = members} -> collect (map (mangleTemplateMember limits owner) members)
+    _ -> Left [ExpectedMangleableMemberDeclaration]
+
+validateLimits :: TemplateMangleLimits -> Either TemplateMangleError ()
+validateLimits limits
+    | maximumMangledTypeDepth limits < 0 = invalid "maximum type depth cannot be negative"
+    | maximumMangledSymbolLength limits <= 0 = invalid "maximum symbol length must be positive"
+    | maximumMangledNameParts limits <= 0 = invalid "maximum qualified-name part count must be positive"
+    | maximumMangledArguments limits < 0 = invalid "maximum argument count cannot be negative"
+    | otherwise = Right ()
+    where
+        invalid = Left . InvalidTemplateMangleLimits
+
+encodeType :: TemplateMangleLimits -> Int -> Type -> Either TemplateMangleError String
+encodeType limits depth valueType
+    | depth > maximumMangledTypeDepth limits = Left (MangledTypeDepthExceeded (maximumMangledTypeDepth limits))
+    | otherwise = case valueType of
+        NamedType name arguments -> do
+            encodedName <- encodeQualifiedName limits name
+            if length arguments > maximumMangledArguments limits
+                then Left (MangledArgumentLimitExceeded (maximumMangledArguments limits) (length arguments))
+                else do
+                    encodedArguments <- traverse (encodeArgument limits (depth + 1)) arguments
+                    pure ("N" ++ encodedName ++ "A" ++ count encodedArguments ++ concat encodedArguments)
+        FunctionType parameters result -> do
+            if length parameters > maximumMangledArguments limits
+                then Left (MangledArgumentLimitExceeded (maximumMangledArguments limits) (length parameters))
+                else do
+                    encodedParameters <- traverse (encodeType limits (depth + 1)) parameters
+                    encodedResult <- encodeType limits (depth + 1) result
+                    pure ("F" ++ count encodedParameters ++ concatMap frame encodedParameters ++ "R" ++ frame encodedResult)
+        TypeVariable name -> Left (OpenTypeCannotBeMangled name)
+        ErrorType -> Left ErrorTypeCannotBeMangled
+
+encodeArgument :: TemplateMangleLimits -> Int -> TemplateArgument -> Either TemplateMangleError String
+encodeArgument limits depth argument = case argument of
+    TypeTemplateArgument nested -> ("T" ++) . frame <$> encodeType limits depth nested
+    ValueTemplateArgument value -> ("V" ++) . frame <$> encodeValue value
+
+encodeValue :: TemplateValue -> Either TemplateMangleError String
+encodeValue value = case value of
+    IntegerTemplateValue integer ->
+        pure (if integer < 0 then "IN" ++ digits (abs integer) else "IP" ++ digits integer)
+    BooleanTemplateValue boolean -> pure (if boolean then "B1" else "B0")
+    CharacterTemplateValue scalar
+        | validScalar scalar -> pure ("C" ++ scalarHex scalar)
+        | otherwise -> Left (InvalidCharacterTemplateValue scalar)
+    TemplateValueParameter name -> Left (OpenTypeCannotBeMangled name)
+    where
+        digits integer = show integer ++ "_"
+
+encodeQualifiedName :: TemplateMangleLimits -> QualifiedName -> Either TemplateMangleError String
+encodeQualifiedName limits (QualifiedName parts)
+    | null parts = Left EmptyQualifiedNameCannotBeMangled
+    | length parts > maximumMangledNameParts limits =
+        Left (MangledNamePartLimitExceeded (maximumMangledNameParts limits) (length parts))
+    | otherwise = do
+        encoded <- traverse encodeIdentifier parts
+        pure (count encoded ++ concatMap frame encoded)
+
+encodeIdentifier :: Identifier -> Either TemplateMangleError String
+encodeIdentifier (Identifier value)
+    | null value = Left EmptyIdentifierCannotBeMangled
+    | otherwise = do
+        encoded <- traverse encodeCharacter value
+        pure (count value ++ concat encoded)
+
+encodeCharacter :: Char -> Either TemplateMangleError String
+encodeCharacter character
+    | validScalar scalar = Right (scalarHex scalar)
+    | otherwise = Left (InvalidIdentifierScalar scalar)
+    where
+        scalar = toInteger (ord character)
+
+scalarHex :: Integer -> String
+scalarHex scalar = replicate (6 - length encoded) '0' ++ encoded
+    where
+        encoded = showHex scalar ""
+
+validScalar :: Integer -> Bool
+validScalar scalar =
+    scalar >= 0
+        && scalar <= 0x10ffff
+        && not (scalar >= 0xd800 && scalar <= 0xdfff)
+
+frame :: String -> String
+frame value = show (length value) ++ "_" ++ value
+
+count :: [value] -> String
+count values = show (length values) ++ "_"
+
+enforceLength :: TemplateMangleLimits -> String -> Either TemplateMangleError ()
+enforceLength limits symbol
+    | length symbol > maximumMangledSymbolLength limits =
+        Left (MangledSymbolLengthExceeded (maximumMangledSymbolLength limits) (length symbol))
+    | otherwise = Right ()
+
+-- | Validate the emitted alphabet and framing invariants of a mangled symbol.
+validMangledTemplateSymbol :: String -> Bool
+validMangledTemplateSymbol symbol =
+    "_TAT1_" `prefixOf` symbol
+        && all validCharacter symbol
+    where
+        validCharacter character =
+            isAsciiLower character
+                || isAsciiUpper character
+                || isDigit character
+                || character == '_'
+
+-- | Render a mangling failure without exposing internal encoding details.
+renderTemplateMangleError :: TemplateMangleError -> String
+renderTemplateMangleError issue = case issue of
+    InvalidTemplateMangleLimits message -> "invalid template mangle limits: " ++ message
+    OpenTypeCannotBeMangled name -> "open template symbol cannot be mangled: " ++ identifierText (resolvedSpelling name)
+    ErrorTypeCannotBeMangled -> "ErrorType cannot be part of a mangled template symbol"
+    EmptyQualifiedNameCannotBeMangled -> "an empty qualified name cannot be mangled"
+    EmptyIdentifierCannotBeMangled -> "an empty identifier cannot be mangled"
+    InvalidIdentifierScalar scalar -> "identifier contains invalid Unicode scalar " ++ show scalar
+    InvalidCharacterTemplateValue scalar -> "character template value is invalid Unicode scalar " ++ show scalar
+    MangledTypeDepthExceeded limit -> "template mangling exceeded type depth " ++ show limit
+    MangledNamePartLimitExceeded limit actual -> limitMessage "qualified-name part" limit actual
+    MangledArgumentLimitExceeded limit actual -> limitMessage "template argument" limit actual
+    MangledSymbolLengthExceeded limit actual -> limitMessage "mangled symbol length" limit actual
+    ExpectedMangleableMemberDeclaration -> "template member mangling requires a closed type or function member"
+    where
+        limitMessage subject limit actual =
+            subject ++ " limit " ++ show limit ++ " was exceeded by " ++ show actual
+
+prefixOf :: String -> String -> Bool
+prefixOf [] _ = True
+prefixOf _ [] = False
+prefixOf (left : leftRest) (right : rightRest) = left == right && prefixOf leftRest rightRest
+
+collect :: [Either problem value] -> Either [problem] [value]
+collect values = case [problem | Left problem <- values] of
+    [] -> Right [value | Right value <- values]
+    problems -> Left problems
+ src/Progmasoft/Tala/Template/MemberReachability.hs view
@@ -0,0 +1,469 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Semantic reachability for members of one template declaration.
+
+Template specialization is lazy at member granularity.  A request for one
+member must nevertheless retain every same-declaration member reached by a
+resolved call from its body.  The call graph is keyed by @SymbolId@, never by
+spelling: overloads can share a source name while remaining distinct semantic
+definitions.
+
+Calls whose target is not a direct member of the declaration are deliberately
+classified as external.  This module does not guess inheritance, extension,
+dynamic dispatch, or another declaration's ownership from a name.  Those
+decisions belong to name and call resolution before specialization.
+-}
+module Progmasoft.Tala.Template.MemberReachability
+    ( TemplateMemberCall (..)
+    , TemplateMemberNode (..)
+    , TemplateMemberGraph (..)
+    , TemplateMemberReachabilityStatistics (..)
+    , TemplateMemberReachability (..)
+    , TemplateMemberReachabilityError (..)
+    , TemplateMemberReachabilityIssue (..)
+    , buildTemplateMemberGraph
+    , selectReachableMembersByName
+    , selectReachableMembersBySymbol
+    , verifyTemplateMemberReachability
+    , renderTemplateMemberReachabilityError
+    , renderTemplateMemberReachabilityIssue
+    ) where
+
+import Data.List (intercalate)
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Progmasoft.Tala.AST
+
+{- | One direct call expression.  Repeated calls remain distinct evidence even
+when they later collapse to one graph edge.
+-}
+data TemplateMemberCall = TemplateMemberCall
+    { templateCallOwner :: SymbolId
+    -- ^ Caller member identity.
+    , templateCallTarget :: SymbolId
+    -- ^ Resolved direct callee identity.
+    , templateCallSpan :: SourceSpan
+    -- ^ Source location of the call expression.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+{- | A node retains source order and declaration identity.  Source order is
+used only to make output deterministic; it never chooses an overload.
+-}
+data TemplateMemberNode = TemplateMemberNode
+    { templateMemberIndex :: Int
+    -- ^ Source-order index within the declaration.
+    , templateMemberSymbol :: SymbolId
+    -- ^ Unique resolved identity, not spelling.
+    , templateMemberName :: Identifier
+    -- ^ Source name; overloads may share it.
+    , templateMemberCallable :: Bool
+    -- ^ Whether the member has an executable body.
+    , templateMemberCalls :: [TemplateMemberCall]
+    -- ^ Direct calls found in its body.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Direct-member call graph partitioned into internal and external calls.
+data TemplateMemberGraph = TemplateMemberGraph
+    { templateGraphNodes :: [TemplateMemberNode]
+    -- ^ Nodes in declaration source order.
+    , templateGraphInternalCalls :: [TemplateMemberCall]
+    -- ^ Calls whose target is another direct member.
+    , templateGraphExternalCalls :: [TemplateMemberCall]
+    -- ^ Calls leaving this declaration's member set.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Counts describing graph construction and selected member closure.
+data TemplateMemberReachabilityStatistics = TemplateMemberReachabilityStatistics
+    { totalTemplateMembers :: Int
+    -- ^ All direct members in the declaration.
+    , callableTemplateMembers :: Int
+    -- ^ Members with callable bodies.
+    , requestedTemplateMemberRoots :: Int
+    -- ^ Unique selected root symbols.
+    , reachableTemplateMembers :: Int
+    -- ^ Members retained by transitive closure.
+    , observedTemplateMemberCalls :: Int
+    -- ^ Internal and external call occurrences.
+    , internalTemplateMemberCalls :: Int
+    -- ^ Calls with same-declaration targets.
+    , externalTemplateMemberCalls :: Int
+    -- ^ Calls whose targets are outside the graph.
+    , uniqueTemplateMemberEdges :: Int
+    -- ^ Distinct internal caller/callee pairs.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Root requests, transitively retained members, edges, and graph metrics.
+data TemplateMemberReachability = TemplateMemberReachability
+    { memberReachabilityRoots :: [SymbolId]
+    -- ^ Selected root identities in source order.
+    , memberReachabilitySymbols :: [SymbolId]
+    -- ^ Roots and all transitively reached members.
+    , memberReachabilityMembers :: [Declaration ResolvedName Type]
+    -- ^ Original member declarations retained.
+    , memberReachabilityEdges :: [(SymbolId, SymbolId)]
+    -- ^ Unique internal call edges among selected members.
+    , memberReachabilityStatistics :: TemplateMemberReachabilityStatistics
+    -- ^ Counts for the analyzed graph.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Invalid root graph or request that cannot be resolved to a direct member.
+data TemplateMemberReachabilityError
+    = -- | Member identity is reserved or invalid.
+      InvalidTemplateMemberSymbol Identifier SymbolId
+    | -- | Multiple member declarations share an identity.
+      DuplicateTemplateMemberSymbol SymbolId [Identifier]
+    | -- | Requested spelling has no member.
+      MissingTemplateMemberName Identifier
+    | -- | Requested semantic identity has no member.
+      MissingTemplateMemberSymbol SymbolId
+    deriving (Eq, Ord, Read, Show)
+
+-- | Independently checkable inconsistency in a claimed reachability result.
+data TemplateMemberReachabilityIssue
+    = -- | A requested root is not declared directly.
+      ReachabilityRootIsNotMember SymbolId
+    | -- | A selected identity is not in the graph.
+      ReachabilitySelectionIsNotMember SymbolId
+    | -- | The root list repeats a symbol.
+      DuplicateReachabilityRoot SymbolId
+    | -- | The selection repeats a symbol.
+      DuplicateReachabilitySelection SymbolId
+    | -- | A root is absent from the closure.
+      ReachabilityRootNotSelected SymbolId
+    | -- | A selected call omits its in-graph callee.
+      ReachabilityDependencyNotSelected SymbolId SymbolId
+    | -- | An edge is not supported by a recorded call.
+      UnexpectedReachabilityEdge SymbolId SymbolId
+    | -- | A recorded internal call has no reported edge.
+      MissingReachabilityEdge SymbolId SymbolId
+    | -- | Declaration and symbol lists disagree.
+      ReachabilityMembersDiffer [SymbolId] [SymbolId]
+    | -- | Reported counts differ from recomputation.
+      IncorrectReachabilityStatistics TemplateMemberReachabilityStatistics TemplateMemberReachabilityStatistics
+    deriving (Eq, Ord, Read, Show)
+
+{- | Construct a declaration-local graph after semantic resolution.  Invalid
+or duplicated symbols are rejected here even though the general template
+verifier normally diagnoses them earlier; this keeps the API safe for
+compiler tests and independently constructed TypedAST values.
+-}
+buildTemplateMemberGraph ::
+    [Declaration ResolvedName Type] ->
+    Either [TemplateMemberReachabilityError] TemplateMemberGraph
+buildTemplateMemberGraph members = case validationProblems of
+    [] -> Right graph
+    problems -> Left problems
+    where
+        nodes = zipWith memberNode [0 ..] members
+        memberSymbols = map templateMemberSymbol nodes
+        memberSet = Set.fromList memberSymbols
+        calls = concatMap templateMemberCalls nodes
+        graph =
+            TemplateMemberGraph
+                nodes
+                [call | call <- calls, templateCallTarget call `Set.member` memberSet]
+                [call | call <- calls, templateCallTarget call `Set.notMember` memberSet]
+        invalid =
+            [ InvalidTemplateMemberSymbol (templateMemberName node) (templateMemberSymbol node)
+            | node <- nodes
+            , symbolIdValue (templateMemberSymbol node) <= 0
+            ]
+        duplicated =
+            [ DuplicateTemplateMemberSymbol symbol [templateMemberName node | node <- nodes, templateMemberSymbol node == symbol]
+            | symbol <- duplicates memberSymbols
+            ]
+        validationProblems = invalid ++ duplicated
+
+-- | Select every overload with a requested spelling and follow internal calls.
+selectReachableMembersByName ::
+    [Identifier] ->
+    [Declaration ResolvedName Type] ->
+    Either [TemplateMemberReachabilityError] TemplateMemberReachability
+selectReachableMembersByName requested members = do
+    graph <- buildTemplateMemberGraph members
+    let names = unique requested
+        nodes = templateGraphNodes graph
+        missing = [name | name <- names, name `notElem` map templateMemberName nodes]
+        roots =
+            [ templateMemberSymbol node
+            | node <- nodes
+            , templateMemberName node `elem` names
+            ]
+    if null missing
+        then Right (selectFromGraph roots members graph)
+        else Left (map MissingTemplateMemberName missing)
+
+-- | Select exact semantic member identities and follow internal calls.
+selectReachableMembersBySymbol ::
+    [SymbolId] ->
+    [Declaration ResolvedName Type] ->
+    Either [TemplateMemberReachabilityError] TemplateMemberReachability
+selectReachableMembersBySymbol requested members = do
+    graph <- buildTemplateMemberGraph members
+    let roots = unique requested
+        available = map templateMemberSymbol (templateGraphNodes graph)
+        missing = [symbol | symbol <- roots, symbol `notElem` available]
+    if null missing
+        then Right (selectFromGraph roots members graph)
+        else Left (map MissingTemplateMemberSymbol missing)
+
+selectFromGraph ::
+    [SymbolId] ->
+    [Declaration ResolvedName Type] ->
+    TemplateMemberGraph ->
+    TemplateMemberReachability
+selectFromGraph roots members graph =
+    TemplateMemberReachability
+        orderedRoots
+        selectedSymbols
+        [member | member <- members, declarationSymbol member `Set.member` selectedSet]
+        selectedEdges
+        statistics
+    where
+        nodes = templateGraphNodes graph
+        sourceOrder = map templateMemberSymbol nodes
+        orderedRoots = [symbol | symbol <- sourceOrder, symbol `elem` roots]
+        adjacency = adjacencyTable (templateGraphInternalCalls graph)
+        selectedSet = closeReachability adjacency Set.empty orderedRoots
+        selectedSymbols = [symbol | symbol <- sourceOrder, symbol `Set.member` selectedSet]
+        selectedEdges =
+            unique
+                [ (templateCallOwner call, templateCallTarget call)
+                | call <- templateGraphInternalCalls graph
+                , templateCallOwner call `Set.member` selectedSet
+                ]
+        calls = templateGraphInternalCalls graph ++ templateGraphExternalCalls graph
+        statistics =
+            TemplateMemberReachabilityStatistics
+                { totalTemplateMembers = length nodes
+                , callableTemplateMembers = length [() | node <- nodes, templateMemberCallable node]
+                , requestedTemplateMemberRoots = length orderedRoots
+                , reachableTemplateMembers = length selectedSymbols
+                , observedTemplateMemberCalls = length calls
+                , internalTemplateMemberCalls = length (templateGraphInternalCalls graph)
+                , externalTemplateMemberCalls = length (templateGraphExternalCalls graph)
+                , uniqueTemplateMemberEdges = length (unique (map callEdge (templateGraphInternalCalls graph)))
+                }
+
+adjacencyTable :: [TemplateMemberCall] -> Map SymbolId [SymbolId]
+adjacencyTable = foldl' insert Map.empty
+    where
+        insert table call =
+            Map.insertWith
+                (\new old -> unique (old ++ new))
+                (templateCallOwner call)
+                [templateCallTarget call]
+                table
+
+closeReachability :: Map SymbolId [SymbolId] -> Set SymbolId -> [SymbolId] -> Set SymbolId
+closeReachability _ visited [] = visited
+closeReachability adjacency visited (symbol : pending)
+    | symbol `Set.member` visited = closeReachability adjacency visited pending
+    | otherwise =
+        let dependencies = Map.findWithDefault [] symbol adjacency
+         in closeReachability adjacency (Set.insert symbol visited) (pending ++ dependencies)
+
+{- | Recompute graph facts from declarations and compare them with a retained
+result.  The specialization-plan verifier uses the selected declaration for
+broader invariants; this verifier protects the reachability object itself.
+-}
+verifyTemplateMemberReachability ::
+    [Declaration ResolvedName Type] ->
+    TemplateMemberReachability ->
+    Either [TemplateMemberReachabilityError] [TemplateMemberReachabilityIssue]
+verifyTemplateMemberReachability members reachability = do
+    graph <- buildTemplateMemberGraph members
+    let available = map templateMemberSymbol (templateGraphNodes graph)
+        roots = memberReachabilityRoots reachability
+        selected = memberReachabilitySymbols reachability
+        actualMembers = map declarationSymbol (memberReachabilityMembers reachability)
+        expected = selectFromGraph roots members graph
+        expectedEdges = memberReachabilityEdges expected
+        actualEdges = memberReachabilityEdges reachability
+        rootProblems =
+            [ReachabilityRootIsNotMember symbol | symbol <- roots, symbol `notElem` available]
+                ++ [DuplicateReachabilityRoot symbol | symbol <- duplicates roots]
+                ++ [ReachabilityRootNotSelected symbol | symbol <- roots, symbol `notElem` selected]
+        selectionProblems =
+            [ReachabilitySelectionIsNotMember symbol | symbol <- selected, symbol `notElem` available]
+                ++ [DuplicateReachabilitySelection symbol | symbol <- duplicates selected]
+        closureProblems =
+            [ ReachabilityDependencyNotSelected owner target
+            | (owner, target) <- expectedEdges
+            , owner `elem` selected
+            , target `notElem` selected
+            ]
+        edgeProblems =
+            [UnexpectedReachabilityEdge owner target | (owner, target) <- actualEdges, (owner, target) `notElem` expectedEdges]
+                ++ [MissingReachabilityEdge owner target | (owner, target) <- expectedEdges, (owner, target) `notElem` actualEdges]
+        memberProblems =
+            [ReachabilityMembersDiffer selected actualMembers | selected /= actualMembers]
+        statisticProblems =
+            [ IncorrectReachabilityStatistics
+                (memberReachabilityStatistics expected)
+                (memberReachabilityStatistics reachability)
+            | memberReachabilityStatistics expected /= memberReachabilityStatistics reachability
+            ]
+    pure (rootProblems ++ selectionProblems ++ closureProblems ++ edgeProblems ++ memberProblems ++ statisticProblems)
+
+memberNode :: Int -> Declaration ResolvedName Type -> TemplateMemberNode
+memberNode index member =
+    TemplateMemberNode
+        index
+        (declarationSymbol member)
+        (resolvedSpelling (declarationName member))
+        (isFunction member)
+        (memberCalls (declarationSymbol member) member)
+
+isFunction :: Declaration name annotation -> Bool
+isFunction FunctionDeclaration {} = True
+isFunction _ = False
+
+memberCalls :: SymbolId -> Declaration ResolvedName Type -> [TemplateMemberCall]
+memberCalls owner declaration = case declaration of
+    FunctionDeclaration {declarationBody = body} -> blockCalls owner body
+    -- Nested types have their own member ownership domain.  Their bodies must
+    -- not accidentally pull siblings into the enclosing template closure.
+    TypeDeclaration {} -> []
+    TemplateTypeDeclaration {} -> []
+    EnumDeclaration {} -> []
+
+blockCalls :: SymbolId -> Block ResolvedName Type -> [TemplateMemberCall]
+blockCalls owner (Block statements) = concatMap (statementCalls owner) statements
+
+statementCalls :: SymbolId -> Statement ResolvedName Type -> [TemplateMemberCall]
+statementCalls owner statement = case statement of
+    BindingStatement _ _ _ _ _ value -> expressionCalls owner value
+    AssignmentStatement _ _ _ value -> expressionCalls owner value
+    ReturnStatement _ value -> maybe [] (expressionCalls owner) value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionCalls owner condition
+            ++ blockCalls owner trueBlock
+            ++ maybe [] (blockCalls owner) falseBlock
+    WhileStatement _ condition body -> expressionCalls owner condition ++ blockCalls owner body
+    DoWhileStatement _ body condition -> blockCalls owner body ++ expressionCalls owner condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] (statementCalls owner) initializer
+            ++ maybe [] (expressionCalls owner) condition
+            ++ concatMap (statementCalls owner) updates
+            ++ blockCalls owner body
+    ForEachStatement _ _ _ _ _ source body -> expressionCalls owner source ++ blockCalls owner body
+    IncrementStatement {} -> []
+    CompoundAssignmentStatement _ _ _ _ value -> expressionCalls owner value
+    DiscardStatement _ value -> expressionCalls owner value
+    BreakStatement _ value -> maybe [] (expressionCalls owner) value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionCalls owner condition ++ blockCalls owner block
+    BlockStatement _ block -> blockCalls owner block
+    ExpressionStatement _ value _ -> expressionCalls owner value
+
+expressionCalls :: SymbolId -> Expression ResolvedName Type -> [TemplateMemberCall]
+expressionCalls owner expression = case expression of
+    NameExpression {} -> []
+    LiteralExpression {} -> []
+    MemberAccessExpression _ receiver _ _ -> expressionCalls owner receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionCalls owner receiver
+    CallExpression spanValue callee arguments _ ->
+        directCall spanValue callee
+            ++ expressionCalls owner callee
+            ++ concatMap (expressionCalls owner) arguments
+    UnaryExpression _ _ value _ -> expressionCalls owner value
+    BinaryExpression _ _ left right _ -> expressionCalls owner left ++ expressionCalls owner right
+    IsPatternExpression _ subject _ _ -> expressionCalls owner subject
+    -- Reachability is path-insensitive: a call in either arm may execute.
+    ConditionalExpression _ condition first second _ ->
+        concatMap (expressionCalls owner) [condition, first, second]
+    CoalesceExpression _ left fallback _ -> expressionCalls owner left ++ expressionCalls owner fallback
+    AssignmentExpression _ _ _ value _ -> expressionCalls owner value
+    IncrementExpression {} -> []
+    LoopExpression _ loop _ -> statementCalls owner loop
+    BlockExpression _ block _ -> blockCalls owner block
+    -- Reachability is path-insensitive: a call in any arm may execute.
+    MatchExpression _ subjects arms _ ->
+        concatMap (expressionCalls owner) (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ captures _ body _ ->
+        concatMap (maybe [] (expressionCalls owner) . captureInitializer) captures
+            ++ callableBodyCalls owner body
+    where
+        directCall spanValue callee = case callee of
+            NameExpression _ target _ -> [TemplateMemberCall owner (resolvedSymbol target) spanValue]
+            _ -> []
+
+callableBodyCalls :: SymbolId -> CallableBody ResolvedName Type -> [TemplateMemberCall]
+callableBodyCalls owner body = case body of
+    CallableExpressionBody value -> expressionCalls owner value
+    CallableBlockBody block -> blockCalls owner block
+
+callEdge :: TemplateMemberCall -> (SymbolId, SymbolId)
+callEdge call = (templateCallOwner call, templateCallTarget call)
+
+declarationSymbol :: Declaration ResolvedName annotation -> SymbolId
+declarationSymbol = resolvedSymbol . declarationName
+
+-- | Render a graph-construction or root-selection failure.
+renderTemplateMemberReachabilityError :: TemplateMemberReachabilityError -> String
+renderTemplateMemberReachabilityError problem = case problem of
+    InvalidTemplateMemberSymbol name symbol ->
+        "template member " ++ identifierText name ++ " has invalid " ++ renderSymbol symbol
+    DuplicateTemplateMemberSymbol symbol names ->
+        renderSymbol symbol
+            ++ " is shared by template members "
+            ++ intercalate ", " (map identifierText names)
+    MissingTemplateMemberName name -> "template declaration has no member named " ++ identifierText name
+    MissingTemplateMemberSymbol symbol -> "template declaration has no member with " ++ renderSymbol symbol
+
+-- | Render a verifier issue from an inconsistent reachability result.
+renderTemplateMemberReachabilityIssue :: TemplateMemberReachabilityIssue -> String
+renderTemplateMemberReachabilityIssue problem = case problem of
+    ReachabilityRootIsNotMember symbol -> "reachability root is not a declaration member: " ++ renderSymbol symbol
+    ReachabilitySelectionIsNotMember symbol -> "selected reachability symbol is not a declaration member: " ++ renderSymbol symbol
+    DuplicateReachabilityRoot symbol -> "reachability root occurs more than once: " ++ renderSymbol symbol
+    DuplicateReachabilitySelection symbol -> "selected reachability symbol occurs more than once: " ++ renderSymbol symbol
+    ReachabilityRootNotSelected symbol -> "reachability root is absent from its selection: " ++ renderSymbol symbol
+    ReachabilityDependencyNotSelected owner target ->
+        "selected " ++ renderSymbol owner ++ " calls unselected " ++ renderSymbol target
+    UnexpectedReachabilityEdge owner target ->
+        "reachability contains an unobserved edge " ++ renderEdge owner target
+    MissingReachabilityEdge owner target ->
+        "reachability omits observed edge " ++ renderEdge owner target
+    ReachabilityMembersDiffer expected actual ->
+        "reachability member declarations differ: expected "
+            ++ renderSymbols expected
+            ++ ", found "
+            ++ renderSymbols actual
+    IncorrectReachabilityStatistics expected actual ->
+        "reachability statistics differ: expected " ++ show expected ++ ", found " ++ show actual
+
+renderSymbol :: SymbolId -> String
+renderSymbol (SymbolId value) = "SymbolId " ++ show value
+
+renderEdge :: SymbolId -> SymbolId -> String
+renderEdge owner target = renderSymbol owner ++ " -> " ++ renderSymbol target
+
+renderSymbols :: [SymbolId] -> String
+renderSymbols values = "[" ++ intercalate ", " (map renderSymbol values) ++ "]"
+
+duplicates :: (Ord value) => [value] -> [value]
+duplicates values = Set.toList (go Set.empty Set.empty values)
+    where
+        go _ repeated [] = repeated
+        go seen repeated (value : remaining)
+            | value `Set.member` seen = go seen (Set.insert value repeated) remaining
+            | otherwise = go (Set.insert value seen) repeated remaining
+
+unique :: (Ord value) => [value] -> [value]
+unique = go Set.empty
+    where
+        go _ [] = []
+        go seen (value : remaining)
+            | value `Set.member` seen = go seen remaining
+            | otherwise = value : go (Set.insert value seen) remaining
+ src/Progmasoft/Tala/Template/Specialization.hs view
@@ -0,0 +1,690 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Declaration-level template specialization planning.
+
+The application binder answers whether arguments fit a declaration and the
+instantiator substitutes them. This module joins those operations into an
+immutable batch plan suitable for a later demand-driven Core lowering pass.
+It deliberately accepts explicit semantic demands.  Resolved calls between
+members of the selected declaration are followed by SymbolId so a lazy member
+request remains closed under its internal call dependencies.  Constraint
+ordering and cross-declaration dispatch remain separate future stages.
+-}
+module Progmasoft.Tala.Template.Specialization
+    ( TemplateDemandScope (..)
+    , TemplateSpecializationDemand (..)
+    , TemplateSpecializationLimits (..)
+    , defaultTemplateSpecializationLimits
+    , TemplateSpecializationId (..)
+    , TemplateSpecialization (..)
+    , TemplateSpecializationStatistics (..)
+    , TemplateSpecializationPlan (..)
+    , TemplateSpecializationError (..)
+    , planTemplateSpecializations
+    , findTemplateSpecialization
+    , findTemplateSpecializationByIdentity
+    , specializationEmissionOrder
+    , specializationTypedAST
+    , renderTemplateSpecializationError
+    ) where
+
+import Data.List (intercalate, sortOn)
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Template.Application
+import Progmasoft.Tala.Template.Freshen
+import Progmasoft.Tala.Template.Instantiation
+import Progmasoft.Tala.Template.Mangling
+import Progmasoft.Tala.Template.MemberReachability
+
+{- | Layout-only requests do not instantiate method bodies. Member requests
+select every overload carrying one of the requested spellings. Complete is
+reserved for an explicit whole-declaration request; it is never inferred.
+-}
+data TemplateDemandScope
+    = -- | Retain layout and signatures without method bodies.
+      TemplateLayoutDemand
+    | -- | Retain named methods and their call closure.
+      TemplateMemberDemand [Identifier]
+    | -- | Explicit request for the complete declaration.
+      TemplateCompleteDemand
+    deriving (Eq, Ord, Read, Show)
+
+-- | One source or compiler request to materialize a template specialization.
+data TemplateSpecializationDemand = TemplateSpecializationDemand
+    { specializationDemandApplication :: TemplateApplication
+    -- ^ Declaration and bound arguments requested.
+    , specializationDemandScope :: TemplateDemandScope
+    -- ^ Portion of the declaration that must be retained.
+    , specializationDemandOrigin :: String
+    -- ^ Stable provenance used in diagnostics and deduplication.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Resource ceilings preventing specialization requests from exhausting the compiler.
+data TemplateSpecializationLimits = TemplateSpecializationLimits
+    { maximumTemplateSpecializations :: Int
+    -- ^ Maximum unique specializations in one plan.
+    , maximumTemplateOrigins :: Int
+    -- ^ Maximum recorded request origins.
+    , maximumMembersPerSpecialization :: Int
+    -- ^ Maximum materialized members per specialization.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Finite defaults suitable for normal compiler and project builds.
+defaultTemplateSpecializationLimits :: TemplateSpecializationLimits
+defaultTemplateSpecializationLimits = TemplateSpecializationLimits 4096 16384 4096
+
+-- | Stable ordinal identity within one specialization plan.
+newtype TemplateSpecializationId = TemplateSpecializationId
+    { templateSpecializationIdValue :: Int
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | One closed specialization and all identities needed by later lowering.
+data TemplateSpecialization = TemplateSpecialization
+    { templateSpecializationId :: TemplateSpecializationId
+    -- ^ Plan-local stable identity.
+    , templateSpecializationIdentity :: String
+    -- ^ Canonical concrete-type identity key.
+    , templateSpecializationType :: Type
+    -- ^ Closed concrete application type.
+    , templateSpecializationScope :: TemplateDemandScope
+    -- ^ Strongest coalesced materialization scope.
+    , templateSpecializationOrigins :: [String]
+    -- ^ All deduplicated request provenance strings.
+    , templateSpecializationDependencies :: [TemplateSpecializationId]
+    -- ^ Specializations referenced by this body.
+    , templateSpecializationDeclaration :: Declaration ResolvedName Type
+    -- ^ Instantiated and symbol-freshened declaration.
+    , templateSpecializationSymbolMap :: [(SymbolId, SymbolId)]
+    -- ^ Original-to-fresh definition identities.
+    , templateSpecializationMangledType :: MangledTemplateType
+    -- ^ Internal stable type symbol.
+    , templateSpecializationMangledMembers :: [MangledTemplateMember]
+    -- ^ Internal member symbols in declaration order.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Demand, coalescing, materialization, and dependency counts.
+data TemplateSpecializationStatistics = TemplateSpecializationStatistics
+    { requestedTemplateSpecializations :: Int
+    -- ^ Number of input demands.
+    , uniqueTemplateSpecializations :: Int
+    -- ^ Number of retained concrete specializations.
+    , coalescedTemplateSpecializations :: Int
+    -- ^ Requests merged into existing entries.
+    , instantiatedTemplateMembers :: Int
+    -- ^ Total members materialized across entries.
+    , retainedTemplateOrigins :: Int
+    -- ^ Provenance strings retained after merging.
+    , templateDependencyEdges :: Int
+    -- ^ Number of specialization-to-specialization edges.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Immutable batch result consumed by dependency ordering and Core lowering.
+data TemplateSpecializationPlan = TemplateSpecializationPlan
+    { plannedTemplateNamespace :: Maybe QualifiedName
+    -- ^ Namespace shared by source declarations.
+    , plannedTemplateSpecializations :: [TemplateSpecialization]
+    -- ^ Closed specializations retained in the plan.
+    , plannedTemplateStatistics :: TemplateSpecializationStatistics
+    -- ^ Aggregate counts for the plan.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | Planning, binding, closure, resource, or internal-name generation failure.
+data TemplateSpecializationError
+    = -- | A configured plan limit is invalid.
+      InvalidTemplateSpecializationLimits String
+    | -- | Binding failed for this demand.
+      TemplateApplicationFailed String [TemplateApplicationError]
+    | -- | Substitution failed during materialization.
+      TemplateInstantiationFailed String TemplateInstantiationError
+    | -- | Bound declaration has no matching source.
+      TemplateDeclarationSourceMissing QualifiedName SymbolId
+    | -- | Requested member spelling is absent.
+      TemplateMemberNotFound QualifiedName Identifier
+    | -- | Type/value variables remain after binding.
+      TemplateSpecializationRemainsOpen String [SymbolId]
+    | -- | Unique specialization count exceeded its ceiling.
+      TemplateSpecializationLimitExceeded Int String
+    | -- | Recorded provenance exceeded its ceiling.
+      TemplateOriginLimitExceeded Int String
+    | -- | Materialized member count exceeded its ceiling.
+      TemplateMemberLimitExceeded QualifiedName Int Int
+    | -- | Member call closure could not be computed.
+      TemplateMemberReachabilityFailed QualifiedName [TemplateMemberReachabilityError]
+    | -- | Concrete identity could not be encoded.
+      TemplateMangleFailed String [TemplateMangleError]
+    deriving (Eq, Ord, Read, Show)
+
+data PreparedDemand = PreparedDemand
+    { preparedBinding :: TemplateBinding
+    , preparedType :: Type
+    , preparedIdentity :: String
+    , preparedScope :: TemplateDemandScope
+    , preparedOrigins :: [String]
+    }
+
+-- | Bind, coalesce, instantiate, freshen, and dependency-link explicit demands.
+planTemplateSpecializations ::
+    TemplateSpecializationLimits ->
+    TypedAST ->
+    [TemplateSpecializationDemand] ->
+    Either [TemplateSpecializationError] TemplateSpecializationPlan
+planTemplateSpecializations limits typed demands = do
+    validateLimits limits
+    prepared <- collectResults (map (prepareDemand catalog) demands)
+    let coalesced = coalesceDemands prepared
+    validateBatchLimits limits coalesced
+    materialized <- materializeAll limits typed sources coalesced
+    let linked = attachDependencies materialized
+        statistics =
+            TemplateSpecializationStatistics
+                { requestedTemplateSpecializations = length demands
+                , uniqueTemplateSpecializations = length linked
+                , coalescedTemplateSpecializations = length demands - length linked
+                , instantiatedTemplateMembers = sum (map memberCount linked)
+                , retainedTemplateOrigins = sum (map (length . templateSpecializationOrigins) linked)
+                , templateDependencyEdges = sum (map (length . templateSpecializationDependencies) linked)
+                }
+    pure (TemplateSpecializationPlan namespace linked statistics)
+    where
+        TypedAST tree = typed
+        namespace = syntaxNamespace tree
+        catalog = buildTemplateCatalog typed
+        sources = declarationSources (syntaxDeclarations tree)
+
+validateLimits :: TemplateSpecializationLimits -> Either [TemplateSpecializationError] ()
+validateLimits limits
+    | maximumTemplateSpecializations limits <= 0 = invalid "maximum specialization count must be positive"
+    | maximumTemplateOrigins limits <= 0 = invalid "maximum origin count must be positive"
+    | maximumMembersPerSpecialization limits < 0 = invalid "maximum member count cannot be negative"
+    | otherwise = Right ()
+    where
+        invalid message = Left [InvalidTemplateSpecializationLimits message]
+
+prepareDemand ::
+    TemplateCatalog ->
+    TemplateSpecializationDemand ->
+    Either [TemplateSpecializationError] PreparedDemand
+prepareDemand catalog demand = case bindTemplateApplication catalog application of
+    Left failures -> Left [TemplateApplicationFailed origin failures]
+    Right binding -> case substituteType binding (templateApplicationType binding) of
+        Left failure -> Left [TemplateApplicationFailed origin [failure]]
+        Right concrete ->
+            let identity = renderSpecializationIdentity concrete
+                open = collectOpenSymbols concrete
+             in if null open
+                    then
+                        Right
+                            ( PreparedDemand
+                                binding
+                                concrete
+                                identity
+                                (normalizeScope (specializationDemandScope demand))
+                                [origin]
+                            )
+                    else Left [TemplateSpecializationRemainsOpen identity open]
+    where
+        application = specializationDemandApplication demand
+        origin = specializationDemandOrigin demand
+
+templateApplicationType :: TemplateBinding -> Type
+templateApplicationType binding =
+    NamedType
+        (templateDeclarationName (templateBindingDeclaration binding))
+        (concatMap (map unwrap . snd) (templateBindingArguments binding))
+    where
+        unwrap (ExplicitTemplateArgument argument) = argument
+        unwrap (DefaultTemplateArgument argument) = argument
+
+coalesceDemands :: [PreparedDemand] -> [PreparedDemand]
+coalesceDemands = Map.elems . foldl' insert Map.empty
+    where
+        insert table demand = Map.insertWith merge (preparedIdentity demand) demand table
+        -- insertWith supplies the new request first. Keep the old binding so
+        -- source-order defaults remain the canonical representative, while
+        -- merging scope and unique origins from both requests.
+        merge new old =
+            old
+                { preparedScope = mergeScope (preparedScope old) (preparedScope new)
+                , preparedOrigins = unique (preparedOrigins old ++ preparedOrigins new)
+                }
+
+normalizeScope :: TemplateDemandScope -> TemplateDemandScope
+normalizeScope scope = case scope of
+    TemplateMemberDemand names -> TemplateMemberDemand (unique names)
+    _ -> scope
+
+mergeScope :: TemplateDemandScope -> TemplateDemandScope -> TemplateDemandScope
+mergeScope TemplateCompleteDemand _ = TemplateCompleteDemand
+mergeScope _ TemplateCompleteDemand = TemplateCompleteDemand
+mergeScope TemplateLayoutDemand right = right
+mergeScope left TemplateLayoutDemand = left
+mergeScope (TemplateMemberDemand left) (TemplateMemberDemand right) =
+    TemplateMemberDemand (unique (left ++ right))
+
+validateBatchLimits ::
+    TemplateSpecializationLimits ->
+    [PreparedDemand] ->
+    Either [TemplateSpecializationError] ()
+validateBatchLimits limits prepared
+    | length prepared > maximumTemplateSpecializations limits =
+        Left
+            [ TemplateSpecializationLimitExceeded
+                (maximumTemplateSpecializations limits)
+                (preparedIdentity (prepared !! maximumTemplateSpecializations limits))
+            ]
+    | originCount > maximumTemplateOrigins limits =
+        Left
+            [ TemplateOriginLimitExceeded
+                (maximumTemplateOrigins limits)
+                (firstOverflowOrigin (maximumTemplateOrigins limits) prepared)
+            ]
+    | otherwise = Right ()
+    where
+        originCount = sum (map (length . preparedOrigins) prepared)
+
+firstOverflowOrigin :: Int -> [PreparedDemand] -> String
+firstOverflowOrigin limit prepared = case drop limit (concatMap preparedOrigins prepared) of
+    origin : _ -> origin
+    [] -> "<unknown-origin>"
+
+materializeAll ::
+    TemplateSpecializationLimits ->
+    TypedAST ->
+    Map SymbolId (Declaration ResolvedName Type) ->
+    [PreparedDemand] ->
+    Either [TemplateSpecializationError] [TemplateSpecialization]
+materializeAll limits typed sources prepared = go firstFresh 1 [] ordered
+    where
+        SymbolId maximumSource = maximumSymbolInTypedAST typed
+        firstFresh = SymbolId (maximumSource + 1)
+        ordered = sortOn preparedIdentity prepared
+
+        go _ _ output [] = Right (reverse output)
+        go next identifier output (demand : remaining) = do
+            (selected, selectedScope, memberTotal) <- selectDeclarationMembers limits sources demand
+            instantiated <- mapInstantiation demand (instantiateTemplateType (preparedBinding demand) selected)
+            -- The source annotation may retain a namespace-relative spelling.
+            -- The planner owns the canonical concrete application, so the
+            -- cloned declaration crosses into Core with that exact type.
+            let canonicalDeclaration = instantiated {declarationAnnotation = preparedType demand}
+                freshened = freshenDeclaration next canonicalDeclaration
+            mangledType <- mapMangle demand (singleMangle (mangleTemplateType defaultTemplateMangleLimits (preparedType demand)))
+            mangledMembers <-
+                mapMangle
+                    demand
+                    (mangleTemplateMembers defaultTemplateMangleLimits (preparedType demand) (freshenedDeclaration freshened))
+            let specialization =
+                    TemplateSpecialization
+                        { templateSpecializationId = TemplateSpecializationId identifier
+                        , templateSpecializationIdentity = preparedIdentity demand
+                        , templateSpecializationType = preparedType demand
+                        , templateSpecializationScope = selectedScope
+                        , templateSpecializationOrigins = preparedOrigins demand
+                        , templateSpecializationDependencies = []
+                        , templateSpecializationDeclaration = freshenedDeclaration freshened
+                        , templateSpecializationSymbolMap = freshenedSymbols freshened
+                        , templateSpecializationMangledType = mangledType
+                        , templateSpecializationMangledMembers = mangledMembers
+                        }
+            if memberTotal > maximumMembersPerSpecialization limits
+                then
+                    Left
+                        [ TemplateMemberLimitExceeded
+                            (templateDeclarationName (templateBindingDeclaration (preparedBinding demand)))
+                            (maximumMembersPerSpecialization limits)
+                            memberTotal
+                        ]
+                else go (nextFreshSymbol freshened) (identifier + 1) (specialization : output) remaining
+
+selectDeclarationMembers ::
+    TemplateSpecializationLimits ->
+    Map SymbolId (Declaration ResolvedName Type) ->
+    PreparedDemand ->
+    Either [TemplateSpecializationError] (Declaration ResolvedName Type, TemplateDemandScope, Int)
+selectDeclarationMembers _ sources demand = do
+    source <- case Map.lookup symbol sources of
+        Just declaration -> Right declaration
+        Nothing -> Left [TemplateDeclarationSourceMissing name symbol]
+    case source of
+        declaration@TemplateTypeDeclaration {typeMembers = members} -> do
+            (selected, selectedScope) <- selectMembers name (preparedScope demand) members
+            pure (declaration {typeMembers = selected}, selectedScope, length selected)
+        _ -> Left [TemplateDeclarationSourceMissing name symbol]
+    where
+        descriptor = templateBindingDeclaration (preparedBinding demand)
+        name = templateDeclarationName descriptor
+        symbol = templateDeclarationSymbol descriptor
+
+selectMembers ::
+    QualifiedName ->
+    TemplateDemandScope ->
+    [Declaration ResolvedName Type] ->
+    Either [TemplateSpecializationError] ([Declaration ResolvedName Type], TemplateDemandScope)
+selectMembers _ TemplateLayoutDemand _ = Right ([], TemplateLayoutDemand)
+selectMembers _ TemplateCompleteDemand members = Right (members, TemplateCompleteDemand)
+selectMembers owner (TemplateMemberDemand requested) members =
+    case selectReachableMembersByName requested members of
+        Left failures -> Left (mapReachabilityFailures owner failures)
+        Right reachability ->
+            let selected = memberReachabilityMembers reachability
+                selectedNames = unique (map memberSpelling selected)
+             in Right (selected, TemplateMemberDemand selectedNames)
+
+mapReachabilityFailures ::
+    QualifiedName ->
+    [TemplateMemberReachabilityError] ->
+    [TemplateSpecializationError]
+mapReachabilityFailures owner failures =
+    case [name | MissingTemplateMemberName name <- failures] of
+        [] -> [TemplateMemberReachabilityFailed owner failures]
+        names -> map (TemplateMemberNotFound owner) names
+
+memberSpelling :: Declaration ResolvedName Type -> Identifier
+memberSpelling = resolvedSpelling . declarationName
+
+mapInstantiation ::
+    PreparedDemand ->
+    Either TemplateInstantiationError value ->
+    Either [TemplateSpecializationError] value
+mapInstantiation demand result = case result of
+    Left failure -> Left [TemplateInstantiationFailed (preparedIdentity demand) failure]
+    Right value -> Right value
+
+singleMangle :: Either TemplateMangleError value -> Either [TemplateMangleError] value
+singleMangle result = case result of
+    Left failure -> Left [failure]
+    Right value -> Right value
+
+mapMangle ::
+    PreparedDemand ->
+    Either [TemplateMangleError] value ->
+    Either [TemplateSpecializationError] value
+mapMangle demand result = case result of
+    Left failures -> Left [TemplateMangleFailed (preparedIdentity demand) failures]
+    Right value -> Right value
+
+declarationSources ::
+    [Declaration ResolvedName Type] ->
+    Map SymbolId (Declaration ResolvedName Type)
+declarationSources = Map.fromList . concatMap collect
+    where
+        collect declaration@TemplateTypeDeclaration {} =
+            (resolvedSymbol (declarationName declaration), declaration) : concatMap collect (typeMembers declaration)
+        collect TypeDeclaration {typeMembers = members} = concatMap collect members
+        collect FunctionDeclaration {} = []
+        collect EnumDeclaration {} = []
+
+attachDependencies :: [TemplateSpecialization] -> [TemplateSpecialization]
+attachDependencies specializations = map attach specializations
+    where
+        identityIds =
+            Map.fromList
+                [ (templateSpecializationIdentity specialization, templateSpecializationId specialization)
+                | specialization <- specializations
+                ]
+        attach specialization =
+            specialization
+                { templateSpecializationDependencies =
+                    unique
+                        [ dependency
+                        | valueType <- declarationTypes (templateSpecializationDeclaration specialization)
+                        , let identity = renderSpecializationIdentity valueType
+                        , identity /= templateSpecializationIdentity specialization
+                        , Just dependency <- [Map.lookup identity identityIds]
+                        ]
+                }
+
+-- | Find a plan entry by its plan-local identity.
+findTemplateSpecialization ::
+    TemplateSpecializationId ->
+    TemplateSpecializationPlan ->
+    Maybe TemplateSpecialization
+findTemplateSpecialization identifier =
+    first ((== identifier) . templateSpecializationId) . plannedTemplateSpecializations
+
+-- | Find a plan entry by its canonical concrete-type identity string.
+findTemplateSpecializationByIdentity ::
+    String ->
+    TemplateSpecializationPlan ->
+    Maybe TemplateSpecialization
+findTemplateSpecializationByIdentity identity =
+    first ((== identity) . templateSpecializationIdentity) . plannedTemplateSpecializations
+
+{- | Produce a stable dependency-first order. Reference-recursive cycles are
+legal, so a grey node closes the current DFS edge instead of becoming an
+error. Every specialization still appears exactly once.
+-}
+
+-- | Return stable dependency-first IDs for specialization emission.
+specializationEmissionOrder :: TemplateSpecializationPlan -> [TemplateSpecializationId]
+specializationEmissionOrder plan = reverse completed
+    where
+        table = Map.fromList [(templateSpecializationId value, value) | value <- plannedTemplateSpecializations plan]
+        (_, completed) = foldl' visitRoot ([], []) (Map.keys table)
+        visitRoot state identifier = visit table state identifier
+
+visit ::
+    Map TemplateSpecializationId TemplateSpecialization ->
+    ([TemplateSpecializationId], [TemplateSpecializationId]) ->
+    TemplateSpecializationId ->
+    ([TemplateSpecializationId], [TemplateSpecializationId])
+visit table state@(active, completed) identifier
+    | identifier `elem` completed = state
+    | identifier `elem` active = state
+    | otherwise = case Map.lookup identifier table of
+        Nothing -> state
+        Just specialization ->
+            let entered = (identifier : active, completed)
+                (afterActive, afterDependencies) =
+                    foldl' (visit table) entered (templateSpecializationDependencies specialization)
+             in (filter (/= identifier) afterActive, identifier : afterDependencies)
+
+{- | Build the exact closed TypedAST view consumed by declaration lowering.
+Open templates remain in the semantic artifacts but are not copied here.
+-}
+
+-- | Assemble the exact closed typed AST consumed by declaration lowering.
+specializationTypedAST :: TemplateSpecializationPlan -> TypedAST
+specializationTypedAST plan =
+    TypedAST
+        ( SyntaxTree
+            (plannedTemplateNamespace plan)
+            [ templateSpecializationDeclaration specialization
+            | identifier <- specializationEmissionOrder plan
+            , Just specialization <- [findTemplateSpecialization identifier plan]
+            ]
+        )
+
+memberCount :: TemplateSpecialization -> Int
+memberCount specialization = case templateSpecializationDeclaration specialization of
+    TypeDeclaration {typeMembers = members} -> length members
+    _ -> 0
+
+declarationTypes :: Declaration ResolvedName Type -> [Type]
+declarationTypes declaration = case declaration of
+    TypeDeclaration _ _ annotation members -> annotation : concatMap declarationTypes members
+    FunctionDeclaration _ _ annotation _ parameters body _ _ ->
+        annotation : concatMap parameterTypes parameters ++ blockTypes body
+    TemplateTypeDeclaration {} -> []
+    EnumDeclaration {} -> []
+
+parameterTypes :: Parameter ResolvedName Type -> [Type]
+parameterTypes parameter = [parameterAnnotation parameter]
+
+blockTypes :: Block ResolvedName Type -> [Type]
+blockTypes (Block statements) = concatMap statementTypes statements
+
+statementTypes :: Statement ResolvedName Type -> [Type]
+statementTypes statement = case statement of
+    BindingStatement _ _ _ _ annotation value -> annotation : expressionTypes value
+    AssignmentStatement _ _ annotation value -> annotation : expressionTypes value
+    ReturnStatement _ value -> maybe [] expressionTypes value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionTypes condition ++ blockTypes trueBlock ++ maybe [] blockTypes falseBlock
+    WhileStatement _ condition body -> expressionTypes condition ++ blockTypes body
+    DoWhileStatement _ body condition -> blockTypes body ++ expressionTypes condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementTypes initializer
+            ++ maybe [] expressionTypes condition
+            ++ concatMap statementTypes updates
+            ++ blockTypes body
+    ForEachStatement _ _ _ _ annotation source body -> annotation : expressionTypes source ++ blockTypes body
+    IncrementStatement _ _ annotation -> [annotation]
+    CompoundAssignmentStatement _ _ _ annotation value -> annotation : expressionTypes value
+    DiscardStatement _ value -> expressionTypes value
+    BreakStatement _ value -> maybe [] expressionTypes value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionTypes condition ++ blockTypes block
+    BlockStatement _ block -> blockTypes block
+    ExpressionStatement _ value _ -> expressionTypes value
+
+expressionTypes :: Expression ResolvedName Type -> [Type]
+expressionTypes expression = case expression of
+    NameExpression _ _ annotation -> [annotation]
+    LiteralExpression _ _ annotation -> [annotation]
+    MemberAccessExpression _ receiver _ annotation -> annotation : expressionTypes receiver
+    MethodReferenceExpression _ receiver _ annotation -> annotation : expressionTypes receiver
+    CallExpression _ callee arguments annotation ->
+        annotation : expressionTypes callee ++ concatMap expressionTypes arguments
+    UnaryExpression _ _ value annotation -> annotation : expressionTypes value
+    BinaryExpression _ _ left right annotation -> annotation : expressionTypes left ++ expressionTypes right
+    IsPatternExpression _ subject patternValue annotation ->
+        annotation : expressionTypes subject ++ patternTypes patternValue
+    ConditionalExpression _ condition whenTrue whenFalse annotation ->
+        annotation : concatMap expressionTypes [condition, whenTrue, whenFalse]
+    CoalesceExpression _ left fallback annotation ->
+        annotation : expressionTypes left ++ expressionTypes fallback
+    AssignmentExpression _ _ _ value annotation -> annotation : expressionTypes value
+    IncrementExpression _ _ _ annotation -> [annotation]
+    LoopExpression _ loop annotation -> annotation : statementTypes loop
+    BlockExpression _ block annotation -> annotation : blockTypes block
+    MatchExpression _ subjects arms annotation ->
+        annotation
+            : map matchPatternAnnotation (concatMap matchArmPatterns arms)
+            ++ concatMap expressionTypes (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ captures parameters body annotation ->
+        annotation
+            : concatMap captureTypes captures
+            ++ concatMap parameterTypes parameters
+            ++ callableBodyTypes body
+
+patternTypes :: Pattern ResolvedName Type -> [Type]
+patternTypes patternValue = case patternValue of
+    WildcardPattern _ annotation -> [annotation]
+    NullPattern _ annotation -> [annotation]
+    LiteralPattern _ _ annotation -> [annotation]
+    TypePattern _ _ annotation -> [annotation]
+    RelationalPattern _ _ _ annotation -> [annotation]
+    NotPattern _ nested annotation -> annotation : patternTypes nested
+    AndPattern _ left right annotation -> annotation : patternTypes left ++ patternTypes right
+    OrPattern _ left right annotation -> annotation : patternTypes left ++ patternTypes right
+
+captureTypes :: Capture ResolvedName Type -> [Type]
+captureTypes capture = captureAnnotation capture : maybe [] expressionTypes (captureInitializer capture)
+
+callableBodyTypes :: CallableBody ResolvedName Type -> [Type]
+callableBodyTypes body = case body of
+    CallableExpressionBody expression -> expressionTypes expression
+    CallableBlockBody block -> blockTypes block
+
+collectOpenSymbols :: Type -> [SymbolId]
+collectOpenSymbols valueType = unique (collectType valueType)
+    where
+        collectType current = case current of
+            NamedType _ arguments -> concatMap collectArgument arguments
+            FunctionType parameters result -> concatMap collectType (parameters ++ [result])
+            TypeVariable name -> [resolvedSymbol name]
+            ErrorType -> []
+        collectArgument argument = case argument of
+            TypeTemplateArgument nested -> collectType nested
+            ValueTemplateArgument (TemplateValueParameter name) -> [resolvedSymbol name]
+            ValueTemplateArgument _ -> []
+
+renderSpecializationIdentity :: Type -> String
+renderSpecializationIdentity valueType = case valueType of
+    NamedType name arguments -> renderName name ++ renderArguments arguments
+    FunctionType parameters result ->
+        "fn(" ++ intercalate "," (map renderSpecializationIdentity parameters) ++ ")->" ++ renderSpecializationIdentity result
+    TypeVariable name -> "open$" ++ show (symbolIdValue (resolvedSymbol name))
+    ErrorType -> "<error>"
+    where
+        renderArguments [] = ""
+        renderArguments arguments = "<" ++ intercalate "," (map renderArgument arguments) ++ ">"
+        renderArgument (TypeTemplateArgument nested) = "type:" ++ renderSpecializationIdentity nested
+        renderArgument (ValueTemplateArgument value) = "value:" ++ renderValue value
+        renderValue (IntegerTemplateValue value) = "i:" ++ show value
+        renderValue (BooleanTemplateValue value) = "b:" ++ if value then "true" else "false"
+        renderValue (CharacterTemplateValue value) = "c:" ++ show value
+        renderValue (TemplateValueParameter name) = "open$" ++ show (symbolIdValue (resolvedSymbol name))
+
+renderName :: QualifiedName -> String
+renderName (QualifiedName parts) = intercalate "." (map renderPart parts)
+    where
+        renderPart (Identifier value) = show (length value) ++ ":" ++ value
+
+-- | Render a demand, resource, reachability, or mangling failure.
+renderTemplateSpecializationError :: TemplateSpecializationError -> String
+renderTemplateSpecializationError issue = case issue of
+    InvalidTemplateSpecializationLimits message -> "invalid template specialization limits: " ++ message
+    TemplateApplicationFailed origin failures ->
+        "template demand from " ++ origin ++ " failed: " ++ intercalate "; " (map renderTemplateApplicationError failures)
+    TemplateInstantiationFailed identity failure ->
+        "template specialization " ++ identity ++ " failed: " ++ renderTemplateInstantiationError failure
+    TemplateDeclarationSourceMissing name symbol ->
+        "template declaration " ++ renderPlainName name ++ " (symbol " ++ show (symbolIdValue symbol) ++ ") has no typed source"
+    TemplateMemberNotFound name member ->
+        "template declaration " ++ renderPlainName name ++ " has no member named " ++ identifierText member
+    TemplateSpecializationRemainsOpen identity symbols ->
+        "template specialization " ++ identity ++ " remains open over symbols " ++ show (map symbolIdValue symbols)
+    TemplateSpecializationLimitExceeded limit identity ->
+        "template specialization limit " ++ show limit ++ " was exceeded by " ++ identity
+    TemplateOriginLimitExceeded limit origin ->
+        "template origin limit " ++ show limit ++ " was exceeded by " ++ origin
+    TemplateMemberLimitExceeded name limit actual ->
+        "template declaration "
+            ++ renderPlainName name
+            ++ " selected "
+            ++ show actual
+            ++ " members, exceeding limit "
+            ++ show limit
+    TemplateMemberReachabilityFailed name failures ->
+        "template declaration "
+            ++ renderPlainName name
+            ++ " has an invalid member call graph: "
+            ++ intercalate "; " (map renderTemplateMemberReachabilityError failures)
+    TemplateMangleFailed identity failures ->
+        "template specialization "
+            ++ identity
+            ++ " cannot be mangled: "
+            ++ intercalate "; " (map renderTemplateMangleError failures)
+
+renderPlainName :: QualifiedName -> String
+renderPlainName (QualifiedName parts) = intercalate "." (map identifierText parts)
+
+first :: (value -> Bool) -> [value] -> Maybe value
+first _ [] = Nothing
+first predicate (value : remaining)
+    | predicate value = Just value
+    | otherwise = first predicate remaining
+
+unique :: (Eq value) => [value] -> [value]
+unique = foldl' append []
+    where
+        append values value
+            | value `elem` values = values
+            | otherwise = values ++ [value]
+
+collectResults :: [Either [problem] value] -> Either [problem] [value]
+collectResults results = case concat [problems | Left problems <- results] of
+    [] -> Right [value | Right value <- results]
+    problems -> Left problems
+ src/Progmasoft/Tala/Template/Specialization/Verifier.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
+
+{- |
+Invariant verification for declaration-level specialization plans.
+
+The planner is pure, but its output crosses into Core and eventually native
+linkage. Verification keeps malformed hand-built plans and future planner
+regressions from becoming backend assumptions or duplicate linker symbols.
+-}
+module Progmasoft.Tala.Template.Specialization.Verifier
+    ( TemplatePlanIssue (..)
+    , verifyTemplateSpecializationPlan
+    , templateSpecializationPlanIssues
+    , renderTemplatePlanIssue
+    ) where
+
+import Data.List (intercalate, sort)
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Template.Mangling
+import Progmasoft.Tala.Template.Specialization
+
+-- | Independently checkable inconsistency in a specialization plan.
+data TemplatePlanIssue
+    = InvalidPlanSpecializationId TemplateSpecializationId
+    | DuplicatePlanSpecializationId TemplateSpecializationId
+    | EmptyPlanSpecializationIdentity TemplateSpecializationId
+    | DuplicatePlanSpecializationIdentity String
+    | PlanSpecializationTypeIsOpen TemplateSpecializationId [SymbolId]
+    | PlanSpecializationTypeHasError TemplateSpecializationId
+    | PlanDeclarationIsNotClosedType TemplateSpecializationId
+    | PlanDeclarationTypeMismatch TemplateSpecializationId Type Type
+    | EmptyPlanOrigin TemplateSpecializationId
+    | DuplicatePlanOrigin TemplateSpecializationId String
+    | MissingPlanDependency TemplateSpecializationId TemplateSpecializationId
+    | DuplicatePlanDependency TemplateSpecializationId TemplateSpecializationId
+    | SelfPlanDependency TemplateSpecializationId
+    | InvalidFreshSymbol TemplateSpecializationId SymbolId SymbolId
+    | DuplicateFreshSourceSymbol TemplateSpecializationId SymbolId
+    | DuplicateFreshTargetSymbol TemplateSpecializationId SymbolId
+    | SharedFreshTargetSymbol SymbolId [TemplateSpecializationId]
+    | DeclarationSymbolMissingFromFreshMap TemplateSpecializationId SymbolId
+    | LayoutPlanContainsMembers TemplateSpecializationId Int
+    | RequestedPlanMemberMissing TemplateSpecializationId Identifier
+    | UnexpectedPlanMember TemplateSpecializationId Identifier
+    | InvalidPlanMangledType TemplateSpecializationId String
+    | IncorrectPlanMangledType TemplateSpecializationId String String
+    | InvalidPlanMangledMember TemplateSpecializationId SymbolId String
+    | MissingPlanMangledMember TemplateSpecializationId SymbolId
+    | UnexpectedPlanMangledMember TemplateSpecializationId SymbolId
+    | DuplicatePlanMangledMember TemplateSpecializationId SymbolId
+    | DuplicatePlanMangledSymbol String
+    | IncorrectPlanStatistics TemplateSpecializationStatistics TemplateSpecializationStatistics
+    | IncompletePlanEmissionOrder [TemplateSpecializationId] [TemplateSpecializationId]
+    deriving (Eq, Ord, Read, Show)
+
+-- | Return a plan unchanged when every invariant holds, otherwise all issues.
+verifyTemplateSpecializationPlan ::
+    TemplateSpecializationPlan ->
+    Either [TemplatePlanIssue] TemplateSpecializationPlan
+verifyTemplateSpecializationPlan plan = case templateSpecializationPlanIssues plan of
+    [] -> Right plan
+    problems -> Left problems
+
+-- | Recompute plan invariants without trusting the planner that produced it.
+templateSpecializationPlanIssues :: TemplateSpecializationPlan -> [TemplatePlanIssue]
+templateSpecializationPlanIssues plan =
+    idProblems
+        ++ identityProblems
+        ++ concatMap (specializationProblems ids) specializations
+        ++ sharedSymbolProblems specializations
+        ++ duplicateMangledSymbols specializations
+        ++ statisticsProblems plan
+        ++ emissionProblems plan
+    where
+        specializations = plannedTemplateSpecializations plan
+        ids = map templateSpecializationId specializations
+        identities = map templateSpecializationIdentity specializations
+        idProblems =
+            [InvalidPlanSpecializationId identifier | identifier <- ids, templateSpecializationIdValue identifier <= 0]
+                ++ [DuplicatePlanSpecializationId identifier | identifier <- duplicates ids]
+        identityProblems =
+            [ EmptyPlanSpecializationIdentity (templateSpecializationId specialization)
+            | specialization <- specializations
+            , null (templateSpecializationIdentity specialization)
+            ]
+                ++ [DuplicatePlanSpecializationIdentity identity | identity <- duplicates identities]
+
+specializationProblems :: [TemplateSpecializationId] -> TemplateSpecialization -> [TemplatePlanIssue]
+specializationProblems available specialization =
+    typeProblems specialization
+        ++ declarationProblems specialization
+        ++ originProblems specialization
+        ++ dependencyProblems available specialization
+        ++ freshMapProblems specialization
+        ++ scopeProblems specialization
+        ++ mangledTypeProblems specialization
+        ++ mangledMemberProblems specialization
+
+typeProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+typeProblems specialization =
+    [PlanSpecializationTypeIsOpen identifier open | not (null open)]
+        ++ [PlanSpecializationTypeHasError identifier | typeHasError valueType]
+    where
+        identifier = templateSpecializationId specialization
+        valueType = templateSpecializationType specialization
+        open = openTypeSymbols valueType
+
+declarationProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+declarationProblems specialization = case templateSpecializationDeclaration specialization of
+    TypeDeclaration _ _ annotation _ ->
+        [ PlanDeclarationTypeMismatch identifier (templateSpecializationType specialization) annotation
+        | annotation /= templateSpecializationType specialization
+        ]
+            ++ [PlanSpecializationTypeHasError identifier | declarationHasError (templateSpecializationDeclaration specialization)]
+            ++ [ PlanSpecializationTypeIsOpen identifier open
+               | let open = declarationOpenSymbols (templateSpecializationDeclaration specialization)
+               , not (null open)
+               ]
+    _ -> [PlanDeclarationIsNotClosedType identifier]
+    where
+        identifier = templateSpecializationId specialization
+
+originProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+originProblems specialization =
+    [EmptyPlanOrigin identifier | any null origins]
+        ++ [DuplicatePlanOrigin identifier origin | origin <- duplicates origins]
+    where
+        identifier = templateSpecializationId specialization
+        origins = templateSpecializationOrigins specialization
+
+dependencyProblems :: [TemplateSpecializationId] -> TemplateSpecialization -> [TemplatePlanIssue]
+dependencyProblems available specialization =
+    [ MissingPlanDependency owner dependency
+    | dependency <- dependencies
+    , dependency `notElem` available
+    ]
+        ++ [DuplicatePlanDependency owner dependency | dependency <- duplicates dependencies]
+        ++ [SelfPlanDependency owner | owner `elem` dependencies]
+    where
+        owner = templateSpecializationId specialization
+        dependencies = templateSpecializationDependencies specialization
+
+freshMapProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+freshMapProblems specialization =
+    [ InvalidFreshSymbol identifier old new
+    | (old, new) <- mappings
+    , symbolIdValue old <= 0 || symbolIdValue new <= 0 || old == new
+    ]
+        ++ [DuplicateFreshSourceSymbol identifier old | old <- duplicates (map fst mappings)]
+        ++ [DuplicateFreshTargetSymbol identifier new | new <- duplicates (map snd mappings)]
+        ++ [ DeclarationSymbolMissingFromFreshMap identifier symbol
+           | symbol <- declarationDefinitionSymbols (templateSpecializationDeclaration specialization)
+           , symbol `notElem` map snd mappings
+           ]
+    where
+        identifier = templateSpecializationId specialization
+        mappings = templateSpecializationSymbolMap specialization
+
+sharedSymbolProblems :: [TemplateSpecialization] -> [TemplatePlanIssue]
+sharedSymbolProblems specializations =
+    [ SharedFreshTargetSymbol symbol owners
+    | symbol <- duplicates allTargets
+    , let owners =
+            [ templateSpecializationId specialization
+            | specialization <- specializations
+            , symbol `elem` map snd (templateSpecializationSymbolMap specialization)
+            ]
+    ]
+    where
+        allTargets = concatMap (map snd . templateSpecializationSymbolMap) specializations
+
+scopeProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+scopeProblems specialization = case templateSpecializationScope specialization of
+    TemplateLayoutDemand -> [LayoutPlanContainsMembers identifier (length members) | not (null members)]
+    TemplateMemberDemand requested ->
+        [RequestedPlanMemberMissing identifier name | name <- requested, name `notElem` memberNames]
+            ++ [UnexpectedPlanMember identifier name | name <- memberNames, name `notElem` requested]
+    TemplateCompleteDemand -> []
+    where
+        identifier = templateSpecializationId specialization
+        members = declarationMembers (templateSpecializationDeclaration specialization)
+        memberNames = map (resolvedSpelling . declarationName) members
+
+mangledTypeProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+mangledTypeProblems specialization =
+    [InvalidPlanMangledType identifier actual | not (validMangledTemplateSymbol actual)]
+        ++ case mangleTemplateType defaultTemplateMangleLimits (templateSpecializationType specialization) of
+            Left _ -> [InvalidPlanMangledType identifier actual]
+            Right expected ->
+                [ IncorrectPlanMangledType identifier (mangledTemplateTypeText expected) actual
+                | expected /= templateSpecializationMangledType specialization
+                ]
+    where
+        identifier = templateSpecializationId specialization
+        actual = mangledTemplateTypeText (templateSpecializationMangledType specialization)
+
+mangledMemberProblems :: TemplateSpecialization -> [TemplatePlanIssue]
+mangledMemberProblems specialization =
+    invalid
+        ++ missing
+        ++ unexpected
+        ++ repeated
+        ++ incorrect
+    where
+        identifier = templateSpecializationId specialization
+        members = declarationMembers (templateSpecializationDeclaration specialization)
+        memberSymbols = map (resolvedSymbol . declarationName) members
+        mangled = templateSpecializationMangledMembers specialization
+        mangledSymbols = map mangledMemberSourceSymbol mangled
+        invalid =
+            [ InvalidPlanMangledMember identifier (mangledMemberSourceSymbol member) (mangledMemberText member)
+            | member <- mangled
+            , not (validMangledTemplateSymbol (mangledMemberText member))
+            ]
+        missing = [MissingPlanMangledMember identifier symbol | symbol <- memberSymbols, symbol `notElem` mangledSymbols]
+        unexpected = [UnexpectedPlanMangledMember identifier symbol | symbol <- mangledSymbols, symbol `notElem` memberSymbols]
+        repeated = [DuplicatePlanMangledMember identifier symbol | symbol <- duplicates mangledSymbols]
+        incorrect = case mangleTemplateMembers
+            defaultTemplateMangleLimits
+            (templateSpecializationType specialization)
+            (templateSpecializationDeclaration specialization) of
+            Left _ -> [InvalidPlanMangledType identifier (mangledTemplateTypeText (templateSpecializationMangledType specialization))]
+            Right expected ->
+                [ InvalidPlanMangledMember identifier (mangledMemberSourceSymbol actual) (mangledMemberText actual)
+                | actual <- mangled
+                , Just expectedMember <- [findMember (mangledMemberSourceSymbol actual) expected]
+                , mangledMemberText actual /= mangledMemberText expectedMember
+                ]
+
+duplicateMangledSymbols :: [TemplateSpecialization] -> [TemplatePlanIssue]
+duplicateMangledSymbols specializations =
+    [DuplicatePlanMangledSymbol symbol | symbol <- duplicates symbols]
+    where
+        symbols =
+            concat
+                [ mangledTemplateTypeText (templateSpecializationMangledType specialization)
+                    : map mangledMemberText (templateSpecializationMangledMembers specialization)
+                | specialization <- specializations
+                ]
+
+statisticsProblems :: TemplateSpecializationPlan -> [TemplatePlanIssue]
+statisticsProblems plan =
+    [IncorrectPlanStatistics expected actual | expected /= actual]
+    where
+        actual = plannedTemplateStatistics plan
+        specializations = plannedTemplateSpecializations plan
+        expected =
+            actual
+                { uniqueTemplateSpecializations = length specializations
+                , coalescedTemplateSpecializations = requestedTemplateSpecializations actual - length specializations
+                , instantiatedTemplateMembers =
+                    sum (map (length . declarationMembers . templateSpecializationDeclaration) specializations)
+                , retainedTemplateOrigins = sum (map (length . templateSpecializationOrigins) specializations)
+                , templateDependencyEdges = sum (map (length . templateSpecializationDependencies) specializations)
+                }
+
+emissionProblems :: TemplateSpecializationPlan -> [TemplatePlanIssue]
+emissionProblems plan =
+    [IncompletePlanEmissionOrder expected actual | sort expected /= sort actual || length actual /= length expected]
+    where
+        expected = map templateSpecializationId (plannedTemplateSpecializations plan)
+        actual = specializationEmissionOrder plan
+
+declarationMembers :: Declaration ResolvedName Type -> [Declaration ResolvedName Type]
+declarationMembers TypeDeclaration {typeMembers = members} = members
+declarationMembers _ = []
+
+declarationDefinitionSymbols :: Declaration ResolvedName Type -> [SymbolId]
+declarationDefinitionSymbols declaration = case declaration of
+    TypeDeclaration _ name _ members -> resolvedSymbol name : concatMap declarationDefinitionSymbols members
+    FunctionDeclaration _ name _ _ parameters body _ _ ->
+        resolvedSymbol name
+            : map (resolvedSymbol . parameterName) parameters
+            ++ blockDefinitionSymbols body
+    TemplateTypeDeclaration {} -> []
+    EnumDeclaration _ name _ _ _ -> [resolvedSymbol name]
+
+blockDefinitionSymbols :: Block ResolvedName Type -> [SymbolId]
+blockDefinitionSymbols (Block statements) = concatMap statementDefinitionSymbols statements
+
+statementDefinitionSymbols :: Statement ResolvedName Type -> [SymbolId]
+statementDefinitionSymbols statement = case statement of
+    BindingStatement _ _ _ name _ value -> resolvedSymbol name : expressionDefinitionSymbols value
+    AssignmentStatement _ _ _ value -> expressionDefinitionSymbols value
+    ReturnStatement _ value -> maybe [] expressionDefinitionSymbols value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionDefinitionSymbols condition
+            ++ blockDefinitionSymbols trueBlock
+            ++ maybe [] blockDefinitionSymbols falseBlock
+    WhileStatement _ condition body -> expressionDefinitionSymbols condition ++ blockDefinitionSymbols body
+    DoWhileStatement _ body condition -> blockDefinitionSymbols body ++ expressionDefinitionSymbols condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementDefinitionSymbols initializer
+            ++ maybe [] expressionDefinitionSymbols condition
+            ++ concatMap statementDefinitionSymbols updates
+            ++ blockDefinitionSymbols body
+    ForEachStatement _ _ _ name _ source body ->
+        resolvedSymbol name : expressionDefinitionSymbols source ++ blockDefinitionSymbols body
+    IncrementStatement _ name _ -> [resolvedSymbol name]
+    CompoundAssignmentStatement _ _ _ _ value -> expressionDefinitionSymbols value
+    DiscardStatement _ value -> expressionDefinitionSymbols value
+    BreakStatement _ value -> maybe [] expressionDefinitionSymbols value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionDefinitionSymbols condition ++ blockDefinitionSymbols block
+    BlockStatement _ block -> blockDefinitionSymbols block
+    ExpressionStatement _ expression _ -> expressionDefinitionSymbols expression
+
+expressionDefinitionSymbols :: Expression ResolvedName Type -> [SymbolId]
+expressionDefinitionSymbols expression = case expression of
+    MemberAccessExpression _ receiver _ _ -> expressionDefinitionSymbols receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionDefinitionSymbols receiver
+    CallExpression _ callee arguments _ ->
+        expressionDefinitionSymbols callee ++ concatMap expressionDefinitionSymbols arguments
+    UnaryExpression _ _ value _ -> expressionDefinitionSymbols value
+    BinaryExpression _ _ left right _ -> expressionDefinitionSymbols left ++ expressionDefinitionSymbols right
+    IsPatternExpression _ subject _ _ -> expressionDefinitionSymbols subject
+    ConditionalExpression _ condition first second _ ->
+        concatMap expressionDefinitionSymbols [condition, first, second]
+    CoalesceExpression _ left fallback _ ->
+        expressionDefinitionSymbols left ++ expressionDefinitionSymbols fallback
+    AssignmentExpression _ _ _ value _ -> expressionDefinitionSymbols value
+    IncrementExpression {} -> []
+    LoopExpression _ loop _ -> statementDefinitionSymbols loop
+    BlockExpression _ block _ -> blockDefinitionSymbols block
+    MatchExpression _ subjects arms _ ->
+        [ resolvedSymbol name
+        | arm <- arms
+        , Just name <- map matchPatternBinding (matchArmPatterns arm)
+        ]
+            ++ concatMap expressionDefinitionSymbols (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ captures parameters body _ ->
+        map (resolvedSymbol . captureName) captures
+            ++ map (resolvedSymbol . parameterName) parameters
+            ++ callableBodyDefinitionSymbols body
+    NameExpression {} -> []
+    LiteralExpression {} -> []
+
+callableBodyDefinitionSymbols :: CallableBody ResolvedName Type -> [SymbolId]
+callableBodyDefinitionSymbols body = case body of
+    CallableExpressionBody expression -> expressionDefinitionSymbols expression
+    CallableBlockBody block -> blockDefinitionSymbols block
+
+declarationOpenSymbols :: Declaration ResolvedName Type -> [SymbolId]
+declarationOpenSymbols declaration = unique (concatMap openTypeSymbols (declarationTypes declaration))
+
+declarationHasError :: Declaration ResolvedName Type -> Bool
+declarationHasError = any typeHasError . declarationTypes
+
+declarationTypes :: Declaration ResolvedName Type -> [Type]
+declarationTypes declaration = case declaration of
+    TypeDeclaration _ _ annotation members -> annotation : concatMap declarationTypes members
+    FunctionDeclaration _ _ annotation _ parameters body _ _ ->
+        annotation : map parameterAnnotation parameters ++ blockTypes body
+    TemplateTypeDeclaration _ _ annotation parameters members ->
+        annotation : map templateParameterAnnotation parameters ++ concatMap declarationTypes members
+    EnumDeclaration _ _ annotation _ _ -> [annotation]
+
+blockTypes :: Block ResolvedName Type -> [Type]
+blockTypes (Block statements) = concatMap statementTypes statements
+
+statementTypes :: Statement ResolvedName Type -> [Type]
+statementTypes statement = case statement of
+    BindingStatement _ _ _ _ annotation value -> annotation : expressionTypes value
+    AssignmentStatement _ _ annotation value -> annotation : expressionTypes value
+    ReturnStatement _ value -> maybe [] expressionTypes value
+    IfStatement _ condition trueBlock falseBlock ->
+        expressionTypes condition ++ blockTypes trueBlock ++ maybe [] blockTypes falseBlock
+    WhileStatement _ condition body -> expressionTypes condition ++ blockTypes body
+    DoWhileStatement _ body condition -> blockTypes body ++ expressionTypes condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementTypes initializer
+            ++ maybe [] expressionTypes condition
+            ++ concatMap statementTypes updates
+            ++ blockTypes body
+    ForEachStatement _ _ _ _ annotation source body -> annotation : expressionTypes source ++ blockTypes body
+    IncrementStatement _ _ annotation -> [annotation]
+    CompoundAssignmentStatement _ _ _ annotation value -> annotation : expressionTypes value
+    DiscardStatement _ value -> expressionTypes value
+    BreakStatement _ value -> maybe [] expressionTypes value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionTypes condition ++ blockTypes block
+    BlockStatement _ block -> blockTypes block
+    ExpressionStatement _ value _ -> expressionTypes value
+
+expressionTypes :: Expression ResolvedName Type -> [Type]
+expressionTypes expression = case expression of
+    NameExpression _ _ annotation -> [annotation]
+    LiteralExpression _ _ annotation -> [annotation]
+    MemberAccessExpression _ receiver _ annotation -> annotation : expressionTypes receiver
+    MethodReferenceExpression _ receiver _ annotation -> annotation : expressionTypes receiver
+    CallExpression _ callee arguments annotation ->
+        annotation : expressionTypes callee ++ concatMap expressionTypes arguments
+    UnaryExpression _ _ value annotation -> annotation : expressionTypes value
+    BinaryExpression _ _ left right annotation -> annotation : expressionTypes left ++ expressionTypes right
+    IsPatternExpression _ subject patternValue annotation ->
+        annotation : expressionTypes subject ++ patternTypes patternValue
+    ConditionalExpression _ condition first second annotation ->
+        annotation : concatMap expressionTypes [condition, first, second]
+    CoalesceExpression _ left fallback annotation ->
+        annotation : expressionTypes left ++ expressionTypes fallback
+    AssignmentExpression _ _ _ value annotation -> annotation : expressionTypes value
+    IncrementExpression _ _ _ annotation -> [annotation]
+    LoopExpression _ loop annotation -> annotation : statementTypes loop
+    BlockExpression _ block annotation -> annotation : blockTypes block
+    MatchExpression _ subjects arms annotation ->
+        annotation
+            : map matchPatternAnnotation (concatMap matchArmPatterns arms)
+            ++ concatMap expressionTypes (subjects ++ concatMap matchArmExpressions arms)
+    CallableExpression _ _ captures parameters body annotation ->
+        annotation
+            : map captureAnnotation captures
+            ++ map parameterAnnotation parameters
+            ++ callableBodyTypes body
+
+patternTypes :: Pattern ResolvedName Type -> [Type]
+patternTypes patternValue = case patternValue of
+    WildcardPattern _ annotation -> [annotation]
+    NullPattern _ annotation -> [annotation]
+    LiteralPattern _ _ annotation -> [annotation]
+    TypePattern _ _ annotation -> [annotation]
+    RelationalPattern _ _ _ annotation -> [annotation]
+    NotPattern _ nested annotation -> annotation : patternTypes nested
+    AndPattern _ left right annotation -> annotation : patternTypes left ++ patternTypes right
+    OrPattern _ left right annotation -> annotation : patternTypes left ++ patternTypes right
+
+callableBodyTypes :: CallableBody ResolvedName Type -> [Type]
+callableBodyTypes body = case body of
+    CallableExpressionBody expression -> expressionTypes expression
+    CallableBlockBody block -> blockTypes block
+
+openTypeSymbols :: Type -> [SymbolId]
+openTypeSymbols valueType = unique (collectType valueType)
+    where
+        collectType current = case current of
+            NamedType _ arguments -> concatMap collectArgument arguments
+            FunctionType parameters result -> concatMap collectType (parameters ++ [result])
+            TypeVariable name -> [resolvedSymbol name]
+            ErrorType -> []
+        collectArgument argument = case argument of
+            TypeTemplateArgument nested -> collectType nested
+            ValueTemplateArgument (TemplateValueParameter name) -> [resolvedSymbol name]
+            ValueTemplateArgument _ -> []
+
+typeHasError :: Type -> Bool
+typeHasError valueType = case valueType of
+    NamedType _ arguments -> any argumentHasError arguments
+    FunctionType parameters result -> any typeHasError parameters || typeHasError result
+    TypeVariable _ -> False
+    ErrorType -> True
+    where
+        argumentHasError (TypeTemplateArgument nested) = typeHasError nested
+        argumentHasError (ValueTemplateArgument _) = False
+
+findMember :: SymbolId -> [MangledTemplateMember] -> Maybe MangledTemplateMember
+findMember _ [] = Nothing
+findMember symbol (member : remaining)
+    | mangledMemberSourceSymbol member == symbol = Just member
+    | otherwise = findMember symbol remaining
+
+duplicates :: (Ord value) => [value] -> [value]
+duplicates values =
+    [ firstValue
+    | duplicate@(firstValue : _) <- grouped (sort values)
+    , length duplicate > 1
+    ]
+    where
+        grouped [] = []
+        grouped (value : remaining) =
+            let (same, rest) = span (== value) remaining
+             in (value : same) : grouped rest
+
+unique :: (Eq value) => [value] -> [value]
+unique = foldl append []
+    where
+        append values value
+            | value `elem` values = values
+            | otherwise = values ++ [value]
+
+-- | Render one plan-verification issue as a diagnostic-ready message.
+renderTemplatePlanIssue :: TemplatePlanIssue -> String
+renderTemplatePlanIssue issue = case issue of
+    InvalidPlanSpecializationId identifier -> prefix identifier ++ "has a non-positive id"
+    DuplicatePlanSpecializationId identifier -> prefix identifier ++ "duplicates a specialization id"
+    EmptyPlanSpecializationIdentity identifier -> prefix identifier ++ "has an empty canonical identity"
+    DuplicatePlanSpecializationIdentity identity -> "template plan duplicates canonical identity " ++ identity
+    PlanSpecializationTypeIsOpen identifier symbols -> prefix identifier ++ "remains open over " ++ intercalate "," (map showSymbol symbols)
+    PlanSpecializationTypeHasError identifier -> prefix identifier ++ "contains ErrorType"
+    PlanDeclarationIsNotClosedType identifier -> prefix identifier ++ "does not contain an ordinary closed type declaration"
+    PlanDeclarationTypeMismatch identifier expected actual -> prefix identifier ++ "declaration type differs: " ++ show expected ++ " /= " ++ show actual
+    EmptyPlanOrigin identifier -> prefix identifier ++ "contains an empty diagnostic origin"
+    DuplicatePlanOrigin identifier origin -> prefix identifier ++ "duplicates origin " ++ origin
+    MissingPlanDependency owner dependency -> prefix owner ++ "references missing dependency " ++ showId dependency
+    DuplicatePlanDependency owner dependency -> prefix owner ++ "duplicates dependency " ++ showId dependency
+    SelfPlanDependency identifier -> prefix identifier ++ "depends directly on itself"
+    InvalidFreshSymbol identifier old new -> prefix identifier ++ "has invalid fresh mapping " ++ showSymbol old ++ " -> " ++ showSymbol new
+    DuplicateFreshSourceSymbol identifier symbol -> prefix identifier ++ "maps source symbol twice: " ++ showSymbol symbol
+    DuplicateFreshTargetSymbol identifier symbol -> prefix identifier ++ "allocates target symbol twice: " ++ showSymbol symbol
+    SharedFreshTargetSymbol symbol owners -> "fresh symbol " ++ showSymbol symbol ++ " is shared by " ++ showIds owners
+    DeclarationSymbolMissingFromFreshMap identifier symbol -> prefix identifier ++ "definition is absent from fresh map: " ++ showSymbol symbol
+    LayoutPlanContainsMembers identifier count -> prefix identifier ++ "layout-only demand contains " ++ show count ++ " members"
+    RequestedPlanMemberMissing identifier name -> prefix identifier ++ "is missing requested member " ++ identifierText name
+    UnexpectedPlanMember identifier name -> prefix identifier ++ "contains unexpected member " ++ identifierText name
+    InvalidPlanMangledType identifier symbol -> prefix identifier ++ "has invalid mangled type symbol " ++ symbol
+    IncorrectPlanMangledType identifier expected actual -> prefix identifier ++ "mangled type differs: " ++ expected ++ " /= " ++ actual
+    InvalidPlanMangledMember identifier symbol name -> prefix identifier ++ "has invalid mangled member " ++ showSymbol symbol ++ ": " ++ name
+    MissingPlanMangledMember identifier symbol -> prefix identifier ++ "has no mangled name for member " ++ showSymbol symbol
+    UnexpectedPlanMangledMember identifier symbol -> prefix identifier ++ "has a mangled name for unknown member " ++ showSymbol symbol
+    DuplicatePlanMangledMember identifier symbol -> prefix identifier ++ "mangles member twice: " ++ showSymbol symbol
+    DuplicatePlanMangledSymbol symbol -> "template plan emits duplicate mangled symbol " ++ symbol
+    IncorrectPlanStatistics expected actual -> "template plan statistics differ: " ++ show expected ++ " /= " ++ show actual
+    IncompletePlanEmissionOrder expected actual -> "template emission order differs: " ++ showIds expected ++ " /= " ++ showIds actual
+    where
+        prefix identifier = "template specialization " ++ showId identifier ++ " "
+        showId = show . templateSpecializationIdValue
+        showIds = intercalate "," . map showId
+        showSymbol = show . symbolIdValue
+ src/Progmasoft/Tala/Template/Verifier.hs view
@@ -0,0 +1,187 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Structural verification for typed template declarations.
+
+These checks defend the boundary consumed by application binding and future
+declaration cloning. They intentionally do not reject multiple declarations
+with one public name: constrained specialization permits that surface, and
+constraint ordering will disambiguate it in a later pass.
+-}
+module Progmasoft.Tala.Template.Verifier
+    ( TemplateVerificationError (..)
+    , verifyTemplateDeclarations
+    , verifyTemplateDeclaration
+    , renderTemplateVerificationError
+    ) where
+
+import Progmasoft.Tala.AST
+
+-- | Invalid declaration, parameter, default, or member invariant.
+data TemplateVerificationError
+    = -- | Declaration identity is reserved or non-positive.
+      TemplateDeclarationHasInvalidSymbol QualifiedName SymbolId
+    | -- | Parameter identity is invalid.
+      TemplateParameterHasInvalidSymbol QualifiedName Identifier SymbolId
+    | -- | Parameters reuse a semantic identity.
+      TemplateParameterSymbolIsDuplicated QualifiedName SymbolId
+    | -- | Parameters repeat a source spelling.
+      TemplateParameterSpellingIsDuplicated QualifiedName Identifier
+    | -- | Annotation contains ErrorType.
+      TemplateParameterHasErrorType QualifiedName ResolvedName
+    | -- | Parameter pack declares an unsupported default.
+      TemplateParameterPackHasDefault QualifiedName ResolvedName
+    | -- | Default does not match parameter kind.
+      TemplateParameterDefaultCategoryMismatch QualifiedName ResolvedName
+    | -- | Template-template parameter accepts no shapes.
+      TemplateTemplateSignatureIsEmpty QualifiedName ResolvedName
+    | -- | Member identity is reserved or non-positive.
+      TemplateMemberHasInvalidSymbol QualifiedName ResolvedName
+    deriving (Eq, Ord, Read, Show)
+
+-- | Verify every open template declaration in one typed compilation unit.
+verifyTemplateDeclarations :: TypedAST -> Either [TemplateVerificationError] ()
+verifyTemplateDeclarations (TypedAST (SyntaxTree namespace declarations)) =
+    finish (concatMap (verifyTop namespace) declarations)
+    where
+        verifyTop owner declaration = case declaration of
+            TemplateTypeDeclaration {} -> verifyTemplateDeclarationProblems owner declaration
+            _ -> []
+
+-- | Verify a single declaration in its already-resolved owner namespace.
+verifyTemplateDeclaration ::
+    Maybe QualifiedName ->
+    Declaration ResolvedName Type ->
+    Either [TemplateVerificationError] ()
+verifyTemplateDeclaration namespace declaration = case declaration of
+    TemplateTypeDeclaration {} -> finish (verifyTemplateDeclarationProblems namespace declaration)
+    _ -> Right ()
+
+verifyTemplateDeclarationProblems ::
+    Maybe QualifiedName ->
+    Declaration ResolvedName Type ->
+    [TemplateVerificationError]
+verifyTemplateDeclarationProblems namespace declaration@TemplateTypeDeclaration {} =
+    declarationSymbolProblems
+        ++ concatMap (parameterProblems declarationNameValue) parameters
+        ++ duplicateSymbolProblems declarationNameValue parameters
+        ++ duplicateSpellingProblems declarationNameValue parameters
+        ++ concatMap (memberProblems declarationNameValue) (typeMembers declaration)
+    where
+        declarationNameValue = qualify namespace (resolvedSpelling (declarationName declaration))
+        parameters = declarationTemplateParameters declaration
+        declarationSymbolProblems =
+            [ TemplateDeclarationHasInvalidSymbol declarationNameValue (resolvedSymbol (declarationName declaration))
+            | symbolIdValue (resolvedSymbol (declarationName declaration)) <= 0
+            ]
+verifyTemplateDeclarationProblems _ _ = []
+
+parameterProblems ::
+    QualifiedName ->
+    TemplateParameter ResolvedName Type ->
+    [TemplateVerificationError]
+parameterProblems owner parameter =
+    invalidSymbol
+        ++ errorType
+        ++ packDefault
+        ++ categoryDefault
+        ++ shapeProblems
+    where
+        name = templateParameterName parameter
+        invalidSymbol =
+            [ TemplateParameterHasInvalidSymbol owner (resolvedSpelling name) (resolvedSymbol name)
+            | symbolIdValue (resolvedSymbol name) <= 0
+            ]
+        errorType = [TemplateParameterHasErrorType owner name | templateParameterAnnotation parameter == ErrorType]
+        packDefault =
+            [ TemplateParameterPackHasDefault owner name
+            | templateParameterIsPack parameter && templateParameterDefault parameter /= Nothing
+            ]
+        categoryDefault =
+            [ TemplateParameterDefaultCategoryMismatch owner name
+            | not (defaultMatchesKind (templateParameterKind parameter) (templateParameterDefault parameter))
+            ]
+        shapeProblems = case templateParameterKind parameter of
+            TemplateTemplateParameter [] -> [TemplateTemplateSignatureIsEmpty owner name]
+            TemplateTemplateParameter shapes -> concatMap (nestedShapeProblems owner name) shapes
+            _ -> []
+
+defaultMatchesKind :: TemplateParameterKind -> Maybe TemplateDefault -> Bool
+defaultMatchesKind _ Nothing = True
+defaultMatchesKind TemplateTypeParameter (Just TemplateTypeDefault {}) = True
+defaultMatchesKind TemplateValueParameterKind {} (Just TemplateValueDefault {}) = True
+defaultMatchesKind TemplateTemplateParameter {} (Just TemplateTypeDefault {}) = True
+defaultMatchesKind _ _ = False
+
+nestedShapeProblems :: QualifiedName -> ResolvedName -> TemplateParameterShape -> [TemplateVerificationError]
+nestedShapeProblems owner name shape = case templateParameterShapeKind shape of
+    TemplateTemplateParameterShape [] -> [TemplateTemplateSignatureIsEmpty owner name]
+    TemplateTemplateParameterShape nested -> concatMap (nestedShapeProblems owner name) nested
+    _ -> []
+
+duplicateSymbolProblems ::
+    QualifiedName ->
+    [TemplateParameter ResolvedName Type] ->
+    [TemplateVerificationError]
+duplicateSymbolProblems owner parameters =
+    [ TemplateParameterSymbolIsDuplicated owner symbol
+    | symbol <- duplicates (map (resolvedSymbol . templateParameterName) parameters)
+    ]
+
+duplicateSpellingProblems ::
+    QualifiedName ->
+    [TemplateParameter ResolvedName Type] ->
+    [TemplateVerificationError]
+duplicateSpellingProblems owner parameters =
+    [ TemplateParameterSpellingIsDuplicated owner spelling
+    | spelling <- duplicates (map (resolvedSpelling . templateParameterName) parameters)
+    ]
+
+memberProblems :: QualifiedName -> Declaration ResolvedName Type -> [TemplateVerificationError]
+memberProblems owner member =
+    [ TemplateMemberHasInvalidSymbol owner (declarationName member)
+    | symbolIdValue (resolvedSymbol (declarationName member)) <= 0
+    ]
+
+duplicates :: (Eq value) => [value] -> [value]
+duplicates = go [] []
+    where
+        go _ output [] = reverse output
+        go seen output (value : remaining)
+            | value `elem` seen && value `notElem` output = go seen (value : output) remaining
+            | otherwise = go (value : seen) output remaining
+
+qualify :: Maybe QualifiedName -> Identifier -> QualifiedName
+qualify Nothing name = QualifiedName [name]
+qualify (Just (QualifiedName owner)) name = QualifiedName (owner ++ [name])
+
+finish :: [problem] -> Either [problem] ()
+finish [] = Right ()
+finish problems = Left problems
+
+-- | Render one declaration or parameter verification error.
+renderTemplateVerificationError :: TemplateVerificationError -> String
+renderTemplateVerificationError issue = case issue of
+    TemplateDeclarationHasInvalidSymbol name symbol ->
+        renderName name ++ " has invalid declaration symbol " ++ show (symbolIdValue symbol)
+    TemplateParameterHasInvalidSymbol owner name symbol ->
+        parameterPrefix owner name ++ " has invalid symbol " ++ show (symbolIdValue symbol)
+    TemplateParameterSymbolIsDuplicated owner symbol ->
+        renderName owner ++ " repeats template parameter symbol " ++ show (symbolIdValue symbol)
+    TemplateParameterSpellingIsDuplicated owner name ->
+        renderName owner ++ " repeats template parameter spelling " ++ identifierText name
+    TemplateParameterHasErrorType owner name -> parameterPrefix owner (resolvedSpelling name) ++ " has ErrorType"
+    TemplateParameterPackHasDefault owner name -> parameterPrefix owner (resolvedSpelling name) ++ " is a pack with a default"
+    TemplateParameterDefaultCategoryMismatch owner name -> parameterPrefix owner (resolvedSpelling name) ++ " has a default of the wrong category"
+    TemplateTemplateSignatureIsEmpty owner name -> parameterPrefix owner (resolvedSpelling name) ++ " has an empty template signature"
+    TemplateMemberHasInvalidSymbol owner name -> parameterPrefix owner (resolvedSpelling name) ++ " has an invalid member symbol"
+
+parameterPrefix :: QualifiedName -> Identifier -> String
+parameterPrefix owner name = "template parameter " ++ identifierText name ++ " in " ++ renderName owner
+
+renderName :: QualifiedName -> String
+renderName (QualifiedName parts) = join (map identifierText parts)
+    where
+        join [] = ""
+        join [value] = value
+        join (value : remaining) = value ++ "." ++ join remaining
+ src/Progmasoft/Tala/TemplateValue.hs view
@@ -0,0 +1,216 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Exact evaluation for compile-time values embedded in type syntax.
+
+This module intentionally does not reuse runtime numeric coercion. Template
+values participate in specialization identity, so host overflow, floating
+approximation, and truthy conversions would make artifacts non-portable.
+-}
+module Progmasoft.Tala.TemplateValue
+    ( TemplateValueError (..)
+    , evaluateTemplateValue
+    , evaluateFixedArraySize
+    , renderTemplateValueError
+    , templateValueSyntaxSpan
+    ) where
+
+import Data.Bits (complement, xor, (.&.), (.|.))
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.IntegerEvaluation
+    ( CompileTimeIntegerError (..)
+    , checkCompileTimeInteger
+    , evaluateCompileTimePower
+    , evaluateCompileTimeShiftLeft
+    , evaluateCompileTimeShiftRight
+    , multiplyCompileTimeIntegers
+    )
+
+-- | Failure while evaluating a template constant or fixed-array extent.
+data TemplateValueError
+    = -- | Name is not a compile-time constant.
+      TemplateValueIsNotConstant QualifiedName
+    | -- | Division has a zero divisor.
+      TemplateValueDivisionByZero
+    | -- | Floor division has a zero divisor.
+      TemplateValueFloorDivisionByZero
+    | -- | Remainder has a zero divisor.
+      TemplateValueRemainderByZero
+    | -- | Integer exponent is negative.
+      TemplateValueNegativeExponent Integer
+    | -- | Exact arithmetic exceeded its resource bound.
+      TemplateValueEvaluationLimitExceeded
+    | -- | Fixed-array extent is not integral.
+      TemplateValueRequiresInteger TemplateValue
+    | -- | Fixed-array extent is negative.
+      TemplateValueNegativeArraySize Integer
+    deriving (Eq, Ord, Read, Show)
+
+data ExactValue
+    = ExactInteger Integer
+    | ExactBoolean Bool
+    | ExactCharacter Integer
+    deriving (Eq, Ord, Read, Show)
+
+-- | Evaluate type-syntax constants exactly, preserving their source-level kind.
+evaluateTemplateValue :: TemplateValueSyntax -> Either TemplateValueError TemplateValue
+evaluateTemplateValue syntax = exactToTemplate <$> evaluateExact syntax
+
+-- | Evaluate a fixed-array extent and reject negative or non-integer values.
+evaluateFixedArraySize :: TemplateValueSyntax -> Either TemplateValueError TemplateValue
+evaluateFixedArraySize syntax = do
+    value <- evaluateTemplateValue syntax
+    case value of
+        IntegerTemplateValue integer
+            | integer < 0 -> Left (TemplateValueNegativeArraySize integer)
+            | otherwise -> Right value
+        CharacterTemplateValue scalar -> Right (IntegerTemplateValue scalar)
+        other -> Left (TemplateValueRequiresInteger other)
+
+evaluateExact :: TemplateValueSyntax -> Either TemplateValueError ExactValue
+evaluateExact syntax = case syntax of
+    TemplateIntegerSyntax _ value -> ExactInteger <$> checkedTemplateInteger value
+    TemplateCharacterSyntax _ value -> ExactCharacter <$> checkedTemplateInteger value
+    TemplateBooleanSyntax _ value -> Right (ExactBoolean value)
+    TemplateNameSyntax _ name -> Left (TemplateValueIsNotConstant name)
+    TemplateUnarySyntax _ operator operand -> do
+        value <- evaluateExact operand
+        evaluateUnary operator value
+    TemplateBinarySyntax _ operator left right -> do
+        -- Both operands must be exact values before specialization identity can
+        -- be formed; failures are reported here rather than deferred to Core.
+        leftValue <- evaluateExact left
+        rightValue <- evaluateExact right
+        evaluateBinary operator leftValue rightValue
+
+evaluateUnary :: UnaryOperator -> ExactValue -> Either TemplateValueError ExactValue
+evaluateUnary operator value = case operator of
+    UnaryPlus -> requireInteger value >>= (ExactInteger <$>) . checkedTemplateInteger
+    UnaryNegate -> requireInteger value >>= (ExactInteger <$>) . checkedTemplateInteger . negate
+    LogicalNot -> ExactBoolean . not <$> requireBooleanContext value
+    BitwiseNot -> requireInteger value >>= (ExactInteger <$>) . checkedTemplateInteger . complement
+
+evaluateBinary :: BinaryOperator -> ExactValue -> ExactValue -> Either TemplateValueError ExactValue
+evaluateBinary operator left right = case operator of
+    Add -> integerBinary (+)
+    Subtract -> integerBinary (-)
+    Multiply -> integerBinaryEither multiplyCompileTimeIntegers
+    Divide -> do
+        divisor <- requireInteger right
+        if divisor == 0
+            then Left TemplateValueDivisionByZero
+            else do
+                dividend <- requireInteger left
+                ExactInteger <$> checkedTemplateInteger (dividend `quot` divisor)
+    FloorDivide -> do
+        divisor <- requireInteger right
+        if divisor == 0
+            then Left TemplateValueFloorDivisionByZero
+            else do
+                dividend <- requireInteger left
+                ExactInteger <$> checkedTemplateInteger (roundedIntegerDivision dividend divisor)
+    Remainder -> do
+        divisor <- requireInteger right
+        if divisor == 0
+            then Left TemplateValueRemainderByZero
+            else do
+                dividend <- requireInteger left
+                ExactInteger <$> checkedTemplateInteger (dividend `rem` divisor)
+    Power -> do
+        exponentValue <- requireInteger right
+        if exponentValue < 0
+            then Left (TemplateValueNegativeExponent exponentValue)
+            else do
+                base <- requireInteger left
+                ExactInteger <$> mapCompileTimeError exponentValue (evaluateCompileTimePower base exponentValue)
+    ShiftLeft -> integerBinaryEither evaluateCompileTimeShiftLeft
+    ShiftRight -> integerBinaryEither evaluateCompileTimeShiftRight
+    BitwiseAnd -> integerBinary (.&.)
+    BitwiseXor -> integerBinary xor
+    BitwiseOr -> integerBinary (.|.)
+    LessThan -> comparison (<)
+    LessEqual -> comparison (<=)
+    GreaterThan -> comparison (>)
+    GreaterEqual -> comparison (>=)
+    Equal -> Right (ExactBoolean (left == right))
+    NotEqual -> Right (ExactBoolean (left /= right))
+    LogicalAnd -> logical (&&)
+    LogicalOr -> logical (||)
+    where
+        integerBinary operation = do
+            lhs <- requireInteger left
+            rhs <- requireInteger right
+            ExactInteger <$> checkedTemplateInteger (operation lhs rhs)
+        integerBinaryEither operation = do
+            lhs <- requireInteger left
+            rhs <- requireInteger right
+            ExactInteger <$> mapCompileTimeError rhs (operation lhs rhs)
+        comparison operation = do
+            lhs <- requireInteger left
+            rhs <- requireInteger right
+            Right (ExactBoolean (operation lhs rhs))
+        logical operation = do
+            lhs <- requireBooleanContext left
+            rhs <- requireBooleanContext right
+            Right (ExactBoolean (operation lhs rhs))
+
+roundedIntegerDivision :: Integer -> Integer -> Integer
+roundedIntegerDivision 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
+
+requireInteger :: ExactValue -> Either TemplateValueError Integer
+requireInteger value = case value of
+    ExactInteger integer -> Right integer
+    ExactCharacter scalar -> Right scalar
+    ExactBoolean boolean -> Left (TemplateValueRequiresInteger (BooleanTemplateValue boolean))
+
+requireBooleanContext :: ExactValue -> Either TemplateValueError Bool
+requireBooleanContext value = case value of
+    ExactBoolean boolean -> Right boolean
+    ExactInteger integer -> Right (integer /= 0)
+    ExactCharacter scalar -> Right (scalar /= 0)
+
+exactToTemplate :: ExactValue -> TemplateValue
+exactToTemplate value = case value of
+    ExactInteger integer -> IntegerTemplateValue integer
+    ExactBoolean boolean -> BooleanTemplateValue boolean
+    ExactCharacter scalar -> CharacterTemplateValue scalar
+
+-- | Return the source span enclosing one template-value syntax expression.
+templateValueSyntaxSpan :: TemplateValueSyntax -> SourceSpan
+templateValueSyntaxSpan syntax = case syntax of
+    TemplateIntegerSyntax spanValue _ -> spanValue
+    TemplateCharacterSyntax spanValue _ -> spanValue
+    TemplateBooleanSyntax spanValue _ -> spanValue
+    TemplateNameSyntax spanValue _ -> spanValue
+    TemplateUnarySyntax spanValue _ _ -> spanValue
+    TemplateBinarySyntax spanValue _ _ _ -> spanValue
+
+-- | Render a constant-evaluation failure for a source diagnostic.
+renderTemplateValueError :: TemplateValueError -> String
+renderTemplateValueError issue = case issue of
+    TemplateValueIsNotConstant (QualifiedName parts) ->
+        "template value " ++ joinQualified (map identifierText parts) ++ " is not a resolved compile-time constant"
+    TemplateValueDivisionByZero -> "template value performs division by zero"
+    TemplateValueFloorDivisionByZero -> "template value performs floor division by zero"
+    TemplateValueRemainderByZero -> "template value performs remainder by zero"
+    TemplateValueNegativeExponent value -> "template value uses a negative integer exponent: " ++ show value
+    TemplateValueEvaluationLimitExceeded -> "template integer expression exceeds the compile-time evaluation limit"
+    TemplateValueRequiresInteger _ -> "fixed System.Array size must evaluate to an integer"
+    TemplateValueNegativeArraySize value -> "fixed System.Array size cannot be negative: " ++ show value
+
+joinQualified :: [String] -> String
+joinQualified [] = "<empty>"
+joinQualified [part] = part
+joinQualified (part : parts) = part ++ "." ++ joinQualified parts
+
+checkedTemplateInteger :: Integer -> Either TemplateValueError Integer
+checkedTemplateInteger = mapCompileTimeError 0 . checkCompileTimeInteger
+
+mapCompileTimeError :: Integer -> Either CompileTimeIntegerError value -> Either TemplateValueError value
+mapCompileTimeError exponentValue result = case result of
+    Right value -> Right value
+    Left CompileTimeNegativeExponent -> Left (TemplateValueNegativeExponent exponentValue)
+    Left CompileTimeIntegerLimitExceeded -> Left TemplateValueEvaluationLimitExceeded
+ src/Progmasoft/Tala/TypeChecker.hs view
@@ -0,0 +1,1458 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Semantic checker for trees whose references already have stable identities.
+
+The checker attaches types and returns diagnostics without rewriting unresolved
+names. It runs after Renamer and Name Resolution so lexical spelling is never
+used as a substitute for symbol identity.
+-}
+module Progmasoft.Tala.TypeChecker (TypeChecker (..), defaultTypeChecker, runTypeChecker) where
+
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.NumericSemantics
+import Progmasoft.Tala.TypeChecker.Branching
+import Progmasoft.Tala.TypeChecker.Console
+import Progmasoft.Tala.TypeChecker.Context
+import Progmasoft.Tala.TypeChecker.Enums
+import Progmasoft.Tala.TypeChecker.Literals
+import Progmasoft.Tala.TypeChecker.Loops
+import Progmasoft.Tala.TypeChecker.Returns
+
+-- | A resolved-tree checker that produces a typed tree only when checking succeeds.
+newtype TypeChecker = TypeChecker {checkResolvedAST :: ResolvedAST -> Either [Diagnostic] TypedAST}
+
+-- | Apply a checker to the output of Name Resolution.
+runTypeChecker :: TypeChecker -> ResolvedAST -> Either [Diagnostic] TypedAST
+runTypeChecker = checkResolvedAST
+
+-- | The production checker for the currently implemented Tala subset.
+defaultTypeChecker :: TypeChecker
+defaultTypeChecker = TypeChecker checkTree
+
+{- | Type-check every top-level declaration and collect independent diagnostics.
+No partially typed AST escapes when any declaration has an error.
+-}
+checkTree :: ResolvedAST -> Either [Diagnostic] TypedAST
+checkTree (ResolvedAST (SyntaxTree namespace declarations)) =
+    let catalog = inferAutoReturns declarations (catalogDeclarations declarations)
+        checked = map (checkTopDeclaration catalog) declarations
+        problems = concatMap snd checked
+     in if null problems then Right (TypedAST (SyntaxTree namespace (map fst checked))) else Left problems
+
+catalogDeclarations :: [Declaration ResolvedName ()] -> TypeCatalog
+catalogDeclarations declarations =
+    TypeCatalog
+        [ (resolvedSymbol (declarationName declaration), declarationName declaration)
+        | declaration <- declarations
+        , isTypeDeclaration declaration
+        ]
+        [ MethodCandidate (resolvedSymbol (declarationName owner)) member
+        | owner <- declarations
+        , isTypeDeclaration owner
+        , member <- typeMembersOf owner
+        , case member of FunctionDeclaration {} -> True; _ -> False
+        ]
+        (enumInfos declarations)
+        []
+    where
+        isTypeDeclaration TypeDeclaration {} = True
+        isTypeDeclaration TemplateTypeDeclaration {} = True
+        isTypeDeclaration _ = False
+        typeMembersOf TypeDeclaration {typeMembers = members} = members
+        typeMembersOf TemplateTypeDeclaration {typeMembers = members} = members
+        typeMembersOf _ = []
+
+{- | Infer the return types of the methods declared with @auto@ before any
+caller is checked against them.
+
+A method's return type comes from its own returns, and those may be calls of
+other such methods, in any class and declared later. The bodies are therefore
+checked in rounds. In a round, a call of a method whose type is not known yet
+has no type and takes no part in the inference, so a method is inferred as
+soon as one of its returns is independent of the methods still unknown: a
+recursive method from its base case, and a chain of methods from its end
+towards its start. A round that learns nothing ends the inference; every
+round before it resolves at least one method, so there are at most as many
+rounds as methods. A method that is still unknown then has no independent
+result, which is reported when its declaration is checked. The diagnostics
+of the rounds are dropped: every body is checked once more against the
+final catalog.
+-}
+inferAutoReturns :: [Declaration ResolvedName ()] -> TypeCatalog -> TypeCatalog
+inferAutoReturns declarations = rounds (length inferable)
+    where
+        inferable =
+            [ (owner, member)
+            | owner <- declarations
+            , member <- membersOf owner
+            , FunctionDeclaration {declarationReturnSyntax = AutoType} <- [member]
+            ]
+        membersOf declaration = case declaration of
+            TypeDeclaration {typeMembers = members} -> members
+            TemplateTypeDeclaration {typeMembers = members} -> members
+            _ -> []
+        rounds :: Int -> TypeCatalog -> TypeCatalog
+        rounds remaining catalog
+            | remaining <= 0 || learned == catalogInferredReturns catalog = catalog
+            | otherwise = rounds (remaining - 1) catalog {catalogInferredReturns = learned}
+            where
+                learned =
+                    [ (inferredReturnKey member, result)
+                    | (owner, member) <- inferable
+                    , let (context, globals) = memberScope catalog owner
+                    , FunctionDeclaration {declarationAnnotation = FunctionType _ result} <-
+                        [fst (checkDeclarationWith context globals member)]
+                    , result /= ErrorType
+                    ]
+
+-- | What identifies a method declaration among its overloads.
+inferredReturnKey :: Declaration ResolvedName annotation -> (SymbolId, SourceSpan)
+inferredReturnKey declaration = (resolvedSymbol (declarationName declaration), declarationSpan declaration)
+
+{- | The context and the names in scope of the members of a type: the
+signatures of its members and, for a template, its value parameters.
+-}
+memberScope :: TypeCatalog -> Declaration ResolvedName () -> (TemplateContext, TypeEnvironment)
+memberScope catalog declaration = case declaration of
+    TemplateTypeDeclaration _ name _ parameters members ->
+        let context = templateContext catalog (Just (resolvedSymbol name)) parameters
+            templateValues =
+                [ (resolvedSymbol (templateParameterName parameter), (templateParameterAnnotation parameter, False))
+                | parameter <- map (typeTemplateParameter context) parameters
+                , case templateParameterKind parameter of TemplateValueParameterKind _ -> True; _ -> False
+                ]
+         in (context, templateValues ++ signaturesOf context members)
+    TypeDeclaration _ name _ members ->
+        let context = emptyTemplateContext catalog (Just (resolvedSymbol name))
+         in (context, signaturesOf context members)
+    _ -> (emptyTemplateContext catalog Nothing, [])
+    where
+        signaturesOf context members =
+            [(resolvedSymbol (declarationName member), (signature context member, False)) | member <- members]
+
+signature :: TemplateContext -> Declaration ResolvedName () -> Type
+signature context declaration = case declaration of
+    FunctionDeclaration _ _ _ returnSyntax parameters _ _ _ ->
+        FunctionType
+            (map (syntaxTypeIn context . parameterTypeSyntax) parameters)
+            ( case returnSyntax of
+                AutoType ->
+                    maybe ErrorType id (lookup (inferredReturnKey declaration) (catalogInferredReturns (templateCatalog context)))
+                _ -> syntaxTypeIn context returnSyntax
+            )
+    TypeDeclaration _ name _ _ -> NamedType (QualifiedName [resolvedSpelling name]) []
+    TemplateTypeDeclaration _ name _ parameters _ ->
+        NamedType
+            (QualifiedName [resolvedSpelling name])
+            (map templateParameterAsArgument parameters)
+    EnumDeclaration _ name _ _ _ -> enumTypeOf (templateCatalog context) name
+
+-- | The type of the values of a declared enum.
+enumTypeOf :: TypeCatalog -> ResolvedName -> Type
+enumTypeOf catalog name = maybe ErrorType enumInfoType (enumBySymbol (catalogEnums catalog) (resolvedSymbol name))
+
+checkTopDeclaration :: TypeCatalog -> Declaration ResolvedName () -> (Declaration ResolvedName Type, [Diagnostic])
+checkTopDeclaration catalog declaration = case declaration of
+    TypeDeclaration spanValue name _ members ->
+        let (context, signatures) = memberScope catalog declaration
+            checked = map (checkDeclarationWith context signatures) members
+            overloadProblems = duplicateOverloadProblems context members
+            valueType = NamedType (QualifiedName [resolvedSpelling name]) []
+         in (TypeDeclaration spanValue name valueType (map fst checked), overloadProblems ++ concatMap snd checked)
+    TemplateTypeDeclaration spanValue name _ parameters members ->
+        let (context, scope) = memberScope catalog declaration
+            typedTemplateParameters = map (typeTemplateParameter context) parameters
+            checked = map (checkDeclarationWith context scope) members
+            parameterProblems = validateTemplateParameters context parameters
+            overloadProblems = duplicateOverloadProblems context members
+            valueType =
+                NamedType
+                    (QualifiedName [resolvedSpelling name])
+                    (map templateParameterAsArgument typedTemplateParameters)
+         in ( TemplateTypeDeclaration spanValue name valueType typedTemplateParameters (map fst checked)
+            , parameterProblems ++ overloadProblems ++ concatMap snd checked
+            )
+    FunctionDeclaration {} -> checkDeclarationWith (emptyTemplateContext catalog Nothing) [] declaration
+    EnumDeclaration spanValue name _ underlying cases ->
+        ( EnumDeclaration spanValue name (enumTypeOf catalog name) underlying cases
+        , enumDeclarationProblems declaration
+        )
+
+checkDeclarationWith ::
+    TemplateContext -> TypeEnvironment -> Declaration ResolvedName () -> (Declaration ResolvedName Type, [Diagnostic])
+checkDeclarationWith context globals declaration@FunctionDeclaration {} =
+    let parameters =
+            [ (resolvedSymbol (parameterName parameter), (syntaxTypeIn context (parameterTypeSyntax parameter), False))
+            | parameter <- declarationParameters declaration
+            ]
+        expected = syntaxTypeIn context (declarationReturnSyntax declaration)
+        (body, _, _, problems) = checkBlockWith context (parameters ++ globals) expected outsideLoops (declarationBody declaration)
+        finalReturn = finalExpressionType body
+        -- Every return of the body counts, also one reached through an
+        -- expression; the returns of a nested callable are its own.
+        returns = blockReturnTypes body ++ maybe [] (: []) finalReturn
+        inferred = inferReturn expected returns
+        isInferred = case declarationReturnSyntax declaration of AutoType -> True; _ -> False
+        returnProblems
+            | expected /= ErrorType && any (not . compatible expected) returns =
+                [ Diagnostic
+                    TypeCheckerStage
+                    Error
+                    "TAT0001"
+                    (Just (declarationSpan declaration))
+                    "return expression does not match the declared function type"
+                ]
+            | isInferred && any (not . compatible inferred) returns =
+                [ problem
+                    (declarationSpan declaration)
+                    "TAT0062"
+                    "the return statements of this method carry values of different types"
+                ]
+            -- Every result of the method is a call that depends on the
+            -- method itself: nothing gives it a type.
+            | isInferred && inferred == ErrorType && null problems =
+                [ problem
+                    (declarationSpan declaration)
+                    "TAT0063"
+                    "the return type of this method cannot be inferred: no result is independent of the method itself"
+                ]
+            | otherwise = []
+        typedParameters = map (typeParameterWith context) (declarationParameters declaration)
+        signatureProblems =
+            typeSyntaxProblemsIn context (declarationReturnSyntax declaration)
+                ++ concatMap (typeSyntaxProblemsIn context . parameterTypeSyntax) (declarationParameters declaration)
+        functionType = FunctionType (map parameterAnnotation typedParameters) inferred
+     in ( FunctionDeclaration
+            (declarationSpan declaration)
+            (declarationName declaration)
+            functionType
+            (declarationReturnSyntax declaration)
+            typedParameters
+            body
+            (declarationIsStatic declaration)
+            (declarationAccess declaration)
+        , signatureProblems ++ problems ++ returnProblems
+        )
+checkDeclarationWith context _ declaration@TypeDeclaration {} = checkTopDeclaration (templateCatalog context) declaration
+checkDeclarationWith context _ declaration@TemplateTypeDeclaration {} = checkTopDeclaration (templateCatalog context) declaration
+checkDeclarationWith context _ declaration@EnumDeclaration {} = checkTopDeclaration (templateCatalog context) declaration
+
+-- A method overload is distinguished only by its ordered parameter types.
+-- Access, return type, and static-ness intentionally do not rescue duplicate
+-- signatures; that matches the declaration rules in Spec/Language/Decls.tala.
+duplicateOverloadProblems :: TemplateContext -> [Declaration ResolvedName ()] -> [Diagnostic]
+duplicateOverloadProblems context members = reverse problems
+    where
+        -- The signatures seen so far are kept by method name, so that a
+        -- method is compared with its own overloads only. Comparing it with
+        -- every earlier member made a type with thousands of methods take
+        -- time with the square of their number.
+        (_, problems) = foldl inspect (Map.empty, []) members
+        inspect (seen, diagnostics) declaration@FunctionDeclaration {} =
+            let spelling = resolvedSpelling (declarationName declaration)
+                parameterTypes = methodParameterTypes declaration
+                duplicate = parameterTypes `elem` Map.findWithDefault [] spelling seen
+                currentDiagnostics =
+                    if duplicate
+                        then
+                            [ problem
+                                (declarationSpan declaration)
+                                "TAT0028"
+                                ("method overload has a duplicate parameter signature: " ++ identifierText spelling)
+                            ]
+                        else []
+             in (Map.insertWith (++) spelling [parameterTypes] seen, reverse currentDiagnostics ++ diagnostics)
+        inspect state _ = state
+        methodParameterTypes FunctionDeclaration {declarationParameters = parameters} =
+            map (syntaxTypeIn context . parameterTypeSyntax) parameters
+        methodParameterTypes _ = []
+
+typeParameterWith :: TemplateContext -> Parameter ResolvedName () -> Parameter ResolvedName Type
+typeParameterWith context parameter =
+    Parameter
+        (parameterSpan parameter)
+        (parameterName parameter)
+        (syntaxTypeIn context (parameterTypeSyntax parameter))
+        (parameterTypeSyntax parameter)
+
+inferReturn :: Type -> [Type] -> Type
+inferReturn declared _ | declared /= ErrorType = declared
+inferReturn _ [] = voidType
+inferReturn _ values = case filter (/= ErrorType) values of [] -> ErrorType; first : _ -> first
+
+compatible :: Type -> Type -> Bool
+compatible ErrorType _ = True
+compatible _ ErrorType = True
+compatible left right = left == right
+
+-- | The context inside a block used as a value at the given place.
+insideValueBlock :: TemplateContext -> LoopContext
+insideValueBlock context = LoopContext StatementLoop (ValueBlockEdge : enclosingLoops (contextLoops context))
+
+{- | The context of the condition of the loop that is about to be checked in
+the given context: a transfer there reaches that loop.
+-}
+inLoopCondition :: LoopContext -> TemplateContext -> TemplateContext
+inLoopCondition loops context = context {contextLoops = loopCondition loops}
+
+{- | The checker for expressions and statements as the branching rules of
+"Progmasoft.Tala.TypeChecker.Branching" receive it: applied to the template
+context and to the type that a @return@ must have.
+-}
+branchChecker :: TemplateContext -> Type -> BranchChecker LoopContext
+branchChecker context expected =
+    BranchChecker
+        { branchExpression = checkExpressionExpectedWith context
+        , branchStatements = \environment loops statements ->
+            let (Block typed, final, returns, problems) = checkBlockWith context environment expected loops (Block statements)
+             in (typed, final, returns, problems)
+        , branchType = \syntax -> (syntaxTypeIn context syntax, typeSyntaxProblemsIn context syntax)
+        , branchLiteral = literalTypeInContext
+        , branchEnumMember = enumMemberValue (catalogEnums (templateCatalog context))
+        , branchHasEffect = effectCapable
+        , branchValueLoops = insideValueBlock context
+        }
+
+-- | The context of a statement that belongs to a loop header, not its body.
+settledLoops :: LoopContext -> LoopContext
+settledLoops loops = loops {pendingLoop = StatementLoop}
+
+checkBlockWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Type ->
+    LoopContext ->
+    Block ResolvedName () ->
+    (Block ResolvedName Type, TypeEnvironment, [Type], [Diagnostic])
+checkBlockWith context environment expected loops (Block statements) =
+    let (checked, final, returns, problems) = go environment statements in (Block checked, final, returns, problems)
+    where
+        go env [] = ([], env, [], [])
+        go env (statement : rest) =
+            let (typed, next, returned, firstProblems) = checkStatementWith context env expected loops statement
+                (remaining, final, laterReturns, laterProblems) = go next rest
+             in (typed : remaining, final, returned ++ laterReturns, firstProblems ++ laterProblems)
+
+checkStatementWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Type ->
+    LoopContext ->
+    Statement ResolvedName () ->
+    (Statement ResolvedName Type, TypeEnvironment, [Type], [Diagnostic])
+checkStatementWith outer environment expected loops =
+    checkStatementIn (outer {contextReturn = expected, contextLoops = loops}) environment expected loops
+
+-- The context already names the return type and the loops of the statement.
+checkStatementIn ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Type ->
+    LoopContext ->
+    Statement ResolvedName () ->
+    (Statement ResolvedName Type, TypeEnvironment, [Type], [Diagnostic])
+checkStatementIn context environment expected loops statement = case statement of
+    BindingStatement spanValue kind syntax name _ value ->
+        let declared = syntaxTypeIn context syntax
+            target = if declared == ErrorType then Nothing else Just declared
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment target value
+            bindingType = if declared == ErrorType then valueType else declared
+            mismatch =
+                if compatible bindingType valueType then [] else [problem spanValue "TAT0002" "binding initializer has the wrong type"]
+            constantProblems = constantRangeProblems spanValue bindingType typedValue
+            mutable = kind == MutableBinding
+         in ( BindingStatement spanValue kind syntax name bindingType typedValue
+            , (resolvedSymbol name, (bindingType, mutable)) : environment
+            , []
+            , typeSyntaxProblemsIn context syntax ++ problems ++ mismatch ++ constantProblems
+            )
+    AssignmentStatement spanValue name _ value ->
+        -- The target type is context for the value, as a declared type is
+        -- for a binding initializer: it types an otherwise untyped literal.
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            valueExpected = if targetType == ErrorType then Nothing else Just targetType
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment valueExpected value
+            immutable = case target of Just (_, False) -> [problem spanValue "TAT0003" "cannot assign to an immutable binding"]; _ -> []
+            mismatch = if compatible targetType valueType then [] else [problem spanValue "TAT0004" "assignment value has the wrong type"]
+         in (AssignmentStatement spanValue name targetType typedValue, environment, [], problems ++ immutable ++ mismatch)
+    ReturnStatement spanValue value ->
+        let (typedValue, valueType, problems) = checkOptionalExpectedWith context environment (Just expected) value
+            mismatch = if compatible expected valueType then [] else [problem spanValue "TAT0005" "return value has the wrong type"]
+         in (ReturnStatement spanValue typedValue, environment, [valueType], problems ++ mismatch)
+    IfStatement spanValue condition trueBlock falseBlock ->
+        let (typedCondition, conditionType, conditionProblems) = checkExpressionWith context environment condition
+            conditionMismatch =
+                if booleanContextType conditionType
+                    then []
+                    else [problem spanValue "TAT0006" "if condition must be bool or numeric"]
+            (typedTrue, _, trueReturns, trueProblems) = checkBlockWith context environment expected loops trueBlock
+            (typedFalse, falseReturns, falseProblems) = case falseBlock of
+                Nothing -> (Nothing, [], [])
+                Just value ->
+                    let (block, _, returns, problems) = checkBlockWith context environment expected loops value
+                     in (Just block, returns, problems)
+         in ( IfStatement spanValue typedCondition typedTrue typedFalse
+            , environment
+            , trueReturns ++ falseReturns
+            , conditionProblems ++ conditionMismatch ++ trueProblems ++ falseProblems
+            )
+    WhileStatement spanValue condition body ->
+        let (typedCondition, conditionType, conditionProblems) = checkExpressionWith (inLoopCondition loops context) environment condition
+            conditionProblems' =
+                if booleanContextType conditionType
+                    then []
+                    else [problem spanValue "TAT0020" "while condition must be bool or numeric"]
+            (typedBody, _, returns, bodyProblems) = checkBlockWith context environment expected (enterLoop loops) body
+         in ( WhileStatement spanValue typedCondition typedBody
+            , environment
+            , returns
+            , conditionProblems ++ conditionProblems' ++ bodyProblems
+            )
+    DoWhileStatement spanValue body condition ->
+        let (typedBody, _, returns, bodyProblems) = checkBlockWith context environment expected (enterLoop loops) body
+            (typedCondition, conditionType, conditionProblems) = checkExpressionWith (inLoopCondition loops context) environment condition
+            conditionProblems' =
+                if booleanContextType conditionType
+                    then []
+                    else [problem spanValue "TAT0020" "do/while condition must be bool or numeric"]
+         in ( DoWhileStatement spanValue typedBody typedCondition
+            , environment
+            , returns
+            , bodyProblems ++ conditionProblems ++ conditionProblems'
+            )
+    ForStatement spanValue initializer condition updates body ->
+        let (typedInitializer, loopEnvironment, initializerProblems) = case initializer of
+                Nothing -> (Nothing, environment, [])
+                Just value ->
+                    let (typed, nested, _, problems) = checkStatementWith context environment expected (settledLoops loops) value
+                     in (Just typed, nested, problems)
+            (typedCondition, conditionType, conditionProblems) = case condition of
+                Nothing -> (Nothing, boolType, [])
+                Just value ->
+                    let (typed, valueType, problems) = checkExpressionWith (inLoopCondition loops context) loopEnvironment value
+                     in (Just typed, valueType, problems)
+            conditionProblems' =
+                if booleanContextType conditionType
+                    then []
+                    else [problem spanValue "TAT0020" "for condition must be bool or numeric"]
+            (typedBody, _, returns, bodyProblems) = checkBlockWith context loopEnvironment expected (enterLoop loops) body
+            (typedUpdates, updateProblems) = checkStatementsWith context loopEnvironment expected (loopUpdate loops) updates
+         in ( ForStatement spanValue typedInitializer typedCondition typedUpdates typedBody
+            , environment
+            , returns
+            , initializerProblems ++ conditionProblems ++ conditionProblems' ++ bodyProblems ++ updateProblems
+            )
+    ForEachStatement spanValue kind syntax name _ source body ->
+        let valueType = syntaxTypeIn context syntax
+            (typedSource, _, sourceProblems) = checkExpressionWith context environment source
+            (typedBody, _, returns, bodyProblems) =
+                checkBlockWith
+                    context
+                    ((resolvedSymbol name, (valueType, kind == MutableBinding)) : environment)
+                    expected
+                    (enterLoop loops)
+                    body
+            unsupported = problem spanValue "TAT0021" "enumerable for loops require the generator and Enumerable ABI, which is not implemented"
+         in ( ForEachStatement spanValue kind syntax name valueType typedSource typedBody
+            , environment
+            , returns
+            , unsupported : typeSyntaxProblemsIn context syntax ++ sourceProblems ++ bodyProblems
+            )
+    IncrementStatement spanValue name _ ->
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            writableProblems = case target of
+                Just (_, False) -> [problem spanValue "TAT0022" "increment cannot modify an immutable binding"]
+                Nothing -> [problem spanValue "TAT0023" "increment target is not defined"]
+                _ -> []
+            numericProblems =
+                if isNumericType targetType && targetType /= boolType
+                    then []
+                    else [problem spanValue "TAT0024" "increment target must have a numeric type"]
+         in (IncrementStatement spanValue name targetType, environment, [], writableProblems ++ numericProblems)
+    CompoundAssignmentStatement spanValue Add name _ value
+        | Just (targetType, writable) <- lookup (resolvedSymbol name) environment
+        , targetType == stringType ->
+            -- `text += value` is `text = text + value`: the value is
+            -- written as text and joined to what the target holds.
+            let (typedValue, valueType, problems) = checkExpressionWith context environment value
+                (joined, _, joinProblems) =
+                    concatenation spanValue (NameExpression spanValue name stringType, stringType) (typedValue, valueType)
+                immutable = [problem spanValue "TAT0003" "cannot assign to an immutable binding" | not writable]
+             in ( AssignmentStatement spanValue name stringType joined
+                , environment
+                , []
+                , problems ++ immutable ++ joinProblems
+                )
+    CompoundAssignmentStatement spanValue operator name _ value ->
+        -- `target op= value` has the typing of `target = target op value`:
+        -- the operator rule is applied to the target type and the result
+        -- must be storable without a conversion.
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            operandExpected = if targetType == ErrorType then Nothing else Just targetType
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment operandExpected value
+            immutable = case target of
+                Just (_, False) -> [problem spanValue "TAT0003" "cannot assign to an immutable binding"]
+                _ -> []
+            rule = binaryNumericRule operator targetType valueType
+            known = targetType /= ErrorType && valueType /= ErrorType
+            operatorProblems = if known then ruleProblems spanValue "TAT0012" rule else []
+            resultProblems =
+                if known && null operatorProblems && numericRuleType rule /= targetType
+                    then [problem spanValue "TAT0035" "compound assignment result does not have the target type"]
+                    else []
+         in ( CompoundAssignmentStatement spanValue operator name targetType typedValue
+            , environment
+            , []
+            , problems ++ immutable ++ operatorProblems ++ resultProblems
+            )
+    DiscardStatement spanValue value ->
+        let (typedValue, _, problems) = checkExpressionWith context environment value
+         in (DiscardStatement spanValue typedValue, environment, [], problems)
+    BreakStatement spanValue value ->
+        -- A break leaves the innermost loop. Whether it may, or must, carry
+        -- a value is decided by how that loop is used; the value takes its
+        -- context from the place that receives the loop's value.
+        let target = transferTarget loops
+            valueExpected = case target of
+                Just (ExpressionLoop expectedValue) -> expectedValue
+                Just (LoopCondition (ExpressionLoop expectedValue)) -> expectedValue
+                Just (LoopUpdate (ExpressionLoop expectedValue)) -> expectedValue
+                _ -> Nothing
+            (typedValue, _, valueProblems) = checkOptionalExpectedWith context environment valueExpected value
+            placement kind = case (kind, value) of
+                (StatementLoop, Just _) ->
+                    [problem spanValue "TAT0026" "a value-carrying break is only valid in a loop used as an expression"]
+                (ExpressionLoop _, Nothing) ->
+                    [problem spanValue "TAT0040" "a loop used as an expression must be left by a break that carries a value"]
+                -- A break in the condition or the update clause of a loop
+                -- leaves that loop.
+                (LoopCondition loop, _) -> placement loop
+                (LoopUpdate loop, _) -> placement loop
+                _ -> []
+            placementProblems = case target of
+                Nothing -> [problem spanValue "TAT0025" "break is only valid inside a loop"]
+                Just kind -> placement kind
+         in (BreakStatement spanValue typedValue, environment, [], placementProblems ++ valueProblems)
+    ContinueStatement spanValue ->
+        -- A continue in the condition of a loop evaluates that condition
+        -- again, and one in the update clause ends the update: the
+        -- condition of the loop is tested next.
+        let problems = case transferTarget loops of
+                Nothing -> [problem spanValue "TAT0027" "continue is only valid inside a loop"]
+                _ -> []
+         in (ContinueStatement spanValue, environment, [], problems)
+    GuardStatement spanValue condition block ->
+        let (typedCondition, conditionType, conditionProblems) = checkExpressionWith context environment condition
+            conditionMismatch =
+                [ problem spanValue "TAT0060" "guard condition must be bool or numeric"
+                | conditionType /= ErrorType
+                , not (booleanContextType conditionType)
+                ]
+            (typedBlock, _, returns, blockProblems) = checkBlockWith context environment expected loops block
+         in ( GuardStatement spanValue typedCondition typedBlock
+            , environment
+            , returns
+            , conditionProblems ++ conditionMismatch ++ blockProblems ++ guardBlockProblems spanValue typedBlock
+            )
+    -- A nested block is checked in the scope it starts in; what it declares
+    -- does not reach the statements after it.
+    BlockStatement spanValue block ->
+        let (typedBlock, _, returns, problems) = checkBlockWith context environment expected loops block
+         in (BlockStatement spanValue typedBlock, environment, returns, problems)
+    -- A match that is a statement of its own: its arms are statements, they
+    -- may return and may leave the enclosing loop, and no arm has to accept.
+    ExpressionStatement spanValue (MatchExpression matchSpan subjects arms _) terminated
+        | terminated ->
+            let (typedMatch, _, returns, problems) =
+                    checkMatch (branchChecker context expected) (MatchStatement loops) environment Nothing matchSpan subjects arms
+             in (ExpressionStatement spanValue typedMatch terminated, environment, returns, problems)
+    ExpressionStatement spanValue value terminated ->
+        let (typedValue, _, problems) = checkExpressionWith context environment value
+            effectProblems =
+                if terminated && not (effectCapable value)
+                    then [problem spanValue "TAT0013" "pure value expression cannot be used as a statement"]
+                    else []
+         in (ExpressionStatement spanValue typedValue terminated, environment, [], problems ++ effectProblems)
+
+checkStatementsWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Type ->
+    LoopContext ->
+    [Statement ResolvedName ()] ->
+    ([Statement ResolvedName Type], [Diagnostic])
+checkStatementsWith context environment expected loops = go environment
+    where
+        go _ [] = ([], [])
+        go current (statement : remaining) =
+            let (typed, next, _, problems) = checkStatementWith context current expected loops statement
+                (later, laterProblems) = go next remaining
+             in (typed : later, problems ++ laterProblems)
+
+finalExpressionType :: Block ResolvedName Type -> Maybe Type
+finalExpressionType (Block statements) = case reverse statements of
+    ExpressionStatement _ expression False : _ -> Just (typedExpressionType expression)
+    _ -> Nothing
+
+effectCapable :: Expression name annotation -> Bool
+effectCapable CallExpression {} = True
+effectCapable (IsPatternExpression _ subject _ _) = effectCapable subject
+effectCapable (ConditionalExpression _ condition first second _) = any effectCapable [condition, first, second]
+effectCapable (CoalesceExpression _ left fallback _) = effectCapable left || effectCapable fallback
+effectCapable AssignmentExpression {} = True
+effectCapable IncrementExpression {} = True
+effectCapable LoopExpression {} = True
+effectCapable (BlockExpression _ (Block statements) _) = not (null statements)
+effectCapable (MatchExpression _ subjects arms _) =
+    any effectCapable (subjects ++ concatMap matchArmExpressions arms)
+effectCapable CallableExpression {} = False
+effectCapable _ = False
+
+checkOptionalWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe (Expression ResolvedName ()) ->
+    (Maybe (Expression ResolvedName Type), Type, [Diagnostic])
+checkOptionalWith _ _ Nothing = (Nothing, voidType, [])
+checkOptionalWith context environment (Just value) =
+    let (typed, valueType, problems) = checkExpressionWith context environment value
+     in (Just typed, valueType, problems)
+
+checkOptionalExpectedWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe Type ->
+    Maybe (Expression ResolvedName ()) ->
+    (Maybe (Expression ResolvedName Type), Type, [Diagnostic])
+checkOptionalExpectedWith _ _ _ Nothing = (Nothing, voidType, [])
+checkOptionalExpectedWith context environment expected (Just value) =
+    let (typed, valueType, problems) = checkExpressionExpectedWith context environment expected value
+     in (Just typed, valueType, problems)
+
+checkExpressionWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Expression ResolvedName () ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkExpressionWith context environment = checkExpressionExpectedWith context environment Nothing
+
+-- Expected types are semantic context, not conversions.  They choose the
+-- representation of an otherwise untyped numeric literal and allow the
+-- boolean numeric rule, but never silently convert a computed value.
+checkExpressionExpectedWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe Type ->
+    Expression ResolvedName () ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkExpressionExpectedWith context environment expected expression = case expression of
+    NameExpression spanValue name _ ->
+        let valueType = maybe ErrorType fst (lookup (resolvedSymbol name) environment)
+            problems = if valueType == ErrorType then [problem spanValue "TAT0007" "name has no known type"] else []
+         in (NameExpression spanValue name valueType, valueType, problems)
+    LiteralExpression spanValue literal _ ->
+        let (valueType, problems) = literalTypeInContext spanValue expected literal
+         in (LiteralExpression spanValue literal valueType, valueType, problems)
+    -- @Enum.Member@ is the value of that member. It is a constant of the
+    -- enum's type, and it is kept as the integer literal it stands for.
+    MemberAccessExpression spanValue (NameExpression _ name _) member _
+        | Just info <- enumBySymbol (catalogEnums (templateCatalog context)) (resolvedSymbol name) ->
+            let valueType = enumInfoType info
+             in case lookup member (enumInfoMembers info) of
+                    Just value -> (LiteralExpression spanValue (IntegerLiteral value) valueType, valueType, [])
+                    Nothing ->
+                        ( LiteralExpression spanValue (IntegerLiteral 0) valueType
+                        , ErrorType
+                        ,
+                            [ problem
+                                spanValue
+                                "TAT0064"
+                                ( "the enum "
+                                    ++ identifierText (resolvedSpelling name)
+                                    ++ " has no member named "
+                                    ++ identifierText member
+                                )
+                            ]
+                        )
+    -- @.Member@ is the member of that name of the enum the place expects.
+    -- Without an expected enum type there is nothing to select from.
+    MemberAccessExpression spanValue (LiteralExpression _ UnitLiteral _) member _ ->
+        let enums = catalogEnums (templateCatalog context)
+         in case [valueType | Just valueType <- [expected], isEnumType valueType] of
+                valueType : _ -> case enumMemberValue enums valueType member of
+                    Just value -> (LiteralExpression spanValue (IntegerLiteral value) valueType, valueType, [])
+                    Nothing ->
+                        ( LiteralExpression spanValue (IntegerLiteral 0) valueType
+                        , ErrorType
+                        , [problem spanValue "TAT0064" ("the expected enum has no member named " ++ identifierText member)]
+                        )
+                [] ->
+                    ( LiteralExpression spanValue (IntegerLiteral 0) ErrorType
+                    , ErrorType
+                    ,
+                        [ problem
+                            spanValue
+                            "TAT0069"
+                            "a target-typed .Member needs a place whose type is known to be an enum"
+                        ]
+                    )
+    MemberAccessExpression spanValue receiver member _ ->
+        let (typedReceiver, _, receiverProblems) = checkExpressionWith context environment receiver
+            memberProblems = [problem spanValue "TAT0034" "member selection is currently supported only as a type-qualified method call"]
+         in (MemberAccessExpression spanValue typedReceiver member ErrorType, ErrorType, receiverProblems ++ memberProblems)
+    -- @Type::Method@ is the static method as a callable value.
+    MethodReferenceExpression spanValue (NameExpression _ name _) member _
+        | resolvedSymbol name `elem` map fst (catalogTypes (templateCatalog context)) ->
+            methodValue context expected spanValue (resolvedSymbol name) member
+    -- A reference through a value is a method bound to that value, which
+    -- needs the methods of values.
+    MethodReferenceExpression spanValue receiver member _ ->
+        let (typedReceiver, _, receiverProblems) = checkExpressionWith context environment receiver
+            memberProblems =
+                [ problem
+                    spanValue
+                    "TAT0081"
+                    "a method reference is currently supported only for a static method named through its type"
+                ]
+         in (MethodReferenceExpression spanValue typedReceiver member ErrorType, ErrorType, receiverProblems ++ memberProblems)
+    CallExpression spanValue callee arguments _ ->
+        case callee of
+            MemberAccessExpression _ receiver member _ ->
+                checkTypeQualifiedCall context environment expected spanValue receiver member arguments
+            NameExpression calleeSpan name _
+                | Just owner <- memberOwnerForSymbol context (resolvedSymbol name)
+                , Just owner == templateCurrentType context ->
+                    checkMemberOverloadCall
+                        context
+                        environment
+                        expected
+                        spanValue
+                        calleeSpan
+                        (overloadsFor context owner (resolvedSpelling name))
+                        arguments
+                        False
+            _ -> checkOrdinaryCall context environment spanValue callee arguments
+    UnaryExpression spanValue operator value _ ->
+        let operandExpected = if operator == LogicalNot then Nothing else expected
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment operandExpected value
+            rule = unaryNumericRule operator valueType
+            -- An operand without a type was reported already, or never
+            -- yields a value; either way the operator has nothing to check.
+            (resultType, mismatch) =
+                if valueType == ErrorType
+                    then (ErrorType, [])
+                    else (numericRuleType rule, ruleProblems spanValue "TAT0011" rule)
+         in (UnaryExpression spanValue operator typedValue resultType, resultType, problems ++ mismatch)
+    BinaryExpression spanValue operator left right _ ->
+        -- A Boolean result does not imply Boolean operands: pushing the return
+        -- context into 1 == 2 would convert both literals to true. Comparisons
+        -- infer their operand domain; logical operands may use distinct numeric
+        -- types and therefore do not borrow each other's expected type.
+        let leftResult = case (operator, expected) of
+                (FloorDivide, Just target)
+                    | isIntegerType target ->
+                        let inferred@(_, inferredType, _) = checkExpressionWith context environment left
+                         in if isIntegerType inferredType
+                                then checkExpressionExpectedWith context environment (Just target) left
+                                else inferred
+                (FloorDivide, _) -> checkExpressionWith context environment left
+                _ -> checkExpressionExpectedWith context environment (if booleanResult operator then Nothing else expected) left
+            (typedLeft, leftType, leftProblems) = leftResult
+            -- The other side of a string is whatever it is by itself: `+`
+            -- writes it as text, so it does not borrow the string's type.
+            rightExpected =
+                if operator `elem` [LogicalAnd, LogicalOr] || leftType == stringType then Nothing else Just leftType
+            (typedRight, rightType, rightProblems) = checkExpressionExpectedWith context environment rightExpected right
+            rule = binaryNumericRule operator leftType rightType
+            typed = leftType /= ErrorType && rightType /= ErrorType
+            -- `+` with a string on either side joins the two as text, and
+            -- `==` and `\=` on two strings compare their characters.
+            joins = typed && operator == Add && (leftType == stringType || rightType == stringType)
+            comparesText = typed && operator `elem` [Equal, NotEqual] && leftType == stringType && rightType == stringType
+            -- An operand without a type was reported already, or never
+            -- yields a value; either way the operator has nothing to check.
+            -- Values of an enum are compared for equality with values of
+            -- the same enum, and take part in no other operation.
+            (resultType, mismatch)
+                | leftType == ErrorType || rightType == ErrorType =
+                    (if booleanResult operator then boolType else ErrorType, [])
+                | isEnumType leftType || isEnumType rightType =
+                    if operator `elem` [Equal, NotEqual] && leftType == rightType
+                        then (boolType, [])
+                        else
+                            ( if booleanResult operator then boolType else ErrorType
+                            ,
+                                [ problem
+                                    spanValue
+                                    "TAT0065"
+                                    "values of an enum are only compared, with == and \\=, with values of the same enum"
+                                ]
+                            )
+                | otherwise = (numericRuleType rule, ruleProblems spanValue "TAT0012" rule)
+            withOperands (value, valueType, problems) = (value, valueType, leftProblems ++ rightProblems ++ problems)
+         in if joins
+                then withOperands (concatenation spanValue (typedLeft, leftType) (typedRight, rightType))
+                else
+                    if comparesText
+                        then withOperands (textEquality spanValue (operator == Equal) typedLeft typedRight)
+                        else
+                            ( BinaryExpression spanValue operator typedLeft typedRight resultType
+                            , resultType
+                            , leftProblems ++ rightProblems ++ mismatch
+                            )
+    IsPatternExpression spanValue subject patternValue _ ->
+        let (typedSubject, subjectType, subjectProblems) = checkExpressionWith context environment subject
+            (typedPattern, patternProblems) = checkPatternWith context subjectType patternValue
+         in ( IsPatternExpression spanValue typedSubject typedPattern boolType
+            , boolType
+            , subjectProblems ++ patternProblems
+            )
+    ConditionalExpression spanValue condition first second _ ->
+        let (typedCondition, conditionType, conditionProblems) = checkExpressionWith context environment condition
+            conditionMismatch =
+                [ problem (sourceSpanOf condition) "TAT0036" "conditional test must be bool or numeric"
+                | conditionType /= ErrorType
+                , not (booleanContextType conditionType)
+                ]
+            ((typedFirst, firstType, firstProblems), (typedSecond, secondType, secondProblems)) =
+                checkOperandPair context environment expected first second
+            -- A block that leaves instead of completing has no value, so
+            -- the other block alone gives the expression its type.
+            -- When neither completes, the expression never yields a value:
+            -- it is annotated void, and what receives it is not held to a
+            -- type, because that place is never reached.
+            (annotation, resultType, resultProblems) = case (doesNotComplete typedFirst, doesNotComplete typedSecond) of
+                (True, True) -> (voidType, ErrorType, [])
+                (True, False) -> (secondType, secondType, [])
+                (False, True) -> (firstType, firstType, [])
+                -- A string is selected like a scalar: one of the two is
+                -- the value, and the other is never made.
+                (False, False)
+                    | firstType == stringType && secondType == stringType -> (stringType, stringType, [])
+                    | otherwise ->
+                        let (valueType, problems) =
+                                selectedValueType spanValue "TAT0037" "conditional results must have the same type" firstType secondType
+                         in (valueType, valueType, problems)
+         in ( ConditionalExpression spanValue typedCondition typedFirst typedSecond annotation
+            , resultType
+            , conditionProblems ++ conditionMismatch ++ firstProblems ++ secondProblems ++ resultProblems
+            )
+    CoalesceExpression spanValue left fallback _ ->
+        let ((typedLeft, leftType, leftProblems), (typedFallback, fallbackType, fallbackProblems)) =
+                checkOperandPair context environment expected left fallback
+            (resultType, resultProblems) =
+                selectedValueType
+                    spanValue
+                    "TAT0038"
+                    "truthy coalescing operands must have the same type"
+                    leftType
+                    fallbackType
+         in ( CoalesceExpression spanValue typedLeft typedFallback resultType
+            , resultType
+            , leftProblems ++ fallbackProblems ++ resultProblems
+            )
+    -- An assignment used as a value has the typing of its statement form and
+    -- yields the stored value, so its type is the target type. The context
+    -- that receives the value does not flow into the right operand: the
+    -- target alone decides what may be stored.
+    AssignmentExpression spanValue Nothing name value _ ->
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            valueExpected = if targetType == ErrorType then Nothing else Just targetType
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment valueExpected value
+            immutable = immutableTargetProblems spanValue target
+            mismatch =
+                [problem spanValue "TAT0004" "assignment value has the wrong type" | not (compatible targetType valueType)]
+         in ( AssignmentExpression spanValue Nothing name typedValue targetType
+            , targetType
+            , problems ++ immutable ++ mismatch
+            )
+    AssignmentExpression spanValue (Just Add) name value _
+        | target@(Just (targetType, _)) <- lookup (resolvedSymbol name) environment
+        , targetType == stringType ->
+            let (typedValue, valueType, problems) = checkExpressionWith context environment value
+                (joined, _, joinProblems) =
+                    concatenation spanValue (NameExpression spanValue name stringType, stringType) (typedValue, valueType)
+             in ( AssignmentExpression spanValue Nothing name joined stringType
+                , stringType
+                , problems ++ immutableTargetProblems spanValue target ++ joinProblems
+                )
+    AssignmentExpression spanValue (Just operator) name value _ ->
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            operandExpected = if targetType == ErrorType then Nothing else Just targetType
+            (typedValue, valueType, problems) = checkExpressionExpectedWith context environment operandExpected value
+            immutable = immutableTargetProblems spanValue target
+            rule = binaryNumericRule operator targetType valueType
+            known = targetType /= ErrorType && valueType /= ErrorType
+            operatorProblems = if known then ruleProblems spanValue "TAT0012" rule else []
+            resultProblems =
+                [ problem spanValue "TAT0035" "compound assignment result does not have the target type"
+                | known
+                , null operatorProblems
+                , numericRuleType rule /= targetType
+                ]
+         in ( AssignmentExpression spanValue (Just operator) name typedValue targetType
+            , targetType
+            , problems ++ immutable ++ operatorProblems ++ resultProblems
+            )
+    IncrementExpression spanValue isPrefix name _ ->
+        let target = lookup (resolvedSymbol name) environment
+            targetType = maybe ErrorType fst target
+            writableProblems = case target of
+                Just (_, False) -> [problem spanValue "TAT0022" "increment cannot modify an immutable binding"]
+                Nothing -> [problem spanValue "TAT0023" "increment target is not defined"]
+                _ -> []
+            numericProblems =
+                [ problem spanValue "TAT0024" "increment target must have a numeric type"
+                | not (isNumericType targetType && targetType /= boolType)
+                ]
+         in ( IncrementExpression spanValue isPrefix name targetType
+            , targetType
+            , writableProblems ++ numericProblems
+            )
+    -- A loop used as an expression yields the operand of the break that
+    -- leaves it. It must not be able to end any other way, so its condition
+    -- is the constant true, or absent in a `for`, and every break that
+    -- leaves it carries a value.
+    LoopExpression spanValue loop _ ->
+        let loops = LoopContext (ExpressionLoop expected) []
+            (typedLoop, _, _, loopProblems) = checkStatementWith context environment (contextReturn context) loops loop
+            breakTypes = loopBreakTypes typedLoop
+            -- A loop that no break leaves never yields a value; it leaves
+            -- through a return or does not end. That is a fact about its
+            -- control flow, so it is annotated void and what receives it is
+            -- not held to a type.
+            -- A loop that only runs forever is still reported as lacking
+            -- a value: it is far more likely a forgotten break.
+            neverYields =
+                null breakTypes
+                    && not (null (blockReturnTypes (Block [typedLoop])))
+                    && doesNotComplete (LoopExpression spanValue typedLoop voidType)
+            (annotation, resultType, resultProblems)
+                | neverYields = (voidType, ErrorType, [])
+                | otherwise = let (valueType, problems) = loopValueType spanValue breakTypes in (valueType, valueType, problems)
+            endProblems =
+                [ problem
+                    spanValue
+                    "TAT0041"
+                    "a loop used as an expression can end without a value; its condition must be the constant true"
+                | loopMayEndWithoutValue typedLoop
+                ]
+         in ( LoopExpression spanValue typedLoop annotation
+            , resultType
+            , loopProblems ++ endProblems ++ resultProblems
+            )
+    BlockExpression spanValue block _ ->
+        checkValueBlock (branchChecker context (contextReturn context)) environment expected spanValue block
+    MatchExpression spanValue subjects arms _ ->
+        let (typedMatch, resultType, _, problems) =
+                checkMatch (branchChecker context (contextReturn context)) MatchValue environment expected spanValue subjects arms
+         in (typedMatch, resultType, problems)
+    CallableExpression spanValue explicit captures parameters body _ ->
+        let checkedCaptures = checkCapturesWith context environment captures
+            captureEnvironment =
+                [ (resolvedSymbol (captureName capture), (captureAnnotation capture, True))
+                | capture <- map firstCapture checkedCaptures
+                ]
+            typedParameters = map (typeCallableParameterWith context) parameters
+            parameterEnvironment =
+                [ (resolvedSymbol (parameterName parameter), (parameterAnnotation parameter, False))
+                | parameter <- typedParameters
+                ]
+            callableEnvironment = parameterEnvironment ++ captureEnvironment ++ environment
+            (typedBody, resultType, bodyProblems) = checkCallableBodyWith context callableEnvironment body
+            callableType = FunctionType (map parameterAnnotation typedParameters) resultType
+            -- The result type is inferred from the returns of the body,
+            -- which are read through expressions as well; they must agree,
+            -- or the callable has no one type to return.
+            returnProblems = case typedBody of
+                CallableBlockBody block ->
+                    [ problem
+                        spanValue
+                        "TAT0062"
+                        "the return statements of this callable carry values of different types"
+                    | any (not . compatible resultType) (blockReturnTypes block)
+                    ]
+                CallableExpressionBody _ -> []
+            captureProblems = concatMap captureDiagnostics checkedCaptures
+            parameterProblems = concatMap (typeSyntaxProblemsIn context . parameterTypeSyntax) parameters
+            -- A parameter without a written type takes it from the place
+            -- that expects the callable. Until that is implemented its type
+            -- stays unknown and no later stage can lower the callable.
+            inferredProblems =
+                [ problem
+                    (parameterSpan parameter)
+                    "TAT0082"
+                    "the type of a callable parameter is not inferred from its context yet; write the type"
+                | parameter@Parameter {parameterTypeSyntax = AutoType} <- parameters
+                ]
+         in ( CallableExpression
+                spanValue
+                explicit
+                (map firstCapture checkedCaptures)
+                typedParameters
+                typedBody
+                callableType
+            , callableType
+            , captureProblems ++ parameterProblems ++ inferredProblems ++ bodyProblems ++ returnProblems
+            )
+
+{- | Result type of a loop expression from the types of its break values.
+
+The value is materialized in one storage slot, like the result of a
+conditional expression, so all break values have one type. Bool, numeric,
+enum and string results are lowered today.
+-}
+loopValueType :: SourceSpan -> [Type] -> (Type, [Diagnostic])
+loopValueType spanValue breakTypes = case filter (/= ErrorType) breakTypes of
+    [] ->
+        ( ErrorType
+        , [problem spanValue "TAT0042" "a loop used as an expression has no break that carries a value" | null breakTypes]
+        )
+    first : remaining
+        | any (/= first) remaining ->
+            (first, [problem spanValue "TAT0043" "the break values of a loop used as an expression must have the same type"])
+        | not (booleanContextType first)
+        , not (isEnumType first)
+        , first /= stringType ->
+            ( first
+            , [problem spanValue "TAT0044" "loop expressions currently support only bool, numeric, enum and string results"]
+            )
+        | otherwise -> (first, [])
+
+-- | Whether the loop can finish by its condition becoming false.
+loopMayEndWithoutValue :: Statement ResolvedName Type -> Bool
+loopMayEndWithoutValue loop = case loop of
+    WhileStatement _ condition _ -> not (isConstantTrue condition)
+    ForStatement _ _ condition _ _ -> maybe False (not . isConstantTrue) condition
+    _ -> True
+    where
+        isConstantTrue expression = case expression of
+            LiteralExpression _ (BooleanLiteral True) _ -> True
+            _ -> False
+
+immutableTargetProblems :: SourceSpan -> Maybe (Type, Bool) -> [Diagnostic]
+immutableTargetProblems spanValue target = case target of
+    Just (_, False) -> [problem spanValue "TAT0003" "cannot assign to an immutable binding"]
+    _ -> []
+
+{- | Check the two value operands of a conditional form exactly once each.
+
+An operand made only of untyped numeric literals takes its type from the
+other operand, in either direction, so @flag ? 1 : wide@ selects the type of
+@wide@ just as @flag ? wide : 1@ does. The order is chosen from syntax
+before either operand is checked; checking an operand twice would make the
+cost exponential in the nesting depth of chained conditionals.
+-}
+checkOperandPair ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe Type ->
+    Expression ResolvedName () ->
+    Expression ResolvedName () ->
+    ( (Expression ResolvedName Type, Type, [Diagnostic])
+    , (Expression ResolvedName Type, Type, [Diagnostic])
+    )
+checkOperandPair context environment expected first second
+    | expected == Nothing && takesContextualType first && not (takesContextualType second) =
+        let secondResult@(_, secondType, _) = checkExpressionWith context environment second
+         in (checkExpressionExpectedWith context environment (contextFrom secondType) first, secondResult)
+    | otherwise =
+        let firstResult@(_, firstType, _) = checkExpressionExpectedWith context environment expected first
+            secondExpected = maybe (contextFrom firstType) Just expected
+         in (firstResult, checkExpressionExpectedWith context environment secondExpected second)
+    where
+        contextFrom valueType = if valueType == ErrorType then Nothing else Just valueType
+
+-- | Whether an expression consists only of numeric literals and arithmetic.
+takesContextualType :: Expression name annotation -> Bool
+takesContextualType expression = case expression of
+    LiteralExpression _ (IntegerLiteral _) _ -> True
+    LiteralExpression _ (FloatingLiteral _) _ -> True
+    UnaryExpression _ operator value _ -> operator /= LogicalNot && takesContextualType value
+    BinaryExpression _ operator left right _ ->
+        not (booleanResult operator) && takesContextualType left && takesContextualType right
+    _ -> False
+
+{- | Result type shared by the two value operands of a conditional form.
+
+The result is materialized in one storage slot, so both operands must have
+the same type. Only bool and numeric results are lowered today; owned values
+need move and release rules for the slot that the backend does not have yet.
+-}
+selectedValueType :: SourceSpan -> String -> String -> Type -> Type -> (Type, [Diagnostic])
+selectedValueType spanValue mismatchCode mismatchMessage firstType secondType
+    | firstType == ErrorType = (secondType, [])
+    | secondType == ErrorType = (firstType, [])
+    | firstType /= secondType = (firstType, [problem spanValue mismatchCode mismatchMessage])
+    | not (booleanContextType firstType)
+    , not (isEnumType firstType) =
+        ( firstType
+        , [problem spanValue "TAT0039" "conditional expressions currently support only bool, numeric and enum results"]
+        )
+    | otherwise = (firstType, [])
+
+checkOrdinaryCall ::
+    TemplateContext ->
+    TypeEnvironment ->
+    SourceSpan ->
+    Expression ResolvedName () ->
+    [Expression ResolvedName ()] ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkOrdinaryCall context environment spanValue callee arguments =
+    let (typedCallee, calleeType, calleeProblems) = checkExpressionExpectedWith context environment Nothing callee
+        parameterTypes = case calleeType of FunctionType parameters _ -> parameters; _ -> []
+        checkedArguments =
+            zipWith
+                (\index argument -> checkExpressionExpectedWith context environment (safeIndex parameterTypes index) argument)
+                [0 ..]
+                arguments
+        argumentTypes = map (\(_, valueType, _) -> valueType) checkedArguments
+        (resultType, callProblems) = case calleeType of
+            FunctionType parameters result
+                | length parameters /= length argumentTypes ->
+                    (result, [problem spanValue "TAT0008" "call argument count does not match"])
+                | and (zipWith compatible parameters argumentTypes) -> (result, [])
+                | otherwise -> (result, [problem spanValue "TAT0009" "call argument type does not match"])
+            ErrorType -> (ErrorType, [])
+            _ -> (ErrorType, [problem spanValue "TAT0010" "expression is not callable"])
+     in ( CallExpression spanValue typedCallee (map (\(value, _, _) -> value) checkedArguments) resultType
+        , resultType
+        , calleeProblems ++ concatMap (\(_, _, ps) -> ps) checkedArguments ++ callProblems
+        )
+
+{- | A static method referred to through its type, @Type::Method@: the
+method as a callable value, like its bare name inside the type.
+
+A name with one static method that is accessible is that method. A name
+with several is the one whose signature the place expects; where the place
+expects none of them, or nothing, the reference does not say which is meant.
+-}
+methodValue ::
+    TemplateContext -> Maybe Type -> SourceSpan -> SymbolId -> Identifier -> (Expression ResolvedName Type, Type, [Diagnostic])
+methodValue context expected spanValue owner member = case chosen of
+    [declaration] ->
+        let valueType = signature context declaration
+         in (NameExpression spanValue (declarationName declaration) valueType, valueType, [])
+    _ -> (NameExpression spanValue (ResolvedName (SymbolId (-1)) member) ErrorType, ErrorType, [failure])
+    where
+        candidates = overloadsFor context owner member
+        static = [candidate | candidate@(MethodCandidate _ FunctionDeclaration {declarationIsStatic = True}) <- candidates]
+        visible = map candidateDeclaration (filter (candidateVisibleFrom context) static)
+        chosen = case visible of
+            [_] -> visible
+            _ -> [declaration | declaration <- visible, Just (signature context declaration) == expected]
+        failure
+            | null candidates = problem spanValue "TAT0029" "no method with this name is declared on the selected type"
+            | null static = problem spanValue "TAT0031" "an instance method cannot be referred to through a type name"
+            | null visible = problem spanValue "TAT0033" "the selected method is not accessible from this declaration"
+            | otherwise =
+                problem
+                    spanValue
+                    "TAT0080"
+                    "the method name has several overloads and the place does not expect the type of one of them"
+
+typeQualifiedReceiver :: Expression ResolvedName () -> Maybe ResolvedName
+typeQualifiedReceiver (NameExpression _ name _) = Just name
+typeQualifiedReceiver _ = Nothing
+
+memberOwnerForSymbol :: TemplateContext -> SymbolId -> Maybe SymbolId
+memberOwnerForSymbol context symbol =
+    candidateOwner <$> firstMatch
+    where
+        firstMatch = findCandidate (catalogMethods (templateCatalog context))
+        findCandidate [] = Nothing
+        findCandidate (candidate : remaining)
+            | resolvedSymbol (declarationName (candidateDeclaration candidate)) == symbol = Just candidate
+            | otherwise = findCandidate remaining
+
+overloadsFor :: TemplateContext -> SymbolId -> Identifier -> [MethodCandidate]
+overloadsFor context owner name =
+    [ candidate
+    | candidate <- catalogMethods (templateCatalog context)
+    , candidateOwner candidate == owner
+    , resolvedSpelling (declarationName (candidateDeclaration candidate)) == name
+    ]
+
+checkTypeQualifiedCall ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe Type ->
+    SourceSpan ->
+    Expression ResolvedName () ->
+    Identifier ->
+    [Expression ResolvedName ()] ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkTypeQualifiedCall context environment _ callSpan receiver member arguments
+    | consoleReceiver receiver =
+        checkConsoleCall (checkExpressionExpectedWith context environment) callSpan member arguments
+checkTypeQualifiedCall context environment expected callSpan receiver member arguments =
+    case typeQualifiedReceiver receiver of
+        Just typeName
+            | let owner = resolvedSymbol typeName
+            , owner `elem` map fst (catalogTypes (templateCatalog context)) ->
+                checkMemberOverloadCall
+                    context
+                    environment
+                    expected
+                    callSpan
+                    (sourceSpanOf receiver)
+                    (overloadsFor context owner member)
+                    arguments
+                    True
+        _ ->
+            let (typedReceiver, _, receiverProblems) = checkExpressionWith context environment receiver
+                argumentsChecked = map (checkExpressionWith context environment) arguments
+                typedArguments = [value | (value, _, _) <- argumentsChecked]
+                diagnostics =
+                    receiverProblems
+                        ++ concat [problems | (_, _, problems) <- argumentsChecked]
+                        ++ [problem callSpan "TAT0032" "the left side of a static member call must name a declared type"]
+             in ( CallExpression
+                    callSpan
+                    (MemberAccessExpression callSpan typedReceiver member ErrorType)
+                    typedArguments
+                    ErrorType
+                , ErrorType
+                , diagnostics
+                )
+
+sourceSpanOf :: Expression name annotation -> SourceSpan
+sourceSpanOf 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
+
+checkMemberOverloadCall ::
+    TemplateContext ->
+    TypeEnvironment ->
+    Maybe Type ->
+    SourceSpan ->
+    SourceSpan ->
+    [MethodCandidate] ->
+    [Expression ResolvedName ()] ->
+    Bool ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkMemberOverloadCall context environment _ callSpan calleeSpan candidates arguments requireStatic =
+    let callableCandidates = if requireStatic then filter candidateIsStatic candidates else candidates
+        visibleCandidates = filter (candidateVisibleFrom context) callableCandidates
+        candidateAttempts = map attempt visibleCandidates
+        viable = [value | value@(_, _, _, True) <- candidateAttempts]
+        arityMatches = filter candidateArityMatches visibleCandidates
+        failureCode
+            | null candidates = "TAT0029"
+            | requireStatic && null callableCandidates = "TAT0031"
+            | null visibleCandidates = "TAT0033"
+            | null arityMatches = "TAT0008"
+            | otherwise = "TAT0009"
+        failureMessage
+            | null candidates = "no method with this name is declared on the selected type"
+            | requireStatic && null callableCandidates = "an instance method cannot be called through a type name"
+            | null visibleCandidates = "the selected method is not accessible from this declaration"
+            | null arityMatches = "call argument count does not match any overload"
+            | otherwise = "call argument types do not match any overload"
+        (selected, resultType, typedArguments, diagnostics) = case viable of
+            [(candidate, valueType, checked, _)] -> (Just candidate, valueType, checked, [])
+            [] ->
+                ( Nothing
+                , ErrorType
+                , map (\argument -> fst3 (checkExpressionWith context environment argument)) arguments
+                , [problem callSpan failureCode failureMessage]
+                )
+            _ ->
+                ( Nothing
+                , ErrorType
+                , map (\argument -> fst3 (checkExpressionWith context environment argument)) arguments
+                , [problem callSpan "TAT0030" "the call is ambiguous between multiple equally viable overloads"]
+                )
+        typedCallee = case selected of
+            Just candidate ->
+                let declaration = candidateDeclaration candidate
+                 in NameExpression calleeSpan (declarationName declaration) (signature context declaration)
+            Nothing -> case visibleCandidates of
+                candidate : _ ->
+                    let declaration = candidateDeclaration candidate
+                     in NameExpression calleeSpan (declarationName declaration) (signature context declaration)
+                [] -> NameExpression calleeSpan (ResolvedName (SymbolId (-1)) (Identifier "<unresolved-member>")) ErrorType
+     in (CallExpression callSpan typedCallee typedArguments resultType, resultType, diagnostics)
+    where
+        attempt candidate =
+            let declaration = candidateDeclaration candidate
+                functionType = signature context declaration
+                (parameters, resultType) = case functionType of
+                    FunctionType types result -> (types, result)
+                    _ -> ([], ErrorType)
+                checkedArguments =
+                    zipWith
+                        (\index argument -> checkExpressionExpectedWith context environment (safeIndex parameters index) argument)
+                        [0 ..]
+                        arguments
+                argumentTypes = [valueType | (_, valueType, _) <- checkedArguments]
+                problems = concat [nested | (_, _, nested) <- checkedArguments]
+                matching = length parameters == length arguments && null problems && and (zipWith compatible parameters argumentTypes)
+             in (candidate, resultType, [value | (value, _, _) <- checkedArguments], matching)
+        candidateIsStatic (MethodCandidate _ FunctionDeclaration {declarationIsStatic = isStatic}) = isStatic
+        candidateIsStatic _ = False
+        candidateArityMatches candidate = case candidateDeclaration candidate of
+            FunctionDeclaration {declarationParameters = parameters} -> length parameters == length arguments
+            _ -> False
+
+candidateVisibleFrom :: TemplateContext -> MethodCandidate -> Bool
+candidateVisibleFrom context (MethodCandidate owner FunctionDeclaration {declarationAccess = access}) =
+    case access of
+        PrivateAccess -> templateCurrentType context == Just owner
+        ProtectedAccess -> templateCurrentType context == Just owner
+        _ -> True
+candidateVisibleFrom _ _ = False
+
+fst3 :: (a, b, c) -> a
+fst3 (first, _, _) = first
+
+-- A pattern is checked against the already typed subject. This keeps literal
+-- inference deterministic and makes the later decision-tree lowering free of
+-- source-level conversion guesses.
+checkPatternWith :: TemplateContext -> Type -> Pattern ResolvedName () -> (Pattern ResolvedName Type, [Diagnostic])
+checkPatternWith context subjectType patternValue = case patternValue of
+    WildcardPattern spanValue _ -> (WildcardPattern spanValue subjectType, [])
+    NullPattern spanValue _ ->
+        let problems =
+                if isReferenceType subjectType
+                    then []
+                    else [problem spanValue "TAT0020" "null pattern requires an AARC reference subject"]
+         in (NullPattern spanValue subjectType, problems)
+    LiteralPattern spanValue literal _ ->
+        let (literalType, literalProblems) = literalTypeInContext spanValue (Just subjectType) literal
+            rule = binaryNumericRule Equal subjectType literalType
+            problems = literalProblems ++ ruleProblems spanValue "TAT0021" rule
+         in (LiteralPattern spanValue literal literalType, problems)
+    TypePattern spanValue syntax _ ->
+        let targetType = syntaxTypeIn context syntax
+            syntaxProblems = typeSyntaxProblemsIn context syntax
+            possible =
+                targetType /= ErrorType
+                    && subjectType /= ErrorType
+                    && (compatible subjectType targetType || isReferenceType subjectType && isReferenceType targetType)
+            relationProblems =
+                if possible
+                    then []
+                    else [problem spanValue "TAT0022" "type pattern can never match the subject type"]
+         in (TypePattern spanValue syntax targetType, syntaxProblems ++ relationProblems)
+    RelationalPattern spanValue operator literal _ ->
+        let (literalType, literalProblems) = literalTypeInContext spanValue (Just subjectType) literal
+            binary = relationalPatternBinary operator
+            rule = binaryNumericRule binary subjectType literalType
+            problems = literalProblems ++ ruleProblems spanValue "TAT0023" rule
+         in (RelationalPattern spanValue operator literal literalType, problems)
+    NotPattern spanValue nested _ ->
+        let (typed, problems) = checkPatternWith context subjectType nested
+         in (NotPattern spanValue typed boolType, problems)
+    AndPattern spanValue left right _ -> checkPatternPair AndPattern spanValue left right
+    OrPattern spanValue left right _ -> checkPatternPair OrPattern spanValue left right
+    where
+        checkPatternPair constructor spanValue left right =
+            let (typedLeft, leftProblems) = checkPatternWith context subjectType left
+                (typedRight, rightProblems) = checkPatternWith context subjectType right
+             in (constructor spanValue typedLeft typedRight boolType, leftProblems ++ rightProblems)
+
+relationalPatternBinary :: RelationalPatternOperator -> BinaryOperator
+relationalPatternBinary operator = case operator of
+    PatternLessThan -> LessThan
+    PatternLessEqual -> LessEqual
+    PatternGreaterThan -> GreaterThan
+    PatternGreaterEqual -> GreaterEqual
+    PatternEqual -> Equal
+    PatternNotEqual -> NotEqual
+
+type CheckedCapture = (Capture ResolvedName Type, [Diagnostic])
+
+booleanResult :: BinaryOperator -> Bool
+booleanResult operator =
+    operator `elem` [LogicalAnd, LogicalOr, Equal, NotEqual, LessThan, LessEqual, GreaterThan, GreaterEqual]
+
+firstCapture :: CheckedCapture -> Capture ResolvedName Type
+firstCapture = fst
+
+captureDiagnostics :: CheckedCapture -> [Diagnostic]
+captureDiagnostics = snd
+
+checkCapturesWith :: TemplateContext -> TypeEnvironment -> [Capture ResolvedName ()] -> [CheckedCapture]
+checkCapturesWith context environment = map checkCapture
+    where
+        checkCapture (Capture spanValue mode name _ initializer) =
+            let (typedInitializer, valueType, problems) = checkOptionalWith context environment initializer
+                ownershipProblems = case mode of
+                    StrongCapture -> []
+                    _ | isReferenceType valueType -> []
+                    WeakCapture -> [problem spanValue "TAT0014" "weak capture requires an AARC reference value"]
+                    UnownedCapture -> [problem spanValue "TAT0015" "unowned capture requires an AARC reference value"]
+             in (Capture spanValue mode name valueType typedInitializer, problems ++ ownershipProblems)
+
+-- String and callable values are AARC references. Every canonical scalar is a
+-- value, not merely the handful historically accepted by closure tests. Named
+-- user and library types remain conservative until resolved declaration
+-- metadata connects the ownership catalog to this check.
+isReferenceType :: Type -> Bool
+isReferenceType valueType = case valueType of
+    FunctionType _ _ -> True
+    _ | typeToScalarType valueType /= Nothing -> False
+    NamedType _ _ -> valueType /= unitType && valueType /= voidType
+    _ -> False
+
+typeCallableParameterWith :: TemplateContext -> Parameter ResolvedName () -> Parameter ResolvedName Type
+typeCallableParameterWith context parameter =
+    let valueType = case parameterTypeSyntax parameter of
+            AutoType -> TypeVariable (parameterName parameter)
+            syntax -> syntaxTypeIn context syntax
+     in Parameter
+            (parameterSpan parameter)
+            (parameterName parameter)
+            valueType
+            (parameterTypeSyntax parameter)
+
+checkCallableBodyWith ::
+    TemplateContext ->
+    TypeEnvironment ->
+    CallableBody ResolvedName () ->
+    (CallableBody ResolvedName Type, Type, [Diagnostic])
+checkCallableBodyWith outer environment body = case body of
+    CallableExpressionBody expression ->
+        -- A callable is a function of its own: nothing in its body returns
+        -- from, or leaves a loop of, the function that creates it.
+        let context = outer {contextReturn = ErrorType, contextLoops = outsideLoops}
+            (typed, valueType, problems) = checkExpressionWith context environment expression
+         in (CallableExpressionBody typed, valueType, problems)
+    CallableBlockBody block ->
+        let (typed, _, _, problems) = checkBlockWith outer environment ErrorType outsideLoops block
+            finalType = maybe (inferReturn ErrorType (blockReturnTypes typed)) id (finalExpressionType typed)
+         in (CallableBlockBody typed, finalType, problems)
+
+-- An expression without a type was reported already, or never yields a
+-- value; a condition of either kind has nothing to check.
+booleanContextType :: Type -> Bool
+booleanContextType valueType = valueType == ErrorType || acceptsBooleanContext valueType
+
+safeIndex :: [a] -> Int -> Maybe a
+safeIndex values index
+    | index < 0 = Nothing
+    | otherwise = case drop index values of value : _ -> Just value; [] -> Nothing
+ src/Progmasoft/Tala/TypeChecker/Branching.hs view
@@ -0,0 +1,419 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Typing rules of value blocks, @match@, and @guard@.
+
+The rules need the checker for expressions and statements, which the main
+type checker owns together with its template and loop context. They receive
+it through 'BranchChecker', already applied to that context, so this module
+states the rules of the branching forms without knowing how names, templates,
+or loops are tracked.
+-}
+module Progmasoft.Tala.TypeChecker.Branching
+    ( BranchChecker (..)
+    , BranchEnvironment
+    , MatchUse (..)
+    , checkValueBlock
+    , checkMatch
+    , guardBlockProblems
+    , matchArmsAlwaysAccept
+    , blockCannotComplete
+    , doesNotComplete
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Completion
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.NumericSemantics
+
+-- | Type and mutability of every local in scope, innermost first.
+type BranchEnvironment = [(SymbolId, (Type, Bool))]
+
+-- | The parts of the main type checker that the branching rules are built on.
+data BranchChecker loops = BranchChecker
+    { branchExpression ::
+        BranchEnvironment ->
+        Maybe Type ->
+        Expression ResolvedName () ->
+        (Expression ResolvedName Type, Type, [Diagnostic])
+    -- ^ Check an expression, optionally in the context of an expected type.
+    , branchStatements ::
+        BranchEnvironment ->
+        loops ->
+        [Statement ResolvedName ()] ->
+        ([Statement ResolvedName Type], BranchEnvironment, [Type], [Diagnostic])
+    {- ^ Check statements in order. The result also has the environment after
+    them and the types of the values their @return@ statements carry.
+    -}
+    , branchType :: TypeSyntax -> (Type, [Diagnostic])
+    -- ^ Resolve a type written in source.
+    , branchLiteral :: SourceSpan -> Maybe Type -> Literal -> (Type, [Diagnostic])
+    -- ^ Type a literal in the context of an expected type.
+    , branchEnumMember :: Type -> Identifier -> Maybe Integer
+    -- ^ The value of a member of the enum with the given type.
+    , branchHasEffect :: Expression ResolvedName () -> Bool
+    -- ^ Whether evaluating an expression can do more than produce a value.
+    , branchValueLoops :: loops
+    {- ^ The loop context inside a block used as a value: the loops around
+    the expression the block belongs to, seen across the edge of the block.
+    -}
+    }
+
+-- | How a @match@ is used, which decides what its arms must provide.
+data MatchUse loops
+    = {- | An expression: every arm yields a value of one type, and some arm
+      accepts whatever the subjects are.
+      -}
+      MatchValue
+    | {- | A statement inside the given loop context: the arms are statements
+      and it is not an error when no arm accepts.
+      -}
+      MatchStatement loops
+
+problem :: SourceSpan -> String -> String -> Diagnostic
+problem spanValue code message = Diagnostic TypeCheckerStage Error code (Just spanValue) message
+
+{- | Check a block used as a value.
+
+Its value is its final expression, written without a semicolon, and that
+expression is typed in the context that receives the value. A block may
+instead leave: with @return@ it leaves the enclosing function, with @break@
+or @continue@ the enclosing loop, under the rules those statements have
+anywhere else. A block that cannot complete normally produces no value and
+needs no final expression, and a block whose final expression never
+completes produces none either. Whether a block completes is a fact about
+its control flow, kept apart from its type: 'doesNotComplete' answers it from
+the typed tree, the annotation of such a block is @void@, and the form that
+holds it takes its type from the blocks that do complete. A block that can
+complete normally must end with its value.
+
+A @return@ in such a block is checked against the declared return type of
+the enclosing function like any other. Where that type is inferred, the
+returns of the body are read from its typed tree afterwards, through
+expressions as well ("Progmasoft.Tala.TypeChecker.Returns").
+-}
+checkValueBlock ::
+    BranchChecker loops ->
+    BranchEnvironment ->
+    Maybe Type ->
+    SourceSpan ->
+    Block ResolvedName () ->
+    (Expression ResolvedName Type, Type, [Diagnostic])
+checkValueBlock checker environment expected spanValue (Block statements) =
+    let (leading, final) = case reverse statements of
+            ExpressionStatement finalSpan value False : before -> (reverse before, Just (finalSpan, value))
+            _ -> (statements, Nothing)
+        (typedLeading, inner, _, leadingProblems) =
+            branchStatements checker environment (branchValueLoops checker) leading
+     in case final of
+            Nothing
+                | blockCannotComplete (Block typedLeading) ->
+                    (BlockExpression spanValue (Block typedLeading) voidType, voidType, leadingProblems)
+                | otherwise ->
+                    ( BlockExpression spanValue (Block typedLeading) ErrorType
+                    , ErrorType
+                    , leadingProblems
+                        ++ [ problem
+                                spanValue
+                                "TAT0046"
+                                "a block used as a value must end with an expression that has no semicolon, or leave on every path"
+                           ]
+                    )
+            Just (finalSpan, value) ->
+                let (typedValue, valueType, valueProblems) = branchExpression checker inner expected value
+                    -- A final expression that never completes gives the
+                    -- block no value and therefore no type.
+                    blockType = if doesNotComplete typedValue then voidType else valueType
+                 in ( BlockExpression
+                        spanValue
+                        (Block (typedLeading ++ [ExpressionStatement finalSpan typedValue False]))
+                        blockType
+                    , valueType
+                    , leadingProblems ++ valueProblems
+                    )
+
+{- | Check a @match@.
+
+The subjects are typed first, without context. Every arm is then checked
+against them: its patterns, its guard in the scope of the names the patterns
+bind, and its body. The result also carries the types of the values returned
+by @return@ statements in the arms of a statement match.
+-}
+checkMatch ::
+    BranchChecker loops ->
+    MatchUse loops ->
+    BranchEnvironment ->
+    Maybe Type ->
+    SourceSpan ->
+    [Expression ResolvedName ()] ->
+    [MatchArm ResolvedName ()] ->
+    (Expression ResolvedName Type, Type, [Type], [Diagnostic])
+checkMatch checker use environment expected spanValue subjects arms =
+    let checkedSubjects = [branchExpression checker environment Nothing subject | subject <- subjects]
+        typedSubjects = [typed | (typed, _, _) <- checkedSubjects]
+        subjectTypes = [valueType | (_, valueType, _) <- checkedSubjects]
+        subjectProblems =
+            concat [problems | (_, _, problems) <- checkedSubjects]
+                ++ [ problem
+                        (expressionSpanOf typed)
+                        "TAT0058"
+                        "match subjects currently support only bool, numeric, enum and string values"
+                   | (typed, valueType) <- zip typedSubjects subjectTypes
+                   , valueType /= ErrorType
+                   , not (acceptsBooleanContext valueType)
+                   , Nothing <- [enumUnderlyingType valueType]
+                   , valueType /= stringType
+                   ]
+        (typedArms, armTypes, returns, armProblems) = checkArms subjectTypes expected arms
+        -- An arm that does not complete produces no value; the arms that
+        -- complete give the match its type. When none completes, the match
+        -- never yields a value: it is annotated void, like the statement
+        -- form it is lowered as, and what receives it is not held to a
+        -- type, because that place is never reached.
+        valueTypes = [armType | (arm, armType) <- zip typedArms armTypes, not (doesNotComplete (matchArmBody arm))]
+        noArmCompletes = null valueTypes && not (null typedArms)
+        (annotation, resultType, resultProblems) = case use of
+            MatchStatement _ -> (voidType, voidType, [])
+            MatchValue
+                | noArmCompletes -> (voidType, ErrorType, [])
+                | otherwise -> let (valueType, problems) = matchValueType spanValue valueTypes in (valueType, valueType, problems)
+        coverageProblems = case use of
+            MatchStatement _ -> []
+            MatchValue ->
+                [ problem
+                    spanValue
+                    "TAT0052"
+                    "a match used as an expression must accept every value of its subjects; add a '_' arm"
+                | not (matchArmsAlwaysAccept subjectTypes typedArms)
+                ]
+     in ( MatchExpression spanValue typedSubjects typedArms annotation
+        , resultType
+        , returns
+        , subjectProblems ++ armProblems ++ unreachableArmProblems typedArms ++ resultProblems ++ coverageProblems
+        )
+    where
+        -- An arm made only of untyped literals takes its type from the
+        -- context, and without one from the first arm that has a type.
+        checkArms _ _ [] = ([], [], [], [])
+        checkArms subjectTypes context (arm : remaining) =
+            let (typedArm, armType, armReturns, problems) = checkArm checker use environment context subjectTypes arm
+                nextContext = case context of
+                    Just _ -> context
+                    Nothing -> if armType == ErrorType || armType == voidType then Nothing else Just armType
+                (later, laterTypes, laterReturns, laterProblems) = checkArms subjectTypes nextContext remaining
+             in (typedArm : later, armType : laterTypes, armReturns ++ laterReturns, problems ++ laterProblems)
+
+checkArm ::
+    BranchChecker loops ->
+    MatchUse loops ->
+    BranchEnvironment ->
+    Maybe Type ->
+    [Type] ->
+    MatchArm ResolvedName () ->
+    (MatchArm ResolvedName Type, Type, [Type], [Diagnostic])
+checkArm checker use environment expected subjectTypes (MatchArm spanValue patterns guard body) =
+    let arityProblems =
+            [ problem
+                spanValue
+                "TAT0048"
+                ( "a match arm needs one pattern for each subject: expected "
+                    ++ show (length subjectTypes)
+                    ++ ", found "
+                    ++ show (length patterns)
+                )
+            | length patterns /= length subjectTypes
+            ]
+        -- A surplus pattern has no subject; it is checked against the error
+        -- type so that it reports nothing beyond the arity problem.
+        checkedPatterns = zipWith (checkMatchPattern checker) (subjectTypes ++ repeat ErrorType) patterns
+        typedPatterns = map fst checkedPatterns
+        patternProblems = concatMap snd checkedPatterns
+        -- A pattern binding is an ordinary local: it may be assigned, like
+        -- every binding that is not declared final.
+        armEnvironment =
+            [ (resolvedSymbol name, (matchPatternAnnotation patternValue, True))
+            | patternValue <- reverse typedPatterns
+            , Just name <- [matchPatternBinding patternValue]
+            ]
+                ++ environment
+        (typedGuard, guardProblems) = case guard of
+            Nothing -> (Nothing, [])
+            Just condition ->
+                let (typed, conditionType, problems) = branchExpression checker armEnvironment Nothing condition
+                    mismatch =
+                        [ problem (expressionSpanOf typed) "TAT0049" "match guard must be bool or numeric"
+                        | conditionType /= ErrorType
+                        , not (acceptsBooleanContext conditionType)
+                        ]
+                 in (Just typed, problems ++ mismatch)
+        (typedBody, bodyType, returns, bodyProblems) = case (use, body) of
+            -- The block of a statement arm is an ordinary statement block. A
+            -- match in its last position was parsed as the value of the
+            -- block; here nothing takes that value, so it is a statement.
+            (MatchStatement loops, BlockExpression blockSpan (Block statements) _) ->
+                let (typedStatements, _, blockReturns, problems) =
+                        branchStatements checker armEnvironment loops (lastMatchAsStatement statements)
+                 in (BlockExpression blockSpan (Block typedStatements) voidType, voidType, blockReturns, problems)
+            (MatchStatement _, expression) ->
+                let (typed, _, problems) = branchExpression checker armEnvironment Nothing expression
+                    effectProblems =
+                        [ problem (expressionSpanOf typed) "TAT0013" "pure value expression cannot be used as a statement"
+                        | not (branchHasEffect checker expression)
+                        ]
+                 in (typed, voidType, [], problems ++ effectProblems)
+            (MatchValue, expression) ->
+                let (typed, valueType, problems) = branchExpression checker armEnvironment expected expression
+                 in (typed, valueType, [], problems)
+     in ( MatchArm spanValue typedPatterns typedGuard typedBody
+        , bodyType
+        , returns
+        , arityProblems ++ patternProblems ++ guardProblems ++ bodyProblems
+        )
+
+lastMatchAsStatement :: [Statement name annotation] -> [Statement name annotation]
+lastMatchAsStatement statements = case reverse statements of
+    ExpressionStatement spanValue value@MatchExpression {} False : before ->
+        reverse (ExpressionStatement spanValue value True : before)
+    _ -> statements
+
+{- | Check one pattern against the type of its subject.
+
+A literal is typed as the other operand of a comparison with the subject, so
+the lowering compares two values of one type. A type pattern can only name
+the subject's own type: there are no class hierarchies to test against yet,
+so its use is to bind the value. The annotation of a typed pattern is the
+type of the value it accepts.
+-}
+checkMatchPattern ::
+    BranchChecker loops -> Type -> MatchPattern ResolvedName () -> (MatchPattern ResolvedName Type, [Diagnostic])
+checkMatchPattern checker subjectType patternValue = case patternValue of
+    MatchWildcardPattern spanValue _ -> (MatchWildcardPattern spanValue subjectType, [])
+    -- A string is matched by the characters it holds, like @==@ on two
+    -- strings, and by nothing that is not a string.
+    MatchLiteralPattern spanValue literal@(StringLiteral _) _
+        | subjectType == stringType -> (MatchLiteralPattern spanValue literal stringType, [])
+    MatchLiteralPattern spanValue literal _
+        | subjectType == stringType ->
+            ( MatchLiteralPattern spanValue literal ErrorType
+            , [problem spanValue "TAT0054" "a pattern of a string subject is a string literal"]
+            )
+    MatchLiteralPattern spanValue literal _ ->
+        let (literalType, literalProblems) = branchLiteral checker spanValue (contextOf subjectType) literal
+            rule = binaryNumericRule Equal subjectType literalType
+            ruleProblems = case numericRuleError rule of
+                Just issue | subjectType /= ErrorType -> [problem spanValue "TAT0054" (renderNumericRuleError issue)]
+                _ -> []
+         in (MatchLiteralPattern spanValue literal literalType, literalProblems ++ ruleProblems)
+    MatchNullPattern spanValue _ ->
+        ( MatchNullPattern spanValue subjectType
+        , [problem spanValue "TAT0055" "a null pattern requires a reference subject, which match does not support yet"]
+        )
+    -- @.Member@ names a member of the subject's enum. It accepts the value
+    -- of that member, so it is kept as the literal pattern of that value:
+    -- two members with one value are then one pattern, and the second of
+    -- them can never be selected.
+    MatchCasePattern spanValue name _ -> case (enumUnderlyingType subjectType, branchEnumMember checker subjectType name) of
+        (Just _, Just value) -> (MatchLiteralPattern spanValue (IntegerLiteral value) subjectType, [])
+        (Just _, Nothing) ->
+            ( MatchCasePattern spanValue name subjectType
+            , [problem spanValue "TAT0064" ("the enum of the subject has no member named " ++ identifierText name)]
+            )
+        (Nothing, _) ->
+            ( MatchCasePattern spanValue name subjectType
+            ,
+                [ problem spanValue "TAT0056" "an enum case pattern requires a subject of an enum type"
+                | subjectType /= ErrorType
+                ]
+            )
+    MatchTypePattern spanValue syntax name _ ->
+        let (namedTypeValue, syntaxProblems) = branchType checker syntax
+            relationProblems =
+                [ problem
+                    spanValue
+                    "TAT0057"
+                    "a type pattern must name the type of its subject; class hierarchies are not implemented"
+                | namedTypeValue /= ErrorType
+                , subjectType /= ErrorType
+                , namedTypeValue /= subjectType
+                ]
+         in (MatchTypePattern spanValue syntax name subjectType, syntaxProblems ++ relationProblems)
+    where
+        contextOf valueType = if valueType == ErrorType then Nothing else Just valueType
+
+{- | Result type of a match expression from the types of its arms.
+
+The value is materialized in one storage slot, like the result of a
+conditional expression, so every arm has the same type. Bool, numeric, enum
+and string results are lowered today.
+-}
+matchValueType :: SourceSpan -> [Type] -> (Type, [Diagnostic])
+matchValueType spanValue armTypes = case filter (/= ErrorType) armTypes of
+    [] -> (ErrorType, [])
+    first : remaining
+        | any (/= first) remaining ->
+            (first, [problem spanValue "TAT0050" "the arms of a match used as an expression must have the same type"])
+        | not (acceptsBooleanContext first)
+        , Nothing <- enumUnderlyingType first
+        , first /= stringType ->
+            ( first
+            , [problem spanValue "TAT0051" "match expressions currently support only bool, numeric, enum and string results"]
+            )
+        | otherwise -> (first, [])
+
+{- | Arms that can never be selected because an earlier arm without a guard
+accepts everything they accept: pattern by pattern, the earlier one accepts
+every value or names the same literal.
+-}
+unreachableArmProblems :: [MatchArm name annotation] -> [Diagnostic]
+unreachableArmProblems = go []
+    where
+        go _ [] = []
+        go earlier (arm : remaining) =
+            [ problem
+                (matchArmSpan arm)
+                "TAT0053"
+                "this match arm can never be selected: an earlier arm accepts everything it accepts"
+            | any (`shadows` arm) earlier
+            ]
+                ++ go (earlier ++ [arm]) remaining
+        shadows first second =
+            isUnguarded first
+                && length (matchArmPatterns first) == length (matchArmPatterns second)
+                && and (zipWith covers (matchArmPatterns first) (matchArmPatterns second))
+        covers first second = case (first, second) of
+            (MatchLiteralPattern _ left _, MatchLiteralPattern _ right _) -> left == right
+            _ -> acceptsEveryValue first
+
+{- | Problems of the else block of a @guard@.
+
+The statements after a guard rely on its condition, so the block must not
+complete normally on any path. That is decided from its control flow by
+'blockCannotComplete', not from the spelling of its last statement.
+-}
+guardBlockProblems :: SourceSpan -> Block name Type -> [Diagnostic]
+guardBlockProblems spanValue block =
+    [ problem
+        spanValue
+        "TAT0061"
+        "the else block of a guard can complete normally; every path through it must leave the enclosing scope"
+    | not (blockCannotComplete block)
+    ]
+
+expressionSpanOf :: Expression name annotation -> SourceSpan
+expressionSpanOf 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
+ src/Progmasoft/Tala/TypeChecker/Console.hs view
@@ -0,0 +1,357 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Text and the console in the type checker.
+
+@System.Console@ is not declared in any source file. Its methods are known
+to the compiler, and a call of one is checked here and rewritten into calls
+of the runtime: the typed tree that leaves the type checker has no
+@Console.Printf@ in it, only the conversions of its format, joined, and one
+write. The stages after the type checker therefore need to know nothing of
+formats, and a format that is wrong never reaches them.
+
+The same holds for the two operations of the language on strings that need
+the runtime: @+@, which joins two strings and writes a value that is not a
+string as text first, and @==@ and @\\=@, which compare the characters of
+two strings and not the places they are kept.
+
+A runtime call is written in the typed tree as a call of a name whose
+symbol is reserved for one function of the runtime catalog; see
+'runtimeName'. No source name has such a symbol.
+-}
+module Progmasoft.Tala.TypeChecker.Console
+    ( CheckExpression
+    , consoleReceiver
+    , checkConsoleCall
+    , concatenation
+    , textEquality
+    , runtimeCall
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.RuntimeCall
+import Progmasoft.Tala.TypeChecker.Format
+import Progmasoft.Tala.TypeChecker.Returns (typedExpressionType)
+
+{- | The type checker for an expression, given the type its place expects.
+This module is called by the checker and calls it back for the arguments.
+-}
+type CheckExpression =
+    Maybe Type -> Expression ResolvedName () -> (Expression ResolvedName Type, Type, [Diagnostic])
+
+type Checked = (Expression ResolvedName Type, Type, [Diagnostic])
+
+{- | Whether the receiver of a member call is the console: @Console@ or
+@System.Console@, where neither name has been declared by the program. A
+declaration of the program with one of those names is found by the renamer
+first and has an ordinary symbol.
+-}
+consoleReceiver :: Expression ResolvedName () -> Bool
+consoleReceiver receiver = case receiver of
+    NameExpression _ name _ -> resolvedSymbol name == builtinConsoleSymbol
+    MemberAccessExpression _ (NameExpression _ name _) member _ ->
+        resolvedSymbol name == builtinSystemSymbol && identifierText member == "Console"
+    _ -> False
+
+-- | A call of a runtime function, with the type its row of the catalog gives it.
+runtimeCall :: SourceSpan -> RuntimeFunction -> [Expression ResolvedName Type] -> Expression ResolvedName Type
+runtimeCall spanValue function arguments =
+    CallExpression
+        spanValue
+        (NameExpression spanValue (runtimeName function) (FunctionType (map typedExpressionType arguments) result))
+        arguments
+        result
+    where
+        result = case runtimeResult function of
+            NoResult -> voidType
+            TextResult -> stringType
+            TruthResult -> boolType
+
+integer :: SourceSpan -> Integer -> Expression ResolvedName Type
+integer spanValue value = LiteralExpression spanValue (IntegerLiteral value) intType
+
+text :: SourceSpan -> String -> Expression ResolvedName Type
+text spanValue value = LiteralExpression spanValue (StringLiteral value) stringType
+
+problem :: SourceSpan -> String -> String -> Diagnostic
+problem spanValue code message = Diagnostic TypeCheckerStage Error code (Just spanValue) message
+
+-- | What a failed check leaves in the tree; the diagnostics stop compilation.
+failed :: SourceSpan -> [Diagnostic] -> Checked
+failed spanValue problems = (LiteralExpression spanValue UnitLiteral ErrorType, ErrorType, problems)
+
+typeName :: Type -> String
+typeName valueType = case valueType of
+    NamedType (QualifiedName parts) [] | not (null parts) -> identifierText (last parts)
+    FunctionType _ _ -> "callable"
+    _ -> "value of this type"
+
+-- | The scalar types the runtime does not take yet, with what they wait for.
+pendingScalar :: Type -> Maybe String
+pendingScalar valueType = case valueType of
+    NamedType (QualifiedName [Identifier name]) []
+        | name `elem` ["longint", "ulongint"] -> Just "128-bit integers are not written as text yet"
+        | name == "double" -> Just "128-bit floating-point numbers are not written as text yet"
+    _ -> Nothing
+
+isFloating :: Type -> Bool
+isFloating = runtimeAccepts FloatingParameter
+
+{- | A value as text, the way @Console.Print@ and @+@ write it: a string is
+itself, an integer is written in decimal, a Boolean as @true@ or @false@ and
+a character as itself. A floating-point number has no form of its own here:
+how many digits to write is a decision @%f@ makes with its precision.
+-}
+asText :: SourceSpan -> Expression ResolvedName Type -> Type -> Either [Diagnostic] (Expression ResolvedName Type)
+asText spanValue value valueType
+    | valueType == ErrorType = Left []
+    | valueType == stringType = Right value
+    | runtimeAccepts SignedParameter valueType = Right (runtimeCall spanValue TextFromSigned [value])
+    | runtimeAccepts UnsignedParameter valueType = Right (runtimeCall spanValue TextFromUnsigned [value])
+    | runtimeAccepts BoolParameter valueType = Right (runtimeCall spanValue TextFromBool [value])
+    | runtimeAccepts CharParameter valueType = Right (runtimeCall spanValue TextFromChar [value])
+    | Just reason <- pendingScalar valueType = Left [problem spanValue "TAT0079" reason]
+    | isFloating valueType =
+        Left
+            [ problem
+                spanValue
+                "TAT0079"
+                "a floating-point number has no text form of its own yet; write it with Console.Printf or Console.Format and %f"
+            ]
+    | otherwise =
+        Left [problem spanValue "TAT0073" ("a " ++ typeName valueType ++ " cannot be written as text")]
+
+{- | @left + right@ where at least one side is a string: the two as text,
+joined. The operands are already checked.
+-}
+concatenation :: SourceSpan -> (Expression ResolvedName Type, Type) -> (Expression ResolvedName Type, Type) -> Checked
+concatenation spanValue (left, leftType) (right, rightType) =
+    case (asText (expressionSourceSpan left) left leftType, asText (expressionSourceSpan right) right rightType) of
+        (Right first, Right second) -> (runtimeCall spanValue TextConcat [first, second], stringType, [])
+        (first, second) -> failed spanValue (either id (const []) first ++ either id (const []) second)
+
+{- | @left == right@ or @left \\= right@ on two strings: whether they hold
+the same characters.
+-}
+textEquality :: SourceSpan -> Bool -> Expression ResolvedName Type -> Expression ResolvedName Type -> Checked
+textEquality spanValue equal left right =
+    ( if equal then same else UnaryExpression spanValue LogicalNot same boolType
+    , boolType
+    , []
+    )
+    where
+        same = runtimeCall spanValue TextEquals [left, right]
+
+{- | Check a call of a method of the console and rewrite it into runtime
+calls.
+-}
+checkConsoleCall :: CheckExpression -> SourceSpan -> Identifier -> [Expression ResolvedName ()] -> Checked
+checkConsoleCall check callSpan member arguments = case identifierText member of
+    "Print" -> write consoleOutput
+    "Println" -> write consoleOutputLine
+    "Error" -> write consoleError
+    "Errorln" -> write consoleErrorLine
+    "Printf" -> formatted (Just consoleOutput)
+    "Printfn" -> formatted (Just consoleOutputLine)
+    "Errorf" -> formatted (Just consoleError)
+    "Errorfn" -> formatted (Just consoleErrorLine)
+    "Format" -> formatted Nothing
+    name
+        | name `elem` ["Stdin", "Stdout", "Stderr"] ->
+            failed
+                callSpan
+                ( argumentProblems
+                    ++ [problem callSpan "TAT0079" ("Console." ++ name ++ " needs stream objects and is not implemented yet")]
+                )
+        | otherwise ->
+            failed callSpan (argumentProblems ++ [problem callSpan "TAT0071" ("Console has no method named " ++ name)])
+    where
+        method = "Console." ++ identifierText member
+        argumentProblems = concat [problems | (_, _, problems) <- map (check Nothing) arguments]
+
+        -- One value, written as text.
+        write target = case arguments of
+            [argument] ->
+                let (value, valueType, problems) = check Nothing argument
+                 in case asText (expressionSourceSpan value) value valueType of
+                        Right written ->
+                            (runtimeCall callSpan ConsoleWrite [written, integer callSpan target], voidType, problems)
+                        Left more -> failed callSpan (problems ++ more)
+            _ ->
+                failed
+                    callSpan
+                    ( argumentProblems
+                        ++ [problem callSpan "TAT0072" (method ++ " takes one argument; " ++ given (length arguments))]
+                    )
+
+        -- A format and its arguments: written when there is a target, and
+        -- the string itself when there is none.
+        formatted target = case arguments of
+            [] -> failed callSpan [problem callSpan "TAT0072" (method ++ " takes a format; " ++ given 0)]
+            formatArgument : values -> case formatArgument of
+                LiteralExpression formatSpan (StringLiteral format) _ -> case parseFormat format of
+                    Left wrong ->
+                        failed
+                            callSpan
+                            ( concat [problems | (_, _, problems) <- map (check Nothing) values]
+                                ++ [ problem
+                                        formatSpan
+                                        (formatProblemCode wrong)
+                                        ( formatProblemMessage wrong
+                                            ++ " (character "
+                                            ++ show (formatProblemOffset wrong)
+                                            ++ " of the format)"
+                                        )
+                                   ]
+                            )
+                    Right parsed ->
+                        let needed = sum [conversionArgumentCount value | ConversionPiece value <- parsed]
+                         in if needed /= length values
+                                then
+                                    failed
+                                        callSpan
+                                        ( concat [problems | (_, _, problems) <- map (check Nothing) values]
+                                            ++ [ problem
+                                                    callSpan
+                                                    "TAT0077"
+                                                    ( "the format of "
+                                                        ++ method
+                                                        ++ " takes "
+                                                        ++ counted needed
+                                                        ++ " after it; "
+                                                        ++ given (length values)
+                                                    )
+                                               ]
+                                        )
+                                else finish target (convert formatSpan parsed values)
+                _ ->
+                    failed
+                        callSpan
+                        ( argumentProblems
+                            ++ [ problem
+                                    (expressionSourceSpan formatArgument)
+                                    "TAT0074"
+                                    ("the format of " ++ method ++ " must be a string literal, so that it can be checked when the program is compiled")
+                               ]
+                        )
+
+        finish target (pieces, problems)
+            | not (null problems) = failed callSpan problems
+            | otherwise =
+                let joined = case pieces of
+                        [] -> text callSpan ""
+                        first : rest -> foldl (\whole part -> runtimeCall callSpan TextConcat [whole, part]) first rest
+                 in case target of
+                        Just stream -> (runtimeCall callSpan ConsoleWrite [joined, integer callSpan stream], voidType, [])
+                        Nothing -> (joined, stringType, [])
+
+        -- Each piece of the format as a string expression, taking the
+        -- arguments in the order the conversions name them.
+        convert _ [] _ = ([], [])
+        convert formatSpan (piece : later) values = case piece of
+            LiteralPiece literal -> prepend (text formatSpan literal, []) (convert formatSpan later values)
+            NewlinePiece -> prepend (runtimeCall formatSpan TextNewline [], []) (convert formatSpan later values)
+            ConversionPiece value ->
+                let (width, afterWidth, widthProblems) = size (conversionWidth value) values
+                    (precision, afterPrecision, precisionProblems) = size (conversionPrecision value) afterWidth
+                 in case afterPrecision of
+                        argument : remaining ->
+                            let (converted, valueProblems) = converting value width precision argument
+                             in prepend
+                                    (converted, widthProblems ++ precisionProblems ++ valueProblems)
+                                    (convert formatSpan later remaining)
+                        -- The count was checked before; nothing is missing.
+                        [] -> ([], widthProblems ++ precisionProblems)
+        prepend (piece, problems) (pieces, more) = (piece : pieces, problems ++ more)
+
+        -- A width or a precision: absent, written in the format, or the
+        -- next argument, which is an int.
+        size written values = case written of
+            NoSize -> (integer callSpan absent, values, [])
+            FixedSize amount -> (integer callSpan amount, values, [])
+            ArgumentSize -> case values of
+                argument : remaining ->
+                    let (value, valueType, problems) = check (Just intType) argument
+                        mismatch =
+                            [ problem
+                                (expressionSourceSpan value)
+                                "TAT0078"
+                                ("a width or a precision written as * takes an int, and the argument is a " ++ typeName valueType)
+                            | valueType /= ErrorType
+                            , valueType /= intType
+                            ]
+                     in (value, remaining, problems ++ mismatch)
+                [] -> (integer callSpan absent, values, [])
+
+        converting value width precision argument =
+            let kind = conversionKind value
+                (typed, valueType, problems) = check (expectedFor kind) argument
+                spanValue = expressionSourceSpan typed
+                flags = integer spanValue (conversionFlagBits value)
+                call function operand = runtimeCall spanValue function [flags, width, precision, operand]
+                plain = null (conversionFlags value) && conversionWidth value == NoSize && conversionPrecision value == NoSize
+                mismatch wanted =
+                    ( typed
+                    , problems
+                        ++ [ problem
+                                spanValue
+                                "TAT0078"
+                                ( "the conversion %"
+                                    ++ [conversionLetter kind]
+                                    ++ " takes "
+                                    ++ wanted
+                                    ++ ", and the argument is a "
+                                    ++ typeName valueType
+                                )
+                           | valueType /= ErrorType
+                           ]
+                    )
+                accepted
+                    | valueType == ErrorType = (typed, problems)
+                    | Just reason <- pendingScalar valueType = (typed, problems ++ [problem spanValue "TAT0079" reason])
+                    | otherwise = case kind of
+                        SignedDecimal
+                            | runtimeAccepts SignedParameter valueType -> (call TextFormatSigned typed, problems)
+                            | otherwise -> mismatch "a signed integer"
+                        UnsignedDecimal
+                            | runtimeAccepts UnsignedParameter valueType -> (call TextFormatUnsigned typed, problems)
+                            | otherwise -> mismatch "an unsigned integer"
+                        Hexadecimal
+                            | runtimeAccepts SignedParameter valueType -> (call TextFormatSigned typed, problems)
+                            | runtimeAccepts UnsignedParameter valueType -> (call TextFormatUnsigned typed, problems)
+                            | otherwise -> mismatch "an integer"
+                        FixedPoint
+                            | isFloating valueType -> (call TextFormatFloating typed, problems)
+                            | otherwise -> mismatch "a floating-point number"
+                        Text
+                            | valueType /= stringType -> mismatch "a String"
+                            | plain -> (typed, problems)
+                            | otherwise -> (call TextFormatString typed, problems)
+                        Character
+                            | runtimeAccepts CharParameter valueType -> (call TextFormatChar typed, problems)
+                            | otherwise -> mismatch "a char"
+                        Truth
+                            | not (runtimeAccepts BoolParameter valueType) -> mismatch "a bool"
+                            | plain -> (runtimeCall spanValue TextFromBool [typed], problems)
+                            | otherwise -> (call TextFormatString (runtimeCall spanValue TextFromBool [typed]), problems)
+             in accepted
+
+        -- A literal has the type its conversion takes; a value that already
+        -- has a type keeps it and is then held to the conversion.
+        expectedFor kind = case kind of
+            UnsignedDecimal -> Just (namedType "uint")
+            FixedPoint -> Just (namedType "float")
+            _ -> Nothing
+
+        counted :: Int -> String
+        counted amount = case amount of
+            0 -> "no argument"
+            1 -> "one argument"
+            _ -> show amount ++ " arguments"
+
+        given :: Int -> String
+        given amount = case amount of
+            0 -> "none was given"
+            1 -> "one was given"
+            _ -> show amount ++ " were given"
+ src/Progmasoft/Tala/TypeChecker/Context.hs view
@@ -0,0 +1,255 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | What the type checker carries from place to place, and how a type
+written in source becomes a type.
+
+The catalog is read from the whole source set before any body is checked,
+so that a use may precede its declaration. The template context is the
+catalog seen from one declaration: its template parameters, the type that
+owns it, the type a @return@ must carry and the loops around the place
+being checked. A type written in source is resolved in that context,
+because the same spelling names a template parameter in one declaration and
+a declared type in another.
+-}
+module Progmasoft.Tala.TypeChecker.Context
+    ( TypeEnvironment
+    , MethodCandidate (..)
+    , TypeCatalog (..)
+    , TemplateContext (..)
+    , emptyTemplateContext
+    , templateContext
+    , templateParameterAsArgument
+    , syntaxTypeIn
+    , syntaxTemplateArgumentIn
+    , syntaxTemplateValueIn
+    , typeTemplateParameter
+    , validateTemplateParameters
+    , typeSyntaxProblemsIn
+    , templateArgumentProblemsIn
+    , templateValueProblemsIn
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.TemplateValue
+import Progmasoft.Tala.TypeChecker.Enums
+import Progmasoft.Tala.TypeChecker.Literals
+import Progmasoft.Tala.TypeChecker.Loops
+
+-- | The names in scope: each symbol with its type and whether it may be assigned.
+type TypeEnvironment = [(SymbolId, (Type, Bool))]
+
+{- | A method that a call may select, with the type that declares it.
+
+The type checker owns a whole source-set catalog before it checks any body.
+That permits calls to later-declared classes without making parsing depend
+on declaration order or mutating the Renamer's lexical environment.
+-}
+data MethodCandidate = MethodCandidate
+    { candidateOwner :: SymbolId
+    , candidateDeclaration :: Declaration ResolvedName ()
+    }
+
+-- | What the checker knows of a whole source set before it checks a body.
+data TypeCatalog = TypeCatalog
+    { catalogTypes :: [(SymbolId, ResolvedName)]
+    , catalogMethods :: [MethodCandidate]
+    , catalogEnums :: [EnumInfo]
+    -- ^ The classic enums of the source set.
+    , catalogInferredReturns :: [((SymbolId, SourceSpan), Type)]
+    {- ^ The return types inferred for the methods declared with @auto@, by
+    the symbol and the place of the declaration. A method that is absent has
+    no return type that is known yet.
+    -}
+    }
+
+{- | The catalog as one declaration sees it.
+
+Type syntax deliberately keeps source spellings. This side environment is
+the bridge from those spellings to the SymbolIds assigned by the renamer.
+Type and value parameters are separate because @T@ in a type position and
+@N@ in @[T; N]@ have different semantic representations.
+-}
+data TemplateContext = TemplateContext
+    { templateTypeNames :: [(Identifier, ResolvedName)]
+    , templateValueNames :: [(Identifier, ResolvedName)]
+    , templateCatalog :: TypeCatalog
+    , templateCurrentType :: Maybe SymbolId
+    , contextReturn :: Type
+    {- ^ The type a @return@ must carry at the place being checked, or the
+    error type where it is not declared. Statements know it already; it is
+    kept here for the statements of a block used as a value, which are
+    reached through an expression.
+    -}
+    , contextLoops :: LoopContext
+    -- ^ The loops around the place being checked, for the same reason.
+    }
+
+-- | The context of a declaration that has no template parameters.
+emptyTemplateContext :: TypeCatalog -> Maybe SymbolId -> TemplateContext
+emptyTemplateContext catalog owner = TemplateContext [] [] catalog owner ErrorType outsideLoops
+
+{- | Keep type and value parameters in separate lookup tables.
+Identical source spelling in the two categories must not collapse their roles.
+-}
+templateContext :: TypeCatalog -> Maybe SymbolId -> [TemplateParameter ResolvedName annotation] -> TemplateContext
+templateContext catalog owner parameters =
+    TemplateContext
+        [ (resolvedSpelling name, name)
+        | parameter <- parameters
+        , case templateParameterKind parameter of
+            TemplateTypeParameter -> True
+            TemplateTemplateParameter _ -> True
+            _ -> False
+        , let name = templateParameterName parameter
+        ]
+        [ (resolvedSpelling name, name)
+        | parameter <- parameters
+        , case templateParameterKind parameter of TemplateValueParameterKind _ -> True; _ -> False
+        , let name = templateParameterName parameter
+        ]
+        catalog
+        owner
+        ErrorType
+        outsideLoops
+
+-- | A template parameter as the argument that stands for itself.
+templateParameterAsArgument :: TemplateParameter ResolvedName annotation -> TemplateArgument
+templateParameterAsArgument parameter = case templateParameterKind parameter of
+    TemplateValueParameterKind _ -> ValueTemplateArgument (TemplateValueParameter (templateParameterName parameter))
+    _ -> TypeTemplateArgument (TypeVariable (templateParameterName parameter))
+
+-- | The type a type written in source denotes in a context.
+syntaxTypeIn :: TemplateContext -> TypeSyntax -> Type
+syntaxTypeIn _ AutoType = ErrorType
+syntaxTypeIn context (QualifiedTypeSyntax name arguments) =
+    case name of
+        QualifiedName [identifier] | Just resolved <- lookup identifier (templateTypeNames context) -> TypeVariable resolved
+        _ -> NamedType name (map (syntaxTemplateArgumentIn context) arguments)
+syntaxTypeIn context (BuiltinArrayTypeSyntax element) =
+    -- `[]T` is a language type, not a public class invented by the compiler.
+    -- Its structural spelling keeps that distinction visible through Core
+    -- until ownership-aware lowering assigns the final runtime layout.
+    NamedType (QualifiedName [Identifier "[]"]) [TypeTemplateArgument (syntaxTypeIn context element)]
+syntaxTypeIn context (ArrayTypeSyntax element) =
+    NamedType
+        (QualifiedName [Identifier "System", Identifier "Array"])
+        [TypeTemplateArgument (syntaxTypeIn context element)]
+syntaxTypeIn context (FixedArrayTypeSyntax element size) =
+    NamedType
+        (QualifiedName [Identifier "System", Identifier "Array"])
+        [TypeTemplateArgument (syntaxTypeIn context element), ValueTemplateArgument (syntaxTemplateValueIn context size)]
+syntaxTypeIn context (DictionaryTypeSyntax key value) =
+    NamedType
+        (QualifiedName [Identifier "System", Identifier "Dictionary"])
+        [TypeTemplateArgument (syntaxTypeIn context key), TypeTemplateArgument (syntaxTypeIn context value)]
+syntaxTypeIn context (CallableTypeSyntax parameters result) = FunctionType (map (syntaxTypeIn context) parameters) (syntaxTypeIn context result)
+syntaxTypeIn context (ExplicitType identifier@(Identifier name)) = case lookup identifier (templateTypeNames context) of
+    Just resolved -> TypeVariable resolved
+    Nothing -> case name of
+        "String" -> stringType
+        "unit" -> unitType
+        "void" -> voidType
+        _
+            | Just info <- enumBySpelling (catalogEnums (templateCatalog context)) identifier -> enumInfoType info
+            | otherwise -> maybe (NamedType (QualifiedName [Identifier name]) []) scalarTypeToType (lookupScalar name)
+    where
+        lookupScalar spelling = lookup spelling [(scalarTypeName scalar, scalar) | scalar <- scalarTypes]
+
+-- | The template argument a written argument denotes in a context.
+syntaxTemplateArgumentIn :: TemplateContext -> TemplateArgumentSyntax -> TemplateArgument
+syntaxTemplateArgumentIn context argument = case argument of
+    TemplateTypeSyntax valueType -> TypeTemplateArgument (syntaxTypeIn context valueType)
+    TemplateValueArgumentSyntax value -> ValueTemplateArgument (syntaxTemplateValueIn context value)
+
+-- Parser construction guarantees the expression tree is side-effect free.
+-- Exact evaluation here gives every concrete specialization one canonical
+-- identity. Invalid arithmetic becomes a sentinel and is diagnosed by the
+-- type-syntax validation pass before Core can be emitted.
+
+{- | Canonicalize a template argument before it contributes to specialization identity.
+The fallback value is only a recovery sentinel; validation reports the original
+invalid expression before a specialization plan is emitted.
+-}
+syntaxTemplateValueIn :: TemplateContext -> TemplateValueSyntax -> TemplateValue
+syntaxTemplateValueIn context value = case value of
+    TemplateNameSyntax _ (QualifiedName [identifier])
+        | Just resolved <- lookup identifier (templateValueNames context) -> TemplateValueParameter resolved
+    _ -> case evaluateTemplateValue value of
+        Right result -> result
+        _ -> IntegerTemplateValue 0
+
+-- | A template parameter with the type of its values.
+typeTemplateParameter :: TemplateContext -> TemplateParameter ResolvedName () -> TemplateParameter ResolvedName Type
+typeTemplateParameter context parameter =
+    TemplateParameter
+        (templateParameterSpan parameter)
+        (templateParameterName parameter)
+        annotation
+        (templateParameterKind parameter)
+        (templateParameterIsPack parameter)
+        (templateParameterDefault parameter)
+    where
+        annotation = case templateParameterKind parameter of
+            TemplateValueParameterKind valueType -> syntaxTypeIn context valueType
+            _ -> TypeVariable (templateParameterName parameter)
+
+-- | The problems of the template parameters of a declaration.
+validateTemplateParameters :: TemplateContext -> [TemplateParameter ResolvedName ()] -> [Diagnostic]
+validateTemplateParameters context = concatMap validate
+    where
+        validate parameter =
+            kindProblems parameter
+                ++ defaultProblems parameter
+                ++ packDefaultProblems parameter
+        kindProblems parameter = case templateParameterKind parameter of
+            TemplateTypeParameter -> []
+            TemplateValueParameterKind valueType -> typeSyntaxProblemsIn context valueType
+            TemplateTemplateParameter shapes -> concatMap shapeProblems shapes
+        shapeProblems shape = case templateParameterShapeKind shape of
+            TemplateTypeParameterShape -> []
+            TemplateValueParameterShape valueType -> typeSyntaxProblemsIn context valueType
+            TemplateTemplateParameterShape shapes -> concatMap shapeProblems shapes
+        defaultProblems parameter = case templateParameterDefault parameter of
+            Nothing -> []
+            Just (TemplateTypeDefault valueType) -> typeSyntaxProblemsIn context valueType
+            Just (TemplateValueDefault value) -> templateValueProblemsIn context "TAT0020" value
+        packDefaultProblems parameter
+            | templateParameterIsPack parameter
+            , Just _ <- templateParameterDefault parameter =
+                [problem (templateParameterSpan parameter) "TAT0019" "a template parameter pack cannot have a default"]
+            | otherwise = []
+
+-- | The problems of a type written in source.
+typeSyntaxProblemsIn :: TemplateContext -> TypeSyntax -> [Diagnostic]
+typeSyntaxProblemsIn context syntax = case syntax of
+    ExplicitType _ -> []
+    AutoType -> []
+    BuiltinArrayTypeSyntax element -> typeSyntaxProblemsIn context element
+    ArrayTypeSyntax element -> typeSyntaxProblemsIn context element
+    DictionaryTypeSyntax key value -> typeSyntaxProblemsIn context key ++ typeSyntaxProblemsIn context value
+    CallableTypeSyntax parameters result -> concatMap (typeSyntaxProblemsIn context) parameters ++ typeSyntaxProblemsIn context result
+    QualifiedTypeSyntax _ arguments -> concatMap (templateArgumentProblemsIn context) arguments
+    FixedArrayTypeSyntax element size ->
+        typeSyntaxProblemsIn context element
+            ++ case syntaxTemplateValueIn context size of
+                TemplateValueParameter _ -> []
+                _ -> case evaluateFixedArraySize size of
+                    Left issue -> [problem (templateValueSyntaxSpan size) "TAT0016" (renderTemplateValueError issue)]
+                    Right _ -> []
+
+-- | The problems of a template argument written in source.
+templateArgumentProblemsIn :: TemplateContext -> TemplateArgumentSyntax -> [Diagnostic]
+templateArgumentProblemsIn context argument = case argument of
+    TemplateTypeSyntax valueType -> typeSyntaxProblemsIn context valueType
+    TemplateValueArgumentSyntax value -> templateValueProblemsIn context "TAT0017" value
+
+-- | The problems of a template value written in source, reported with the given code.
+templateValueProblemsIn :: TemplateContext -> String -> TemplateValueSyntax -> [Diagnostic]
+templateValueProblemsIn context code value = case syntaxTemplateValueIn context value of
+    TemplateValueParameter _ -> []
+    _ -> case evaluateTemplateValue value of
+        Left issue -> [problem (templateValueSyntaxSpan value) code (renderTemplateValueError issue)]
+        Right _ -> []
+ src/Progmasoft/Tala/TypeChecker/Enums.hs view
@@ -0,0 +1,204 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | Classic enums: their members, their values and their rules.
+
+A classic enum is a value type whose members are named integers of one
+underlying type. Members are numbered from zero, a member without a written
+value takes the value after that of the member before it, and two members
+may have the same value. A written value is a constant integer expression:
+integer literals and earlier members of the same enum, joined by the
+arithmetic, shift and bitwise operators. Inside its own declaration the name
+of a member stands for its number; that is where the numbers behind the names
+are defined, and it is the only place where a member is a number. An enum is
+a type of its own: its values are
+compared with @==@ and @\\=@ and with nothing else, and there is no
+conversion between an enum and an integer in either direction.
+
+The checker reads the enums of a source set into a table before it checks
+any body, so that a use may precede the declaration.
+-}
+module Progmasoft.Tala.TypeChecker.Enums
+    ( EnumInfo (..)
+    , enumInfos
+    , enumBySymbol
+    , enumBySpelling
+    , enumMemberValue
+    , enumDeclarationProblems
+    , isEnumType
+    ) where
+
+import Data.List (nub, sort)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.ConstantEvaluation
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.TypeChecker.Literals (problem)
+
+-- | What the checker knows of one enum.
+data EnumInfo = EnumInfo
+    { enumInfoName :: ResolvedName
+    -- ^ The declared name and its symbol.
+    , enumInfoType :: Type
+    -- ^ The type of its values, as 'enumType' encodes it.
+    , enumInfoMembers :: [(Identifier, Integer)]
+    -- ^ Its members in source order, each with its value.
+    }
+
+-- | The enums among the declarations of a source set.
+enumInfos :: [Declaration ResolvedName ()] -> [EnumInfo]
+enumInfos declarations =
+    [ EnumInfo name (enumType (resolvedSpelling name) valueType (sort (nub (map snd members)))) members
+    | EnumDeclaration _ name _ underlying cases <- declarations
+    , let valueType = underlyingType underlying
+    , let members = [(enumCaseName member, value) | (member, value, _) <- numbered valueType cases]
+    ]
+
+{- | Each member with its value: the written one, or one more than the last,
+and what is wrong with the written one when it has no value. A member whose
+written value has none is numbered as if nothing had been written, so that
+one mistake is reported once and the members after it keep their numbers.
+-}
+numbered :: Type -> [EnumCase] -> [(EnumCase, Integer, Maybe String)]
+numbered valueType = go 0 Map.empty
+    where
+        go _ _ [] = []
+        go next earlier (member : remaining) =
+            let (value, issue) = case enumCaseValue member of
+                    Nothing -> (next, Nothing)
+                    Just written -> case memberConstant valueType earlier written of
+                        Right computed -> (computed, Nothing)
+                        Left reason -> (next, Just reason)
+             in (member, value, issue) : go (value + 1) (Map.insert (enumCaseName member) value earlier) remaining
+
+{- | The value of a constant integer expression written for a member, given
+the values of the members before it.
+
+The expression is given the underlying type of the enum and the earlier
+members their numbers, and the constant evaluator of the language computes
+it: the same arithmetic, the same rounding of @//@, the same complement of
+@!@ for the width and signedness of the type, and the same refusal of a
+division by zero as anywhere else in a program.
+-}
+memberConstant :: Type -> Map.Map Identifier Integer -> Expression Identifier () -> Either String Integer
+memberConstant valueType earlier written = do
+    typed <- typedConstant written
+    case evaluateConstantInteger typed of
+        Right (Just value) -> Right value
+        Right Nothing -> Left notConstant
+        Left issue -> Left (renderConstantIntegerError issue)
+    where
+        typedConstant :: Expression Identifier () -> Either String (Expression Identifier Type)
+        typedConstant expression = case expression of
+            LiteralExpression spanValue (IntegerLiteral value) _ ->
+                Right (LiteralExpression spanValue (IntegerLiteral value) valueType)
+            NameExpression spanValue name _ -> case Map.lookup name earlier of
+                Just value -> Right (LiteralExpression spanValue (IntegerLiteral value) valueType)
+                Nothing -> Left (identifierText name ++ " is not an earlier member of this enum")
+            UnaryExpression spanValue operator value _
+                | operator `elem` [UnaryPlus, UnaryNegate, BitwiseNot] ->
+                    (\operand -> UnaryExpression spanValue operator operand valueType) <$> typedConstant value
+            BinaryExpression spanValue operator left right _
+                | operator `elem` constantOperators ->
+                    (\first second -> BinaryExpression spanValue operator first second valueType)
+                        <$> typedConstant left
+                        <*> typedConstant right
+            _ -> Left notConstant
+        constantOperators =
+            [ Add
+            , Subtract
+            , Multiply
+            , Divide
+            , FloorDivide
+            , Remainder
+            , Power
+            , ShiftLeft
+            , ShiftRight
+            , BitwiseAnd
+            , BitwiseXor
+            , BitwiseOr
+            ]
+        notConstant =
+            "the value of an enum member is a constant integer expression: integer literals and earlier members of "
+                ++ "the enum, joined by arithmetic, shift and bitwise operators"
+
+{- | The underlying type an enum declares, @int@ when it declares none. A
+type that is not an integer type is reported by 'enumDeclarationProblems';
+the enum is then checked as if it had declared none.
+-}
+underlyingType :: Maybe TypeSyntax -> Type
+underlyingType syntax = maybe intType scalarTypeToType (syntax >>= integerScalar)
+
+integerScalar :: TypeSyntax -> Maybe ScalarType
+integerScalar syntax = case syntax of
+    ExplicitType (Identifier spelling) ->
+        case [scalar | scalar <- scalarTypes, scalarTypeName scalar == spelling, isInteger scalar] of
+            scalar : _ -> Just scalar
+            [] -> Nothing
+    _ -> Nothing
+    where
+        isInteger scalar = scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily]
+
+-- | The enum a name in an expression refers to.
+enumBySymbol :: [EnumInfo] -> SymbolId -> Maybe EnumInfo
+enumBySymbol enums symbol = case [info | info <- enums, resolvedSymbol (enumInfoName info) == symbol] of
+    info : _ -> Just info
+    [] -> Nothing
+
+-- | The enum a name in a type refers to.
+enumBySpelling :: [EnumInfo] -> Identifier -> Maybe EnumInfo
+enumBySpelling enums spelling = case [info | info <- enums, resolvedSpelling (enumInfoName info) == spelling] of
+    info : _ -> Just info
+    [] -> Nothing
+
+-- | The value of a member of the enum with the given type.
+enumMemberValue :: [EnumInfo] -> Type -> Identifier -> Maybe Integer
+enumMemberValue enums valueType member =
+    case [value | info <- enums, enumInfoType info == valueType, (name, value) <- enumInfoMembers info, name == member] of
+        value : _ -> Just value
+        [] -> Nothing
+
+-- | Whether a type is the type of an enum.
+isEnumType :: Type -> Bool
+isEnumType valueType = case enumUnderlyingType valueType of
+    Just _ -> True
+    Nothing -> False
+
+{- | The problems of an enum declaration by itself: an underlying type that
+is not an integer type, a member named twice, a written value that is not a
+constant integer expression or has no value, and a value its underlying type
+cannot hold.
+-}
+enumDeclarationProblems :: Declaration ResolvedName () -> [Diagnostic]
+enumDeclarationProblems declaration = case declaration of
+    EnumDeclaration spanValue _ _ underlying cases ->
+        let scalar = maybe (Just defaultScalar) integerScalar underlying
+            members = numbered (underlyingType underlying) cases
+         in [ problem spanValue "TAT0066" "the underlying type of an enum must be an integer type"
+            | Nothing <- [scalar]
+            ]
+                ++ [ problem
+                        (enumCaseSpan member)
+                        "TAT0067"
+                        ("the enum already has a member named " ++ identifierText (enumCaseName member))
+                   | (index, member) <- zip [0 :: Int ..] cases
+                   , enumCaseName member `elem` map enumCaseName (take index cases)
+                   ]
+                ++ [ problem (enumCaseSpan member) "TAT0070" reason
+                   | (member, _, Just reason) <- members
+                   ]
+                ++ [ problem
+                        (enumCaseSpan member)
+                        "TAT0068"
+                        ( "the value "
+                            ++ show value
+                            ++ " of this enum member does not fit "
+                            ++ scalarTypeName (maybe defaultScalar id scalar)
+                        )
+                   | (member, value, Nothing) <- members
+                   , not (integerFits (maybe defaultScalar id scalar) value)
+                   ]
+    _ -> []
+    where
+        defaultScalar = defaultIntegerScalar
+ src/Progmasoft/Tala/TypeChecker/Format.hs view
@@ -0,0 +1,275 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | The output format grammar of @Console.Printf@ and @Console.Format@.
+
+A format is a compile-time string. It is read here, once, into literal text
+and conversions, and everything about it that does not depend on the types
+of the arguments is decided here: which conversions exist, which flags each
+one takes, which flags exclude each other, and where a width or a precision
+may stand. A format that is wrong is an error of the program, found when it
+is compiled; nothing about a format is left to be discovered while the
+program runs.
+
+A conversion is written
+
+> % flags width .precision letter
+
+where the flags are any of @-@, @0@, @+@, a space, @#@ and @'@; the width is
+a decimal number or @*@; and the precision is a decimal number or @*@ after a
+point. A @*@ takes the width or the precision from an @int@ argument that
+stands before the value.
+
+The conversions:
+
+[@%d@] a signed integer in decimal
+[@%u@] an unsigned integer in decimal
+[@%x@] an integer in hexadecimal; a negative number is a minus sign and its
+magnitude, never the bit pattern of its representation
+[@%f@] a floating-point number with a fixed number of digits after the point
+[@%s@] a string
+[@%c@] a character
+[@%b@] a Boolean, as @true@ or @false@
+[@%n@] the line terminator of the platform; takes no argument
+[@%%@] a percent sign; takes no argument
+
+@%A@ and @%O@, the debug and display forms of an object, are part of the
+grammar and need the object model; they are recognized and reported as not
+implemented.
+-}
+module Progmasoft.Tala.TypeChecker.Format
+    ( FormatPiece (..)
+    , Conversion (..)
+    , ConversionKind (..)
+    , Flag (..)
+    , Size (..)
+    , FormatProblem (..)
+    , parseFormat
+    , conversionLetter
+    , conversionFlagBits
+    , conversionArgumentCount
+    ) where
+
+import Data.Char (isDigit)
+import Data.List (nub)
+import Progmasoft.Tala.RuntimeCall
+
+-- | One part of a format, in the order it is written.
+data FormatPiece
+    = -- | Text that is written as it stands.
+      LiteralPiece String
+    | -- | A conversion of one argument.
+      ConversionPiece Conversion
+    | -- | @%n@.
+      NewlinePiece
+    deriving (Eq, Ord, Read, Show)
+
+-- | What a conversion writes.
+data ConversionKind
+    = SignedDecimal
+    | UnsignedDecimal
+    | Hexadecimal
+    | FixedPoint
+    | Text
+    | Character
+    | Truth
+    deriving (Eq, Ord, Read, Show)
+
+-- | A flag written between @%@ and the width.
+data Flag
+    = -- | @-@: the value stands at the left of its field.
+      LeftFlag
+    | -- | @0@: a number is padded with zeros after its sign.
+      ZeroFlag
+    | -- | @+@: a number that is not negative is written with a plus sign.
+      PlusFlag
+    | -- | space: a number that is not negative is written with a leading space.
+      SpaceFlag
+    | -- | @#@: a hexadecimal number is written with the prefix @0x@.
+      AlternateFlag
+    | -- | @'@: integer digits are grouped in threes with apostrophes.
+      GroupFlag
+    deriving (Eq, Ord, Read, Show)
+
+-- | A width or a precision.
+data Size
+    = -- | Not written.
+      NoSize
+    | -- | Written as a number in the format.
+      FixedSize Integer
+    | -- | Written as @*@: taken from an @int@ argument.
+      ArgumentSize
+    deriving (Eq, Ord, Read, Show)
+
+-- | One conversion with everything written in it.
+data Conversion = Conversion
+    { conversionKind :: ConversionKind
+    , conversionFlags :: [Flag]
+    , conversionWidth :: Size
+    , conversionPrecision :: Size
+    , conversionOffset :: Int
+    -- ^ The position of its @%@ in the format, counted from one.
+    }
+    deriving (Eq, Ord, Read, Show)
+
+{- | What is wrong with a format: the diagnostic code, the position of the
+conversion in the format counted from one, and what to say.
+-}
+data FormatProblem = FormatProblem
+    { formatProblemCode :: String
+    , formatProblemOffset :: Int
+    , formatProblemMessage :: String
+    }
+    deriving (Eq, Ord, Read, Show)
+
+-- | The letter a conversion is written with.
+conversionLetter :: ConversionKind -> Char
+conversionLetter kind = case kind of
+    SignedDecimal -> 'd'
+    UnsignedDecimal -> 'u'
+    Hexadecimal -> 'x'
+    FixedPoint -> 'f'
+    Text -> 's'
+    Character -> 'c'
+    Truth -> 'b'
+
+{- | The flags of a conversion as the runtime takes them: the sum of the
+@TALA_TEXT_FLAG_*@ values of the flags written, and the hexadecimal flag for
+@%x@, which the runtime takes as a flag of the integer conversions.
+-}
+conversionFlagBits :: Conversion -> Integer
+conversionFlagBits conversion =
+    sum (map bit (conversionFlags conversion)) + (if conversionKind conversion == Hexadecimal then flagHexadecimal else 0)
+    where
+        bit flag = case flag of
+            LeftFlag -> flagLeft
+            ZeroFlag -> flagZero
+            PlusFlag -> flagPlus
+            SpaceFlag -> flagSpace
+            AlternateFlag -> flagAlternate
+            GroupFlag -> flagGroup
+
+{- | How many arguments a conversion takes: its value, and one more for each
+of a width and a precision written as @*@.
+-}
+conversionArgumentCount :: Conversion -> Int
+conversionArgumentCount conversion =
+    1 + length (filter (== ArgumentSize) [conversionWidth conversion, conversionPrecision conversion])
+
+{- | Read a format. The result is its pieces in order, with neighbouring
+literal text joined, or the first thing wrong with it.
+-}
+parseFormat :: String -> Either FormatProblem [FormatPiece]
+parseFormat = fmap joined . pieces 1
+    where
+        joined (LiteralPiece first : LiteralPiece second : rest) = joined (LiteralPiece (first ++ second) : rest)
+        joined (piece : rest) = piece : joined rest
+        joined [] = []
+
+pieces :: Int -> String -> Either FormatProblem [FormatPiece]
+pieces _ [] = Right []
+pieces offset ('%' : rest) = do
+    (piece, used) <- readConversion offset rest
+    (piece :) <$> pieces (offset + 1 + used) (drop used rest)
+pieces offset text =
+    let (literal, rest) = break (== '%') text
+     in (LiteralPiece literal :) <$> pieces (offset + length literal) rest
+
+{- | Read what follows a @%@. The result is the piece and the number of
+characters after the @%@ that belong to it.
+-}
+readConversion :: Int -> String -> Either FormatProblem (FormatPiece, Int)
+readConversion offset text = do
+    let (flagText, afterFlags) = span (`elem` "-0+ #'") text
+    flags <- mapM flagOf flagText
+    (width, afterWidth) <- sizeOf afterFlags
+    (precision, afterPrecision) <- case afterWidth of
+        '.' : afterPoint -> do
+            (size, rest) <- sizeOf afterPoint
+            case size of
+                NoSize -> failure "TAT0075" "a precision needs a number or * after its point"
+                _ -> Right (size, rest)
+        _ -> Right (NoSize, afterWidth)
+    let used = length text - length afterPrecision + 1
+        plain = null flags && width == NoSize && precision == NoSize
+    case afterPrecision of
+        [] -> failure "TAT0075" "the format ends inside a conversion"
+        letter : _ -> case letter of
+            '%'
+                | plain -> Right (LiteralPiece "%", used)
+                | otherwise -> failure "TAT0076" "%% takes no flag, width or precision"
+            'n'
+                | plain -> Right (NewlinePiece, used)
+                | otherwise -> failure "TAT0076" "%n takes no flag, width or precision"
+            _ -> case lookup letter kinds of
+                Just kind -> do
+                    checked <- validated (Conversion kind flags width precision offset)
+                    Right (ConversionPiece checked, used)
+                Nothing
+                    | letter `elem` "AO" ->
+                        failure "TAT0079" ("the conversion %" ++ [letter] ++ " needs the object model and is not implemented yet")
+                    | otherwise -> failure "TAT0075" ("%" ++ [letter] ++ " is not a conversion")
+    where
+        failure :: String -> String -> Either FormatProblem a
+        failure code message = Left (FormatProblem code offset message)
+        kinds =
+            [ ('d', SignedDecimal)
+            , ('u', UnsignedDecimal)
+            , ('x', Hexadecimal)
+            , ('f', FixedPoint)
+            , ('s', Text)
+            , ('c', Character)
+            , ('b', Truth)
+            ]
+        flagOf character = case character of
+            '-' -> Right LeftFlag
+            '0' -> Right ZeroFlag
+            '+' -> Right PlusFlag
+            ' ' -> Right SpaceFlag
+            '#' -> Right AlternateFlag
+            _ -> Right GroupFlag
+        -- A number, a star, or nothing. A number is written without a sign.
+        sizeOf value = case value of
+            '*' : rest -> Right (ArgumentSize, rest)
+            _ ->
+                let (digits, rest) = span isDigit value
+                 in if null digits
+                        then Right (NoSize, rest)
+                        else
+                            if length digits > 9
+                                then failure "TAT0075" "a width or a precision written in a format has at most nine digits"
+                                else Right (FixedSize (read digits), rest)
+        validated value
+            | flags' /= nub flags' = flagFailure "repeats a flag"
+            | LeftFlag `elem` flags' && ZeroFlag `elem` flags' = flagFailure "combines - and 0, which exclude each other"
+            | PlusFlag `elem` flags' && SpaceFlag `elem` flags' = flagFailure "combines + and a space, which exclude each other"
+            | unwanted : _ <- filter (`notElem` allowedFlags kind) flags' =
+                flagFailure ("takes no " ++ flagName unwanted ++ " flag")
+            | conversionPrecision value /= NoSize && kind `notElem` [FixedPoint, Text] =
+                flagFailure "takes no precision"
+            | otherwise = Right value
+            where
+                flags' = conversionFlags value
+                kind = conversionKind value
+                flagFailure :: String -> Either FormatProblem a
+                flagFailure text' = failure "TAT0076" ("the conversion %" ++ [conversionLetter kind] ++ " " ++ text')
+
+-- | The flags each conversion has a meaning for.
+allowedFlags :: ConversionKind -> [Flag]
+allowedFlags kind = case kind of
+    SignedDecimal -> [LeftFlag, ZeroFlag, PlusFlag, SpaceFlag, GroupFlag]
+    UnsignedDecimal -> [LeftFlag, ZeroFlag, GroupFlag]
+    Hexadecimal -> [LeftFlag, ZeroFlag, AlternateFlag]
+    FixedPoint -> [LeftFlag, ZeroFlag, PlusFlag, SpaceFlag, GroupFlag]
+    Text -> [LeftFlag]
+    Character -> [LeftFlag]
+    Truth -> [LeftFlag]
+
+flagName :: Flag -> String
+flagName flag = case flag of
+    LeftFlag -> "-"
+    ZeroFlag -> "0"
+    PlusFlag -> "+"
+    SpaceFlag -> "space"
+    AlternateFlag -> "#"
+    GroupFlag -> "'"
+ src/Progmasoft/Tala/TypeChecker/Literals.hs view
@@ -0,0 +1,78 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | The types of literals and the range of constant expressions.
+
+A literal takes its type from the place that receives it, and a constant
+expression must fit the type it is stored in. Both are rules of
+"Progmasoft.Tala.NumericSemantics" applied at a source position; the checker
+of expressions calls them and adds what they report.
+-}
+module Progmasoft.Tala.TypeChecker.Literals
+    ( literalTypeInContext
+    , integerLiteralType
+    , floatingLiteralType
+    , ruleProblems
+    , constantRangeProblems
+    , problem
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes
+import Progmasoft.Tala.ConstantEvaluation
+import Progmasoft.Tala.Diagnostic
+import Progmasoft.Tala.NumericSemantics
+
+-- | The type of a literal at a place that expects the given type, if any.
+literalTypeInContext :: SourceSpan -> Maybe Type -> Literal -> (Type, [Diagnostic])
+literalTypeInContext spanValue expected literal = case literal of
+    IntegerLiteral value -> integerLiteralType spanValue expected value
+    FloatingLiteral _ -> floatingLiteralType expected
+    CharacterLiteral _ -> (scalarTypeToType CharacterScalar, [])
+    BooleanLiteral _ -> (boolType, [])
+    StringLiteral _ -> (stringType, [])
+    UnitLiteral -> (unitType, [])
+
+-- | The type of an integer literal, and the problems of a value its type cannot hold.
+integerLiteralType :: SourceSpan -> Maybe Type -> Integer -> (Type, [Diagnostic])
+integerLiteralType spanValue expected value =
+    let context = maybe NoNumericContext targetContext expected
+        rule = integerLiteralRule context value
+        code = case numericRuleError rule of Just (UntargetedIntegerOutsideInt _) -> "TAT0017"; _ -> "TAT0016"
+     in (numericRuleType rule, ruleProblems spanValue code rule)
+    where
+        targetContext target | target == boolType = BooleanNumericContext
+        targetContext target = TargetNumericType target
+
+-- | The type of a floating literal at a place that expects the given type, if any.
+floatingLiteralType :: Maybe Type -> (Type, [Diagnostic])
+floatingLiteralType expected =
+    let context = maybe NoNumericContext TargetNumericType expected
+        rule = floatingLiteralRule context
+     in (numericRuleType rule, [])
+
+-- | The diagnostic of a numeric rule that failed, with the given code.
+ruleProblems :: SourceSpan -> String -> NumericRuleResult -> [Diagnostic]
+ruleProblems spanValue code rule = case numericRuleError rule of
+    Nothing -> []
+    Just issue -> [problem spanValue code (renderNumericRuleError issue)]
+
+-- | The problems of a constant expression whose value its target type cannot hold.
+constantRangeProblems :: SourceSpan -> Type -> Expression ResolvedName Type -> [Diagnostic]
+constantRangeProblems spanValue target expression = case evaluateConstantInteger expression of
+    Left issue -> [problem spanValue "TAT0019" (renderConstantIntegerError issue)]
+    Right (Just value) -> case typeToScalarType target of
+        Just scalar
+            | scalarTypeFamily scalar `elem` [SignedIntegerFamily, UnsignedIntegerFamily]
+            , not (integerFits scalar value) ->
+                [ problem
+                    spanValue
+                    "TAT0018"
+                    ("constant expression result " ++ show value ++ " does not fit " ++ scalarTypeName scalar)
+                ]
+        _ -> []
+    Right Nothing -> []
+
+-- | An error of the type checker at a source position.
+problem :: SourceSpan -> String -> String -> Diagnostic
+problem spanValue code message = Diagnostic TypeCheckerStage Error code (Just spanValue) message
+ src/Progmasoft/Tala/TypeChecker/Loops.hs view
@@ -0,0 +1,84 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | The loops around the place the type checker is at.
+
+A @break@ or a @continue@ is checked against the innermost loop, and what a
+@break@ may carry depends on how that loop is used. Blocks used as values and
+loop headers are not loops, but a transfer has to cross them to reach one, so
+they are entries of the same stack.
+-}
+module Progmasoft.Tala.TypeChecker.Loops
+    ( LoopKind (..)
+    , LoopContext (..)
+    , outsideLoops
+    , enterLoop
+    , loopCondition
+    , loopUpdate
+    , transferTarget
+    ) where
+
+import Progmasoft.Tala.AST (Type)
+
+-- | How a loop is used, which decides what its @break@ statements carry.
+data LoopKind
+    = -- | A loop statement: @break@ carries no value.
+      StatementLoop
+    | {- | A loop used as an expression: every @break@ carries the loop's
+      value, typed in the context that receives it.
+      -}
+      ExpressionLoop (Maybe Type)
+    | {- | Not a loop: the edge of a block used as a value. A @break@ or
+      @continue@ inside it leaves the block without a value and targets the
+      loop around the expression the block belongs to.
+      -}
+      ValueBlockEdge
+    | {- | The condition of a loop of the given kind. A @break@ there leaves
+      that loop, like one in its body. A @continue@ there abandons the rest
+      of the condition and evaluates the condition of that loop again.
+      -}
+      LoopCondition LoopKind
+    | {- | The update clause of a loop of the given kind. A @continue@ there
+      ends the update, and the condition of the loop is tested next. A
+      @break@ there leaves that loop.
+      -}
+      LoopUpdate LoopKind
+
+{- | The loops around a statement, innermost first, and the kind of the loop
+statement that is about to be checked. A loop expression checks its loop
+statement with the pending kind set; every loop moves the pending kind onto
+the stack for its own body.
+-}
+data LoopContext = LoopContext
+    { pendingLoop :: LoopKind
+    , enclosingLoops :: [LoopKind]
+    }
+
+-- | The context of a statement that no loop encloses.
+outsideLoops :: LoopContext
+outsideLoops = LoopContext StatementLoop []
+
+-- | The context of the body of the loop statement being checked.
+enterLoop :: LoopContext -> LoopContext
+enterLoop loops = LoopContext StatementLoop (pendingLoop loops : enclosingLoops loops)
+
+-- | The context of the condition of the loop statement about to be checked.
+loopCondition :: LoopContext -> LoopContext
+loopCondition loops = LoopContext StatementLoop (LoopCondition (pendingLoop loops) : enclosingLoops loops)
+
+-- | The context of the update clause of the loop statement about to be checked.
+loopUpdate :: LoopContext -> LoopContext
+loopUpdate loops = LoopContext StatementLoop (LoopUpdate (pendingLoop loops) : enclosingLoops loops)
+
+{- | The loop a @break@ or @continue@ targets: the innermost entry that is
+not the edge of a block used as a value. A transfer crosses such an edge
+freely; the block it leaves simply yields no value.
+-}
+transferTarget :: LoopContext -> Maybe LoopKind
+transferTarget loops = case dropWhile isValueBlockEdge (enclosingLoops loops) of
+    kind : _ -> Just kind
+    [] -> Nothing
+    where
+        isValueBlockEdge kind = case kind of
+            ValueBlockEdge -> True
+            _ -> False
+ src/Progmasoft/Tala/TypeChecker/Returns.hs view
@@ -0,0 +1,110 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- | What the typed tree of a body returns and what leaves its loops.
+
+The checker for statements reports the returns it meets, but a @return@ may
+also stand in a block used as a value, which is reached through an
+expression. The types a body returns are therefore read from its typed tree
+here, statements and expressions alike. A nested callable is a function of
+its own: its returns are not the returns of the body that creates it.
+-}
+module Progmasoft.Tala.TypeChecker.Returns
+    ( typedExpressionType
+    , blockReturnTypes
+    , loopBreakTypes
+    ) where
+
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.Completion
+
+-- | The type the checker gave an expression.
+typedExpressionType :: Expression name Type -> Type
+typedExpressionType expression = case expression of
+    NameExpression _ _ valueType -> valueType
+    LiteralExpression _ _ valueType -> valueType
+    MemberAccessExpression _ _ _ valueType -> valueType
+    MethodReferenceExpression _ _ _ valueType -> valueType
+    CallExpression _ _ _ valueType -> valueType
+    UnaryExpression _ _ _ valueType -> valueType
+    BinaryExpression _ _ _ _ valueType -> valueType
+    IsPatternExpression _ _ _ valueType -> valueType
+    ConditionalExpression _ _ _ _ valueType -> valueType
+    CoalesceExpression _ _ _ valueType -> valueType
+    AssignmentExpression _ _ _ _ valueType -> valueType
+    IncrementExpression _ _ _ valueType -> valueType
+    LoopExpression _ _ valueType -> valueType
+    BlockExpression _ _ valueType -> valueType
+    MatchExpression _ _ _ valueType -> valueType
+    CallableExpression _ _ _ _ _ valueType -> valueType
+
+{- | The types of the values every @return@ of a block carries, in source
+order. A @return@ without a value carries @void@. A returned expression that
+never yields a value carries none; the returns inside it are listed in its
+place.
+-}
+blockReturnTypes :: Block name Type -> [Type]
+blockReturnTypes (Block statements) = concatMap statementReturnTypes statements
+
+statementReturnTypes :: Statement name Type -> [Type]
+statementReturnTypes statement = case statement of
+    ReturnStatement _ Nothing -> [voidType]
+    ReturnStatement _ (Just value)
+        | doesNotComplete value -> expressionReturnTypes value
+        | otherwise -> typedExpressionType value : expressionReturnTypes value
+    BindingStatement _ _ _ _ _ value -> expressionReturnTypes value
+    AssignmentStatement _ _ _ value -> expressionReturnTypes value
+    IfStatement _ condition whenTrue whenFalse ->
+        expressionReturnTypes condition ++ blockReturnTypes whenTrue ++ maybe [] blockReturnTypes whenFalse
+    WhileStatement _ condition body -> expressionReturnTypes condition ++ blockReturnTypes body
+    DoWhileStatement _ body condition -> blockReturnTypes body ++ expressionReturnTypes condition
+    ForStatement _ initializer condition updates body ->
+        maybe [] statementReturnTypes initializer
+            ++ maybe [] expressionReturnTypes condition
+            ++ blockReturnTypes body
+            ++ concatMap statementReturnTypes updates
+    ForEachStatement _ _ _ _ _ source body -> expressionReturnTypes source ++ blockReturnTypes body
+    IncrementStatement {} -> []
+    CompoundAssignmentStatement _ _ _ _ value -> expressionReturnTypes value
+    DiscardStatement _ value -> expressionReturnTypes value
+    BreakStatement _ value -> maybe [] expressionReturnTypes value
+    ContinueStatement {} -> []
+    GuardStatement _ condition block -> expressionReturnTypes condition ++ blockReturnTypes block
+    BlockStatement _ block -> blockReturnTypes block
+    ExpressionStatement _ value _ -> expressionReturnTypes value
+
+expressionReturnTypes :: Expression name Type -> [Type]
+expressionReturnTypes expression = case expression of
+    NameExpression {} -> []
+    LiteralExpression {} -> []
+    MemberAccessExpression _ receiver _ _ -> expressionReturnTypes receiver
+    MethodReferenceExpression _ receiver _ _ -> expressionReturnTypes receiver
+    CallExpression _ callee arguments _ -> concatMap expressionReturnTypes (callee : arguments)
+    UnaryExpression _ _ value _ -> expressionReturnTypes value
+    BinaryExpression _ _ left right _ -> expressionReturnTypes left ++ expressionReturnTypes right
+    IsPatternExpression _ subject _ _ -> expressionReturnTypes subject
+    ConditionalExpression _ condition first second _ -> concatMap expressionReturnTypes [condition, first, second]
+    CoalesceExpression _ left fallback _ -> expressionReturnTypes left ++ expressionReturnTypes fallback
+    AssignmentExpression _ _ _ value _ -> expressionReturnTypes value
+    IncrementExpression {} -> []
+    LoopExpression _ loop _ -> statementReturnTypes loop
+    BlockExpression _ block _ -> blockReturnTypes block
+    MatchExpression _ subjects arms _ ->
+        concatMap expressionReturnTypes subjects ++ concatMap armReturnTypes arms
+    -- A callable returns from itself.
+    CallableExpression {} -> []
+    where
+        armReturnTypes arm = maybe [] expressionReturnTypes (matchArmGuard arm) ++ expressionReturnTypes (matchArmBody arm)
+
+{- | The types of the values carried by the breaks that leave this loop
+itself, from its body, its condition and its update clause, also out of
+blocks used as values. Breaks of nested loops leave those loops.
+-}
+loopBreakTypes :: Statement name Type -> [Type]
+loopBreakTypes loop = [typedExpressionType value | BreakStatement _ (Just value) <- transfers]
+    where
+        transfers = case loop of
+            WhileStatement _ condition body -> expressionTransfers condition ++ blockTransfers body
+            ForStatement _ _ condition updates body ->
+                maybe [] expressionTransfers condition ++ blockTransfers body ++ concatMap statementTransfers updates
+            _ -> []
+ src/Progmasoft/Tala/TypeClassification.hs view
@@ -0,0 +1,161 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
+
+{- |
+Recursive value/reference classification for resolved Tala types.
+
+The declaration catalog is explicit on purpose. A spelling alone cannot tell
+the frontend whether a user-defined type is a value, reference, or CoW family,
+and silently treating every unknown name as a reference would make ownership
+lowering unsound. The resolver will populate this catalog when all renewed
+declaration families are represented in the typed AST.
+-}
+module Progmasoft.Tala.TypeClassification
+    ( StorageClass (..)
+    , NominalKind (..)
+    , NominalCatalog
+    , CatalogError (..)
+    , emptyNominalCatalog
+    , registerNominal
+    , lookupNominal
+    , nominalCatalogSize
+    , classifyNominal
+    , classifyType
+    , usesAarc
+    , usesCopyOnWrite
+    ) where
+
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Progmasoft.Tala.AST
+import Progmasoft.Tala.BuiltinTypes (typeToScalarType)
+
+-- | Storage behavior needed by Core ownership lowering.
+data StorageClass
+    = -- | No managed reference representation is required.
+      TrivialValueStorage
+    | -- | Value semantics use shared copy-on-write storage.
+      CopyOnWriteValueStorage
+    | -- | Reference semantics use AARC-managed ownership.
+      AarcReferenceStorage
+    | -- | Nominal metadata is absent or contains an open type.
+      UnresolvedStorage
+    deriving (Eq, Ord, Read, Show)
+
+-- | Semantic declaration families, deliberately independent of parser tokens.
+data NominalKind
+    = -- | Value-like data declaration.
+      DataNominal
+    | -- | Named value type declaration.
+      LiteralTypeNominal
+    | -- | Value-like classic enumeration.
+      ClassicEnumNominal
+    | -- | Reference type declaration.
+      ClassNominal
+    | -- | Value-like class declaration.
+      DataClassNominal
+    | -- | Reference-like enumeration declaration.
+      EnumClassNominal
+    | -- | Root reference object declaration.
+      ObjectNominal
+    | -- | Reference contract declaration.
+      InterfaceNominal
+    deriving (Bounded, Enum, Eq, Ord, Read, Show)
+
+-- | Explicit case-sensitive mapping from qualified names to declaration kinds.
+newtype NominalCatalog = NominalCatalog (Map QualifiedName NominalKind)
+    deriving (Eq, Read, Show)
+
+-- | Rejected nominal type registration.
+data CatalogError
+    = -- | Qualified name has no components.
+      EmptyNominalName
+    | -- | One name component is empty.
+      EmptyNominalNameComponent QualifiedName
+    | -- | Name already exists in the catalog.
+      DuplicateNominalName QualifiedName
+    deriving (Eq, Ord, Read, Show)
+
+-- | Create an empty catalog with no inferred or implicit declarations.
+emptyNominalCatalog :: NominalCatalog
+emptyNominalCatalog = NominalCatalog Map.empty
+
+-- | Insert exactly one case-sensitive, fully qualified declaration name.
+registerNominal :: QualifiedName -> NominalKind -> NominalCatalog -> Either CatalogError NominalCatalog
+registerNominal name kind (NominalCatalog declarations)
+    | null parts = Left EmptyNominalName
+    | any (null . identifierText) parts = Left (EmptyNominalNameComponent name)
+    | Map.member name declarations = Left (DuplicateNominalName name)
+    | otherwise = Right (NominalCatalog (Map.insert name kind declarations))
+    where
+        parts = qualifiedNameParts name
+
+-- | Find an exact, case-sensitive qualified declaration name.
+lookupNominal :: QualifiedName -> NominalCatalog -> Maybe NominalKind
+lookupNominal name (NominalCatalog declarations) = Map.lookup name declarations
+
+-- | Return the number of registered nominal declarations.
+nominalCatalogSize :: NominalCatalog -> Int
+nominalCatalogSize (NominalCatalog declarations) = Map.size declarations
+
+-- | Map a declaration family to its baseline ownership/storage category.
+classifyNominal :: NominalKind -> StorageClass
+classifyNominal kind = case kind of
+    DataNominal -> CopyOnWriteValueStorage
+    LiteralTypeNominal -> CopyOnWriteValueStorage
+    ClassicEnumNominal -> CopyOnWriteValueStorage
+    ClassNominal -> AarcReferenceStorage
+    DataClassNominal -> AarcReferenceStorage
+    EnumClassNominal -> AarcReferenceStorage
+    ObjectNominal -> AarcReferenceStorage
+    InterfaceNominal -> AarcReferenceStorage
+
+-- | Classify the complete constructed type, including nested type arguments.
+classifyType :: NominalCatalog -> Type -> StorageClass
+classifyType catalog valueType = case intrinsicStorage valueType of
+    Just storage -> storage
+    Nothing -> case valueType of
+        NamedType name arguments -> case lookupNominal name catalog of
+            Nothing -> UnresolvedStorage
+            Just kind -> refineConstructedValue catalog (classifyNominal kind) arguments
+        TypeVariable _ -> UnresolvedStorage
+        ErrorType -> UnresolvedStorage
+        FunctionType _ _ -> AarcReferenceStorage
+
+-- | Test whether a resolved constructed type requires AARC management.
+usesAarc :: NominalCatalog -> Type -> Bool
+usesAarc catalog valueType = classifyType catalog valueType == AarcReferenceStorage
+
+-- | Test whether a resolved constructed type uses copy-on-write storage.
+usesCopyOnWrite :: NominalCatalog -> Type -> Bool
+usesCopyOnWrite catalog valueType = classifyType catalog valueType == CopyOnWriteValueStorage
+
+intrinsicStorage :: Type -> Maybe StorageClass
+intrinsicStorage valueType
+    | typeToScalarType valueType /= Nothing = Just TrivialValueStorage
+    | valueType == voidType = Just TrivialValueStorage
+    | valueType == unitType = Just TrivialValueStorage
+    | valueType == stringType = Just AarcReferenceStorage
+intrinsicStorage (FunctionType _ _) = Just AarcReferenceStorage
+intrinsicStorage _ = Nothing
+
+refineConstructedValue :: NominalCatalog -> StorageClass -> [TemplateArgument] -> StorageClass
+refineConstructedValue _ AarcReferenceStorage _ = AarcReferenceStorage
+refineConstructedValue _ UnresolvedStorage _ = UnresolvedStorage
+refineConstructedValue _ TrivialValueStorage _ = TrivialValueStorage
+refineConstructedValue catalog CopyOnWriteValueStorage arguments =
+    finish (foldl' inspectArgument (False, False) arguments)
+    where
+        -- Value arguments affect specialization identity, but never ownership.
+        inspectArgument state (ValueTemplateArgument _) = state
+        inspectArgument (hasReference, hasUnresolved) (TypeTemplateArgument argumentType) =
+            case classifyType catalog argumentType of
+                AarcReferenceStorage -> (True, hasUnresolved)
+                UnresolvedStorage -> (hasReference, True)
+                _ -> (hasReference, hasUnresolved)
+
+        -- A known reference dominates unknown siblings. This keeps A<T, String>
+        -- and A<String, T> equivalent even before T has been substituted.
+        finish (True, _) = AarcReferenceStorage
+        finish (False, True) = UnresolvedStorage
+        finish (False, False) = CopyOnWriteValueStorage
+ tala-frontend.cabal view
@@ -0,0 +1,74 @@+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-frontend
+version: 0.1.0
+synopsis: Tala resolution and type-checking frontend
+description:
+    Name resolution, renaming and type checking of Tala programs, from the
+    parsed syntax tree to the typed one.
+    .
+    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.Closure.Analysis
+        Progmasoft.Tala.ConstantEvaluation
+        Progmasoft.Tala.BuiltinTypes
+        Progmasoft.Tala.Frontend
+        Progmasoft.Tala.IntegerEvaluation
+        Progmasoft.Tala.NumericSemantics
+        Progmasoft.Tala.Resolver.NameResolution
+        Progmasoft.Tala.Resolver.Renamer
+        Progmasoft.Tala.SourceSet
+        Progmasoft.Tala.TemplateValue
+        Progmasoft.Tala.Template.Application
+        Progmasoft.Tala.Template.Discovery
+        Progmasoft.Tala.Template.Freshen
+        Progmasoft.Tala.Template.Instantiation
+        Progmasoft.Tala.Template.Mangling
+        Progmasoft.Tala.Template.MemberReachability
+        Progmasoft.Tala.Template.Specialization
+        Progmasoft.Tala.Template.Specialization.Verifier
+        Progmasoft.Tala.Template.Verifier
+        Progmasoft.Tala.TypeClassification
+        Progmasoft.Tala.TypeChecker
+        Progmasoft.Tala.TypeChecker.Branching
+        Progmasoft.Tala.TypeChecker.Console
+        Progmasoft.Tala.TypeChecker.Context
+        Progmasoft.Tala.TypeChecker.Enums
+        Progmasoft.Tala.TypeChecker.Format
+        Progmasoft.Tala.TypeChecker.Literals
+        Progmasoft.Tala.TypeChecker.Loops
+        Progmasoft.Tala.TypeChecker.Returns
+    hs-source-dirs: src
+    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,
+        text >=2.1 && <2.3,
+        tala-syntax >=0.1.0 && <0.2
+    default-language: GHC2024
+
+source-repository head
+    type: git
+    location: https://github.com/Progmasoft/tala
+    subdir: Talac/Haskell/Frontend