packages feed

tala-core-0.1.0: src/Progmasoft/Tala/Desugarer/Laziness.hs

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

{- | What the lowering needs to know to evaluate a value by need.

Tala is a lazy language: a value is computed when it is first needed and
at most once, and a value that is never needed is never computed. Its effects
are not lazy: a store into a variable and a transfer of control happen where
they are written. The source has no notation for either fact; the compiler
tells the two kinds of expression apart.

This module holds that distinction for the initializer of a local binding.
An initializer may be deferred when evaluating it later gives what evaluating
it now would give: it stores nothing, transfers nowhere, and reads the
variables it names as they are where the binding stands. Deferring it is
observable only when it might fail or never return, so only then is it worth
a flag and a test at every use.
-}
module Progmasoft.Tala.Desugarer.Laziness
    ( deferrableExpression
    , worthDeferring
    , expressionNames
    , renameNames
    , capturedSymbols
    , ArgumentNeeds
    , alwaysReads
    , neededTwice
    , neededNext
    ) where

import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core

{- | Whether evaluating an expression at a later point of the same function
gives the value that evaluating it in place would give, provided the
variables it reads still have the values they had.

That holds for expressions built from names, literals, operators, tests and
calls: a call cannot store into a local of its caller. It does not hold for
an expression that stores, that leaves, or that contains statements.
-}
deferrableExpression :: Expression name annotation -> Bool
deferrableExpression expression = case expression of
    NameExpression {} -> True
    LiteralExpression {} -> True
    MemberAccessExpression _ receiver _ _ -> deferrableExpression receiver
    MethodReferenceExpression _ receiver _ _ -> deferrableExpression receiver
    CallExpression _ callee arguments _ -> all deferrableExpression (callee : arguments)
    UnaryExpression _ _ value _ -> deferrableExpression value
    BinaryExpression _ _ left right _ -> deferrableExpression left && deferrableExpression right
    IsPatternExpression _ subject _ _ -> deferrableExpression subject
    ConditionalExpression _ condition first second _ -> all deferrableExpression [condition, first, second]
    CoalesceExpression {} -> False
    AssignmentExpression {} -> False
    IncrementExpression {} -> False
    LoopExpression {} -> False
    BlockExpression {} -> False
    MatchExpression {} -> False
    CallableExpression {} -> False

{- | Whether an expression can fail or never return, which is when it matters
whether it is evaluated at all: it calls something, or it divides.
-}
worthDeferring :: Expression name annotation -> Bool
worthDeferring expression = case expression of
    CallExpression {} -> True
    BinaryExpression _ operator left right _ ->
        operator `elem` [Divide, FloorDivide, Remainder] || worthDeferring left || worthDeferring right
    MemberAccessExpression _ receiver _ _ -> worthDeferring receiver
    MethodReferenceExpression _ receiver _ _ -> worthDeferring receiver
    UnaryExpression _ _ value _ -> worthDeferring value
    IsPatternExpression _ subject _ _ -> worthDeferring subject
    ConditionalExpression _ condition first second _ -> any worthDeferring [condition, first, second]
    _ -> False

-- | The names an expression that may be deferred reads, with their types.
expressionNames :: Expression name annotation -> [(name, annotation)]
expressionNames expression = [(name, annotation) | NameExpression _ name annotation <- nameReads expression]

{- | The reads of a name in an expression that may be deferred, in the order
they are written.

The reads are gathered in front of the ones that follow them. Appending the
reads of a right operand to those of a left one would copy the reads of a
chain of operators once for each operator of the chain.
-}
nameReads :: Expression name annotation -> [Expression name annotation]
nameReads expression = collect expression []
    where
        collect value following = case value of
            NameExpression {} -> value : following
            MemberAccessExpression _ receiver _ _ -> collect receiver following
            MethodReferenceExpression _ receiver _ _ -> collect receiver following
            CallExpression _ callee arguments _ -> foldr collect following (callee : arguments)
            UnaryExpression _ _ operand _ -> collect operand following
            BinaryExpression _ _ left right _ -> collect left (collect right following)
            IsPatternExpression _ subject _ _ -> collect subject following
            ConditionalExpression _ condition first second _ -> foldr collect following [condition, first, second]
            _ -> following

{- | Replace the names an expression that may be deferred reads. A name that
has no replacement is kept.
-}
renameNames :: (name -> name) -> Expression name annotation -> Expression name annotation
renameNames rename expression = case expression of
    NameExpression spanValue name annotation -> NameExpression spanValue (rename name) annotation
    MemberAccessExpression spanValue receiver member annotation ->
        MemberAccessExpression spanValue (renameNames rename receiver) member annotation
    MethodReferenceExpression spanValue receiver member annotation ->
        MethodReferenceExpression spanValue (renameNames rename receiver) member annotation
    CallExpression spanValue callee arguments annotation ->
        CallExpression spanValue (renameNames rename callee) (map (renameNames rename) arguments) annotation
    UnaryExpression spanValue operator value annotation ->
        UnaryExpression spanValue operator (renameNames rename value) annotation
    BinaryExpression spanValue operator left right annotation ->
        BinaryExpression spanValue operator (renameNames rename left) (renameNames rename right) annotation
    IsPatternExpression spanValue subject patternValue annotation ->
        IsPatternExpression spanValue (renameNames rename subject) patternValue annotation
    ConditionalExpression spanValue condition first second annotation ->
        ConditionalExpression
            spanValue
            (renameNames rename condition)
            (renameNames rename first)
            (renameNames rename second)
            annotation
    _ -> expression

{- | For the name of a method whose parameters may be passed by need, which
of them are, in declaration order; nothing for any other name. A call of
such a method does not read an argument it passes by need: the method may
never need it.
-}
type ArgumentNeeds name = name -> Maybe [Bool]

{- | Whether evaluating an expression always reads the given name, whatever
values it meets: the name stands where neither a short-circuit operator, nor
a conditional, nor a parameter that is passed by need can skip it. Blocks,
matches, loops and callables used as values are not searched; the answer for
them is that it is not known.
-}
alwaysReads :: (Eq name) => ArgumentNeeds name -> name -> Expression name annotation -> Bool
alwaysReads needs name expression = case expression of
    NameExpression _ found _ -> found == name
    MemberAccessExpression _ receiver _ _ -> always receiver
    MethodReferenceExpression _ receiver _ _ -> always receiver
    CallExpression _ (NameExpression _ callee _) arguments _
        | Just flags <- needs callee
        , length flags == length arguments ->
            any always [argument | (False, argument) <- zip flags arguments]
    CallExpression _ callee arguments _ -> any always (callee : arguments)
    UnaryExpression _ _ value _ -> always value
    BinaryExpression _ operator left right _
        | operator `elem` [LogicalAnd, LogicalOr] -> always left
        | otherwise -> always left || always right
    IsPatternExpression _ subject _ _ -> always subject
    ConditionalExpression _ condition _ _ _ -> always condition
    AssignmentExpression _ _ _ value _ -> always value
    _ -> False
    where
        always = alwaysReads needs name

{- | The reads of the given values by need that an expression is certain to
evaluate and holds more than once, one read for each such value.

Computing those values ahead of the expression changes nothing that can be
observed except which of two computations fails first when both fail: the
expression has no effect, and it evaluates each of them whenever it is
evaluated.
-}
neededTwice ::
    ArgumentNeeds ResolvedName ->
    [SymbolId] ->
    Expression ResolvedName annotation ->
    [(ResolvedName, Expression ResolvedName annotation)]
neededTwice needs deferred expression
    | null deferred || not (deferrableExpression expression) = []
    | otherwise = go Set.empty namesRead
    where
        namesRead = [(name, found) | found@(NameExpression _ name _) <- nameReads expression]
        byNeed = Set.fromList deferred
        -- How often each value by need is read. Looking for a second read
        -- among the reads that follow would walk them for every name.
        counts = Map.fromListWith (+) [(symbol, 1 :: Int) | (name, _) <- namesRead, let symbol = resolvedSymbol name, Set.member symbol byNeed]
        go _ [] = []
        go seen ((name, found) : remaining)
            | Set.member symbol seen = go seen remaining
            | Map.findWithDefault 0 symbol counts > 1
            , alwaysReads needs name expression =
                (name, found) : go (Set.insert symbol seen) remaining
            | otherwise = go (Set.insert symbol seen) remaining
            where
                symbol = resolvedSymbol name

{- | Whether the statement that follows a binding is certain to read it.

A value that is certain to be needed by the very next statement may be
computed where its binding stands: nothing can be observed between the two
places except which of two computations fails first when both fail. That
saves the flag and the test for the common case of a value that is used at
once.

The next statement reads the name for certain when the expression it
evaluates first always reads it. A binding evaluates its initializer first
only when it is itself evaluated in place, which the second argument
decides for it from the statements after it.
-}
neededNext ::
    (Eq name) =>
    ArgumentNeeds name ->
    -- | Whether a later binding is evaluated in place, given what follows it.
    (name -> annotation -> Expression name annotation -> [Statement name annotation] -> Bool) ->
    name ->
    [Statement name annotation] ->
    Bool
neededNext needs inPlaceBinding name following = case following of
    next : later -> case next of
        BindingStatement _ _ _ bound annotation value ->
            always value && inPlaceBinding bound annotation value later
        AssignmentStatement _ _ _ value -> always value
        CompoundAssignmentStatement _ _ _ _ value -> always value
        ReturnStatement _ (Just value) -> always value
        IfStatement _ condition _ _ -> always condition
        GuardStatement _ condition _ -> always condition
        WhileStatement _ condition _ -> always condition
        DiscardStatement _ value -> always value
        ExpressionStatement _ value _ -> always value
        _ -> False
    [] -> False
    where
        always = alwaysReads needs name

{- | Every local that a closure created by the statements captures, at any
depth. A closure takes the value of a capture when it is created, so a
binding that a closure captures has to have its value by then.
-}
capturedSymbols :: [CoreStatement] -> [SymbolId]
capturedSymbols = concatMap statement
    where
        statement value = case value of
            CoreBind binding -> expression (coreBindingValue binding)
            CoreAssign _ assigned -> expression assigned
            CoreReturn returned -> expression returned
            CoreEvaluate evaluated -> expression evaluated
            CoreIf condition whenTrue whenFalse -> expression condition ++ capturedSymbols whenTrue ++ capturedSymbols whenFalse
            CoreWhile condition body -> expression condition ++ capturedSymbols body
            CoreDoWhile body condition -> capturedSymbols body ++ expression condition
            CoreFor condition body update -> expression condition ++ capturedSymbols body ++ capturedSymbols update
            CoreBreak -> []
            CoreContinue -> []
        expression value = case value of
            CoreVariable {} -> []
            CoreLiteral {} -> []
            CoreApply callee arguments _ -> concatMap expression (callee : arguments)
            CorePrimitive _ arguments _ -> concatMap expression arguments
            CoreLet _ _ bound body _ -> expression bound ++ expression body
            CoreConditional condition whenTrue whenFalse _ -> concatMap expression [condition, whenTrue, whenFalse]
            CoreClosure captures _ _ body _ ->
                map (resolvedSymbol . coreCaptureName) captures
                    ++ concatMap (expression . coreCaptureValue) captures
                    ++ capturedSymbols body