Class PhaseBoundaryGuard
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 oneaccept; 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 missingAggregateDfnGenerator: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 Summary
Modifier and TypeMethodDescriptionstatic voidForbid re-entrantSharedThreadContext.accept(Consumer)on this thread.static booleanstatic voidReport a symbol mutation.static voidstatic voidCalled fromSharedThreadContext.accept(Consumer).static Stringreport()static voidDeliberately trigger one re-entry, so the report carries its own positive control.static voidMark the model complete (or not).static voidsquirrelEnter(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?static voidsquirrelExit(int symbolId) static String
-
Method Details
-
isEnabled
public static boolean isEnabled() -
setFrozen
Mark the model complete (or not). Called at each phase boundary. -
mutation
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
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:
selfTestis 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-entersaccept- supplied by the caller, which is the only place that has the context to make one
-
forbidReentry
public static void forbidReentry()Forbid re-entrantSharedThreadContext.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 fromSharedThreadContext.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.
writersis the necessary condition - if no symbol is ever written by two threads at all, nothing can race.overlapis the sufficient one - two threads inside a put on the same symbol at the same moment. -
squirrelExit
public static void squirrelExit(int symbolId) -
squirrelReport
-
report
-