tala-syntax 0.1.0 → 0.2.0
raw patch · 20 files changed
+838/−65 lines, 20 files
Files
- LICENSE-EXCEPTION +78/−41
- src/Progmasoft/Tala/AST.hs +14/−1
- src/Progmasoft/Tala/CharacterLiteral.hs +1/−1
- src/Progmasoft/Tala/Completion.hs +1/−1
- src/Progmasoft/Tala/Diagnostic.hs +1/−1
- src/Progmasoft/Tala/Diagnostic/Protocol.hs +1/−1
- src/Progmasoft/Tala/Directives.hs +486/−0
- src/Progmasoft/Tala/FloatingLiteral.hs +1/−1
- src/Progmasoft/Tala/Lexer.hs +7/−1
- src/Progmasoft/Tala/NestingLimits.hs +1/−1
- src/Progmasoft/Tala/NumericLiteral.hs +1/−1
- src/Progmasoft/Tala/Parser.hs +20/−2
- src/Progmasoft/Tala/Parser/Cursor.hs +9/−1
- src/Progmasoft/Tala/Parser/Match.hs +1/−1
- src/Progmasoft/Tala/Parser/Testing.hs +167/−0
- src/Progmasoft/Tala/Parser/Token.hs +6/−1
- src/Progmasoft/Tala/RuntimeCall.hs +36/−5
- src/Progmasoft/Tala/SourceText.hs +1/−1
- src/Progmasoft/Tala/SourceWarnings.hs +1/−1
- tala-syntax.cabal +5/−3
LICENSE-EXCEPTION view
@@ -1,81 +1,118 @@-Progmasoft Linking Exception, Version 1.1 +Progmasoft Linking Exception, Version 2.0 -This Exception is an additional permission to the Mozilla Public License, version 2.0 ("MPL-2.0"). It is intended to preserve MPL-2.0's file-level obligations for Covered Software while removing doubt about combining that software with independent code. Capitalized terms not defined here have the meanings given in MPL-2.0. +This Exception is an additional permission granted with the Mozilla Public License, version 2.0 ("MPL-2.0"). It keeps MPL-2.0's file-level copyleft for Covered Software and removes doubt about four things: combining Covered Software with other work, what compilers do to code, what programs built with Covered Software tools owe, and the interfaces other work needs in order to use Covered Software. It does not change the text of MPL-2.0. Capitalized terms not defined here have the meanings given in MPL-2.0, and "You" has the meaning given there. 1. Definitions -"Independent Component" means software contained in one or more Source Code Form files that neither contain Covered Software nor are Modifications of Covered Software. A component does not cease to be independent merely because compilation, static or dynamic linking, loading, packaging, or deployment places its Executable Form in the same file, process, address space, archive, image, or distribution as Covered Software. +"Independent Work" means software or other material, in any form, that is neither Covered Software nor a Modification. Sections 3 and 5 say what does not change that. -"Combined Work" means a Larger Work that combines Covered Software with one or more Independent Components by any technical means, including static or dynamic linking, loading, embedding, plugins, extensions, language bindings, an API, ABI, FFI boundary, command-line interface, protocol, inter-process communication mechanism, or file format. +"Tool" means Covered Software that You run to produce material: a compiler, linker, code generator, build system, package manager, formatter, or similar program. -"Output" means material produced by executing Covered Software, including generated source, object code, executable code, intermediate representations, documentation, reports, diagnostics, formatted text, and analysis results. +"Input" means the material You give a Tool to process, other than the Tool's own files. -2. Additional Permission for Combined Works +"Output" means the material a Tool writes when it runs, including source code, intermediate representations, object code, executable code, packages, documentation, reports, diagnostics, formatted text, and analysis results. -Subject to compliance with MPL-2.0 for the Covered Software, each recipient has a worldwide, royalty-free, non-exclusive, irrevocable additional copyright permission to create, reproduce, use, modify, display, perform, make available, and distribute a Combined Work under terms of the recipient's choice, including proprietary or closed-source terms. +"Generated Code" means the part of an Output that a Tool copies, expands, or derives from its own Covered Software in its ordinary operation, rather than from Input. Examples are templates, parser skeletons, entry points, stubs, thunks, tables, metadata, instantiated generics, and project scaffolding. -Those chosen terms may govern Independent Components and other portions of the Combined Work, but must not restrict or alter any recipient's rights in the Covered Software under MPL-2.0 and this Exception. No distributor is required to relicense an Independent Component merely because it forms part of a Combined Work. +"Runtime Material" means Covered Software that (a) the copyright holders of a Tool identify, in the documentation they publish, as that Tool's runtime library or standard library, and (b) is compiled, linked, embedded into, or placed beside an Output because the Output uses it when it runs. A license text or other file installed for documentation only is not Runtime Material. -3. Boundary of Covered Software +"Interface Material" means declarations, type and data layouts, constants, enumerations, macros, function and method signatures, attributes, wire and file formats, and the short inline definitions that an interface needs in order to be used, to the minimum extent necessary to call, implement, extend, bind to, or interoperate with Covered Software. -Linking, loading, calling, communicating with, packaging, installing, or distributing an Independent Component with Covered Software does not, by itself: +2. Larger Works -(a) make the Independent Component Covered Software or a Modification; -(b) subject the Independent Component's Source Code Form to MPL-2.0; or -(c) expand MPL-2.0 beyond files that are Covered Software or Modifications under MPL-2.0. +You may create, reproduce, use, modify, make available, and distribute a Larger Work that combines Covered Software with Independent Work under terms of Your choice, including proprietary terms, provided You comply with MPL-2.0 for the Covered Software as this Exception adjusts it. The combination may be made by any technical means, including static or dynamic linking, loading, embedding, plugins, language bindings, an API, ABI, or FFI boundary, a command-line interface, a protocol, inter-process communication, or a file format. It does not matter whether the works end up in one file, process, address space, archive, image, or deployment. The word "separate" in the definition of Larger Work in MPL-2.0 Section 1.7 refers to Source Code Form. -If Source Code Form from Covered Software is copied into another file, or a new Source Code Form file contains Covered Software, MPL-2.0 determines whether that file is a Modification. Labels, file extensions, build boundaries, code generation, or later separation of files do not avoid that determination. +Your terms may govern the Independent Work and the Larger Work as a whole. They must not limit or alter a recipient's rights in the Source Code Form of the Covered Software. -4. Outputs +Neither MPL-2.0 nor this Exception requires You, for a Larger Work, to provide the Source Code Form of Independent Work, to provide object files or anything else that allows relinking, to allow reverse engineering of Independent Work, or to provide installation information, keys, or credentials. -An Output does not become Covered Software merely because Covered Software produced, transformed, linked, or processed it. The copyright license chosen for input material does not change merely because Covered Software processes that material. +3. What Compilation Does Not Change -If an Output contains copyrightable Source Code Form or other copyrightable material copied or derived from Covered Software, this Exception does not remove MPL-2.0 obligations from that incorporated material. Facts, ideas, interfaces, names, signatures, diagnostics, and other material not protected by copyright are not converted into Covered Software by this Exception. +The following do not make Independent Work into Covered Software or a Modification, and do not subject its Source Code Form to MPL-2.0: -5. Preservation of MPL-2.0 Obligations +(a) calling, linking to, loading, subclassing, implementing, or otherwise using Covered Software through its interfaces; +(b) including, importing, or referring to a Covered Software header, module, or package from a file of Independent Work; and +(c) macro expansion, template or generic instantiation, specialization, inlining, link-time or whole-program optimization, just-in-time compilation, or any comparable compiler transformation that places Covered Software in the compiled form of Independent Work. -This Exception does not remove, replace, waive, or narrow any MPL-2.0 obligation applicable to Covered Software. In particular: +Material placed in a compiled form under (c) is the Executable Form of the Covered Software it came from. Section 4 or Section 5 releases it when it is Generated Code, Runtime Material, or Interface Material. Otherwise MPL-2.0 Section 3.2 applies to it, and Section 7 says how to meet that Section. -(a) Covered Software in Source Code Form and every Modification remain subject to MPL-2.0; -(b) distribution in Executable Form remains subject to MPL-2.0 Section 3.2, including the obligation to inform recipients how to obtain the corresponding Source Code Form when that section applies; -(c) notices protected by MPL-2.0 Section 3.4 must be retained; and -(d) this Exception grants no permission to withhold Source Code Form that MPL-2.0 requires to be made available. +If You copy Covered Software in Source Code Form into a file, other than as Section 4 or Section 5 allows, that file is a Modification. Labels, file extensions, build boundaries, or later separation of files do not change that. -Distribution of a Combined Work under terms of choice is permission for the Combined Work as a whole and its Independent Components. It is not permission to relicense a Covered Software file. +4. Generated Code and Programs Built with Tools -6. Packaging and Deployment +4.1 Output is not Covered Software merely because a Tool produced it. Running a Tool over Input does not change the license of the Input, and material in an Output that comes from Input has the same license as that Input. -Placing Covered Software and an Independent Component in the same archive, package, installer, application bundle, container image, firmware image, distribution medium, hosted deployment, or process does not change their respective copyright licenses. Network interaction or operation as separate processes is not required for independence. +4.2 You may use, modify, and distribute Generated Code, in any form, under terms of Your choice. MPL-2.0 Sections 3.1 through 3.4 do not apply to it. You need not offer its source, keep its notices, or attribute it. -7. Direct Grant to Recipients +4.3 You may distribute Runtime Material in Executable Form as part of, or together with, an Output that uses it, under terms of Your choice. MPL-2.0 Sections 3.1 through 3.4 do not apply to that distribution. The Source Code Form of Runtime Material stays under MPL-2.0, and this Section does not permit distributing Runtime Material on its own. This Section covers Runtime Material You have not changed, and a Modification of it only when You make that Modification available under Section 7. -The additional permission in this Exception is granted directly by the copyright holders applying it to every recipient of the applicable Covered Software. A distributor need not sublicense this Exception. Distribution through one or more intermediaries does not reduce the permission, and termination of an upstream distributor's rights does not terminate rights already received by a compliant downstream recipient. +4.4 Limits. This Section 4 does not apply to: -8. Termination and Reinstatement +(a) Covered Software given to a Tool as Input, unless it is Runtime Material. Compiling, formatting, or transforming Covered Software with a Tool leaves its obligations in place; +(b) material that an unmodified Tool would not write for the same Input and options, unless You make the change that causes it available under Section 7. Changing a Tool so that it copies more of its Covered Software into Output does not widen this Section; or +(c) an Output produced for the purpose of obtaining a Tool's Covered Software rather than for using the Tool. Such material remains subject to MPL-2.0. -This Exception has no separate patent-litigation, field-of-use, combination, or compatibility termination condition. A recipient may exercise it only while entitled to exercise the corresponding MPL-2.0 copyright rights. If those rights terminate under MPL-2.0 Section 5.1, exercise of this Exception for the affected Covered Software is suspended to the same extent and is reinstated when, and to the extent that, the MPL-2.0 rights are reinstated. +4.5 Intermediate representations and every other kind of Output are treated alike. An Output keeps the permissions of this Section after it leaves You. If Your rights in a Tool end, that does not affect Output You have already made or the rights of anyone who received it. -A recipient may elect to use Covered Software under MPL-2.0 without relying on this Exception. +4.6 These permissions lift conditions; they do not end the license. Generated Code and Runtime Material stay licensed to You under MPL-2.0 Section 2, and MPL-2.0 Sections 6 and 7 continue to apply to them. -9. Other Rights +5. Interface Material and Examples -This Exception grants copyright permission only. It grants no trademark, trade dress, service mark, publicity, confidential-information, or patent rights. Patent rights, if any, arise only under MPL-2.0 or a separately applicable patent grant. Nothing in this Exception limits rights arising independently under exhaustion, fair use, fair dealing, implied license, estoppel, or other applicable law. +You may copy Interface Material into Independent Work, and translate it into another language, to the extent needed to use or interoperate with Covered Software. The result is not a Modification, and MPL-2.0 Section 3 does not apply to it, in Source Code Form or in Executable Form. This covers header files, bindings, and declarations in another language. It does not cover the implementation behind an interface. +You may use, copy, and adapt code that the documentation of Covered Software presents as an example or as a starting point for Your own project, under terms of Your choice and without any obligation of MPL-2.0 Section 3. + +6. What This Exception Keeps + +Except where Sections 4 and 5 say otherwise: + +(a) Covered Software in Source Code Form, and every Modification, stay under MPL-2.0 and cannot be relicensed through this Exception; +(b) distributing Covered Software in Executable Form stays subject to MPL-2.0 Section 3.2, as Section 7 explains; +(c) notices protected by MPL-2.0 Section 3.4 stay in place in Source Code Form; and +(d) nothing here allows You to withhold Source Code Form that MPL-2.0 requires You to make available. + +7. Telling Recipients How to Obtain Source + +For Covered Software You have not changed, You satisfy MPL-2.0 Section 3.2(a) by giving recipients of the Executable Form a notice that names the Covered Software and its version and gives an address where the corresponding Source Code Form is publicly available. You may put that notice in documentation, in an about screen, in a notice file, or in a software bill of materials. The copyright holders' own public release of that version is a sufficient source; You need not host a copy, and You are not in breach if that release later stops being available. + +For a Modification, the same notice must give an address where the Source Code Form of the exact version You distributed can be obtained by reasonable means and in a timely manner. An address that follows a branch or otherwise changes over time is not enough. + +8. Contributors and Direct Grant + +Every Contributor who distributes Covered Software that carries an identification under Section 12 grants this Exception for that Contributor's Contributions. + +This Exception is granted directly to every recipient of Covered Software, for the duration of the copyright in it. A distributor need not sublicense it. Distribution through intermediaries does not reduce it, and the end of a distributor's rights does not end rights already received by others. + +You may use Covered Software under MPL-2.0 without relying on this Exception. As an exception to MPL-2.0 Section 3.4, You may remove the identification of this Exception from a file You modify; Your version of that file is then offered by You under MPL-2.0 alone. Earlier copies keep this Exception. + +9. Termination + +This Exception has no termination condition of its own. You may rely on Sections 2, 3, and 5 only while You hold the corresponding rights under MPL-2.0. If those rights end under MPL-2.0 Section 5.1 or 5.2, those permissions end for the affected Covered Software to the same extent, and return if and to the extent those rights are reinstated. This does not reach Independent Work, nor the rights of recipients that MPL-2.0 Section 5.3 preserves, nor Section 4.5. + 10. Relationship to MPL-2.0 -MPL-2.0 and this Exception are read together. If this Exception and MPL-2.0 cannot both be given effect with respect to the creation or distribution of a Combined Work, this Exception controls only to the minimum extent necessary to provide the additional permission stated in Sections 2 through 6. MPL-2.0 controls in every other respect. +MPL-2.0 and this Exception are read together. This Exception is a permission from copyright holders and is not a modified version of MPL-2.0. Where the two cannot both be given effect, this Exception controls only as far as needed to give the permissions in Sections 2 through 5 and 7 through 9, and MPL-2.0 controls in every other respect. This Exception continues to apply if You use Covered Software under a later version of the Mozilla Public License as MPL-2.0 Section 10.2 allows. -Nothing here changes whether Covered Software is "Incompatible With Secondary Licenses" or changes the operation of MPL-2.0 Section 3.3 for a work governed by a Secondary License. +MPL-2.0 Sections 6, 7, 8, and 9 apply to this Exception. It is provided without warranty. -11. Applicability and Version +Nothing here changes whether Covered Software is "Incompatible With Secondary Licenses" or changes the operation of MPL-2.0 Section 3.3 for a work governed by a Secondary License. When You distribute under a Secondary License as that Section allows, the Secondary License governs that distribution. -This Exception applies only to a file that expressly identifies it with this exact SPDX license expression: +11. Other Rights -MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +This Exception concerns copyright only. It grants no trademark, trade dress, service mark, publicity, or confidential-information rights, and no patent rights beyond those MPL-2.0 or a separately applicable patent grant provides. Nothing here limits rights that arise independently under exhaustion, fair use, fair dealing, implied license, estoppel, or other applicable law. -Reference to Version 1.1 does not incorporate any later version. A copy of this text supplied in a repository or distribution identifies the permission; the per-file expression determines which files receive it. +12. Applicability and Versions -12. Severability +This Exception applies to Covered Software identified with this SPDX license expression, or an expression that contains it: -If any provision of this Exception is held unenforceable, it shall be reformed only to the minimum extent necessary to make it enforceable. If reformation is unavailable, that provision is severed and the remaining provisions continue to the maximum extent permitted by law. +MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 +The identification may be in the file itself, in a companion license file, in a machine-readable file such as REUSE.toml that assigns the expression to that file, or in a notice of the project that names the file or the directory holding it. A compiled form has the identification of the source it was compiled from and does not need to carry it. + +Progmasoft may publish later versions of this Exception. You may use Covered Software under the version that identifies it or under any later version Progmasoft publishes. + +Progmasoft also grants this version for its own Contributions in every release it has made under Version 1.0 or Version 1.1 of this Exception. For those releases You may rely on either version. + +13. Severability + +If any provision of this Exception is held unenforceable, it shall be reformed only to the minimum extent necessary to make it enforceable. If reformation is unavailable, that provision is severed and the remaining provisions continue to the maximum extent permitted by law.
src/Progmasoft/Tala/AST.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Public syntax and phase-indexed tree model shared by the Tala frontend. Parsed trees retain source spelling, renamed trees attach stable @@ -14,6 +14,7 @@ , SyntaxTree (..) , Declaration (..) , EnumCase (..) + , FactAttribute (..) , enumType , enumUnderlyingType , enumMemberValues @@ -145,6 +146,8 @@ , declarationBody :: Block name annotation , declarationIsStatic :: Bool , declarationAccess :: Access + , declarationFact :: Maybe FactAttribute + -- ^ The @[Fact]@ attribute, which makes the method a test. } | TemplateTypeDeclaration { declarationSpan :: SourceSpan @@ -163,6 +166,16 @@ , enumUnderlying :: Maybe TypeSyntax , enumCases :: [EnumCase] } + deriving stock (Eq, Ord, Read, Show) + +{- | The @[Fact]@ attribute of a method: the method is a test. The name is +the one @[Fact("...")]@ gives it in a report; without one a report names +the class and the method. +-} +data FactAttribute = FactAttribute + { factSpan :: SourceSpan + , factName :: Maybe String + } deriving stock (Eq, Ord, Read, Show) {- | A member of a classic enum. A member without a written value takes the
src/Progmasoft/Tala/CharacterLiteral.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -- | Decoder and u32 packer for Tala character literals. module Progmasoft.Tala.CharacterLiteral
src/Progmasoft/Tala/Completion.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Whether control can reach the end of a statement or an expression.
src/Progmasoft/Tala/Diagnostic.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -- | Structured compiler diagnostics shared by frontend stages. module Progmasoft.Tala.Diagnostic
src/Progmasoft/Tala/Diagnostic/Protocol.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Versioned, bounded binary representation of compiler diagnostics.
+ src/Progmasoft/Tala/Directives.hs view
@@ -0,0 +1,486 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + +{- | Conditional compiler directives and regions. + +The pass stands between the lexer and the parser. It reads the directive +lines of one source unit, chooses the branch of every @#if@ group that the +target selects, and hands the parser the tokens of the chosen branches +without the directive lines. It is not macro preprocessing: no token is +rewritten or substituted, and a directive stands only at a boundary between +declarations, members or statements, where removing a branch cannot leave a +fragment of an expression behind. + +A condition is built from @Fact@, which holds when tests are compiled, the +predicates @os(...)@ and @family(...)@ of the target, @true@, @false@, +@not@, @&&@, @||@ and parentheses. Directive symbols, which @#define@ would +declare, are not implemented yet, and neither are the diagnostic directives; +both are refused by name. + +The pass also decides where @#expect@ may stand. It is test code: the body +of a method with the @[Fact]@ attribute, or a branch that only a test build +selects, such as the body of @#if Fact@. +-} +module Progmasoft.Tala.Directives + ( OperatingSystem (..) + , DirectiveEnvironment (..) + , hostDirectiveEnvironment + , operatingSystemNamed + , operatingSystemName + , resolveDirectives + ) where + +import Data.List (intercalate) +import Progmasoft.Tala.AST (SourcePosition (..), SourceSpan (..)) +import Progmasoft.Tala.Diagnostic +import Progmasoft.Tala.Parser.Token +import System.Info qualified + +-- | The operating systems @os(...)@ names. Any other target matches none. +data OperatingSystem = Windows | Linux | MacOS | FreeBSD | OpenBSD | NetBSD | OtherSystem + deriving stock (Bounded, Enum, Eq, Ord, Read, Show) + +-- | What the conditions of a compilation are evaluated against. +data DirectiveEnvironment = DirectiveEnvironment + { directiveSystem :: OperatingSystem + -- ^ The operating system of the target. + , directiveTesting :: Bool + -- ^ Whether tests are compiled; @Fact@ holds exactly then. + } + deriving stock (Eq, Ord, Read, Show) + +-- | A compilation for the machine the compiler runs on, without tests. +hostDirectiveEnvironment :: DirectiveEnvironment +hostDirectiveEnvironment = DirectiveEnvironment hostSystem False + where + hostSystem = case System.Info.os of + "mingw32" -> Windows + "linux" -> Linux + "darwin" -> MacOS + "freebsd" -> FreeBSD + "openbsd" -> OpenBSD + "netbsd" -> NetBSD + _ -> OtherSystem + +-- | The spelling @os(...)@ takes for a system, and @other@ for the rest. +operatingSystemName :: OperatingSystem -> String +operatingSystemName system = case system of + Windows -> "Windows" + Linux -> "Linux" + MacOS -> "macOS" + FreeBSD -> "FreeBSD" + OpenBSD -> "OpenBSD" + NetBSD -> "NetBSD" + OtherSystem -> "other" + +-- | The system a spelling of 'operatingSystemName' names. +operatingSystemNamed :: String -> Maybe OperatingSystem +operatingSystemNamed name = lookup name [(operatingSystemName system, system) | system <- [minBound .. maxBound]] + +familyMembers :: String -> Maybe [OperatingSystem] +familyMembers name = case name of + "Windows" -> Just [Windows] + "Unix" -> Just [Linux, MacOS, FreeBSD, OpenBSD, NetBSD] + "BSD" -> Just [FreeBSD, OpenBSD, NetBSD] + _ -> Nothing + +-- | A condition after it has been read. +data Condition + = Constant Bool + | Testing + | System OperatingSystem + | Family [OperatingSystem] + | Not Condition + | Both Condition Condition + | Either Condition Condition + +holds :: DirectiveEnvironment -> Condition -> Bool +holds environment condition = case condition of + Constant value -> value + Testing -> directiveTesting environment + System system -> directiveSystem environment == system + Family members -> directiveSystem environment `elem` members + Not inner -> not (holds environment inner) + Both left right -> holds environment left && holds environment right + Either left right -> holds environment left || holds environment right + +{- | One open @#if@ group or @#region@. + +A group is followed under two environments at once, the one compiled for +and the same one with @Fact@ flipped, so that a branch is known to be test +code when the test build selects it and the other build does not. +-} +data Frame = GroupFrame Group | RegionFrame SourceSpan String + +-- | An open @#if@ group and its current branch. +data Group = Group + { groupOpening :: SourceSpan + , frameChosen :: (Bool, Bool) + {- ^ Whether the current branch is chosen under the testing and the + other environment. + -} + , frameTest :: Bool + -- ^ Whether the current branch is test code. + , frameTaken :: (Bool, Bool) + {- ^ Whether an earlier branch was chosen under the testing and the + other environment. + -} + , frameOuter :: (Bool, Bool) + -- ^ Whether the enclosing code is chosen under the two. + , frameOuterTest :: Bool + , frameSeenElse :: Bool + , frameBrackets :: [String] + -- ^ The brackets open when the group began. + } + +frameOpening :: Frame -> SourceSpan +frameOpening frame = case frame of + GroupFrame group -> groupOpening group + RegionFrame opening _ -> opening + +data State = State + { stateFrames :: [Frame] + , stateBrackets :: [String] + -- ^ Brackets open at this point, innermost first, in every branch. + , statePrevious :: Maybe Token + -- ^ The last token that is not part of a directive line. + , stateFact :: FactState + , stateOutput :: [Token] + } + +-- | Where the pass stands with respect to the body of a @[Fact]@ method. +data FactState + = OutsideFact + | -- | After a @[Fact]@ attribute, before the body opens. + FactPending + | -- | Inside a body; the number is the bracket depth outside it. + InsideFact Int + +{- | Choose the branches the environment selects, and drop the directive +lines. The result keeps the end-of-file token. +-} +resolveDirectives :: DirectiveEnvironment -> [Token] -> Either [Diagnostic] [Token] +resolveDirectives environment = run (State [] [] Nothing OutsideFact []) + where + run state tokens = case tokens of + [] -> finish state + token : rest + | tokenKind token == EndOfFileToken -> case stateFrames state of + frame : _ -> Left [problem (frameOpening frame) "TAP0051" (unclosed frame)] + [] -> finish (state {stateOutput = token : stateOutput state}) >>= \done -> Right (done ++ rest) + | tokenKind token == DirectiveToken && tokenText token /= "#expect" -> do + let (line, after) = span (onLine token) rest + checkFirstOnLine state token + next <- directive state token line + run next after + | otherwise -> step state token >>= (`run` rest) + finish state = Right (reverse (stateOutput state)) + + onLine token next = + tokenKind next /= EndOfFileToken + && sourceLine (sourceStart (tokenSpan next)) == sourceLine (sourceStart (tokenSpan token)) + + checkFirstOnLine state token = case statePrevious state of + Just previous + | sourceLine (sourceEnd (tokenSpan previous)) == sourceLine (sourceStart (tokenSpan token)) -> + Left [problem (tokenSpan token) "TAP0042" (tokenText token ++ " must begin its line")] + _ -> Right () + + -- The innermost group decides; a region changes nothing. + innermost frames = case frames of + GroupFrame group : _ -> Just group + RegionFrame {} : more -> innermost more + [] -> Nothing + chosen state = maybe (True, True) frameChosen (innermost (stateFrames state)) + active state = + let (testing, other) = chosen state + in if directiveTesting environment then testing else other + testCode state = maybe False frameTest (innermost (stateFrames state)) + + -- An ordinary token: kept when its branch is compiled, and counted + -- for the brackets in every branch. + step state token = do + brackets <- bracket state token + let isActive = active state + fact <- if isActive then factStep state token else Right (stateFact state) + if tokenKind token == DirectiveToken && isActive && not (testCode state) && not (insideFact fact) + then + Left + [ problem + (tokenSpan token) + "TAP0044" + "#expect belongs to test code: write it in a [Fact] method or in a branch of #if Fact" + ] + else + Right + state + { stateBrackets = brackets + , statePrevious = Just token + , stateFact = fact + , stateOutput = if isActive then token : stateOutput state else stateOutput state + } + + insideFact fact = case fact of + InsideFact _ -> True + _ -> False + + bracket state token + | tokenKind token /= SymbolToken = Right (stateBrackets state) + | tokenText token `elem` ["(", "[", "{"] = Right (tokenText token : stateBrackets state) + | tokenText token `elem` [")", "]", "}"] = case stateBrackets state of + opened : outer + | length (stateBrackets state) > groupFloor state -> Right outer + | otherwise -> Left [problem (tokenSpan token) "TAP0043" (closes opened token)] + [] -> Right [] + | otherwise = Right (stateBrackets state) + closes opened token = + "this " ++ show (tokenText token) ++ " closes the " ++ show opened ++ " that stands before the directive group it is in" + groupFloor state = case [frameBrackets group | GroupFrame group <- stateFrames state] of + floorBrackets : _ -> length floorBrackets + [] -> 0 + + -- The body of a [Fact] method: the attribute is followed by the + -- signature, and the first brace after it opens the body. + factStep state token = case stateFact state of + InsideFact depth + | tokenText token == "}" && tokenKind token == SymbolToken && length (stateBrackets state) == depth + 1 -> + Right OutsideFact + | otherwise -> Right (InsideFact depth) + FactPending + | tokenText token == "{" && tokenKind token == SymbolToken -> Right (InsideFact (length (stateBrackets state))) + | tokenText token == ";" && tokenKind token == SymbolToken -> Right OutsideFact + | otherwise -> Right FactPending + OutsideFact + | tokenKind token == IdentifierToken + , tokenText token == "Fact" + , Just previous <- statePrevious state + , tokenKind previous == SymbolToken + , tokenText previous == "[" -> + Right FactPending + | otherwise -> Right OutsideFact + + directive state token line = case tokenText token of + "#if" -> do + checkBoundary state token + condition <- readCondition token line + Right (branch state token condition Nothing) + "#elseif" -> do + group <- currentGroup state token + if frameSeenElse group + then Left [problem (tokenSpan token) "TAP0049" "#elseif cannot follow #else in the same group"] + else Right () + checkBoundary state token + checkBalanced state group token + condition <- readCondition token line + Right (branch (popFrame state group) token condition (Just group)) + "#else" -> do + group <- currentGroup state token + if frameSeenElse group + then Left [problem (tokenSpan token) "TAP0049" "a group has at most one #else"] + else Right () + noOperands token line + checkBoundary state token + checkBalanced state group token + let next = branch (popFrame state group) token (Constant True) (Just group) + Right (markElse next) + "#endif" -> do + group <- currentGroup state token + noOperands token line + checkBoundary state token + checkBalanced state group token + Right (popFrame state group) + "#region" -> Right state {stateFrames = RegionFrame (tokenSpan token) (unwords (map tokenText line)) : stateFrames state} + "#endregion" -> case stateFrames state of + RegionFrame _ name : outer + | null line || unwords (map tokenText line) == name -> Right state {stateFrames = outer} + | otherwise -> + Left + [ problem + (tokenSpan token) + "TAP0052" + ("#endregion names " ++ show (unwords (map tokenText line)) ++ ", but the open region is " ++ show name) + ] + GroupFrame _ : _ -> Left [problem (tokenSpan token) "TAP0050" "#endregion closes a region, but the innermost open directive is #if"] + [] -> Left [problem (tokenSpan token) "TAP0050" "#endregion has no #region to close"] + name + | name `elem` ["#define", "#setdef", "#undef"] -> + Left [problem (tokenSpan token) "TAP0053" (name ++ " and directive symbols are not implemented yet")] + | name `elem` ["#error", "#warn", "#info", "#message"] -> + Left [problem (tokenSpan token) "TAP0053" ("the diagnostic directive " ++ name ++ " is not implemented yet")] + | otherwise -> Left [problem (tokenSpan token) "TAP0054" ("unknown directive " ++ name)] + + markElse state = case stateFrames state of + GroupFrame group : outer -> state {stateFrames = GroupFrame group {frameSeenElse = True} : outer} + _ -> state + + currentGroup state token = case stateFrames state of + GroupFrame group : _ -> Right group + RegionFrame {} : _ -> + Left + [ problem + (tokenSpan token) + "TAP0050" + (tokenText token ++ " cannot stand inside a #region that the branch opened; close the region first") + ] + [] -> Left [problem (tokenSpan token) "TAP0050" (tokenText token ++ " has no #if to belong to")] + + popFrame state group = state {stateFrames = drop 1 (stateFrames state), stateBrackets = frameBrackets group} + + -- Open the next branch of a group: the first that holds is chosen, + -- under the environment compiled for and under the other one. + -- The state passed for a later branch has its group popped, so the + -- enclosing code is the same for every branch of one group. + branch state token condition previous = + let (outerTesting, outerOther) = maybe (chosen state) frameOuter previous + outerTest = maybe (testCode state) frameOuterTest previous + (takenTesting, takenOther) = maybe (False, False) frameTaken previous + chosenTesting = outerTesting && not takenTesting && holds environment {directiveTesting = True} condition + chosenOther = outerOther && not takenOther && holds environment {directiveTesting = False} condition + frame = + Group + { groupOpening = maybe (tokenSpan token) groupOpening previous + , frameChosen = (chosenTesting, chosenOther) + , frameTest = outerTest || chosenTesting && not chosenOther + , frameTaken = (takenTesting || chosenTesting, takenOther || chosenOther) + , frameOuter = (outerTesting, outerOther) + , frameOuterTest = outerTest + , frameSeenElse = maybe False frameSeenElse previous + , frameBrackets = stateBrackets state + } + in state {stateFrames = GroupFrame frame : stateFrames state} + + -- A directive stands where a declaration, a member or a statement + -- may begin: outside parentheses and brackets, after a semicolon or + -- a brace, or at the start of the unit. + checkBoundary state token = do + case stateBrackets state of + opened : _ + | opened /= "{" -> + Left + [ problem + (tokenSpan token) + "TAP0043" + (tokenText token ++ " cannot stand inside " ++ show opened ++ "; a directive may not split an expression") + ] + _ -> Right () + case statePrevious state of + Just previous + | not (tokenKind previous == SymbolToken && tokenText previous `elem` [";", "{", "}"]) -> + Left + [ problem + (tokenSpan token) + "TAP0043" + ( tokenText token + ++ " follows " + ++ show (tokenText previous) + ++ "; a directive stands between declarations, members or statements and may not split one" + ) + ] + _ -> Right () + + checkBalanced state group token = + case stateBrackets state of + opened : _ + | length (stateBrackets state) /= length (frameBrackets group) -> + Left + [ problem + (tokenSpan token) + "TAP0043" + ("the branch before " ++ tokenText token ++ " leaves " ++ show opened ++ " open; every branch closes what it opens") + ] + _ -> Right () + + noOperands token line = case line of + [] -> Right () + extra : _ -> Left [problem (tokenSpan extra) "TAP0048" (tokenText token ++ " takes no condition")] + + unclosed frame = case frame of + GroupFrame _ -> "#if has no #endif" + RegionFrame _ name -> "#region " ++ name ++ " has no #endregion" + +-- | Read the condition of @#if@ or @#elseif@ from the rest of its line. +readCondition :: Token -> [Token] -> Either [Diagnostic] Condition +readCondition directiveToken line = case line of + [] -> Left [problem (tokenSpan directiveToken) "TAP0045" (tokenText directiveToken ++ " needs a condition")] + _ -> do + (condition, rest) <- disjunction line + case rest of + [] -> Right condition + extra : _ -> Left [problem (tokenSpan extra) "TAP0045" ("unexpected " ++ show (tokenText extra) ++ " in the condition")] + where + disjunction tokens = do + (left, rest) <- conjunction tokens + case rest of + operator : more | isSymbol "||" operator -> do + (right, after) <- disjunction more + Right (Either left right, after) + _ -> Right (left, rest) + conjunction tokens = do + (left, rest) <- unary tokens + case rest of + operator : more | isSymbol "&&" operator -> do + (right, after) <- conjunction more + Right (Both left right, after) + _ -> Right (left, rest) + unary tokens = case tokens of + token : rest | tokenKind token == KeywordToken && tokenText token == "not" -> do + (inner, after) <- unary rest + Right (Not inner, after) + _ -> primary tokens + primary tokens = case tokens of + token : rest + | isSymbol "(" token -> do + (inner, after) <- disjunction rest + case after of + close : more | isSymbol ")" close -> Right (inner, more) + _ -> Left [problem (tokenSpan token) "TAP0045" "this parenthesis of the condition is not closed"] + | tokenKind token == KeywordToken && tokenText token `elem` ["true", "false"] -> + Right (Constant (tokenText token == "true"), rest) + | tokenKind token == IdentifierToken && tokenText token == "Fact" -> Right (Testing, rest) + | tokenKind token == IdentifierToken && tokenText token `elem` ["os", "family"] -> predicate token rest + | tokenKind token == IdentifierToken -> + Left + [ problem + (tokenSpan token) + "TAP0047" + ("unknown directive symbol " ++ tokenText token ++ "; #define and directive symbols are not implemented yet") + ] + | otherwise -> + Left [problem (tokenSpan token) "TAP0045" ("unexpected " ++ show (tokenText token) ++ " in the condition")] + [] -> Left [problem (tokenSpan directiveToken) "TAP0045" "the condition ends too early"] + predicate token rest = case rest of + open : name : close : after + | isSymbol "(" open && isSymbol ")" close && tokenKind name `elem` [IdentifierToken, KeywordToken] -> + case (tokenText token, tokenText name) of + ("os", spelling) | Just system <- operatingSystemNamed spelling, system /= OtherSystem -> Right (System system, after) + ("family", spelling) | Just members <- familyMembers spelling -> Right (Family members, after) + (kind, spelling) -> + Left + [ problem + (tokenSpan name) + "TAP0046" + ( kind + ++ "(" + ++ spelling + ++ ") names no known " + ++ (if kind == "os" then "operating system" else "family") + ++ "; known: " + ++ known kind + ) + ] + _ -> + Left + [ problem + (tokenSpan token) + "TAP0045" + (tokenText token ++ " takes one name in parentheses, as " ++ tokenText token ++ "(Windows)") + ] + known kind + | kind == "os" = intercalate ", " [operatingSystemName system | system <- [minBound .. maxBound], system /= OtherSystem] + | otherwise = "Windows, Unix, BSD" + +isSymbol :: String -> Token -> Bool +isSymbol text token = tokenKind token == SymbolToken && tokenText token == text + +problem :: SourceSpan -> String -> String -> Diagnostic +problem spanValue code message = Diagnostic ParserStage Error code (Just spanValue) message
src/Progmasoft/Tala/FloatingLiteral.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -- | Source-accurate floating-point literal scanning and validation. module Progmasoft.Tala.FloatingLiteral
src/Progmasoft/Tala/Lexer.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -- | Configurable lexical scanner that returns source-positioned tokens. module Progmasoft.Tala.Lexer (LexerInput (..), Lexer (..), defaultLexer, runLexer) where @@ -66,6 +66,12 @@ (column + length text) remaining (token IntegerToken text line column line (column + length text) : output) + -- A directive is @#@ and a word with no space between them. + | character == '#' + , (word@(first : _), remaining) <- span (\c -> isAlphaNum c || c == '_') rest + , isAlpha first = + let text = '#' : word + in go line (column + length text) remaining (token DirectiveToken text line column line (column + length text) : output) | character == '"' = lexString line column rest output | character == '\'' = case scanCharacterLiteral input of Left issue -> Left [diagnostic "TAL0007" line column (renderCharacterLiteralError issue)]
src/Progmasoft/Tala/NestingLimits.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Limits on how deeply a program may nest, checked on the parsed tree.
src/Progmasoft/Tala/NumericLiteral.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Lexical rules for Tala integer literals.
src/Progmasoft/Tala/Parser.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | The syntax parser turns one lexed source unit into a positioned parsed AST. @@ -16,6 +16,7 @@ import Progmasoft.Tala.NumericLiteral import Progmasoft.Tala.Parser.Cursor import Progmasoft.Tala.Parser.Match +import Progmasoft.Tala.Parser.Testing import Progmasoft.Tala.Parser.Token -- | Tokens and diagnostic source identity for one physical input file. @@ -248,6 +249,7 @@ parseMemberBody :: P (Declaration Identifier ()) parseMemberBody = do + fact <- parseFactAttribute access <- parseAccess isStatic <- maybe False (const True) <$> optionalParser (keyword "static") returnType <- parseTypeSyntax @@ -260,7 +262,17 @@ -- therefore admit that form; nested control-flow blocks may not. body <- parseBlock (returnType /= ExplicitType (Identifier "void")) pure - (FunctionDeclaration (mergeSpan nameSpan (blockSpan body nameSpan)) name () returnType parameters body isStatic access) + ( FunctionDeclaration + (mergeSpan nameSpan (blockSpan body nameSpan)) + name + () + returnType + parameters + body + isStatic + access + fact + ) parseAccess :: P Access parseAccess = do @@ -510,6 +522,9 @@ first : _ | tokenKind first == KeywordToken && tokenText first == "break" -> parseBreak first : _ | tokenKind first == KeywordToken && tokenText first == "continue" -> parseContinue first : _ | tokenKind first == KeywordToken && tokenText first == "guard" -> parseGuardStatement branchGrammar + first : _ + | tokenKind first == DirectiveToken && tokenText first == "#expect" -> + parseExpectStatement (parseCondition branchGrammar "an #expect") -- No expression starts with a brace, so a brace that starts a -- statement opens a nested block. first : _ | tokenKind first == SymbolToken && tokenText first == "{" -> do @@ -1238,6 +1253,9 @@ value <- parseExpression _ <- symbol ")" pure value + Just token + | tokenKind token == DirectiveToken && tokenText token == "#expect" -> + failAt (tokenSpan token) "TAP0055" "#expect is a statement and has no value" Just token -> failAt (tokenSpan token) "TAP0004" ("expected expression, found " ++ show (tokenText token)) Nothing -> failCurrent "TAP0005" "expected expression at end of input"
src/Progmasoft/Tala/Parser/Cursor.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 module Progmasoft.Tala.Parser.Cursor ( P @@ -21,6 +21,7 @@ , failAt , problemAt , withSpan + , withTokens , endOfInput ) where @@ -183,6 +184,13 @@ let finish = maybe start id (previousSpan next) spanValue = start {sourceEnd = sourceEnd finish} in Accepted (value, spanValue) next + +-- | The value of a parser and the tokens it consumed, in order. +withTokens :: P a -> P (a, [Token]) +withTokens parser = P $ \cursor -> case step parser cursor of + Rejected problem offset -> Rejected problem offset + Accepted value next -> + Accepted (value, take (consumedTokens next - consumedTokens cursor) (remainingTokens cursor)) next -- The public Parser accepts token streams as well as lexer output. An EOF in -- the middle must never hide the suffix of a malformed supplied stream.
src/Progmasoft/Tala/Parser/Match.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Grammar of @match@, of @if@ used as an expression, and of @guard@.
+ src/Progmasoft/Tala/Parser/Testing.hs view
@@ -0,0 +1,167 @@+-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + +{- | The syntax of tests: the @[Fact]@ attribute and the @#expect@ statement. + +@#expect condition;@ leaves the parser as ordinary statements. Its +operands are bound once, the condition is tested, and when it is false a +call of the test runtime records the failure with a report the parser has +already written: the source location and the text of the condition, and +for a comparison the text of each operand that is not a literal, which the +type checker pairs with its value. Every stage after the parser sees only +bindings, an @if@ and a call; none of them knows about tests. + +The call is a method of @$Test@, a name the language declares for every +program and that no source can spell, because no identifier starts with +@$@. Where @#expect@ may stand is the directive pass's decision. +-} +module Progmasoft.Tala.Parser.Testing + ( parseFactAttribute + , parseExpectStatement + , testReceiverName + ) where + +import Data.Char (isAlphaNum) +import Progmasoft.Tala.AST +import Progmasoft.Tala.Parser.Cursor +import Progmasoft.Tala.Parser.Token + +-- | The spelling of the receiver of the test runtime's methods. +testReceiverName :: Identifier +testReceiverName = Identifier "$Test" + +{- | @[Fact]@ or @[Fact("name")]@ before a method, when one stands there. +@[Fact]@ followed by a name and a parenthesis is an array type, the return +type of a method named after it, and is left alone. +-} +parseFactAttribute :: P (Maybe FactAttribute) +parseFactAttribute = do + tokens <- peekTokens 5 + case tokens of + open : name : close : next : afterNext + | isSymbol "[" open + , isFact name + , isSymbol "]" close + , not (tokenKind next == IdentifierToken && take 1 (map (isSymbol "(") afterNext) == [True]) -> do + _ <- takeToken + _ <- takeToken + end <- takeToken + pure (Just (FactAttribute (mergeSpans (tokenSpan open) (tokenSpan end)) Nothing)) + open : name : parenthesis : _ + | isSymbol "[" open + , isFact name + , isSymbol "(" parenthesis -> do + _ <- takeToken + _ <- takeToken + _ <- takeToken + label <- satisfy ((== StringToken) . tokenKind) "the name of the test as a string literal" + _ <- symbol ")" + end <- symbol "]" + pure (Just (FactAttribute (mergeSpans (tokenSpan open) (tokenSpan end)) (Just (tokenText label)))) + _ -> pure Nothing + where + isFact token = tokenKind token == IdentifierToken && tokenText token == "Fact" + +{- | @#expect condition;@, with the condition parser of @if@. The result is +a block that names the operands it binds with spellings no source can +write, so they hide nothing of the program. +-} +parseExpectStatement :: P (Expression Identifier ()) -> P (Statement Identifier ()) +parseExpectStatement conditionParser = do + start <- takeToken + (condition, tokens) <- withTokens conditionParser + end <- symbol ";" + let spanValue = mergeSpans (tokenSpan start) (tokenSpan end) + position = sourceStart (tokenSpan start) + header = + sourceFile (tokenSpan start) + ++ ":" + ++ show (sourceLine position) + ++ ":" + ++ show (sourceColumn position) + ++ ": expectation failed: " + ++ renderTokens tokens + pure (BlockStatement spanValue (Block (lower spanValue header condition tokens))) + where + lower spanValue header condition tokens = case condition of + BinaryExpression operatorSpan operator left right () + | operator `elem` [Equal, NotEqual, LessThan, LessEqual, GreaterThan, GreaterEqual] -> + let leftName = Identifier "$left" + rightName = Identifier "$right" + bind name value = BindingStatement (expressionSourceSpan value) ImmutableBinding AutoType name () value + reference name value = NameExpression (expressionSourceSpan value) name () + compared = + BinaryExpression operatorSpan operator (reference leftName left) (reference rightName right) () + shown = + concat + [ [LiteralExpression spanValue (StringLiteral (renderTokens (operandTokens value tokens))) (), reference name value] + | (name, value) <- [(leftName, left), (rightName, right)] + , not (isLiteral value) + ] + in [bind leftName left, bind rightName right, check spanValue header compared shown] + _ -> [check spanValue header condition []] + + check spanValue header condition shown = + IfStatement + spanValue + (UnaryExpression spanValue LogicalNot condition ()) + ( Block + [ ExpressionStatement + spanValue + ( CallExpression + spanValue + (MemberAccessExpression spanValue (NameExpression spanValue testReceiverName ()) (Identifier "Fail") ()) + (LiteralExpression spanValue (StringLiteral header) () : shown) + () + ) + True + ] + ) + Nothing + + isLiteral value = case value of + LiteralExpression {} -> True + UnaryExpression _ UnaryNegate LiteralExpression {} _ -> True + _ -> False + +-- | The tokens of a condition that fall inside the span of one operand. +operandTokens :: Expression Identifier () -> [Token] -> [Token] +operandTokens value = filter inside + where + spanValue = expressionSourceSpan value + inside token = + position (sourceStart (tokenSpan token)) >= position (sourceStart spanValue) + && position (sourceEnd (tokenSpan token)) <= position (sourceEnd spanValue) + position point = (sourceLine point, sourceColumn point) + +{- | Tokens as source text: the spaces between tokens on one line are kept, +a line break becomes one space, and a string is written with quotes and +escapes again. +-} +renderTokens :: [Token] -> String +renderTokens tokens = concat (zipWith piece (Nothing : map Just tokens) tokens) + where + piece previous token = gap previous token ++ spelling token + gap previous token = case previous of + Nothing -> "" + Just before + | sourceLine (sourceEnd (tokenSpan before)) == sourceLine (sourceStart (tokenSpan token)) -> + replicate (max 0 (sourceColumn (sourceStart (tokenSpan token)) - sourceColumn (sourceEnd (tokenSpan before)))) ' ' + | otherwise -> " " + spelling token = case tokenKind token of + StringToken -> "\"" ++ concatMap escape (tokenText token) ++ "\"" + _ -> tokenText token + escape character = case character of + '"' -> "\\\"" + '\\' -> "\\\\" + '\n' -> "\\n" + '\t' -> "\\t" + '\r' -> "\\r" + _ | isAlphaNum character || character >= ' ' -> [character] + _ -> "?" + +isSymbol :: String -> Token -> Bool +isSymbol text token = tokenKind token == SymbolToken && tokenText token == text + +mergeSpans :: SourceSpan -> SourceSpan -> SourceSpan +mergeSpans first final = first {sourceEnd = sourceEnd final}
src/Progmasoft/Tala/Parser/Token.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 -- | Token types shared between the lexer, parser, and parser cursor. module Progmasoft.Tala.Parser.Token (TokenKind (..), Token (..)) where @@ -15,6 +15,11 @@ | FloatingToken | CharacterToken | StringToken + | {- | A compiler directive: @#@ and the word after it, as @#if@ or + @#expect@. The directive pass consumes the conditional and region + directives; @#expect@ reaches the parser. + -} + DirectiveToken | EndOfFileToken deriving stock (Bounded, Enum, Eq, Ord, Read, Show)
src/Progmasoft/Tala/RuntimeCall.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | The catalog of runtime calls. @@ -38,10 +38,12 @@ , runtimeFunctionOfName , builtinSystemSymbol , builtinConsoleSymbol + , builtinTestSymbol , RuntimeDefect (..) , runtimeCallDefect , runtimeDefectText - -- * Conversion flags + + -- * Conversion flags , flagLeft , flagZero , flagPlus @@ -50,7 +52,8 @@ , flagGroup , flagHexadecimal , absent - -- * Console targets + + -- * Console targets , consoleOutput , consoleOutputLine , consoleError @@ -87,6 +90,14 @@ ConsoleWrite | -- | Whether two strings hold the same characters. TextEquals + | -- | Record a failed @#expect@ of the current test and write its report. + TestFail + | -- | Close the current test, named by the argument, and write whether it passed. + TestEnd + | {- | Write how many tests passed and failed. A failure makes the + program end with a nonzero status. + -} + TestFinish deriving (Bounded, Enum, Eq, Ord, Read, Show) {- | What an argument of a runtime function may be. A function takes a family @@ -141,6 +152,9 @@ TextNewline -> 11 ConsoleWrite -> 12 TextEquals -> 13 + TestFail -> 14 + TestEnd -> 15 + TestFinish -> 16 -- | The function with the given identity, when there is one. runtimeFunctionOf :: Integer -> Maybe RuntimeFunction @@ -162,6 +176,9 @@ TextNewline -> "tala_text_newline" ConsoleWrite -> "tala_console_write" TextEquals -> "tala_text_equals" + TestFail -> "tala_test_fail" + TestEnd -> "tala_test_end" + TestFinish -> "tala_test_finish" -- | The arguments of the function, in order. runtimeParameters :: RuntimeFunction -> [RuntimeParameter] @@ -179,6 +196,9 @@ TextNewline -> [] ConsoleWrite -> [TextParameter, CountParameter] TextEquals -> [TextParameter, TextParameter] + TestFail -> [TextParameter] + TestEnd -> [TextParameter] + TestFinish -> [] where -- Flags, width and precision, and then the value. The value stands -- last because that is the order of a format's arguments: a width @@ -191,6 +211,9 @@ runtimeResult function = case function of ConsoleWrite -> NoResult TextEquals -> TruthResult + TestFail -> NoResult + TestEnd -> NoResult + TestFinish -> NoResult _ -> TextResult -- | The type a call of the function has. @@ -205,7 +228,7 @@ never removed, repeated or moved. -} runtimeObservable :: RuntimeFunction -> Bool -runtimeObservable function = function == ConsoleWrite +runtimeObservable function = function `elem` [ConsoleWrite, TestFail, TestEnd, TestFinish] -- | Whether an argument of the given type may stand where the parameter is. runtimeAccepts :: RuntimeParameter -> Type -> Bool @@ -232,7 +255,9 @@ -} runtimeName :: RuntimeFunction -> ResolvedName runtimeName function = - ResolvedName (SymbolId (negate (runtimeNameBase + fromInteger (runtimeFunctionIdentity function)))) (Identifier ('$' : runtimeFunctionSymbol function)) + ResolvedName + (SymbolId (negate (runtimeNameBase + fromInteger (runtimeFunctionIdentity function)))) + (Identifier ('$' : runtimeFunctionSymbol function)) -- | The runtime function a name of the typed tree stands for, if it is one. runtimeFunctionOfName :: ResolvedName -> Maybe RuntimeFunction @@ -250,6 +275,12 @@ builtinSystemSymbol, builtinConsoleSymbol :: SymbolId builtinSystemSymbol = SymbolId (-2) builtinConsoleSymbol = SymbolId (-3) + +{- | The symbol of @$Test@, the receiver of the calls @#expect@ and the +runner of a test build make of the test runtime. No source can spell it. +-} +builtinTestSymbol :: SymbolId +builtinTestSymbol = SymbolId (-4) -- | Why a runtime call is malformed. data RuntimeDefect
src/Progmasoft/Tala/SourceText.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Lossless lexical structure for source-oriented tools.
src/Progmasoft/Tala/SourceWarnings.hs view
@@ -1,5 +1,5 @@ -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 {- | Warnings about source text that is valid but probably not what its author meant.
tala-syntax.cabal view
@@ -1,8 +1,8 @@ cabal-version: 3.12 -- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 name: tala-syntax -version: 0.1.0 +version: 0.2.0 synopsis: Tala source model, lexer, parser, and diagnostics description: The source model of Tala: source text and positions, the lexer, the @@ -11,7 +11,7 @@ This is an implementation package of the Tala compiler. Its modules may change in any release; tools should depend on tala-tooling instead. . - The source is also offered under the Progmasoft Linking Exception 1.1 + The source is also offered under the Progmasoft Linking Exception 2.0 (LICENSE-EXCEPTION), an additional permission to MPL-2.0 for works that combine this package with independent code. license: MPL-2.0 @@ -32,6 +32,7 @@ Progmasoft.Tala.CharacterLiteral Progmasoft.Tala.Diagnostic Progmasoft.Tala.Diagnostic.Protocol + Progmasoft.Tala.Directives Progmasoft.Tala.FloatingLiteral Progmasoft.Tala.Lexer Progmasoft.Tala.Completion @@ -44,6 +45,7 @@ other-modules: Progmasoft.Tala.Parser.Cursor Progmasoft.Tala.Parser.Match + Progmasoft.Tala.Parser.Testing Progmasoft.Tala.Parser.Token hs-source-dirs: src build-depends: