Class PhaseBoundaryGuard

java.lang.Object
org.ek9lang.core.PhaseBoundaryGuard

public final class PhaseBoundaryGuard extends Object
Checks, mechanically, that the compiler's phase boundaries mean what they say.

Two invariants, hooked at two different points because they are different claims:

  • No symbol MUTATION after the model is complete (E5/F35) - hooked into the symbol write primitives.
  • No RE-ENTRANT program access during emission (E8/H7) - hooked into SharedThreadContext.accept(Consumer). Phase 10 USED to run its whole body inside one accept; it now takes the program reference once and generates outside the lock, so a re-entering read is a plain acquisition rather than a deadlock. CodeGenerationAggregates' javadoc records the deadlock that dissolved. It remains a RESOLUTION question, not a mutation one.

Originally two classes; merged because the stack capture, de-duplication and reporting are identical and the two questions are asked of the same compile.

The compiler's phase boundaries assert that by code generation it is emitting - reading a finished model and writing bytes - and CodeGenerationAggregates' own javadoc records an entire phase once kept single-threaded to accommodate re-entrant lazy resolution. This class is how that claim gets checked mechanically rather than argued. See SPEC-module-scope-lock-contention.md §5.0 and §4.2.

Two modes, and the survey mode is the one to run first.

  • survey (-Dek9.mutationGuard=survey) - RECORDS each offending site with a full stack and carries on. One run yields the complete list. Throwing would abort on the first site and need one run per site, which is how a hand-built inventory ends up incomplete - §4.2's table is already missing AggregateDfnGenerator:364.
  • enforce (-Dek9.mutationGuard=enforce) - THROWS. This is the permanent guard §5.0 calls for, so that "no mutation after SYMBOL_COMPLETION" stays enforced as the compiler grows rather than decaying back into a comment.

Off entirely by default: MODE is static final, so every call site is eliminated when unset.

Mutation, not access. Deliberately hooked into the symbol WRITE primitives rather than into SharedThreadContext.accept. Every symbol READ passes through accept too (§11.4), so flagging that would bury the handful of real mutations under hundreds of thousands of legitimate reads. That distinction is the whole point: reads become lock-free and sound after the freeze; writes are what must stop.

  • Method Details

    • isEnabled

      public static boolean isEnabled()
    • setFrozen

      public static void setFrozen(boolean value, String phase)
      Mark the model complete (or not). Called at each phase boundary.
    • mutation

      public static void mutation(String what, String detail)
      Report a symbol mutation. Cheap and inlined away when the guard is off; only does real work when the model is supposed to be frozen already.
      Parameters:
      what - short description of the mutation, e.g. "define" or "setType"
      detail - the symbol or scope involved, for the report
    • selfTest

      public static void selfTest(Runnable oneAccept)
      Deliberately trigger one re-entry, so the report carries its own positive control.

      A negative from an instrument that has not been shown capable of a positive is worthless - notebook M14 records five occasions where exactly that produced a confident wrong conclusion, two of which had the lock declared innocent when it accounted for 70 s of parked time. So the guard proves itself on every run rather than asking to be trusted: selfTest is called from inside a region already holding the lock, and if the report does not show it PASSED then every "no re-entry observed" line in that report means nothing.

      Parameters:
      oneAccept - a call that re-enters accept - supplied by the caller, which is the only place that has the context to make one
    • forbidReentry

      public static void forbidReentry()
      Forbid re-entrant SharedThreadContext.accept(Consumer) on this thread. Called around a region that is already holding the lock and is a candidate for parallelisation.
    • permitReentry

      public static void permitReentry()
    • programAccess

      public static void programAccess()
      Called from SharedThreadContext.accept(Consumer). Records a re-entry if one is forbidden here.
    • squirrelEnter

      public static void squirrelEnter(int symbolId, int sizeBefore)
      Probe for the one question `Symbol.squirrelStore`'s javadoc leaves open: is a put() into the squirrel map ever concurrent on the SAME symbol?

      It matters because the map changed from `EnumMap` to `HashMap(4)`. An EnumMap writes into a fixed Object[81] and never resizes, so a racing put is at worst a lost update. A HashMap(4) has threshold 3 and resizes on the 4th distinct key - and phase 5 writes 24 distinct keys, with eight separate writers landing on a single MethodSymbol (F38). A racing put DURING a resize can corrupt the table, which is a failure mode EnumMap did not have.

      Two signals, because they answer different things. writers is the necessary condition - if no symbol is ever written by two threads at all, nothing can race. overlap is the sufficient one - two threads inside a put on the same symbol at the same moment.

    • squirrelExit

      public static void squirrelExit(int symbolId)
    • squirrelReport

      public static String squirrelReport()
    • report

      public static String report()