tala-compiler-0.1.0: test/StaticMemberParserTests.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Grammar-boundary tests for member selectors. These assert the parsed tree,
not merely successful token consumption, so later stages receive an explicit
receiver and a separately preserved member spelling.
-}
module StaticMemberParserTests (staticMemberParserTests) where
import Progmasoft.Tala.AST
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.Lexer
import Progmasoft.Tala.Parser
staticMemberParserTests :: [(String, Bool)]
staticMemberParserTests =
[ ("one-level member call has a selector node", oneLevelCall)
, ("member call keeps its receiver as a name", receiverIsPreserved)
, ("member selector keeps the source member spelling", memberSpellingIsPreserved)
, ("method call wraps the selector rather than flattening its name", callWrapsSelector)
, ("qualified selectors associate from the left", selectorsAssociateLeft)
, ("qualified selector chain preserves all path components", selectorPathIsPreserved)
, ("ordinary call remains an ordinary call", ordinaryCallHasNoSelector)
, ("selector may be the receiver of an outer call", selectorCanBeNested)
, ("selector call may appear as a binding initializer", selectorBindingInitializer)
, ("selector call may appear in a return expression", selectorReturnExpression)
, ("selector call may appear below arithmetic", selectorArithmeticOperand)
, ("selector call may appear below a comparison", selectorComparisonOperand)
, ("selector call may appear in a condition", selectorCondition)
, ("selector call may appear in a callable expression", selectorCallableExpression)
, ("selector spelling is case-sensitive", selectorCaseIsPreserved)
, ("selector span begins at its receiver", selectorSpanBeginsAtReceiver)
, ("selector span ends at the selected name", selectorSpanEndsAtMember)
, ("call span includes its closing parenthesis", callSpanIncludesClose)
, ("selector after a parenthesized receiver parses", parenthesizedReceiver)
, ("a trailing dot without a member is rejected", trailingDotRejected)
, ("a dot without a receiver is a target-typed member selection", leadingDotIsTargetTyped)
, ("two dots without a component are rejected", repeatedDotRejected)
, ("a numeric token cannot be a member name", numericMemberRejected)
, ("a keyword cannot be a member name", keywordMemberRejected)
, ("a missing call close remains a parser diagnostic", missingCloseRejected)
, ("a missing call argument remains a parser diagnostic", missingArgumentRejected)
, ("a second selector after a call is retained", selectorAfterCall)
, ("a selector after a nested call is retained", nestedCallSelector)
, ("comment punctuation does not create a selector", commentIsNotSelector)
, ("string punctuation does not create a selector", stringIsNotSelector)
, ("raw string punctuation does not create a selector", rawStringIsNotSelector)
, ("character punctuation does not create a selector", characterIsNotSelector)
, ("selector expression is not an assignment target", selectorAssignmentRejected)
, ("selector expression can be grouped without flattening", groupedSelector)
, ("a selector is not confused with a namespace declaration", namespaceAndSelector)
, ("a selector can be nested in a second argument", secondArgumentSelector)
, ("multiple selector calls retain argument order", selectorArgumentsPreserved)
, ("zero-argument selector call has an empty argument list", zeroArgumentCall)
, ("member selector without a call is syntactically representable", bareSelector)
, ("selector paths can contain many components", longSelectorPath)
, ("member access remains available after unary grouping", unaryGroupedSelector)
, ("member access remains available under Boolean negation", negatedSelector)
, ("member access remains available under bitwise negation", bitwiseNegatedSelector)
, ("member access remains available in a nested block", nestedBlockSelector)
, ("member access remains available in an else block", elseBlockSelector)
, ("member access remains available in a while condition", whileConditionSelector)
, ("member access remains available in a loop body", loopBodySelector)
, ("a malformed selector reports the parser stage", malformedSelectorHasStage)
, ("a malformed selector keeps a source position", malformedSelectorHasSpan)
, ("a selector's member name is not resolved by the parser", selectorIsUnresolvedSyntax)
]
oneLevelCall :: Bool
oneLevelCall = case parseSource "class Program { void Run() { Counter.Current(); return; } }" of
Right
( ParsedAST
( SyntaxTree
_
[ TypeDeclaration
{ typeMembers =
[ FunctionDeclaration
{ declarationBody = Block (ExpressionStatement _ (CallExpression _ (MemberAccessExpression {}) [] _) True : _)
}
]
}
]
)
) -> True
_ -> False
receiverIsPreserved :: Bool
receiverIsPreserved = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
Just
(CallExpression _ (MemberAccessExpression _ (NameExpression _ (Identifier "Counter") _) (Identifier "Current") _) _ _) -> True
_ -> False
memberSpellingIsPreserved :: Bool
memberSpellingIsPreserved = case firstCall "class Program { void Run() { Math.Sqrt(4.0); return; } }" of
Just (CallExpression _ (MemberAccessExpression _ _ (Identifier "Sqrt") _) _ _) -> True
_ -> False
callWrapsSelector :: Bool
callWrapsSelector = case firstCall "class Program { void Run() { Service.Start(); return; } }" of
Just CallExpression {} -> case firstCallCallee "class Program { void Run() { Service.Start(); return; } }" of
Just (MemberAccessExpression {}) -> True
_ -> False
_ -> False
selectorsAssociateLeft :: Bool
selectorsAssociateLeft = case firstCallCallee "class Program { void Run() { A.B.C(); return; } }" of
Just
( MemberAccessExpression
_
(MemberAccessExpression _ (NameExpression _ (Identifier "A") _) (Identifier "B") _)
(Identifier "C")
_
) -> True
_ -> False
selectorPathIsPreserved :: Bool
selectorPathIsPreserved = case firstCallCallee "class Program { void Run() { One.Two.Three.Four(); return; } }" of
Just callee -> selectorPath callee == map Identifier ["One", "Two", "Three", "Four"]
_ -> False
ordinaryCallHasNoSelector :: Bool
ordinaryCallHasNoSelector = case firstCallCallee "class Program { void Run() { Execute(); return; } }" of
Just NameExpression {} -> True
_ -> False
selectorCanBeNested :: Bool
selectorCanBeNested = case firstCall "class Program { void Run() { Consume(Counter.Current()); return; } }" of
Just (CallExpression _ _ [CallExpression _ (MemberAccessExpression {}) [] _] _) -> True
_ -> False
selectorBindingInitializer :: Bool
selectorBindingInitializer = case bindingInitializer "class Program { void Run() { int value = Counter.Current(); return; } }" of
Just CallExpression {} -> True
_ -> False
selectorReturnExpression :: Bool
selectorReturnExpression = case firstExpression "class Program { int Run() { return Counter.Current(); } }" of
Just (CallExpression _ (MemberAccessExpression {}) [] _) -> True
_ -> False
selectorArithmeticOperand :: Bool
selectorArithmeticOperand = case firstExpression "class Program { int Run() { return Counter.Current() + 1; } }" of
Just (BinaryExpression _ Add CallExpression {} _ _) -> True
_ -> False
selectorComparisonOperand :: Bool
selectorComparisonOperand = case firstExpression "class Program { bool Run() { return Counter.Current() < 1; } }" of
Just (BinaryExpression _ LessThan CallExpression {} _ _) -> True
_ -> False
selectorCondition :: Bool
selectorCondition = case firstExpression "class Program { void Run() { if (Counter.Ready()) { return; } return; } }" of
Just CallExpression {} -> True
_ -> False
selectorCallableExpression :: Bool
selectorCallableExpression = case bindingInitializer "class Program { void Run() { auto action = \\() -> Counter.Current(); return; } }" of
Just CallableExpression {} -> True
_ -> False
selectorCaseIsPreserved :: Bool
selectorCaseIsPreserved =
selectorPathFor "class Program { void Run() { Counter.cUrReNt(); return; } }"
== Just [Identifier "Counter", Identifier "cUrReNt"]
selectorSpanBeginsAtReceiver :: Bool
selectorSpanBeginsAtReceiver = case firstCallCallee "class Program { void Run() { Counter.Current(); return; } }" of
Just expression -> sourceStart (spanOf expression) == SourcePosition 1 30
_ -> False
selectorSpanEndsAtMember :: Bool
selectorSpanEndsAtMember = case firstCallCallee "class Program { void Run() { Counter.Current(); return; } }" of
Just expression -> sourceEnd (spanOf expression) == SourcePosition 1 45
_ -> False
callSpanIncludesClose :: Bool
callSpanIncludesClose = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
Just expression -> sourceEnd (spanOf expression) == SourcePosition 1 47
_ -> False
parenthesizedReceiver :: Bool
parenthesizedReceiver =
selectorPathFor "class Program { void Run() { (Counter).Current(); return; } }"
== Just [Identifier "Counter", Identifier "Current"]
trailingDotRejected :: Bool
trailingDotRejected = parseSource "class Program { void Run() { Counter.; return; } }" `isLeft` True
-- `.Member` selects from the enum that the context expects. The parser keeps
-- the missing receiver as the unit literal, which no source can write; whether
-- there is an enum to select from is the type checker's question.
leadingDotIsTargetTyped :: Bool
leadingDotIsTargetTyped = case firstExpression "class Program { void Run() { .Current(); return; } }" of
Just (CallExpression _ (MemberAccessExpression _ (LiteralExpression _ UnitLiteral _) (Identifier "Current") _) [] _) -> True
_ -> False
repeatedDotRejected :: Bool
repeatedDotRejected = parseSource "class Program { void Run() { Counter..Current(); return; } }" `isLeft` True
numericMemberRejected :: Bool
numericMemberRejected = parseSource "class Program { void Run() { Counter.7(); return; } }" `isLeft` True
keywordMemberRejected :: Bool
keywordMemberRejected = parseSource "class Program { void Run() { Counter.return(); return; } }" `isLeft` True
missingCloseRejected :: Bool
missingCloseRejected = parseSource "class Program { void Run() { Counter.Current(1; return; } }" `isLeft` True
missingArgumentRejected :: Bool
missingArgumentRejected = parseSource "class Program { void Run() { Counter.Current(, 1); return; } }" `isLeft` True
selectorAfterCall :: Bool
selectorAfterCall =
selectorPathFor "class Program { void Run() { Counter.Factory().Current(); return; } }"
== Just [Identifier "Counter", Identifier "Factory", Identifier "Current"]
nestedCallSelector :: Bool
nestedCallSelector =
selectorPathFor "class Program { void Run() { Counter.Factory(Next()).Current(); return; } }"
== Just [Identifier "Counter", Identifier "Factory", Identifier "Current"]
commentIsNotSelector :: Bool
commentIsNotSelector = case parseSource "class Program { void Run() { Execute(); -- Fake.Member()\n return; } }" of
Right _ -> True
_ -> False
stringIsNotSelector :: Bool
stringIsNotSelector = firstCall "class Program { void Run() { Print(\"Fake.Member()\"); return; } }" `hasArguments` 1
rawStringIsNotSelector :: Bool
rawStringIsNotSelector = firstCall "class Program { void Run() { Print([[Fake.Member()]]); return; } }" `hasArguments` 1
characterIsNotSelector :: Bool
characterIsNotSelector = firstCall "class Program { void Run() { Print('.'); return; } }" `hasArguments` 1
selectorAssignmentRejected :: Bool
selectorAssignmentRejected = parseSource "class Program { void Run() { Counter.Current = 1; return; } }" `isLeft` True
groupedSelector :: Bool
groupedSelector =
selectorPathFor "class Program { void Run() { (Counter.Current)(); return; } }"
== Just [Identifier "Counter", Identifier "Current"]
namespaceAndSelector :: Bool
namespaceAndSelector = case parseSource "namespace Demo.Core; class Program { void Run() { Counter.Current(); return; } }" of
Right (ParsedAST (SyntaxTree (Just (QualifiedName [Identifier "Demo", Identifier "Core"])) _)) -> True
_ -> False
secondArgumentSelector :: Bool
secondArgumentSelector = firstCall "class Program { void Run() { Pair(1, Counter.Current()); return; } }" `hasArguments` 2
selectorArgumentsPreserved :: Bool
selectorArgumentsPreserved = case firstCall "class Program { void Run() { Counter.Sum(1, 2); return; } }" of
Just
( CallExpression
_
(MemberAccessExpression {})
[LiteralExpression _ (IntegerLiteral 1) _, LiteralExpression _ (IntegerLiteral 2) _]
_
) -> True
_ -> False
zeroArgumentCall :: Bool
zeroArgumentCall = case firstCall "class Program { void Run() { Counter.Current(); return; } }" of
Just (CallExpression _ _ [] _) -> True
_ -> False
bareSelector :: Bool
bareSelector = case firstExpression "class Program { void Run() { Counter.Current; return; } }" of
Just (MemberAccessExpression {}) -> True
_ -> False
longSelectorPath :: Bool
longSelectorPath =
selectorPathFor "class Program { void Run() { A.B.C.D.E.F.G.H.I.J(); return; } }"
== Just (map Identifier ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"])
unaryGroupedSelector :: Bool
unaryGroupedSelector = case firstExpression "class Program { int Run() { return -(Counter.Current()); } }" of
Just (UnaryExpression _ UnaryNegate CallExpression {} _) -> True
_ -> False
negatedSelector :: Bool
negatedSelector = case firstExpression "class Program { bool Run() { return not Counter.Ready(); } }" of
Just (UnaryExpression _ LogicalNot CallExpression {} _) -> True
_ -> False
bitwiseNegatedSelector :: Bool
bitwiseNegatedSelector = case firstExpression "class Program { int Run() { return !Counter.Mask(); } }" of
Just (UnaryExpression _ BitwiseNot CallExpression {} _) -> True
_ -> False
nestedBlockSelector :: Bool
nestedBlockSelector = case parseSource "class Program { void Run() { if (true) { if (true) { Counter.Current(); } } return; } }" of
Right _ -> True
_ -> False
elseBlockSelector :: Bool
elseBlockSelector = case parseSource "class Program { void Run() { if (true) { return; } else { Counter.Current(); } return; } }" of
Right _ -> True
_ -> False
whileConditionSelector :: Bool
whileConditionSelector = case parseSource "class Program { void Run() { while (Counter.Ready()) { break; } return; } }" of
Right _ -> True
_ -> False
loopBodySelector :: Bool
loopBodySelector = case parseSource "class Program { void Run() { while (true) { Counter.Step(); break; } return; } }" of
Right _ -> True
_ -> False
malformedSelectorHasStage :: Bool
malformedSelectorHasStage = case parseSource "class Program { void Run() { Counter.; return; } }" of
Left (diagnostic : _) -> diagnosticStage diagnostic == ParserStage
_ -> False
malformedSelectorHasSpan :: Bool
malformedSelectorHasSpan = case parseSource "class Program { void Run() { Counter.; return; } }" of
Left diagnostics -> any ((/= Nothing) . diagnosticSpan) diagnostics
_ -> False
selectorIsUnresolvedSyntax :: Bool
selectorIsUnresolvedSyntax = case parseSource "class Program { void Run() { Counter.Current(); return; } }" of
Right (ParsedAST (SyntaxTree _ [TypeDeclaration {typeMembers = [FunctionDeclaration {declarationBody = body}]}])) ->
case blockStatements body of
ExpressionStatement _ (CallExpression _ (MemberAccessExpression _ _ (Identifier "Current") ()) _ ()) True : _ -> True
_ -> False
_ -> False
parseSource :: String -> Either [Diagnostic] ParsedAST
parseSource source = do
tokens <- runLexer defaultLexer (LexerInput "static-member-parser.tala" source)
runParser defaultParser (ParserInput "static-member-parser.tala" tokens)
firstCall :: String -> Maybe (Expression Identifier ())
firstCall source = case firstExpression source of
Just expression -> case expression of
call@CallExpression {} -> Just call
_ -> nestedCall expression
Nothing -> Nothing
where
nestedCall (CallExpression _ _ arguments _) = firstJust [firstCallIn value | value <- arguments]
nestedCall (MemberAccessExpression _ receiver _ _) = nestedCall receiver
nestedCall (UnaryExpression _ _ value _) = nestedCall value
nestedCall (BinaryExpression _ _ left right _) = firstJust [nestedCall left, nestedCall right]
nestedCall _ = Nothing
firstCallIn :: Expression Identifier () -> Maybe (Expression Identifier ())
firstCallIn expression = case expression of
call@CallExpression {} -> Just call
MemberAccessExpression _ receiver _ _ -> firstCallIn receiver
MethodReferenceExpression _ receiver _ _ -> firstCallIn receiver
UnaryExpression _ _ value _ -> firstCallIn value
BinaryExpression _ _ left right _ -> firstJust [firstCallIn left, firstCallIn right]
_ -> Nothing
firstCallCallee :: String -> Maybe (Expression Identifier ())
firstCallCallee source = do
CallExpression _ callee _ _ <- firstCall source
pure callee
firstExpression :: String -> Maybe (Expression Identifier ())
firstExpression source = do
ParsedAST (SyntaxTree _ declarations) <- either (const Nothing) Just (parseSource source)
declaration <- firstMember declarations
case declaration of
FunctionDeclaration {declarationBody = Block statements} -> firstExpressionStatement statements
_ -> Nothing
firstMember :: [Declaration name annotation] -> Maybe (Declaration name annotation)
firstMember declarations =
firstJust [firstMember members | TypeDeclaration {typeMembers = members} <- declarations]
`orElse` firstFunction declarations
where
firstFunction values = firstJust [Just declaration | declaration@FunctionDeclaration {} <- values]
firstExpressionStatement :: [Statement name annotation] -> Maybe (Expression name annotation)
firstExpressionStatement statements = firstJust (map direct statements) `orElse` firstJust (map nested statements)
where
direct statement = case statement of
ExpressionStatement _ expression _ -> Just expression
ReturnStatement _ value -> value
IfStatement _ condition _ _ -> Just condition
WhileStatement _ condition _ -> Just condition
DoWhileStatement _ _ condition -> Just condition
ForStatement _ _ condition _ _ -> condition
ForEachStatement _ _ _ _ _ source _ -> Just source
BreakStatement _ value -> value
_ -> Nothing
nested (IfStatement _ _ yes no) = firstExpressionStatement (blockStatements yes ++ maybe [] blockStatements no)
nested (WhileStatement _ _ body) = firstExpressionStatement (blockStatements body)
nested (DoWhileStatement _ body _) = firstExpressionStatement (blockStatements body)
nested (ForStatement _ _ _ _ body) = firstExpressionStatement (blockStatements body)
nested (ForEachStatement _ _ _ _ _ _ body) = firstExpressionStatement (blockStatements body)
nested _ = Nothing
bindingInitializer :: String -> Maybe (Expression Identifier ())
bindingInitializer source = do
ParsedAST (SyntaxTree _ declarations) <- either (const Nothing) Just (parseSource source)
findBinding declarations
where
findBinding declarations =
firstJust
[ findBlock body
| TypeDeclaration {typeMembers = members} <- declarations
, FunctionDeclaration {declarationBody = body} <- members
]
findBlock (Block statements) = firstJust [Just value | BindingStatement _ _ _ _ _ value <- statements] `orElse` firstJust (map nested statements)
nested (IfStatement _ _ yes no) = findBlock (Block (blockStatements yes ++ maybe [] blockStatements no))
nested _ = Nothing
selectorPath :: Expression Identifier annotation -> [Identifier]
selectorPath expression = case expression of
NameExpression _ name _ -> [name]
MemberAccessExpression _ receiver member _ -> selectorPath receiver ++ [member]
MethodReferenceExpression _ receiver member _ -> selectorPath receiver ++ [member]
CallExpression _ callee _ _ -> selectorPath callee
_ -> []
selectorPathFor :: String -> Maybe [Identifier]
selectorPathFor source = selectorPath <$> firstCallCallee source
hasArguments :: Maybe (Expression Identifier ()) -> Int -> Bool
hasArguments (Just (CallExpression _ _ arguments _)) expected = length arguments == expected
hasArguments _ _ = False
spanOf :: Expression name annotation -> SourceSpan
spanOf expression = case expression of
NameExpression spanValue _ _ -> spanValue
LiteralExpression spanValue _ _ -> spanValue
MemberAccessExpression spanValue _ _ _ -> spanValue
MethodReferenceExpression spanValue _ _ _ -> spanValue
CallExpression spanValue _ _ _ -> spanValue
UnaryExpression spanValue _ _ _ -> spanValue
BinaryExpression spanValue _ _ _ _ -> spanValue
IsPatternExpression spanValue _ _ _ -> spanValue
ConditionalExpression spanValue _ _ _ _ -> spanValue
CoalesceExpression spanValue _ _ _ -> spanValue
AssignmentExpression spanValue _ _ _ _ -> spanValue
IncrementExpression spanValue _ _ _ -> spanValue
LoopExpression spanValue _ _ -> spanValue
BlockExpression spanValue _ _ -> spanValue
MatchExpression spanValue _ _ _ -> spanValue
CallableExpression spanValue _ _ _ _ _ -> spanValue
isLeft :: Either [a] b -> Bool -> Bool
isLeft value expected = case value of
Left _ -> expected
Right _ -> not expected
firstJust :: [Maybe value] -> Maybe value
firstJust values = case values of
Just value : _ -> Just value
Nothing : remaining -> firstJust remaining
[] -> Nothing
orElse :: Maybe value -> Maybe value -> Maybe value
orElse first second = case first of Just _ -> first; Nothing -> second