Skip to content

Parallel analysis: verdicts around cycles depend on thread order #82

Closed

Description

@bnaudts

What

With PARALLEL > 1 (the default is min(8, cpus−2)), the modification analysis can give different verdicts depending on thread timing. The link cache (#76) makes this visible, but did not cause it.

Measured during #76 (FPDUMP with FPDUMP_PARAMS=1, 8 threads):

corpus no cache vs cache cache vs cache (two runs) no cache vs no cache
guava (oracle) 30 lines 14 lines 2 lines
timefold 22 lines 20 lines 0 lines
langchain4j 0 – –

With PARALLEL=1, the cache and no-cache runs are byte-identical on all three corpora. So the cache is exact, and the differences come from thread order. Without the cache, the timing happens to be stable enough to hide them. Cache hits change the timing.

Where

The differing lines are independence and eventual verdicts on types and methods inside cycles. In guava: CaseFormat, CharMatcher.And, FluentIterable.$1/$2, and the util.concurrent futures. This suggests that the fixpoint around cycles (cycle-breaking activation, or the order in which an SCC's members see each other's intermediate values) can settle in more than one place, depending on which member is computed first.

Next steps

  • Reproduce with the cache on, comparing two 8-thread runs, and diff the FPDUMPs to pick one small cycle (CharMatcher.And is a candidate).
  • Find which verdict decision depends on the order: probably an "activate cycle breaking" decision taken from a partial view of the SCC, or a latest-wins write racing with a reader.
  • Goal: verdicts that do not depend on PARALLEL or on timing, so that a parallel FPDUMP A/B is a valid exactness test again.

Activity

  1. bnaudts commented on Oct 29, 2026

    @bnaudts
    Author

    A second reproduction, single-threaded: the worklist's view of eventual writes

    While working on #76 option C (verification passes that find changes on no element), we found 13 sites that write a summary property with a plain set() and only increment the change counter: the eventual verdicts in TypeEventualAnalyzerImpl, SourceContractMaterializer's contracts, AbstractMethodAnalyzerImpl's eventual merges, and DynamicImmutabilityInference. TolerantWrite never sees these writes, so the worklist does not wake their dependents. A full verification pass then counts changes with an empty residue, and the next full pass repeats.

    The fix restores TolerantWrite.markChanged(property, target) and calls it at those 13 sites, leaving the writes themselves unchanged. With PARALLEL=1, compared against the run without the fix:

    langchain4j guava timefold
    FPDUMP diff 0 8 lines 0
    verification passes with empty residue 1 → 0 4 → 0 1 → 0
    full passes 20 → 19 28 → 26 25 → 25

    The analyzer suite passes, 648/648. The guava diff is in eventual verdicts:

    • CharMatcher.Is, CharMatcher.IsNot: eventual null → @Immutable(after="first,original,predicate,second"). These labels are fields of other CharMatcher classes, not of these two.
    • ImmutableMultimap.KeysSerializedForm: null → @Immutable(hc=true)(after="multimap")
    • BenchmarkHelpers.SampleMatcherConfig: @Immutable(hc=true)(after="matcher") → null

    Without threads, the only difference is when dependents re-run. So eventual verdicts depend on the iteration history, not only on the state the analysis converges to: they are written once, and some are held back during a warm-up period. This is the same weakness this issue describes for thread order.

    Parked. The gain is small, about 2 full passes on guava (roughly 30 s of 724 s with the link cache). The code is on the local branch park/76-markchanged in ws/maddi-all/maddi-mod (on top of the link-cache commit), and the diff is below. Once eventual verdicts no longer depend on history, landing it should be result-neutral. The new log line ("Verification-pass changes outside any summary, on: …") names any remaining gap.

    diff
    diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java
    index c478378a..186d0e2b 100644
    --- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java
    +++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/AbstractMethodAnalyzerImpl.java
    @@ -289,6 +289,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
                 }
                 methodInfo.analysis().set(EVENTUAL_METHOD, fromImplementations);
                 DECIDE.debug("AM: Decide eventual of abstract method {} = {}", methodInfo, fromImplementations);
    +            TolerantWrite.markChanged(EVENTUAL_METHOD, methodInfo);
                 propertyChanges.incrementAndGet();
             }
         }
    @@ -359,6 +360,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
             if (agreed != null) {
                 methodInfo.analysis().set(EVENTUALLY_NON_MODIFYING_METHOD, new ValueImpl.SetOfStringsImpl(Set.copyOf(agreed)));
                 DECIDE.debug("AM: Decide eventually-non-modifying of abstract method {} after {}", methodInfo, agreed);
    +            TolerantWrite.markChanged(EVENTUALLY_NON_MODIFYING_METHOD, methodInfo);
                 propertyChanges.incrementAndGet();
             }
         }
    @@ -491,6 +493,7 @@ public class AbstractMethodAnalyzerImpl extends CommonAnalyzerImpl implements Ab
             if (agreed != null) {
                 pi.analysis().set(EVENTUALLY_UNMODIFIED_PARAMETER, new ValueImpl.SetOfStringsImpl(Set.copyOf(agreed)));
                 DECIDE.debug("AM: Decide eventually-unmodified of abstract method parameter {} after {}", pi, agreed);
    +            TolerantWrite.markChanged(EVENTUALLY_UNMODIFIED_PARAMETER, pi);
                 propertyChanges.incrementAndGet();
             }
         }
    diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java
    index fac0bbfa..d8d1b5b0 100644
    --- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java
    +++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/DynamicImmutabilityInference.java
    @@ -31,6 +31,7 @@ import io.codelaser.maddi.cst.api.info.TypeInfo;
     import io.codelaser.maddi.cst.api.variable.FieldReference;
     import io.codelaser.maddi.cst.impl.analysis.PropertyImpl;
     import io.codelaser.maddi.cst.impl.analysis.ValueImpl;
    +import io.codelaser.maddi.modification.common.util.TolerantWrite;
     
     import java.util.ArrayList;
     import java.util.List;
    @@ -123,6 +124,7 @@ public class DynamicImmutabilityInference {
             if (meet.isAtLeastImmutableHC()) {
                 fieldInfo.analysis().set(PropertyImpl.IMMUTABLE_FIELD, meet);
                 CommonAnalyzerImpl.DECIDE.debug("DII: Decide dynamic immutable of field {} = {}", fieldInfo, meet);
    +            TolerantWrite.markChanged(PropertyImpl.IMMUTABLE_FIELD, fieldInfo);
                 propertyChanges.incrementAndGet();
             }
         }
    diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java
    index f706d659..323dcc3a 100644
    --- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java
    +++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/IteratingAnalyzerImpl.java
    @@ -914,6 +914,13 @@ public class IteratingAnalyzerImpl extends CommonAnalyzerImpl implements Iterati
                         LOGGER.info("Verification-pass residue ({} elements): {}", residue.size(), residue.stream()
                                 .map(Info::fullyQualifiedName).sorted().limit(12)
                                 .reduce((a, b) -> a + ", " + b).orElse("-"));
    +                    if (residue.isEmpty()) {
    +                        // changes on no summary: a write the worklist cannot see (a plain set() without
    +                        // TolerantWrite.markChanged), so the next full pass repeats; name where they landed
    +                        LOGGER.info("Verification-pass changes outside any summary, on: {}",
    +                                singleIterationAnalyzer.changedInfos().stream().map(Info::fullyQualifiedName)
    +                                        .sorted().limit(12).reduce((a, b) -> a + ", " + b).orElse("-"));
    +                    }
                         verifying = false;
                     } else if (dirty.isEmpty() || propertiesChanged == 0) {
                         if (incremental) {
    diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java
    index 5f4df373..a6f89724 100644
    --- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java
    +++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/SourceContractMaterializer.java
    @@ -154,6 +154,7 @@ public class SourceContractMaterializer {
                             pi.analysis().set(
                                     io.codelaser.maddi.cst.impl.analysis.PropertyImpl.IGNORE_MODIFICATIONS_PARAMETER, bool);
                             CommonAnalyzerImpl.DECIDE.debug("SCM: Inherited @IgnoreModifications on {} from {}", pi, opi);
    +                        TolerantWrite.markChanged(io.codelaser.maddi.cst.impl.analysis.PropertyImpl.IGNORE_MODIFICATIONS_PARAMETER, pi);
                             propertyChanges.incrementAndGet();
                             break;
                         }
    @@ -201,6 +202,7 @@ public class SourceContractMaterializer {
                     CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = true, from the class-level"
                                                     + " disclaimer on {}", IGNORE_MODIFICATIONS_FIELD, fieldInfo,
                             fieldType);
    +                TolerantWrite.markChanged(IGNORE_MODIFICATIONS_FIELD, fieldInfo);
                     propertyChanges.incrementAndGet();
                     return;
                 }
    @@ -246,6 +248,7 @@ public class SourceContractMaterializer {
                 // MUTABLE is the default: writing it would record an "annotation" indistinguishable from silence
                 info.analysis().set(property, immutable);
                 CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = {}", property, info, immutable);
    +            TolerantWrite.markChanged(property, info);
                 propertyChanges.incrementAndGet();
             }
         }
    @@ -258,6 +261,7 @@ public class SourceContractMaterializer {
             if (contractReader.contracts(info).get(property) instanceof Value.Bool bool && bool.isTrue()) {
                 info.analysis().set(property, bool);
                 CommonAnalyzerImpl.DECIDE.debug("SCM: Contracted {} of {} = {}", property, info, bool);
    +            TolerantWrite.markChanged(property, info);
                 propertyChanges.incrementAndGet();
             }
         }
    diff --git a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java
    index e0676e69..77ebf3ab 100644
    --- a/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java
    +++ b/maddi-modification-analyzer/src/main/java/io/codelaser/maddi/modification/analyzer/impl/TypeEventualAnalyzerImpl.java
    @@ -17,6 +17,7 @@
     
     package io.codelaser.maddi.modification.analyzer.impl;
     
    +import io.codelaser.maddi.modification.common.util.TolerantWrite;
     import io.codelaser.maddi.modification.prepwork.variable.MethodLinkedVariables;
     import io.codelaser.maddi.modification.link.impl.MethodLinkedVariablesImpl;
     import io.codelaser.maddi.modification.prepwork.variable.Link;
    @@ -182,6 +183,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
                     methodInfo.analysis().set(EVENTUAL_METHOD, eventual);
                     DECIDE.debug("TE: {} eventual of method {} = {}", contracted ? "Contracted" : "Decide",
                             methodInfo, eventual);
    +                TolerantWrite.markChanged(EVENTUAL_METHOD, methodInfo);
                     propertyChanges.incrementAndGet();
                     eventualCluster.commitAssumptionBuffer();
                 } else {
    @@ -205,6 +207,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
                     methodInfo.analysis().set(EVENTUALLY_NON_MODIFYING_METHOD,
                             new ValueImpl.SetOfStringsImpl(Set.copyOf(labels)));
                     DECIDE.debug("TE: Decide eventually-non-modifying of method {} after {}", methodInfo, labels);
    +                TolerantWrite.markChanged(EVENTUALLY_NON_MODIFYING_METHOD, methodInfo);
                     propertyChanges.incrementAndGet();
                     eventualCluster.commitAssumptionBuffer();
                 } else {
    @@ -227,6 +230,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
                             pi.analysis().set(EVENTUALLY_UNMODIFIED_PARAMETER,
                                     new ValueImpl.SetOfStringsImpl(Set.copyOf(labels)));
                             DECIDE.debug("TE: Decide eventually-unmodified of parameter {} after {}", pi, labels);
    +                        TolerantWrite.markChanged(EVENTUALLY_UNMODIFIED_PARAMETER, pi);
                             propertyChanges.incrementAndGet();
                             eventualCluster.commitAssumptionBuffer();
                         } else {
    @@ -280,6 +284,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
                 Value.EventuallyImmutable contracted = eventuallyImmutable(typeInfo);
                 if (contracted.isEventual()) {
                     typeInfo.analysis().set(EVENTUALLY_IMMUTABLE_TYPE, contracted);
    +                TolerantWrite.markChanged(EVENTUALLY_IMMUTABLE_TYPE, typeInfo);
                     propertyChanges.incrementAndGet();
                     return; // contracts win
                 }
    @@ -506,6 +511,7 @@ public class TypeEventualAnalyzerImpl extends CommonAnalyzerImpl implements Type
                 typeInfo.analysis().set(EVENTUALLY_IMMUTABLE_TYPE, value);
             }
             DECIDE.debug("TE: Decide eventual immutability of type {} = {}", typeInfo, value);
    +        TolerantWrite.markChanged(EVENTUALLY_IMMUTABLE_TYPE, typeInfo);
             propertyChanges.incrementAndGet();
         }
     
    diff --git a/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java b/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java
    index 969af75f..7fc2cc68 100644
    --- a/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java
    +++ b/maddi-modification-common/src/main/java/io/codelaser/maddi/modification/common/util/TolerantWrite.java
    @@ -79,7 +79,18 @@ public final class TolerantWrite {
             }
         }
     
    -    /** Bookkeeping for a deliberate overwrite made outside this class: counts it and wakes the target's dependents. */
    +    /**
    +     * Bookkeeping for a write made outside this class (a plain {@code set()} of a summary property): counts it and
    +     * wakes the target's dependents. Without it, the worklist never sees the change, and only a full verification
    +     * pass does: it finds changes on no element, and the next full pass repeats (CodeLaser/maddi-mod#76).
    +     */
    +    public static void markChanged(Property property, Object context) {
    +        CHANGES.computeIfAbsent(property.key(), _ -> new java.util.concurrent.atomic.LongAdder()).increment();
    +        if (context != null && !(context instanceof String) && !INTERNAL_PROPERTIES.contains(property.key())) {
    +            CHANGED_TARGETS.add(context);
    +        }
    +    }
    +
         /**
          * The write of an ANALYSIS-ORDER recomputation (the method's slot in the main loop, or the link computer's
          * own doType slot): the latest computation wins over an equal-keyed stored value with different content.
  2. bnaudts commented on Nov 3, 2026

    @bnaudts
    Author

    Re-measured on devel + #89/#80 (ws/maddi-null), guava TestGuava, FPDUMP + FPDUMP_PARAMS

    runs dump lines that differ between runs
    2× PARALLEL=1 0
    4× PARALLEL=8 6–10 between any two runs

    Six verdicts vary, and each comes out differently in a different 8-thread run (so these are races, not two stable outcomes): CaseFormat.$4 eventual, the independence of three constructor parameters (Equivalence.EquivalentToPredicate, SortedMultisets.ElementSet, InterruptibleTask.Blocker) and of two fields (CycleDetectingReentrantReadWriteLock.writeLock, Striped.WeakSafeLock.strongReference).

    Cause, shown for EquivalentToPredicate: in every 8-thread run, never sequentially, its parameter is set to @Independent by cycle breaking (TypeModIndyAnalyzerImpl.doIndependent: "decide the never-decided"), 1–3 times. Whether a later honest computation overwrites that default then decides the final verdict. Cycle breaking settles whatever is undecided at the moment an element is reached; in a parallel pass that moment depends on the threads.

    Experiment: SEQCYCLEBREAKING (6742432, opt-in)

    Once cycle breaking is active, every pass runs sequentially; the passes before stay parallel. 4× PARALLEL=8 on laser1, 445–478 s each:

    • all six verdicts above now equal the sequential run's, in all four runs;
    • two variants remain (p8a = p8b, p8c = p8d), in type independence only: five util.concurrent futures (TimeoutFuture, AbstractCatchingFuture, AbstractTransformFuture, FluentFuture.TrustedFuture, TrustedListenableFutureTask) and SpliteratorTester.GeneralSpliteratorOfObject.

    Every run activates cycle breaking 4 times: after the first certification and again after each MODREACH re-derivation round (cycleBreakingActive = false, IteratingAnalyzerImpl ~l.811). The parallel passes in between take a varying number of iterations (first activation at 19 sequentially, at 20–24 in parallel). In some runs those passes decide TimeoutFuture's independence honestly (@Dependent) before the last activation; in others it stays undecided and cycle breaking sets @Independent (as sequentially). So a parallel pass that is not cycle breaking also leaks order: a value that one order decides and another leaves undecided means some input is read in a state that is not monotone (written, then cleared or overwritten, within a pass).

    Next: find which input TimeoutFuture's type independence reads in the passes before the 4th activation, and whether it is overwritten mid-pass. Separately: the cost of SEQCYCLEBREAKING against an ungated 8-thread run on the same machine.

  3. bnaudts commented on Nov 3, 2026

    @bnaudts
    Author

    Second source found and fixed: the abstract-supertype shortcut in type independence (e7efb20, gate NOSUPERWAIT)

    A per-pass trace (EC_TYPE_DEBUG now also prints type independence per pass, 9f6e3b2) showed where the two remaining variants split: in the pass after a MODREACH reset, FluentFuture.TrustedFuture reads its supertype FluentFuture before that has a verdict. TypeIndependentAnalyzerImpl.independentSuper then falls back to the min over the supertype's concrete methods, ignoring its fields, abstract methods and own supertypes. Read early, that gives @Independent; one iteration later the supertype's own verdict, @Dependent, is there. An @Independent type is never revisited, so the early read stuck and flowed down to TimeoutFuture and four more futures (and the same for SpliteratorTester.GeneralSpliterator's subtypes).

    Fix: a source supertype is waited for; the shortcut stays for library supertypes, which get no verdict of their own.

    guava, laser1 (TestGuava, FPDUMP + FPDUMP_PARAMS):

    result
    analyzer suite 679, 2 skipped, 0 failed
    4× PARALLEL=8 with SEQCYCLEBREAKING byte-identical, 447–459 s each
    1× PARALLEL=1 identical to the 8-thread runs, 912 s

    What the fix changes (sequential, before vs after): 646 verdicts, all type independence, all downward; no immutability, method or parameter verdict moves.

    change types
    @Independent → @Dependent 409
    @Independent → @Independent(hc=true) 116
    @Independent(hc=true) → @Dependent 102
    @Independent(except=…) / (hc, except=…) → @Dependent 17

    Examples: util.concurrent.AbstractCatchingFuture (an AbstractFuture subclass) and anonymous ForwardingObject subclasses in the tests → @Dependent; base.Equivalence.Equals/Identity → @Independent(hc=true). Many are anonymous test classes.

    Open:

  4. bnaudts commented on Nov 4, 2026

    @bnaudts
    Author

    CBSWEEP (2cbbdc3, opt-in) is an alternative to SEQCYCLEBREAKING that keeps the method loop parallel. Under cycle breaking, the per-element loop runs without it. Only on a full pass whose loop changed nothing does a sequential sweep in analysis order apply cycle breaking to the fields and types. That state is the fixpoint without cycle breaking, so it does not depend on thread timing.

    guava, CBSWEEP=1, on the Mac:

    run time verdicts
    3 runs at 8 threads 396–419 s byte-identical
    1 sequential run 1205 s 3 lines differ from the 8-thread runs
    • Sequential vs parallel: the 3 lines are eventual verdicts of three anonymous CaseFormat constants (after="wordBoundary" in the sequential run, null at 8 threads). With SEQCYCLEBREAKING, sequential and parallel were identical, so this is still to trace.
    • Cost: the Mac's earlier ungated 8-thread runs took 363–381 s, but at 3601648, before the supertype fix. That suggests about +10%, against about +50% for SEQCYCLEBREAKING. Not yet measured at a single commit.
    • Analyzer suite with CBSWEEP=1 (laser1): 680 tests, 2 skipped, 0 failed.

    Verdicts compared with SEQCYCLEBREAKING: 249 lines change, mostly upward. Upward changes need a soundness check (corpus).

    change count
    field independence @Dependent → @Independent 189
    type independence @Dependent → @Independent 23
    type independence @Independent → @Independent(hc=true) 18
    type immutability @FinalFields → @Immutable(hc=true) 7
    field independence @Dependent → @Independent(hc=true) 7
    type independence @Dependent → @Independent(hc=true) 6
    eventual and other 6

    The likely reading: the old scheme applied defaults while the loop was still deciding these values.

    Regression test for the supertype fix: TestSubtypeWaitsForAbstractSupertype (737913b). It fails with NOSUPERWAIT=1.

  5. bnaudts commented on Nov 4, 2026

    @bnaudts
    Author

    CBSWEEP with 3e9b0a4 (type analyzers out of the parallel loop) is now thread-count independent on guava.

    Cause of the earlier sequential-vs-parallel difference: the eventual assumption ledger records whether a candidate already had its verdict when a member leaned on it. Built inside the parallel loop, that depended on timing. CaseFormat.$0 and $1 recorded CaseFormat at 8 threads and were retracted with it by cascade. Every ledger write comes from the type analyzers, which under CBSWEEP now run only in the sequential second type pass.

    Run on the Mac (laser1 was full), same commit:

    run time verdicts
    analyzer suite, CBSWEEP=1 680 tests, 2 skipped, 0 failed
    CBSWEEP=1, 8 threads, 2 runs 409, 444 s identical
    CBSWEEP=1, sequential 1320 s identical to the 8-thread runs
    ungated, 8 threads 372 s —
    • Cost: +10–19% against 1 ungated sample on a busy Mac; SEQCYCLEBREAKING cost about +50%.
    • Verdicts, ungated → CBSWEEP: 520 lines change, mostly upward.
    change count
    field independence @Dependent → @Independent 191
    type independence @Dependent → @Independent 23
    type independence @Independent → @Independent(hc=true) 18
    type immutability @FinalFields → @Immutable(hc=true) 7
    field independence @Dependent → @Independent(hc=true) 7
    type independence @Dependent → @Independent(hc=true) 6
    eventual and other ~20
  6. bnaudts commented on Nov 5, 2026

    @bnaudts
    Author

    Fixed in devel: CBSWEEP is now the default (c3ba74b), and NOCBSWEEP=1 turns it off. Under cycle breaking, the defaults are decided in a sequential sweep at the fixpoint, and the type analyzers run outside the parallel loop. guava gives identical verdicts at 8 threads and sequentially.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions