packages feed

tala-compiler-0.1.0: test/CoreLinearInliningTests.hs

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

module CoreLinearInliningTests (coreLinearInliningTests) where

import Data.List (find, nub)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.CorePrep (prepareCore)
import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
import Progmasoft.Tala.Core.Optimizer
import Progmasoft.Tala.Core.Symbols
import Progmasoft.Tala.Core.Verifier (verifyCore)

-- These tests deliberately construct Core directly. Source tests prove that
-- frontend lowering reaches Core; this suite owns the optimizer's identity,
-- evaluation-order, budget, and CorePrep boundary contracts.
coreLinearInliningTests :: [(String, Bool)]
coreLinearInliningTests =
    [ ("linear inliner accepts one immutable local", immutableLocal)
    , ("linear inliner accepts several immutable locals", severalLocals)
    , ("linear inliner preserves local dependency order", localDependencyOrder)
    , ("linear inliner substitutes a literal parameter", literalParameter)
    , ("linear inliner substitutes a variable parameter", variableParameter)
    , ("linear inliner binds a primitive argument once", primitiveArgumentOnce)
    , ("linear inliner binds a direct-call argument once", callArgumentOnce)
    , ("linear inliner preserves a failing argument", failingArgumentOnce)
    , ("linear inliner preserves an allocating argument", allocatingArgumentOnce)
    , ("linear inliner retains left-to-right argument order", argumentOrder)
    , ("linear inliner retains an unused non-trivial argument", unusedArgument)
    , ("linear inliner can discard an unused literal", unusedLiteral)
    , ("linear inliner converts pure evaluation statements", evaluationStep)
    , ("linear inliner keeps multiple evaluations ordered", evaluationOrder)
    , ("linear inliner rejects mutable local state", mutableBodyRejected)
    , ("linear inliner rejects assignment", assignmentBodyRejected)
    , ("linear inliner rejects branches", branchBodyRejected)
    , ("linear inliner rejects early returns", earlyReturnRejected)
    , ("linear inliner rejects missing returns", missingReturnRejectedBeforeOptimization)
    , ("linear inliner starts fresh ids above the entire module", freshFrontier)
    , ("separate call sites receive disjoint local ids", disjointCallSites)
    , ("copied expression lets receive fresh ids", nestedLetFreshened)
    , ("statement candidate accounting is explicit", statementCandidateReport)
    , ("expression candidate accounting remains explicit", expressionCandidateReport)
    , ("non-linear pure bodies are counted", rejectedBodyReport)
    , ("generated let accounting is explicit", generatedLetReport)
    , ("alpha-renaming accounting is explicit", alphaRenameReport)
    , ("expanded body obeys the node budget", expandedBudget)
    , ("candidate estimate obeys the node budget", candidateBudget)
    , ("linear inlining output verifies as Core", outputVerifies)
    , ("linear inlining output prepares successfully", outputPrepares)
    , ("prepared linear-inlining output verifies", preparedOutputVerifies)
    , ("linear inlining is deterministic", deterministicOutput)
    , ("linear inlining reaches an idempotent fixed point", idempotentOutput)
    , ("symbol inventory includes expression lets", inventoryIncludesLets)
    , ("symbol inventory includes closure-owned identities", inventoryIncludesClosure)
    , ("symbol inventory preserves deterministic structural order", inventoryOrder)
    ]

moduleName :: QualifiedName
moduleName = QualifiedName [Identifier "LinearInlining"]

entryName, helperName, leafName :: ResolvedName
entryName = named 1 "Entry"
helperName = named 2 "Helper"
leafName = named 3 "Leaf"

parameterA, parameterB, localA, localB, callerValue :: ResolvedName
parameterA = named 10 "a"
parameterB = named 11 "b"
localA = named 20 "first"
localB = named 21 "second"
callerValue = named 30 "input"

named :: Int -> String -> ResolvedName
named value spelling = ResolvedName (SymbolId value) (Identifier spelling)

integer :: Integer -> CoreExpression
integer value = CoreLiteral (CoreInteger value) intType

boolean :: Bool -> CoreExpression
boolean value = CoreLiteral (CoreBoolean value) boolType

variable :: ResolvedName -> Type -> CoreExpression
variable = CoreVariable

primitive :: CorePrimitive -> [CoreExpression] -> Type -> CoreExpression
primitive = CorePrimitive

call :: ResolvedName -> [Type] -> Type -> [CoreExpression] -> CoreExpression
call name parameterTypes resultType arguments =
    CoreApply (variable name (FunctionType parameterTypes resultType)) arguments resultType

binding :: ResolvedName -> Type -> Bool -> CoreExpression -> CoreStatement
binding name valueType mutable value = CoreBind (CoreBinding name valueType mutable value)

function :: ResolvedName -> [(ResolvedName, Type)] -> Type -> [CoreStatement] -> CoreFunction
function = CoreFunction

entry :: Type -> CoreExpression -> CoreFunction
entry resultType value = function entryName [] resultType [CoreReturn value]

moduleWith :: [CoreFunction] -> CoreModule
moduleWith = CoreModule moduleName

invocation :: [Type] -> Type -> [CoreExpression] -> CoreExpression
invocation = call helperName

inspectionOptions :: OptimizerOptions
inspectionOptions =
    defaultOptimizerOptions
        { optimizerMaximumIterations = 1
        , optimizerConstantPropagation = False
        , optimizerControlFlowSimplification = False
        , optimizerDeadCodeElimination = False
        }

optimize :: CoreModule -> Maybe OptimizationResult
optimize value = either (const Nothing) Just (optimizeCoreWith inspectionOptions value)

optimizeWith :: OptimizerOptions -> CoreModule -> Maybe OptimizationResult
optimizeWith options value = either (const Nothing) Just (optimizeCoreWith options value)

entryReturn :: OptimizationResult -> Maybe CoreExpression
entryReturn result = do
    selected <-
        find ((== resolvedSymbol entryName) . resolvedSymbol . coreFunctionName) (coreModuleFunctions (optimizedCore result))
    case coreFunctionBody selected of
        [CoreReturn value] -> Just value
        _ -> Nothing

optimizedEntry :: CoreModule -> Maybe CoreExpression
optimizedEntry value = optimize value >>= entryReturn

identityHelper :: CoreFunction
identityHelper = function helperName [(parameterA, intType)] intType [CoreReturn (variable parameterA intType)]

oneLocalHelper :: CoreFunction
oneLocalHelper =
    function
        helperName
        [(parameterA, intType)]
        intType
        [ binding localA intType False (primitive CoreAdd [variable parameterA intType, integer 1] intType)
        , CoreReturn (variable localA intType)
        ]

twoLocalHelper :: CoreFunction
twoLocalHelper =
    function
        helperName
        [(parameterA, intType)]
        intType
        [ binding localA intType False (primitive CoreAdd [variable parameterA intType, integer 1] intType)
        , binding localB intType False (primitive CoreMultiply [variable localA intType, integer 2] intType)
        , CoreReturn (variable localB intType)
        ]

immutableLocal :: Bool
immutableLocal = case optimizedEntry fixture of
    Just (CoreLet fresh intBinding value (CoreVariable used intResult) letResult) ->
        symbolIdValue (resolvedSymbol fresh) > maximumCoreSymbolValue fixture
            && resolvedSymbol fresh == resolvedSymbol used
            && intBinding == intType
            && value == primitive CoreAdd [integer 41, integer 1] intType
            && intResult == intType
            && letResult == intType
    _ -> False
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 41]), oneLocalHelper]

severalLocals :: Bool
severalLocals = (countLets <$> optimizedEntry fixture) == Just 2
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 20]), twoLocalHelper]

localDependencyOrder :: Bool
localDependencyOrder = case optimizedEntry fixture of
    Just (CoreLet first _ _ (CoreLet second _ secondValue (CoreVariable returned _) _) _) ->
        resolvedSymbol first /= resolvedSymbol second
            && resolvedSymbol second == resolvedSymbol returned
            && expressionUses (resolvedSymbol first) secondValue
    _ -> False
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 20]), twoLocalHelper]

literalParameter :: Bool
literalParameter = optimizedEntry fixture == Just (integer 42)
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 42]), identityHelper]

variableParameter :: Bool
variableParameter = optimizedEntry fixture == Just (variable callerValue intType)
    where
        caller =
            function
                entryName
                [(callerValue, intType)]
                intType
                [CoreReturn (invocation [intType] intType [variable callerValue intType])]
        fixture = moduleWith [caller, identityHelper]

primitiveArgumentOnce :: Bool
primitiveArgumentOnce = nonTrivialIdentity argument
    where
        argument = primitive CoreAdd [integer 20, integer 22] intType

callArgumentOnce :: Bool
callArgumentOnce = case optimize fixture >>= entryReturn of
    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
        bindingType == intType
            && value == argument
            && resolvedSymbol fresh == resolvedSymbol used
            && resultType == intType
    _ -> False
    where
        callableName = named 31 "producer"
        callableType = FunctionType [] intType
        argument = CoreApply (variable callableName callableType) [] intType
        caller = function entryName [(callableName, callableType)] intType [CoreReturn (invocation [intType] intType [argument])]
        fixture = moduleWith [caller, identityHelper]

failingArgumentOnce :: Bool
failingArgumentOnce = nonTrivialIdentity argument
    where
        argument = primitive CoreDivide [integer 1, integer 0] intType

allocatingArgumentOnce :: Bool
allocatingArgumentOnce = case optimizedEntry fixture of
    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
        bindingType == callableType
            && value == closureValue
            && resolvedSymbol fresh == resolvedSymbol used
            && resultType == callableType
    _ -> False
    where
        callableType = FunctionType [] intType
        closureValue = CoreClosure [] [] intType [CoreReturn (integer 42)] callableType
        helper = function helperName [(parameterA, callableType)] callableType [CoreReturn (variable parameterA callableType)]
        fixture = moduleWith [entry callableType (invocation [callableType] callableType [closureValue]), helper]

nonTrivialIdentity :: CoreExpression -> Bool
nonTrivialIdentity argument = case optimizedEntry fixture of
    Just (CoreLet fresh bindingType value (CoreVariable used resultType) _) ->
        bindingType == intType
            && value == argument
            && resolvedSymbol fresh == resolvedSymbol used
            && resultType == intType
            && countExpression argument value == 1
    _ -> False
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), identityHelper, leaf]
        leaf = function leafName [] intType [CoreReturn (integer 42)]

argumentOrder :: Bool
argumentOrder = case optimizedEntry fixture of
    Just (CoreLet first _ firstValue (CoreLet second _ secondValue _ _) _) ->
        firstValue == firstArgument
            && secondValue == secondArgument
            && resolvedSymbol first /= resolvedSymbol second
    _ -> False
    where
        firstArgument = primitive CoreAdd [integer 1, integer 2] intType
        secondArgument = primitive CoreMultiply [integer 3, integer 4] intType
        result = primitive CoreSubtract [variable parameterA intType, variable parameterB intType] intType
        helper = function helperName [(parameterA, intType), (parameterB, intType)] intType [CoreReturn result]
        fixture = moduleWith [entry intType (invocation [intType, intType] intType [firstArgument, secondArgument]), helper]

unusedArgument :: Bool
unusedArgument = case optimizedEntry fixture of
    Just (CoreLet _ _ value result _) -> value == argument && result == integer 42
    _ -> False
    where
        argument = primitive CoreDivide [integer 1, integer 0] intType
        helper = function helperName [(parameterA, intType)] intType [CoreReturn (integer 42)]
        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]

unusedLiteral :: Bool
unusedLiteral = optimizedEntry fixture == Just (integer 42)
    where
        helper = function helperName [(parameterA, intType)] intType [CoreReturn (integer 42)]
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 99]), helper]

evaluationStep :: Bool
evaluationStep = case optimizedEntry fixture of
    Just (CoreLet _ _ evaluated result _) -> evaluated == integer 1 && result == integer 42
    _ -> False
    where
        helper = function helperName [] intType [CoreEvaluate (integer 1), CoreReturn (integer 42)]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

evaluationOrder :: Bool
evaluationOrder = case optimizedEntry fixture of
    Just (CoreLet _ _ first (CoreLet _ _ second result _) _) ->
        first == integer 1 && second == integer 2 && result == integer 3
    _ -> False
    where
        helper = function helperName [] intType [CoreEvaluate (integer 1), CoreEvaluate (integer 2), CoreReturn (integer 3)]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

mutableBodyRejected :: Bool
mutableBodyRejected = optimizedEntry fixture == Just original
    where
        original = invocation [] intType []
        helper = function helperName [] intType [binding localA intType True (integer 1), CoreReturn (variable localA intType)]
        fixture = moduleWith [entry intType original, helper]

assignmentBodyRejected :: Bool
assignmentBodyRejected = optimizedEntry fixture == Just original
    where
        original = invocation [] intType []
        helper =
            function
                helperName
                []
                intType
                [binding localA intType True (integer 1), CoreAssign localA (integer 2), CoreReturn (variable localA intType)]
        fixture = moduleWith [entry intType original, helper]

branchBodyRejected :: Bool
branchBodyRejected = optimizedEntry fixture == Just original
    where
        original = invocation [] intType []
        helper = function helperName [] intType [CoreIf (boolean True) [CoreReturn (integer 1)] [CoreReturn (integer 2)]]
        fixture = moduleWith [entry intType original, helper]

earlyReturnRejected :: Bool
earlyReturnRejected = optimizedEntry fixture == Just original
    where
        original = invocation [] intType []
        helper = function helperName [] intType [CoreReturn (integer 1), CoreReturn (integer 2)]
        fixture = moduleWith [entry intType original, helper]

missingReturnRejectedBeforeOptimization :: Bool
missingReturnRejectedBeforeOptimization = case optimize fixture of
    Nothing -> True
    Just _ -> False
    where
        helper = function helperName [] intType [CoreEvaluate (integer 1)]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

freshFrontier :: Bool
freshFrontier = case optimizedEntry fixture of
    Just value -> all ((> maximumCoreSymbolValue fixture) . symbolIdValue) (definedExpressionSymbols value)
    Nothing -> False
    where
        highLocal = named 900 "high"
        helper =
            function helperName [] intType [binding highLocal intType False (integer 42), CoreReturn (variable highLocal intType)]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

disjointCallSites :: Bool
disjointCallSites = case optimize fixture of
    Just result -> case functionBody entryName result of
        Just [CoreBind first, CoreReturn second] ->
            let firstIds = definedExpressionSymbols (coreBindingValue first)
                secondIds = definedExpressionSymbols second
             in not (null firstIds) && not (null secondIds) && null [value | value <- firstIds, value `elem` secondIds]
        _ -> False
    Nothing -> False
    where
        firstResult = invocation [intType] intType [integer 1]
        secondResult = invocation [intType] intType [integer 2]
        caller = function entryName [] intType [binding callerValue intType False firstResult, CoreReturn secondResult]
        fixture = moduleWith [caller, oneLocalHelper]

nestedLetFreshened :: Bool
nestedLetFreshened = case optimizedEntry fixture of
    Just value -> all ((> maximumCoreSymbolValue fixture) . symbolIdValue) (definedExpressionSymbols value)
    Nothing -> False
    where
        nestedName = named 700 "nested"
        result = CoreLet nestedName intType (integer 42) (variable nestedName intType) intType
        helper = function helperName [] intType [CoreReturn result]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

statementCandidateReport :: Bool
statementCandidateReport = maybe False ((> 0) . sum . map inlineStatementCandidateCount . optimizationInlineReports) (optimize fixture)
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 41]), oneLocalHelper]

expressionCandidateReport :: Bool
expressionCandidateReport = maybe False ((> 0) . sum . map inlineExpressionCandidateCount . optimizationInlineReports) (optimize fixture)
    where
        fixture = moduleWith [entry intType (invocation [intType] intType [integer 42]), identityHelper]

rejectedBodyReport :: Bool
rejectedBodyReport = maybe False ((> 0) . sum . map inlineRejectedNonLinearBodies . optimizationInlineReports) (optimize fixture)
    where
        helper = function helperName [] intType [CoreIf (boolean True) [CoreReturn (integer 1)] [CoreReturn (integer 2)]]
        fixture = moduleWith [entry intType (invocation [] intType []), helper]

generatedLetReport :: Bool
generatedLetReport = case optimize fixture of
    Just result -> case optimizationInlineReports result of
        report : _ ->
            inlineGeneratedParameterLets report == 1
                && inlineGeneratedLocalLets report == 1
                && inlineGeneratedEvaluationLets report == 1
        [] -> False
    Nothing -> False
    where
        argument = primitive CoreAdd [integer 20, integer 21] intType
        helper =
            function
                helperName
                [(parameterA, intType)]
                intType
                [ CoreEvaluate (integer 0)
                , binding localA intType False (variable parameterA intType)
                , CoreReturn (variable localA intType)
                ]
        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]

alphaRenameReport :: Bool
alphaRenameReport = case optimize fixture of
    Just result -> case optimizationInlineReports result of
        report : _ -> inlineAlphaRenamedSymbols report == 3
        [] -> False
    Nothing -> False
    where
        argument = primitive CoreAdd [integer 20, integer 21] intType
        helper =
            function
                helperName
                [(parameterA, intType)]
                intType
                [ CoreEvaluate (integer 0)
                , binding localA intType False (variable parameterA intType)
                , CoreReturn (variable localA intType)
                ]
        fixture = moduleWith [entry intType (invocation [intType] intType [argument]), helper]

expandedBudget :: Bool
expandedBudget = case optimizeWith options fixture >>= entryReturn of
    Just value -> value == original
    Nothing -> False
    where
        argument = primitive CoreAdd [integer 20, integer 22] intType
        original = invocation [intType] intType [argument]
        fixture = moduleWith [entry intType original, identityHelper]
        options = inspectionOptions {optimizerMaximumInlineExpressionNodes = 4}

candidateBudget :: Bool
candidateBudget = case optimizeWith options fixture >>= entryReturn of
    Just value -> value == original
    Nothing -> False
    where
        original = invocation [intType] intType [integer 20]
        fixture = moduleWith [entry intType original, twoLocalHelper]
        options = inspectionOptions {optimizerMaximumInlineExpressionNodes = 4}

outputVerifies :: Bool
outputVerifies = case optimize fixture of
    Just result -> either (const False) (const True) (verifyCore (optimizedCore result))
    Nothing -> False
    where
        fixture =
            moduleWith
                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]

outputPrepares :: Bool
outputPrepares = case optimize fixture of
    Just result -> either (const False) (const True) (prepareCore (optimizedCore result))
    Nothing -> False
    where
        fixture =
            moduleWith
                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]

preparedOutputVerifies :: Bool
preparedOutputVerifies = case optimize fixture of
    Just result -> case prepareCore (optimizedCore result) of
        Right prepared -> either (const False) (const True) (verifyCorePrep prepared)
        Left _ -> False
    Nothing -> False
    where
        fixture =
            moduleWith
                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]

deterministicOutput :: Bool
deterministicOutput = (optimizedCore <$> optimize fixture) == (optimizedCore <$> optimize fixture)
    where
        fixture =
            moduleWith
                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]

idempotentOutput :: Bool
idempotentOutput = case optimize fixture of
    Just first -> (optimizedCore <$> optimize (optimizedCore first)) == Just (optimizedCore first)
    Nothing -> False
    where
        fixture =
            moduleWith
                [entry intType (invocation [intType] intType [primitive CoreAdd [integer 20, integer 21] intType]), twoLocalHelper]

inventoryIncludesLets :: Bool
inventoryIncludesLets = map resolvedSymbol (coreExpressionSymbols expression) == [SymbolId 80, SymbolId 80]
    where
        letName = named 80 "inside"
        expression = CoreLet letName intType (integer 1) (variable letName intType) intType

inventoryIncludesClosure :: Bool
inventoryIncludesClosure = all (`elem` symbols) [SymbolId 81, SymbolId 82, SymbolId 83]
    where
        captureName = named 81 "capture"
        parameterName = named 82 "parameter"
        localName = named 83 "local"
        capture = CoreCapture StrongCapture captureName intType (integer 1)
        expression =
            CoreClosure
                [capture]
                [(parameterName, intType)]
                intType
                [binding localName intType False (variable captureName intType), CoreReturn (variable parameterName intType)]
                (FunctionType [intType] intType)
        symbols = map resolvedSymbol (coreExpressionSymbols expression)

inventoryOrder :: Bool
inventoryOrder = map resolvedSymbol (coreModuleSymbols fixture) == expected
    where
        fixture =
            moduleWith
                [ function
                    entryName
                    [(parameterA, intType)]
                    intType
                    [binding localA intType False (variable parameterA intType), CoreReturn (variable localA intType)]
                ]
        expected = [SymbolId 1, SymbolId 10, SymbolId 20, SymbolId 10, SymbolId 20]

functionBody :: ResolvedName -> OptimizationResult -> Maybe [CoreStatement]
functionBody name result =
    coreFunctionBody
        <$> find ((== resolvedSymbol name) . resolvedSymbol . coreFunctionName) (coreModuleFunctions (optimizedCore result))

countLets :: CoreExpression -> Int
countLets expression = case expression of
    CoreLet _ _ value body _ -> 1 + countLets value + countLets body
    CoreApply callee arguments _ -> countLets callee + sum (map countLets arguments)
    CorePrimitive _ arguments _ -> sum (map countLets arguments)
    CoreClosure captures _ _ body _ -> sum (map (countLets . coreCaptureValue) captures) + sum (map countStatementLets body)
    _ -> 0

countStatementLets :: CoreStatement -> Int
countStatementLets statement = case statement of
    CoreBind value -> countLets (coreBindingValue value)
    CoreAssign _ value -> countLets value
    CoreReturn value -> countLets value
    CoreEvaluate value -> countLets value
    CoreIf condition yes no -> countLets condition + sum (map countStatementLets (yes ++ no))
    CoreWhile condition body -> countLets condition + sum (map countStatementLets body)
    CoreDoWhile body condition -> sum (map countStatementLets body) + countLets condition
    CoreFor condition updates body ->
        countLets condition + sum (map countStatementLets updates) + sum (map countStatementLets body)
    CoreBreak -> 0
    CoreContinue -> 0

expressionUses :: SymbolId -> CoreExpression -> Bool
expressionUses symbol = elem symbol . map resolvedSymbol . coreExpressionSymbols

definedExpressionSymbols :: CoreExpression -> [SymbolId]
definedExpressionSymbols expression = nub (go expression)
    where
        go value = case value of
            CoreLet name _ bound body _ -> resolvedSymbol name : go bound ++ go body
            CoreApply callee arguments _ -> go callee ++ concatMap go arguments
            CorePrimitive _ arguments _ -> concatMap go arguments
            CoreClosure captures parameters _ body _ ->
                map (resolvedSymbol . coreCaptureName) captures
                    ++ map (resolvedSymbol . fst) parameters
                    ++ concatMap (go . coreCaptureValue) captures
                    ++ concatMap statementDefinitions body
            _ -> []
        statementDefinitions statement = case statement of
            CoreBind value -> resolvedSymbol (coreBindingName value) : go (coreBindingValue value)
            CoreAssign _ value -> go value
            CoreReturn value -> go value
            CoreEvaluate value -> go value
            CoreIf condition yes no -> go condition ++ concatMap statementDefinitions (yes ++ no)
            CoreWhile condition body -> go condition ++ concatMap statementDefinitions body
            CoreDoWhile body condition -> concatMap statementDefinitions body ++ go condition
            CoreFor condition updates body ->
                go condition ++ concatMap statementDefinitions updates ++ concatMap statementDefinitions body
            CoreBreak -> []
            CoreContinue -> []

countExpression :: CoreExpression -> CoreExpression -> Int
countExpression needle expression =
    (if needle == expression then 1 else 0)
        + case expression of
            CoreApply callee arguments _ -> sum (map (countExpression needle) (callee : arguments))
            CorePrimitive _ arguments _ -> sum (map (countExpression needle) arguments)
            CoreLet _ _ value body _ -> countExpression needle value + countExpression needle body
            CoreClosure captures _ _ body _ ->
                sum (map (countExpression needle . coreCaptureValue) captures)
                    + sum (map countStatement body)
            _ -> 0
    where
        countStatement statement = case statement of
            CoreBind value -> countExpression needle (coreBindingValue value)
            CoreAssign _ value -> countExpression needle value
            CoreReturn value -> countExpression needle value
            CoreEvaluate value -> countExpression needle value
            CoreIf condition yes no -> countExpression needle condition + sum (map countStatement (yes ++ no))
            CoreWhile condition body -> countExpression needle condition + sum (map countStatement body)
            CoreDoWhile body condition -> sum (map countStatement body) + countExpression needle condition
            CoreFor condition updates body ->
                countExpression needle condition + sum (map countStatement updates) + sum (map countStatement body)
            CoreBreak -> 0
            CoreContinue -> 0