Class ValueTrackingAnalyzer

java.lang.Object
org.ek9lang.compiler.phase5.flow.ValueTrackingAnalyzer

public final class ValueTrackingAnalyzer extends Object
Tracks variable values through control flow for tautological condition detection.

Maintains a constraint state per program point and manages branch splitting/joining as control flow constructs (if/else, switch, while, for, try/catch) are entered and exited. This is a single-pass AST walk analyzer that exploits EK9's perfectly nested SESE control flow (no break/continue/return/goto) for O(N) analysis.

Phase 1 tracks: Integer, Boolean, and String constants. Variables assigned from function calls or non-pure mutations become TOP (unknown).

The analyzer is scoped per function/method/operator. It is reset when entering a new callable scope and produces results at expression evaluation points within that scope.

  • Constructor Details

    • ValueTrackingAnalyzer

      public ValueTrackingAnalyzer()
      Create a new analyzer (inactive until a callable scope is entered).
  • Method Details

    • enterCallableScope

      public void enterCallableScope()
      Enter a callable scope (function, method, operator). Resets the analyzer state for a fresh analysis.
    • exitCallableScope

      public void exitCallableScope()
      Exit a callable scope.
    • isActive

      public boolean isActive()
      Check if the analyzer is currently tracking within a callable scope.
    • registerModuleConstant

      public void registerModuleConstant(ISymbol symbol, AbstractValue abstractValue)
      Register a module-level constant (from defines constant blocks). These persist across callable scope resets and are available for value lookups.
      Parameters:
      symbol - the constant symbol
      abstractValue - the constant's value
    • recordAssignment

      public void recordAssignment(ISymbol symbol, AbstractValue abstractValue)
      Record a variable assignment with a known constant value.
      Parameters:
      symbol - the variable being assigned
      abstractValue - the abstract value being assigned
    • recordMayValues

      public void recordMayValues(ISymbol symbol, Set<Object> values)
      Record that symbol's value is not known exactly, but is one of values.

      This is the shape an expression with ARMS produces — a ternary, or a switch/try/loop expression whose returning parameter is assigned differently per arm. The MUST side stays TOP because the arms disagree; the MAY side keeps all of them, which is exactly the information a "could this Panic?" question needs.

    • recordUnknownAssignment

      public void recordUnknownAssignment(ISymbol symbol)
    • getMayValues

      public Set<Object> getMayValues(ISymbol symbol)
      The constant values that reach this point on SOME path — the MAY side of the analysis.

      getValue(ISymbol) answers "what is this definitely?", which joins to TOP when branches disagree. This answers "what could this be?", which unions instead. Use it when a SINGLE bad path is already a defect — a constrained construction Panics on the path that reaches it, whether or not the other branch would have been fine.

      Empty means nothing is provable, NOT that the variable is unconstrained.

    • getValue

      public AbstractValue getValue(ISymbol symbol)
    • evaluateCondition

      public ConditionResult evaluateCondition(AbstractValue left, String operator, AbstractValue right)
      Evaluate a condition between two abstract values.
      Parameters:
      left - the left operand value
      operator - the comparison operator
      right - the right operand value
      Returns:
      TRUE if always satisfied, FALSE if never, UNKNOWN if indeterminate
    • evaluateIsSet

      public ConditionResult evaluateIsSet(ISymbol symbol)
      Evaluate the isSet state of a variable based on its tracked value.

      A variable is provably SET if it holds a ConstantValue, RangeValue, or SetStateValue(SET). A variable is provably UNSET only if it holds SetStateValue(UNSET). Otherwise, the result is UNKNOWN.

      Parameters:
      symbol - the variable to check
      Returns:
      TRUE if provably SET, FALSE if provably UNSET, UNKNOWN otherwise
    • evaluateIsEmpty

      public ConditionResult evaluateIsEmpty(ISymbol symbol)
      Evaluate the empty state of a variable based on its tracked value.

      A variable is provably EMPTY if it holds a CollectionStateValue with isEmpty=true. A variable is provably NON-EMPTY if it holds a CollectionStateValue with isEmpty=false. Otherwise, the result is UNKNOWN.

      Parameters:
      symbol - the variable to check
      Returns:
      TRUE if provably EMPTY, FALSE if provably NON-EMPTY, UNKNOWN otherwise
    • narrowForIsSet

      public void narrowForIsSet(ISymbol symbol, boolean isTrueBranch)
      Narrow a variable's value based on an isSet check.

      On the true branch (isSet is true), records the variable as SET. On the false branch (isSet is false), records the variable as UNSET. If the variable already has a more precise value (ConstantValue, RangeValue), preserves the more precise value on the true branch.

      Parameters:
      symbol - the variable being checked
      isTrueBranch - true for the true/if branch, false for the else branch
    • narrowForCondition

      public void narrowForCondition(ISymbol symbol, String operator, long operandValue, boolean isTrueBranch)
      🔑 Narrowing must reduce the MAY side too, or it goes stale. These paths write the MUST side DIRECTLY rather than through recordAssignment(ISymbol, AbstractValue), so without this the may-set keeps a value the narrowing has just ruled out — and a value that provably does NOT arrive would be reported as if it did. Measured: size <- 20; if size == 15 reported 20 as reaching a construction inside the branch, where the value is provably 15.

      The rule is: a narrowing that pins an EXACT constant sets the may-set to exactly that; every other narrowing (range, excludes, set-state) INVALIDATES it, because a range cannot be enumerated as constants and a stale entry is worse than none.

    • enterBranch

      public void enterBranch()
      Enter a branching construct (if/switch). Pushes the current state as a snapshot for the false/else branch and creates a new scope for tracking branch exit states.
    • switchToAlternateBranch

      public void switchToAlternateBranch()
      Record the end of the current branch (e.g., end of if-body) and switch to the alternative branch state (e.g., else-body or else-if).

      Saves the current state as a branch exit state and restores the pre-branch snapshot for the alternative path. The pre-branch state remains on the stack for subsequent alternate branches (else-if chains).

    • exitBranch

      public void exitBranch()
      Exit a branching construct. Joins the current branch state with all accumulated branch exit states from this nesting level.

      Call this after the final branch (else/default) has been processed.

    • exitBranchNoAlternate

      public void exitBranchNoAlternate()
      Exit a branching construct where only the true branch existed (no else).

      When there is no else clause, the false path retains the pre-branch state. Join the true-branch exit with the pre-branch snapshot.

    • enterTry

      public void enterTry()
      Enter a try block. Snapshots the pre-try state for catch block entry and creates a scope for tracking try/catch exit states.
    • enterCatch

      public void enterCatch()
      Enter a catch block. Restores the pre-try snapshot (conservative).

      The catch handler can be entered from any exception-throwing point in the try body, so the constraint state reverts to the pre-try snapshot. This is always sound: may miss optimizable cases but never false-positives.

    • exitTryCatch

      public void exitTryCatch()
      Exit a try/catch construct. Joins the try-exit and catch-exit states.
    • enterFinally

      public void enterFinally()
      Enter a finally block. Joins the try and catch exit states FIRST, so the finally body runs on the joined state and its writes dominate every path out of the construct.

      This ordering is the whole point. finally is the last child of the try construct in the grammar, so without this its writes would land on the catch-exit state alone and the subsequent join would resurrect the very values it had just overwritten.

    • isInsideLoop

      public boolean isInsideLoop()
      Check if currently inside a loop body. Used to suppress tautology detection inside loops because widening has not yet been applied — variables still hold their pre-loop constant values during the first pass through the body.
    • enterLoop

      public void enterLoop()
      Enter a loop body. Snapshots the pre-loop state.
    • exitLoop

      public void exitLoop()
      Exit a loop body. Applies widening: any variable whose value changed during the loop body is widened to TOP.

      Uses bounded widening (2 passes max): if pre-loop value differs from post-body value, the variable is widened to TOP. This is sound for EK9 because loops have no break/continue — the body always fully executes or not at all.

    • exitLoopBodyAlwaysRuns

      public void exitLoopBodyAlwaysRuns()
      Exit a loop body that is GUARANTEED to run at least once - the do ... while form.

      Same MUST-side widening as exitLoop(), but the MAY side is taken from the body alone: the body definitely ran, so a definition it overwrote cannot reach the loop exit. A while body may run zero times and so must keep unioning - that asymmetry is the entire difference between the two forms.

    • markUnreachable

      public void markUnreachable()
      Mark the current state as unreachable (after a throw statement). Subsequent code in the same block is dead.
    • isUnreachable

      public boolean isUnreachable()
      Check if the current program point is unreachable.