packages feed

tala-compiler-0.1.0: test/ParserContractTests.hs

-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1

module ParserContractTests (parserContractTests) where

import Data.List (isInfixOf)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Compiler
import Progmasoft.Tala.Core
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.Lexer
import Progmasoft.Tala.Parser

-- These tests exercise the public source/token boundary rather than testing
-- individual combinators against copies of their own implementation. Diagnostic
-- assertions include the offending source location, not just a Left result.
parserContractTests :: [(String, Bool)]
parserContractTests =
    diagnosticTests
        ++ comparisonTests
        ++ operatorPrecedenceTests
        ++ literalTests
        ++ spanTests
        ++ tokenStreamTests
        ++ typeSyntaxTests
        ++ pipelineTests
        ++ booleanOperandTests

parseSource :: String -> Either [Diagnostic] ParsedAST
parseSource source = do
    tokens <- runLexer defaultLexer (LexerInput "parser-contract.tala" source)
    runParser defaultParser (ParserInput "parser-contract.tala" tokens)

sourceWith :: String -> String
sourceWith body = "class Program { int Value() { " ++ body ++ " } }"

accepted :: String -> Bool
accepted source = case parseSource source of
    Right _ -> True
    Left _ -> False

rejectedAt :: String -> String -> String -> Int -> Int -> Bool
rejectedAt source code message line column = case parseSource source of
    Left [problem] ->
        diagnosticStage problem == ParserStage
            && diagnosticSeverity problem == Error
            && diagnosticCode problem == code
            && message `isInfixOf` diagnosticMessage problem
            && fmap sourceStart (diagnosticSpan problem) == Just (SourcePosition line column)
            && fmap sourceFile (diagnosticSpan problem) == Just "parser-contract.tala"
    _ -> False

-- Keep each failure on a separate line so expected columns are independent of
-- the enclosing class spelling. Consumed optional constructs must retain their
-- own errors: namespace, else, and capture modes are especially important.
diagnosticTests :: [(String, Bool)]
diagnosticTests =
    [ bad "namespace missing name" "namespace ;" "TAP0006" "identifier" 1 11
    , bad "namespace missing terminator" "namespace Demo class C {}" "TAP0006" ";" 1 16
    , bad "namespace missing component" "namespace Demo.;" "TAP0006" "identifier" 1 16
    , bad "namespace keyword is not an identifier" "namespace class;" "TAP0006" "identifier" 1 11
    , bodyBad "return missing value" "return +;" "TAP0004" "expression" 9
    , bodyBad "return missing semicolon" "return 1 }" "TAP0006" ";" 10
    , bodyBad "binding missing initializer" "int value = ;" "TAP0004" "expression" 13
    , bodyBad "binding missing equals" "int value 1;" "TAP0006" "=" 11
    , bodyBad "binding missing semicolon" "int value = 1 }" "TAP0006" ";" 15
    , bodyBad "final binding missing name" "final int = 1;" "TAP0006" "identifier" 11
    , bodyBad "if missing parentheses" "if true {}" "TAP0006" "(" 4
    , bodyBad "if missing condition" "if () {}" "TAP0004" "expression" 5
    , bodyBad "if missing close paren" "if (true {}" "TAP0006" ")" 10
    , bodyBad "if missing block" "if (true) return 1;" "TAP0006" "{" 11
    , bodyBad "else missing block" "if (true) {} else return 1;" "TAP0006" "{" 19
    , bodyBad "else if missing parentheses" "if (true) {} else if true {}" "TAP0006" "(" 22
    , bodyBad "else if missing expression" "if (true) {} else if () {}" "TAP0004" "expression" 23
    , bodyBad "call missing argument" "Call(,);" "TAP0004" "expression" 6
    , bodyBad "call missing close paren" "Call(1;" "TAP0006" ")" 7
    , bodyBad "assignment missing value" "value = ;" "TAP0004" "expression" 9
    , bodyBad "assignment missing semicolon" "value = 1 }" "TAP0006" ";" 11
    , bodyBad "non-name assignment" "Call() = 1;" "TAP0003" "target" 1
    , bodyBad "parenthesized incomplete expression" "return (1 +);" "TAP0004" "expression" 12
    , bodyBad "lambda missing arrow" "auto f = \\(int x) x;" "TAP0006" "->" 19
    , bodyBad "lambda missing typed parameter" "auto f = \\(int) -> 1;" "TAP0006" "identifier" 12
    , bodyBad "capture missing name" "auto f = [weak ]\\ -> 1;" "TAP0006" "identifier" 16
    , bodyBad "capture missing initializer" "auto f = [x = ]\\ -> 1;" "TAP0004" "expression" 15
    , bodyBad "capture missing closing bracket" "auto f = [x;" "TAP0009" "]" 12
    , bodyBad "unit diagnostic survives dispatch" "unit value = 1;" "TAP0013" "unit" 1
    ]
    where
        bad label source code message line column = (label, rejectedAt source code message line column)
        bodyBad label body code message column =
            bad label ("class Program { int Value() {\n" ++ body ++ "\n} }") code message 2 column

comparisonTests :: [(String, Bool)]
comparisonTests =
    [ parses "single less comparison" "return a < b;"
    , parses "single less-equal comparison" "return a <= b;"
    , parses "single greater comparison" "return a > b;"
    , parses "single greater-equal comparison" "return a >= b;"
    , parses "single equality" "return a == b;"
    , parses "single inequality" "return a \\= b;"
    , parses "logical conjunction of comparisons" "return a < b && b < c;"
    , parses "logical disjunction of equalities" "return a == b || b == c;"
    , parses "left parenthesized comparison" "return (a < b) < c;"
    , parses "right parenthesized comparison" "return a < (b < c);"
    , parses "left parenthesized equality" "return (a == b) == c;"
    , parses "right parenthesized equality" "return a == (b == c);"
    , parses "separate precedence groups" "return a < b == c < d;"
    , parses "arithmetic inside comparison" "return a + b < c * d;"
    , parses "negated comparison" "return not (a < b);"
    , chain "less chain" "a < b < c" 7
    , chain "greater chain" "a > b > c" 7
    , chain "mixed relational chain" "a <= b > c" 8
    , chain "reverse mixed relational chain" "a >= b < c" 8
    , chain "equality chain" "a == b == c" 8
    , chain "inequality chain" "a \\= b \\= c" 8
    , chain "mixed equality chain" "a == b \\= c" 8
    , chain "chain inside parentheses" "(a < b < c)" 8
    ]
    where
        parses label body = (label, accepted (sourceWith body))
        chain label expression column =
            ( label
            , rejectedAt
                ("class Program { int Value() {\n" ++ expression ++ "\n} }")
                "TAP0014"
                "cannot be chained"
                2
                column
            )

-- The operator ladder is a language contract.  These cases intentionally mix
-- adjacent precedence groups so a parser change cannot silently flatten the
-- tree while still accepting each token in isolation.
operatorPrecedenceTests :: [(String, Bool)]
operatorPrecedenceTests =
    [ parses "power is right associative" "return 2 ** 3 ** 2;"
    , parses "power binds before unary negation" "return -2 ** 2;"
    , parses "bitwise not is an integer prefix operator" "return !mask;"
    , parses "multiplication binds before addition" "return 1 + 2 * 3;"
    , parses "addition binds before shift" "return 1 + 2 << 3;"
    , parses "shift binds before bitwise and" "return 8 >> 1 & 3;"
    , parses "bitwise and binds before xor" "return 7 & 3 ^ 1;"
    , parses "bitwise xor binds before or" "return 7 ^ 3 | 1;"
    , parses "bitwise or binds before comparison" "return flags | mask == expected;"
    , ("pattern combinators are rejected outside is", not (accepted (sourceWith "return ready and valid or fallback;")))
    , ("exclamation inequality alias is rejected", not (accepted (sourceWith "return left != right;")))
    , ("removed membership operator is rejected", not (accepted (sourceWith "return item in values;")))
    , ("removed not-in operator is rejected", not (accepted (sourceWith "return item not in values;")))
    , ("removed cast operator is rejected", not (accepted (sourceWith "return value as Target;")))
    , ("removed nullable cast operator is rejected", not (accepted (sourceWith "return value as? Target;")))
    , ("removed forced cast operator is rejected", not (accepted (sourceWith "return value as! Target;")))
    ]
    where
        parses label body = (label, accepted (sourceWith body))

-- A string payload that happens to spell punctuation must remain a literal.
-- In particular a closing-brace payload must not terminate a block early.
literalTests :: [(String, Bool)]
literalTests =
    [ ("literal payload remains data: " ++ show payload, literalResult payload)
    | payload <-
        [ "if"
        , "else"
        , "return"
        , "final"
        , "class"
        , "namespace"
        , "static"
        , "not"
        , "+"
        , "-"
        , "*"
        , "=="
        , "{"
        , "}"
        , "("
        , ")"
        , "["
        , "]"
        , ";"
        , ","
        , "->"
        , "true"
        , "false"
        ]
    ]
        ++ [ ("keyword text cannot act as class keyword", not (accepted "\"class\" Program {}"))
           , ("String brace cannot open a class", not (accepted "class Program \"{\" }"))
           , ("String semicolon cannot terminate namespace", not (accepted "namespace Demo \";\" class Program {}"))
           , ("String else cannot introduce branch", not (accepted (sourceWith "if (true) {} \"else\" {}")))
           , ("String operator cannot connect operands", not (accepted (sourceWith "return 1 \"+\" 2;")))
           ]

literalResult :: String -> Bool
literalResult payload = case statements (sourceWith ("return " ++ show payload ++ ";")) of
    Just [ReturnStatement _ (Just (LiteralExpression _ (StringLiteral value) ()))] -> value == payload
    _ -> False

statements :: String -> Maybe [Statement Identifier ()]
statements source = case parseSource source of
    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ _ (Block body) _ _]])) -> Just body
    _ -> Nothing

spanTests :: [(String, Bool)]
spanTests =
    [ ("empty closure retains source file and closing brace", closureSpan "\\ -> {}")
    , ("explicit empty capture span includes brackets", closureSpan "[]\\ -> {}")
    , ("strong capture span includes opening bracket", closureSpan "[value]\\ -> {}")
    , ("weak capture span includes mode and delimiters", closureSpan "[weak value]\\ -> {}")
    , ("unowned capture span includes mode and delimiters", closureSpan "[unowned value]\\ -> {}")
    , ("expression closure span includes expression", closureSpan "\\x -> x + 1")
    , ("typed closure span includes parameter syntax", closureSpan "\\(int x) -> x")
    , ("empty if span includes closing block delimiter", ifSpan "if (true) {}")
    , ("empty else span includes final delimiter", ifSpan "if (true) {} else {}")
    , ("else-if span includes complete chain", ifSpan "if (true) {} else if (false) {} else {}")
    , ("guard span runs from the keyword to the closing brace", guardSpan "guard (true) else { return 1; }")
    , ("nested block span includes both braces", blockSpan "{ int inner = 1; }")
    , ("empty nested block span includes both braces", blockSpan "{}")
    , ("match statement span runs from the keyword to the closing brace", matchStatementSpan "match (1) { 1 -> { }, _ -> { } }")
    , ("match statement without arms keeps its span", matchStatementSpan "match (1) { }")
    , ("match expression span excludes the binding around it", matchExpressionSpan "match (1) { 1 -> 2, _ -> 3 }")
    , ("if expression span runs from the keyword to the else block", ifExpressionSpan "if (true) { 1 } else { 2 }")
    , ("value block span includes both braces", valueBlockSpans)
    , ("match arm span starts at its pattern and includes its comma", armSpans)
    , ("match arm without a comma ends at its body", lastArmSpan)
    , ("match pattern span includes its parentheses", patternSpans)
    , ("type pattern span covers the type and the name", typePatternSpan)
    , ("return span includes semicolon", returnSpan)
    , ("binding span starts at type", bindingSpan)
    , ("member span includes access and closing brace", memberSpan)
    ]

closureSpan :: String -> Bool
closureSpan spelling = case statements ("class Program { int Value() {\nauto f = " ++ spelling ++ ";\nreturn 1; } }") of
    Just (BindingStatement _ _ _ _ _ (CallableExpression value _ _ _ _ _) : _) ->
        exactSpan value 2 10 (10 + length spelling)
    _ -> False

ifSpan :: String -> Bool
ifSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
    Just (IfStatement value _ _ _ : _) -> exactSpan value 2 1 (1 + length spelling)
    _ -> False

guardSpan :: String -> Bool
guardSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
    Just (GuardStatement value _ _ : _) -> exactSpan value 2 1 (1 + length spelling)
    _ -> False

blockSpan :: String -> Bool
blockSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
    Just (BlockStatement value _ : _) -> exactSpan value 2 1 (1 + length spelling)
    _ -> False

matchStatementSpan :: String -> Bool
matchStatementSpan spelling = case statements ("class Program { int Value() {\n" ++ spelling ++ "\nreturn 1; } }") of
    Just (ExpressionStatement outer (MatchExpression inner _ _ _) True : _) ->
        exactSpan outer 2 1 (1 + length spelling) && exactSpan inner 2 1 (1 + length spelling)
    _ -> False

-- The initializer starts at column 10 of `auto r = ...;`.
matchExpressionSpan :: String -> Bool
matchExpressionSpan spelling = case statements ("class Program { int Value() {\nauto r = " ++ spelling ++ ";\nreturn r; } }") of
    Just (BindingStatement _ _ _ _ _ (MatchExpression value _ _ _) : _) -> exactSpan value 2 10 (10 + length spelling)
    _ -> False

ifExpressionSpan :: String -> Bool
ifExpressionSpan spelling = case statements ("class Program { int Value() {\nauto r = " ++ spelling ++ ";\nreturn r; } }") of
    Just (BindingStatement _ _ _ _ _ (ConditionalExpression value _ _ _ _) : _) -> exactSpan value 2 10 (10 + length spelling)
    _ -> False

-- `auto r = if (true) { 1 } else { 2 };`: the blocks are at columns 20 to 24
-- and 31 to 35.
valueBlockSpans :: Bool
valueBlockSpans = case statements "class Program { int Value() {\nauto r = if (true) { 1 } else { 2 };\nreturn r; } }" of
    Just (BindingStatement _ _ _ _ _ (ConditionalExpression _ _ (BlockExpression first _ _) (BlockExpression second _ _) _) : _) ->
        exactSpan first 2 20 25 && exactSpan second 2 31 36
    _ -> False

-- `auto r = match (1) { 1 -> 2, _ -> 3 };`: the first arm is `1 -> 2,` at
-- columns 22 to 28 and the second `_ -> 3` at columns 30 to 35.
armSpans :: Bool
armSpans = case statements "class Program { int Value() {\nauto r = match (1) { 1 -> 2, _ -> 3 };\nreturn r; } }" of
    Just (BindingStatement _ _ _ _ _ (MatchExpression _ _ [first, second] _) : _) ->
        exactSpan (matchArmSpan first) 2 22 29 && exactSpan (matchArmSpan second) 2 30 36
    _ -> False

lastArmSpan :: Bool
lastArmSpan = case statements "class Program { int Value() {\nmatch (1) { _ -> { } }\nreturn 1; } }" of
    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> exactSpan (matchArmSpan arm) 2 13 21
    _ -> False

-- `match (1), (2) { (1), _ -> { } }`: the first pattern is `(1)` at columns
-- 18 to 20 and the second `_` at column 23.
patternSpans :: Bool
patternSpans = case statements "class Program { int Value() {\nmatch (1), (2) { (1), _ -> { } }\nreturn 1; } }" of
    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> case matchArmPatterns arm of
        [first, second] -> exactSpan (matchPatternSpan first) 2 18 21 && exactSpan (matchPatternSpan second) 2 23 24
        _ -> False
    _ -> False

typePatternSpan :: Bool
typePatternSpan = case statements "class Program { int Value() {\nmatch (1) { int other -> { } }\nreturn 1; } }" of
    Just (ExpressionStatement _ (MatchExpression _ _ [arm] _) _ : _) -> case matchArmPatterns arm of
        [patternValue] -> exactSpan (matchPatternSpan patternValue) 2 13 22
        _ -> False
    _ -> False

returnSpan :: Bool
returnSpan = case statements "class Program { void Value() {\nreturn;\n} }" of
    Just [ReturnStatement value Nothing] -> exactSpan value 2 1 8
    _ -> False

bindingSpan :: Bool
bindingSpan = case statements "class Program { int Value() {\nint value = 1;\nreturn value; } }" of
    Just (BindingStatement value _ _ _ _ _ : _) -> exactSpan value 2 1 15
    _ -> False

memberSpan :: Bool
memberSpan = case parseSource "class Program {\npublic static void Main() {}\n}" of
    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [member]])) -> exactSpan (declarationSpan member) 2 1 29
    _ -> False

exactSpan :: SourceSpan -> Int -> Int -> Int -> Bool
exactSpan value line start end =
    sourceFile value == "parser-contract.tala"
        && sourceStart value == SourcePosition line start
        && sourceEnd value == SourcePosition line end

tokenStreamTests :: [(String, Bool)]
tokenStreamTests =
    [ ("empty token stream is an empty module", parsesTokens [])
    , ("single EOF is an empty module", parsesTokens [eofToken])
    ,
        ( "valid supplied stream need not have EOF"
        , case tokensOf "class Program {}" of
            Just tokens -> parsesTokens (filter ((/= EndOfFileToken) . tokenKind) tokens)
            Nothing -> False
        )
    , ("EOF cannot hide a declaration suffix", rejectsTokens [eofToken, wordToken] "TAP0015")
    , ("duplicate EOF cannot hide trailing input", rejectsTokens [eofToken, eofToken] "TAP0015")
    ,
        ( "missing namespace terminator without EOF retains error"
        , case tokensOf "namespace Demo" of
            Just tokens -> rejectsTokens (filter ((/= EndOfFileToken) . tokenKind) tokens) "TAP0007"
            Nothing -> False
        )
    ,
        ( "truncated class without EOF terminates"
        , case tokensOf "class Program {" of
            Just tokens -> not (parsesTokens (filter ((/= EndOfFileToken) . tokenKind) tokens))
            Nothing -> False
        )
    , ("String token cannot masquerade as keyword", rejectsTokens [wordToken {tokenKind = StringToken}, eofToken] "TAP0006")
    ]
    where
        spanValue = SourceSpan "tokens.tala" (SourcePosition 1 1) (SourcePosition 1 2)
        eofToken = Token EndOfFileToken "" spanValue
        wordToken = Token KeywordToken "class" spanValue

tokensOf :: String -> Maybe [Token]
tokensOf source = case runLexer defaultLexer (LexerInput "tokens.tala" source) of
    Left _ -> Nothing
    Right tokens -> Just tokens

parsesTokens :: [Token] -> Bool
parsesTokens tokens = case runParser defaultParser (ParserInput "tokens.tala" tokens) of
    Right _ -> True
    Left _ -> False

rejectsTokens :: [Token] -> String -> Bool
rejectsTokens tokens code = case runParser defaultParser (ParserInput "tokens.tala" tokens) of
    Left [problem] -> diagnosticCode problem == code
    _ -> False

-- Type spelling is tested at the parsed boundary. This makes sugar choices
-- observable without relying on later passes that may normalize System types.
-- In particular Array<T, N> keeps two generic arguments: fixed versus dynamic
-- behavior belongs to overload and template resolution, not to a second class.
typeSyntaxTests :: [(String, Bool)]
typeSyntaxTests =
    [ ("simple type keeps compact syntax", returnSyntax "int" == Just (ExplicitType (Identifier "int")))
    ,
        ( "qualified type preserves every name component"
        , returnSyntax "System.Text.String"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Text", Identifier "String"])
                    []
                )
        )
    ,
        ( "generic type preserves its type argument"
        , returnSyntax "System.Array<int>"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
                )
        )
    ,
        ( "Array<T, N> remains one overloaded generic family"
        , returnSyntax "System.Array<Element, Length>"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [ TemplateTypeSyntax (ExplicitType (Identifier "Element"))
                    , TemplateTypeSyntax (ExplicitType (Identifier "Length"))
                    ]
                )
        )
    ,
        ( "nested generic closing delimiters remain independent"
        , returnSyntax "System.Array<System.Array<int>>"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [ TemplateTypeSyntax
                        ( QualifiedTypeSyntax
                            (QualifiedName [Identifier "System", Identifier "Array"])
                            [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
                        )
                    ]
                )
        )
    ,
        ( "built-in fixed array remains distinct from System.Array"
        , returnSyntax "[]int" == Just (BuiltinArrayTypeSyntax (ExplicitType (Identifier "int")))
        )
    ,
        ( "dynamic array sugar has one element type"
        , returnSyntax "[String]" == Just (ArrayTypeSyntax (ExplicitType (Identifier "String")))
        )
    ,
        ( "dictionary sugar preserves key and value types"
        , returnSyntax "[String to int]"
            == Just (DictionaryTypeSyntax (ExplicitType (Identifier "String")) (ExplicitType (Identifier "int")))
        )
    ,
        ( "callable type preserves parameters and result"
        , returnSyntax "(int, String) -> bool"
            == Just
                ( CallableTypeSyntax
                    [ExplicitType (Identifier "int"), ExplicitType (Identifier "String")]
                    (ExplicitType (Identifier "bool"))
                )
        )
    ,
        ( "zero-parameter callable type is accepted"
        , returnSyntax "() -> void" == Just (CallableTypeSyntax [] (ExplicitType (Identifier "void")))
        )
    ,
        ( "compound binding dispatch recognizes a qualified generic type"
        , bindingSyntax "System.Array<int> values = Source();"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
                )
        )
    ,
        ( "compound binding dispatch recognizes dictionary sugar"
        , bindingSyntax "[String to int] values = Source();"
            == Just (DictionaryTypeSyntax (ExplicitType (Identifier "String")) (ExplicitType (Identifier "int")))
        )
    ,
        ( "callable parameter dispatch recognizes a compound type"
        , callableParameterSyntax "\\(System.Array<int> values) -> 1"
            == Just
                ( QualifiedTypeSyntax
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [TemplateTypeSyntax (ExplicitType (Identifier "int"))]
                )
        )
    , ("underscore parameter label remains accepted", parameterNameFor "_ int value" == Just (Identifier "value"))
    , ("explicit parameter label is accepted", parameterNameFor "input: int value" == Just (Identifier "value"))
    , ("generic argument list cannot be empty", hasParserCode "TAP0016" (functionWithReturn "Array<>"))
    , ("qualified type cannot end after a dot", rejected (parseSource (functionWithReturn "System.")))
    , ("generic type requires a closing delimiter", rejected (parseSource (functionWithReturn "Array<int")))
    ,
        ( "fixed array sugar preserves its compile-time size"
        , case returnSyntax "[int; 3]" of
            Just (FixedArrayTypeSyntax (ExplicitType (Identifier "int")) (TemplateIntegerSyntax _ 3)) -> True
            _ -> False
        )
    ,
        ( "dynamic array sugar normalizes to System.Array<T>"
        , typedParameterType "[int]"
            == Just
                ( NamedType
                    (QualifiedName [Identifier "System", Identifier "Array"])
                    [TypeTemplateArgument intType]
                )
        )
    ,
        ( "dictionary sugar normalizes to System.Dictionary<K, V>"
        , typedParameterType "[String to int]"
            == Just
                ( NamedType
                    (QualifiedName [Identifier "System", Identifier "Dictionary"])
                    [TypeTemplateArgument stringType, TypeTemplateArgument intType]
                )
        )
    ,
        ( "built-in array has no invented public class name"
        , typedParameterType "[]int"
            == Just (NamedType (QualifiedName [Identifier "[]"]) [TypeTemplateArgument intType])
        )
    ,
        ( "callable syntax becomes a structural FunctionType"
        , typedParameterType "(int, String) -> bool"
            == Just (FunctionType [intType, stringType] boolType)
        )
    ]

functionWithReturn :: String -> String
functionWithReturn spelling = "class Program { " ++ spelling ++ " Value() { return Source(); } }"

returnSyntax :: String -> Maybe TypeSyntax
returnSyntax spelling = case parseSource (functionWithReturn spelling) of
    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ syntax _ _ _ _]])) -> Just syntax
    _ -> Nothing

bindingSyntax :: String -> Maybe TypeSyntax
bindingSyntax spelling = case statements (sourceWith spelling) of
    Just (BindingStatement _ _ syntax _ _ _ : _) -> Just syntax
    _ -> Nothing

callableParameterSyntax :: String -> Maybe TypeSyntax
callableParameterSyntax spelling = case statements (sourceWith ("auto value = " ++ spelling ++ ";")) of
    Just (BindingStatement _ _ _ _ _ (CallableExpression _ _ _ [parameter] _ _) : _) ->
        Just (parameterTypeSyntax parameter)
    _ -> Nothing

parameterNameFor :: String -> Maybe Identifier
parameterNameFor spelling = case parseSource ("class Program { void Apply(" ++ spelling ++ ") { return; } }") of
    Right (ParsedAST (SyntaxTree _ [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]])) ->
        Just (parameterName parameter)
    _ -> Nothing

typedParameterType :: String -> Maybe Type
typedParameterType spelling =
    case compileToCorePrep
        (CompilerInput "type-contract.tala" ("class Program { void Apply(_ " ++ spelling ++ " value) { return; } }")) of
        Right artifacts ->
            case syntaxDeclarations (typedSyntaxTree (artifactTypedAST artifacts)) of
                [TypeDeclaration _ _ _ [FunctionDeclaration _ _ _ _ [parameter] _ _ _]] ->
                    Just (parameterAnnotation parameter)
                _ -> Nothing
        Left _ -> Nothing

hasParserCode :: String -> String -> Bool
hasParserCode code source = case parseSource source of
    Left problems -> any (\problem -> diagnosticStage problem == ParserStage && diagnosticCode problem == code) problems
    Right _ -> False

rejected :: Either problems value -> Bool
rejected result = case result of
    Left _ -> True
    Right _ -> False

-- Complete compilation proves that the nested representation of else-if is
-- understood by name resolution, typing, Core optimization, and CorePrep.
pipelineTests :: [(String, Bool)]
pipelineTests =
    [
        ( "first branch of else-if folds"
        , returnsInteger "if (true) { return 1; } else if (true) { return 2; } else { return 3; }" 1
        )
    ,
        ( "middle branch of else-if folds"
        , returnsInteger "if (false) { return 1; } else if (true) { return 2; } else { return 3; }" 2
        )
    ,
        ( "last branch of else-if folds"
        , returnsInteger "if (false) { return 1; } else if (false) { return 2; } else { return 3; }" 3
        )
    ,
        ( "unbraced else-if still requires condition parentheses"
        , not (accepted (sourceWith "if (false) {} else if true {} return 0;"))
        )
    , ("parenthesized comparison results type-check", compiles "return (1 < 2) == (2 < 3);")
    , ("explicit conjunction reaches CorePrep", compiles "return 1 < 2 && 2 < 3;")
    , ("mutable local dispatch reaches CorePrep", returnsInteger "int value = 1; value = 2; return 3;" 3)
    , ("immutable local dispatch reaches CorePrep", returnsInteger "final int value = 7; return value;" 7)
    , ("logical not of name is not a declaration", compiles "bool flag = true; return not flag;")
    , ("floating comparison reaches CorePrep", compiles "return 2.5 < 3.5;")
    , ("floating equality reaches CorePrep", compiles "return 2.5 == 3.5;")
    ,
        ( "live Boolean equality reaches CorePrep"
        , case compileToCorePrep
            (CompilerInput "live.tala" "class Program { bool Same(bool left, bool right) { return left == right; } }") of
            Right _ -> True
            Left _ -> False
        )
    ]

returnsInteger :: String -> Integer -> Bool
returnsInteger body expected = case compileToCorePrep (CompilerInput "parser-contract.tala" (sourceWith body)) of
    Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
        [function] -> case reverse (coreFunctionBody function) of
            CoreReturn (CoreLiteral (CoreInteger value) _) : _ -> value == expected
            _ -> False
        _ -> False
    Left _ -> False

compiles :: String -> Bool
compiles body = case compileToCorePrep (CompilerInput "parser-contract.tala" ("class Program { bool Value() { " ++ body ++ " } }")) of
    Right _ -> True
    Left _ -> False

-- Check the computed result, not only successful compilation. Coercing every
-- integer operand to bool can otherwise make equality silently return true.
booleanOperandTests :: [(String, Bool)]
booleanOperandTests =
    [ ("Boolean result preserves operands: " ++ expression, returnsBoolean expression expected)
    | (expression, expected) <-
        [ ("1 == 2", False)
        , ("2 == 2", True)
        , ("1 \\= 2", True)
        , ("2 \\= 2", False)
        , ("1 < 2", True)
        , ("2 < 1", False)
        , ("2 <= 2", True)
        , ("2 > 1", True)
        , ("1 >= 2", False)
        , ("(1 < 2) == (3 < 4)", True)
        , ("1 < 2 && 3 > 4", False)
        , ("1 > 2 || 3 < 4", True)
        , ("not (1 == 2)", True)
        , ("not (1 < 2)", False)
        , ("not 0", True)
        , ("not 7", False)
        , ("1 \\= 2", True)
        , ("2 \\= 2", False)
        , ("1 + 2 == 3", True)
        , ("1 + 2 == 4", False)
        , ("true == false", False)
        , ("true \\= false", True)
        ]
    ]

returnsBoolean :: String -> Bool -> Bool
returnsBoolean expression expected =
    case compileToCorePrep
        (CompilerInput "parser-contract.tala" ("class Program { bool Value() { return " ++ expression ++ "; } }")) of
        Right artifacts -> case coreModuleFunctions (artifactOptimizedCore artifacts) of
            [function] -> case coreFunctionBody function of
                [CoreReturn (CoreLiteral (CoreBoolean value) _)] -> value == expected
                _ -> False
            _ -> False
        Left _ -> False