From 3ceb3bd26d29d81be676d24964b06e19b15c8d0c Mon Sep 17 00:00:00 2001
From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com>
Date: Wed, 19 Aug 2026 09:41:47 +0000
Subject: [PATCH 01/23] feat(ifds): add field-insensitive BaseOnly access-path
mode
Adds a third access-path representation alongside Tree and Automata. A
BaseOnly access is a packed Long carrying three positional slots
(static / field / terminal) rather than a path tree, so a method summary
family collapses to a bounded number of edges no matter how many concrete
field chains reach it.
The shared access layer gains the extension points BaseOnly needs:
- MethodEdgesInitialToFinalApSet.add now returns every stored final whose
metadata the insertion changed, not just the inserted pair. Tree already
behaved this way; Cactus and Automata silently dropped deltas when a
merged exclusion widened.
- addAll inserts many premises against one conclusion without
materialising a path edge per premise.
- filterEdgesByFinalTo and collectSummariesByFinalTo thread an optional
full final-fact pattern so an indexed storage can narrow candidates.
- ExclusionSet.Concrete accepts a pluggable persistent accessor set and
computes its hash lazily.
ConcurrentReadSafeLong2ObjectMap and ConcurrentReadSafeLongSet are the
primitive-long counterparts of the existing read-safe collections, for the
packed-Long summary storages: one writer, many readers, no removals.
Design and conformance notes live in docs/baseonly-*.md.
---
.../ConcurrentReadSafeLong2ObjectMap.java | 75 ++
.../util/ConcurrentReadSafeLongSet.java | 61 +
.../util/ConcurrentReadSafeObject2IntMap.java | 78 +-
.../dataflow/ap/ifds/ExclusionSet.kt | 90 +-
.../dataflow/ap/ifds/MethodAnalyzerEdges.kt | 50 +-
.../ap/ifds/MethodSummariesUnitStorage.kt | 8 +
.../ap/ifds/SummaryEdgeSubscription.kt | 159 ++-
.../dataflow/ap/ifds/access/ApManager.kt | 38 +-
.../MethodEdgesInitialToFinalAutomataApSet.kt | 63 +-
.../ap/ifds/access/baseonly/BaseOnlyAccess.kt | 239 ++++
.../ifds/access/baseonly/BaseOnlyAccessOps.kt | 555 +++++++++
.../access/baseonly/BaseOnlyAccessView.kt | 65 +
.../ifds/access/baseonly/BaseOnlyApAccess.kt | 30 +
.../ifds/access/baseonly/BaseOnlyApManager.kt | 151 +++
.../ap/ifds/access/baseonly/BaseOnlyDelta.kt | 90 ++
.../ifds/access/baseonly/BaseOnlyExclusion.kt | 18 +
.../access/baseonly/BaseOnlyExclusionSet.kt | 235 ++++
.../BaseOnlyF2FFieldGeneralization.kt | 184 +++
.../access/baseonly/BaseOnlyFinalFactAp.kt | 225 ++++
.../access/baseonly/BaseOnlyFinalFactList.kt | 29 +
.../baseonly/BaseOnlyInitialAccessIndex.kt | 199 ++++
.../BaseOnlyInitialFactAbstraction.kt | 287 +++++
.../access/baseonly/BaseOnlyInitialFactAp.kt | 97 ++
.../access/baseonly/BaseOnlySerializer.kt | 90 ++
.../BaseOnlySideEffectRequirementApStorage.kt | 124 ++
...seOnlySideEffectRequirementDeltaTracker.kt | 63 +
.../access/baseonly/BaseOnlySummaryEdgeOps.kt | 140 +++
.../FactSESummariesBaseOnlyStorage.kt | 53 +
.../MethodBaseOnlyAccessPathSubscription.kt | 156 +++
.../baseonly/MethodEdgesFinalBaseOnlyApSet.kt | 37 +
.../MethodEdgesInitialToFinalBaseOnlyApSet.kt | 338 ++++++
...ethodEdgesNDInitialToFinalBaseOnlyApSet.kt | 52 +
.../MethodFinalBaseOnlyApSummariesStorage.kt | 31 +
...nitialToFinalBaseOnlyApSummariesStorage.kt | 351 ++++++
...nitialToFinalBaseOnlyApSummariesStorage.kt | 77 ++
.../MethodEdgesInitialToFinalCactusApSet.kt | 9 +-
.../ap/ifds/access/common/CommonF2FSet.kt | 46 +-
.../ap/ifds/access/common/CommonF2FSummary.kt | 93 +-
.../ndf2f/DefaultNDF2FSummaryStorageWithAp.kt | 4 +
.../MethodEdgesInitialToFinalTreeApSet.kt | 4 +-
.../org/opentaint/dataflow/util/MapUtils.kt | 55 +
.../dataflow/ap/ifds/ExclusionSetTest.kt | 59 +
.../MethodEdgesInitialToFinalApSetTest.kt | 132 +++
.../baseonly/BaseOnlyAccessPackingTest.kt | 65 +
.../access/baseonly/BaseOnlyAccessTest.kt | 260 ++++
.../access/baseonly/BaseOnlyAnyMatchTest.kt | 90 ++
.../baseonly/BaseOnlyApDeltaConcatTest.kt | 212 ++++
.../baseonly/BaseOnlyAppendFinalTest.kt | 57 +
.../access/baseonly/BaseOnlyClearTableTest.kt | 218 ++++
.../baseonly/BaseOnlyContainsTableTest.kt | 237 ++++
.../baseonly/BaseOnlyDeltaConcatPinTest.kt | 213 ++++
.../access/baseonly/BaseOnlyDeltaEnumTest.kt | 83 ++
.../ifds/access/baseonly/BaseOnlyDeltaTest.kt | 329 ++++++
.../BaseOnlyF2FSummaryStorageLawTest.kt | 1043 +++++++++++++++++
.../access/baseonly/BaseOnlyFactOpsTest.kt | 197 ++++
.../access/baseonly/BaseOnlyFactSetTest.kt | 697 +++++++++++
.../BaseOnlyInitialAccessIndexTest.kt | 252 ++++
...BaseOnlyInitialFactAbstractionCasesTest.kt | 389 ++++++
...yInitialFactAbstractionDifferentialTest.kt | 364 ++++++
.../access/baseonly/BaseOnlyManagerTest.kt | 142 +++
.../baseonly/BaseOnlyRelationLawTest.kt | 88 ++
.../access/baseonly/BaseOnlySerializerTest.kt | 200 ++++
...lySideEffectRequirementDeltaTrackerTest.kt | 89 ++
.../BaseOnlySplitDeltaAlignmentTest.kt | 238 ++++
.../BaseOnlySubscriptionAndReqTest.kt | 496 ++++++++
.../BaseOnlySummaryNormalizationTest.kt | 150 +++
.../ifds/access/baseonly/BaseOnlyTestUtils.kt | 39 +
.../BaseOnlyTreeDifferentialOperationsTest.kt | 773 ++++++++++++
.../BaseOnlyTreeDifferentialStorageTest.kt | 561 +++++++++
.../ConcurrentReadSafeLongCollectionsTest.kt | 156 +++
.../baseonly/contains_pin_mode0.golden.txt | 95 ++
.../baseonly/contains_pin_mode1.golden.txt | 437 +++++++
.../delta_concat_pin_mode0.golden.txt | 65 +
.../delta_concat_pin_mode1.golden.txt | 138 +++
.../splitdelta_align_mode0.golden.txt | 92 ++
.../splitdelta_align_mode1.golden.txt | 389 ++++++
docs/baseonly-access-domain-spec.md | 732 ++++++++++++
docs/baseonly-storage-spec.md | 587 ++++++++++
...bscription-and-polymorphic-proxy-design.md | 64 +
docs/baseonly-summary-edge-filter-design.md | 160 +++
...only-summary-edge-generalization-design.md | 283 +++++
...aseonly-summary-edge-subsumption-design.md | 271 +++++
docs/baseonly-tree-conformance.md | 281 +++++
83 files changed, 16046 insertions(+), 126 deletions(-)
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodInitialToFinalBaseOnlyApSummariesStorage.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodNDInitialToFinalBaseOnlyApSummariesStorage.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSetTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/MethodEdgesInitialToFinalApSetTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessPackingTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAnyMatchTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApDeltaConcatTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAppendFinalTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyClearTableTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyContainsTableTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaConcatPinTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaEnumTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDeltaTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FSummaryStorageLawTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactOpsTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFactSetTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndexTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionCasesTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstractionDifferentialTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyManagerTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyRelationLawTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializerTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTrackerTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySplitDeltaAlignmentTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySubscriptionAndReqTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryNormalizationTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTestUtils.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialOperationsTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyTreeDifferentialStorageTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/kotlin/org/opentaint/dataflow/util/ConcurrentReadSafeLongCollectionsTest.kt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode0.golden.txt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/contains_pin_mode1.golden.txt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode0.golden.txt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/delta_concat_pin_mode1.golden.txt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode0.golden.txt
create mode 100644 core/opentaint-dataflow-core/opentaint-dataflow/src/test/resources/baseonly/splitdelta_align_mode1.golden.txt
create mode 100644 docs/baseonly-access-domain-spec.md
create mode 100644 docs/baseonly-storage-spec.md
create mode 100644 docs/baseonly-subscription-and-polymorphic-proxy-design.md
create mode 100644 docs/baseonly-summary-edge-filter-design.md
create mode 100644 docs/baseonly-summary-edge-generalization-design.md
create mode 100644 docs/baseonly-summary-edge-subsumption-design.md
create mode 100644 docs/baseonly-tree-conformance.md
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java
new file mode 100644
index 000000000..288e46c60
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLong2ObjectMap.java
@@ -0,0 +1,75 @@
+package org.opentaint.dataflow.util;
+
+import it.unimi.dsi.fastutil.HashCommon;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A primitive long map with point reads that tolerate a concurrent rehash.
+ *
+ *
The supported concurrency model is one writer and any number of readers. Removals are not
+ * supported. Iteration must use the captured-table helper in {@code MapUtils.kt}; the inherited
+ * fastutil iterators are not concurrent-read-safe.
+ */
+public final class ConcurrentReadSafeLong2ObjectMap extends Long2ObjectOpenHashMap {
+ @Override
+ public @Nullable V get(long k) {
+ if (k == 0) {
+ if (!containsNullKey) return defRetValue;
+
+ do {
+ int n = this.n;
+ V[] value = this.value;
+ if (value.length == n + 1) return value[n];
+ } while (true);
+ }
+
+ while (true) {
+ long[] key = this.key;
+ V[] value = this.value;
+ int n = this.n;
+
+ // Capture a matching table generation to allow a read during rehash.
+ if (key.length != n + 1 || value.length != n + 1) continue;
+
+ int mask = n - 1;
+ int pos = (int) HashCommon.mix(k) & mask;
+ long curr = key[pos];
+ if (curr == 0) return defRetValue;
+
+ if (k == curr) return value[pos];
+
+ // There's always an unused entry.
+ while (true) {
+ pos = (pos + 1) & mask;
+ curr = key[pos];
+ if (curr == 0) return defRetValue;
+
+ if (k == curr) return value[pos];
+ }
+ }
+ }
+
+ @Override
+ public V remove(long k) {
+ throw new UnsupportedOperationException("Removals are not allowed");
+ }
+
+ public long[] getKeys() {
+ return this.key;
+ }
+
+ public V[] getValues() {
+ return this.value;
+ }
+
+ public int getN() {
+ return this.n;
+ }
+
+ public boolean getContainsNullKey() {
+ return this.containsNullKey;
+ }
+
+ private static final long serialVersionUID = 0L;
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java
new file mode 100644
index 000000000..ba0aaa1be
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeLongSet.java
@@ -0,0 +1,61 @@
+package org.opentaint.dataflow.util;
+
+import it.unimi.dsi.fastutil.HashCommon;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+
+/**
+ * A primitive long set with point reads that tolerate a concurrent rehash.
+ *
+ * The supported concurrency model is one writer and any number of readers. Removals are not
+ * supported. Iteration must use the captured-table helper in {@code MapUtils.kt}; the inherited
+ * fastutil iterators are not concurrent-read-safe.
+ */
+public final class ConcurrentReadSafeLongSet extends LongOpenHashSet {
+ @Override
+ public boolean contains(long k) {
+ if (k == 0) return containsNull;
+
+ while (true) {
+ long[] key = this.key;
+ int n = this.n;
+
+ // Capture one complete table generation to allow a read during rehash.
+ if (key.length != n + 1) continue;
+
+ int mask = n - 1;
+ int pos = (int) HashCommon.mix(k) & mask;
+ long curr = key[pos];
+ if (curr == 0) return false;
+
+ if (k == curr) return true;
+
+ // There's always an unused entry.
+ while (true) {
+ pos = (pos + 1) & mask;
+ curr = key[pos];
+ if (curr == 0) return false;
+
+ if (k == curr) return true;
+ }
+ }
+ }
+
+ @Override
+ public boolean remove(long k) {
+ throw new UnsupportedOperationException("Removals are not allowed");
+ }
+
+ public long[] getKeys() {
+ return this.key;
+ }
+
+ public int getN() {
+ return this.n;
+ }
+
+ public boolean getContainsNull() {
+ return this.containsNull;
+ }
+
+ private static final long serialVersionUID = 0L;
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java
index edbabe33c..00ecd584d 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/java/org/opentaint/dataflow/util/ConcurrentReadSafeObject2IntMap.java
@@ -4,9 +4,18 @@
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.jetbrains.annotations.Nullable;
+/**
+ * A flat object-to-int map supporting one writer and multiple concurrent point readers.
+ *
+ * Writes are published through a sequence counter. Readers retry if a write overlaps their
+ * lookup, which prevents observing a key before its primitive value or a partially published
+ * rehash. Removals are not supported.
+ */
public final class ConcurrentReadSafeObject2IntMap extends Object2IntOpenHashMap {
public static final int NO_VALUE = -1;
+ private volatile long writeSequence;
+
public ConcurrentReadSafeObject2IntMap() {
super();
defaultReturnValue(NO_VALUE);
@@ -14,46 +23,63 @@ public ConcurrentReadSafeObject2IntMap() {
@Override
public int getInt(@Nullable Object k) {
- if (k == null) {
- if (!containsNullKey) return defRetValue;
-
- do {
- int n = this.n;
- int[] value = this.value;
- if (value.length == n + 1) return value[n];
- } while (true);
- }
-
while (true) {
+ long sequenceBefore = writeSequence;
+ if ((sequenceBefore & 1) != 0) continue;
+
K[] key = this.key;
int[] value = this.value;
- int n = this.n;
+ int result = findValue(k, key, value);
- // capture arrays to allow concurrent reads
- if (key.length != n + 1 || value.length != n + 1) continue;
+ if (sequenceBefore == writeSequence) return result;
+ }
+ }
- int mask = n - 1;
+ private int findValue(@Nullable Object k, K[] key, int[] value) {
+ if (k == null) return containsNullKey ? value[value.length - 1] : defRetValue;
- // The starting point.
- int pos = HashCommon.mix(k.hashCode()) & mask;
+ int mask = key.length - 2;
+ int pos = HashCommon.mix(k.hashCode()) & mask;
+ K curr = key[pos];
+ if (curr == null) return defRetValue;
+ if (k.equals(curr)) return value[pos];
- K curr = key[pos];
+ while (true) {
+ pos = (pos + 1) & mask;
+ curr = key[pos];
if (curr == null) return defRetValue;
-
if (k.equals(curr)) return value[pos];
+ }
+ }
- // There's always an unused entry.
- while (true) {
- pos = (pos + 1) & mask;
-
- curr = key[pos];
- if (curr == null) return defRetValue;
+ @Override
+ public int put(K key, int value) {
+ beginWrite();
+ try {
+ return super.put(key, value);
+ } finally {
+ endWrite();
+ }
+ }
- if (k.equals(curr)) return value[pos];
- }
+ @Override
+ public int putIfAbsent(K key, int value) {
+ beginWrite();
+ try {
+ return super.putIfAbsent(key, value);
+ } finally {
+ endWrite();
}
}
+ private void beginWrite() {
+ writeSequence++;
+ }
+
+ private void endWrite() {
+ writeSequence++;
+ }
+
@Override
public int removeInt(Object k) {
throw new UnsupportedOperationException("Removals are not allowed");
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt
index 30453d1e2..648441945 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/ExclusionSet.kt
@@ -34,37 +34,48 @@ sealed interface ExclusionSet {
override fun toString(): String = "*"
}
- data class Concrete(
- val set: PersistentSet,
- private val hash: Int,
+ class Concrete private constructor(
+ val set: Set,
+ @Volatile
+ private var cachedHash: Int?,
) : ExclusionSet {
+ constructor(set: PersistentSet) : this(set, null)
constructor(accessor: Accessor) : this(persistentHashSetOf(accessor), accessor.hashCode())
- override fun hashCode(): Int = hash
+ private constructor(set: Set) : this(set, null)
+ internal constructor(set: PersistentAccessorSet) : this(set, set.hashCode())
+
+ override fun hashCode(): Int {
+ cachedHash?.let { return it }
+
+ return set.hashCode().also { cachedHash = it }
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Concrete) return false
- if (hash != other.hash) return false
+ val currentHash = cachedHash
+ val otherHash = other.cachedHash
+ if (currentHash != null && otherHash != null && currentHash != otherHash) return false
return set == other.set
}
override fun contains(accessor: Accessor): Boolean = set.contains(accessor)
override fun add(accessor: Accessor): ExclusionSet {
- val setWithAccessor = set.add(accessor)
+ val setWithAccessor = set.persistentAdd(accessor)
if (setWithAccessor === set) return this
- return Concrete(setWithAccessor, hash + accessor.hashCode())
+ return Concrete(setWithAccessor, hashCode() + accessor.hashCode())
}
override fun union(other: ExclusionSet): ExclusionSet = when (other) {
Empty -> this
Universe -> other
is Concrete -> {
- val union = set.addAll(other.set)
- if (union === set) this else Concrete(union, union.hashCode())
+ val union = set.persistentAddAll(other.set)
+ if (union === set) this else Concrete(union)
}
}
@@ -72,21 +83,30 @@ sealed interface ExclusionSet {
Empty -> other
Universe -> this
is Concrete -> {
- val intersection = set.retainAll(other.set)
+ val intersection = set.persistentRetainAll(other.set)
when {
intersection === set -> this
intersection.isEmpty() -> Empty
- else -> Concrete(intersection, intersection.hashCode())
+ else -> Concrete(intersection)
}
}
}
override fun subtract(accessor: Accessor): ExclusionSet {
- val subtractResult = set.remove(accessor)
+ val subtractResult = set.persistentRemove(accessor)
return when {
subtractResult === set -> this
subtractResult.isEmpty() -> Empty
- else -> Concrete(subtractResult, hash - accessor.hashCode())
+ else -> Concrete(subtractResult, hashCode() - accessor.hashCode())
+ }
+ }
+
+ internal fun subtract(other: Concrete): ExclusionSet {
+ val subtractResult = set.persistentRemoveAll(other.set)
+ return when {
+ subtractResult === set -> this
+ subtractResult.isEmpty() -> Empty
+ else -> Concrete(subtractResult)
}
}
@@ -99,3 +119,47 @@ sealed interface ExclusionSet {
override fun toString(): String = set.joinToString(prefix = "{", postfix = "}") { it.toSuffix() }
}
}
+
+/** Immutable set operations used by compact AP-specific exclusion representations. */
+internal interface PersistentAccessorSet : Set {
+ fun addPersistent(accessor: Accessor): PersistentAccessorSet
+ fun addAllPersistent(accessors: Set): PersistentAccessorSet
+ fun retainAllPersistent(accessors: Set): PersistentAccessorSet
+ fun removePersistent(accessor: Accessor): PersistentAccessorSet
+ fun removeAllPersistent(accessors: Set): PersistentAccessorSet
+}
+
+private fun Set.persistentAdd(accessor: Accessor): Set =
+ when (this) {
+ is PersistentAccessorSet -> addPersistent(accessor)
+ is PersistentSet -> add(accessor)
+ else -> persistentHashSetOf().addAll(this).add(accessor)
+ }
+
+private fun Set.persistentAddAll(other: Set): Set =
+ when (this) {
+ is PersistentAccessorSet -> addAllPersistent(other)
+ is PersistentSet -> addAll(other)
+ else -> persistentHashSetOf().addAll(this).addAll(other)
+ }
+
+private fun Set.persistentRetainAll(other: Set): Set =
+ when (this) {
+ is PersistentAccessorSet -> retainAllPersistent(other)
+ is PersistentSet -> retainAll(other)
+ else -> persistentHashSetOf().addAll(this).retainAll(other)
+ }
+
+private fun Set.persistentRemove(accessor: Accessor): Set =
+ when (this) {
+ is PersistentAccessorSet -> removePersistent(accessor)
+ is PersistentSet -> remove(accessor)
+ else -> persistentHashSetOf().addAll(this).remove(accessor)
+ }
+
+private fun Set.persistentRemoveAll(other: Set): Set =
+ when (this) {
+ is PersistentAccessorSet -> removeAllPersistent(other)
+ is PersistentSet -> removeAll(other)
+ else -> persistentHashSetOf().addAll(this).removeAll(other)
+ }
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt
index 999c6bac3..110334c8d 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodAnalyzerEdges.kt
@@ -13,6 +13,9 @@ class MethodAnalyzerEdges(
private val methodEntryPoint: MethodEntryPoint,
languageManager: LanguageManager
) {
+ var modificationVersion: Long = 0
+ private set
+
private val maxInstIdx = languageManager.getMaxInstIndex(methodEntryPoint.method)
private val zeroToZeroEdges = SameInitialZeroFactEdges(maxInstIdx, languageManager)
@@ -23,7 +26,9 @@ class MethodAnalyzerEdges(
fun add(edge: Edge): List {
check(edge.methodEntryPoint == methodEntryPoint)
- return addEdge(edge)
+ return addEdge(edge).also { added ->
+ if (added.isNotEmpty()) modificationVersion++
+ }
}
fun reachedStatements() = zeroToZeroEdges.reachedStatements()
@@ -96,18 +101,32 @@ class MethodAnalyzerEdges(
val initialAp = edge.initialFactAp
val finalAp = edge.factAp
- val (addedInitial, addedFinal) = taintedToFactEdges.add(edge.statement, initialAp, finalAp) ?: return emptyList()
-
- if (addedInitial === initialAp && addedFinal === finalAp) return listOf(edge)
+ return taintedToFactEdges.add(edge.statement, initialAp, finalAp).map { (addedInitial, addedFinal) ->
+ if (addedInitial === initialAp && addedFinal === finalAp) {
+ edge
+ } else {
+ Edge.FactToFact(
+ methodEntryPoint = edge.methodEntryPoint,
+ initialFactAp = addedInitial,
+ statement = edge.statement,
+ factAp = addedFinal,
+ )
+ }
+ }
+ }
- return listOf(
- Edge.FactToFact(
- methodEntryPoint = edge.methodEntryPoint,
- initialFactAp = addedInitial,
- statement = edge.statement,
- factAp = addedFinal
- )
- )
+ fun addFactToFactSupports(
+ statement: CommonInst,
+ initialFacts: Iterable,
+ finalFact: FinalFactAp,
+ emitDelta: (InitialFactAp, FinalFactAp) -> Unit,
+ ) {
+ var changed = false
+ taintedToFactEdges.addAll(statement, initialFacts, finalFact) { initial, addedFinal ->
+ changed = true
+ emitDelta(initial, addedFinal)
+ }
+ if (changed) modificationVersion++
}
fun allZeroToFactFactsAtStatement(statement: CommonInst, finalFactPattern: InitialFactAp): List {
@@ -116,6 +135,12 @@ class MethodAnalyzerEdges(
return result
}
+ fun allZeroToFactFactsAtStatement(statement: CommonInst): List {
+ val result = mutableListOf()
+ zeroToFactEdges.collectApAtStatement(result, statement)
+ return result
+ }
+
fun allFactToFactFactsAtStatement(statement: CommonInst, finalFactPattern: InitialFactAp): List> {
val result = mutableListOf>()
taintedToFactEdges.collectApAtStatement(result, statement, finalFactPattern)
@@ -140,6 +165,7 @@ class MethodAnalyzerEdges(
return result
}
+
private class SameInitialZeroFactEdges(
maxInstIdx: Int,
private val languageManager: LanguageManager
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt
index e54ea56cb..897f31fb5 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/MethodSummariesUnitStorage.kt
@@ -78,6 +78,14 @@ open class MethodSummariesUnitStorage(
return methodStorage.factToFactEdges(finalFactBase)
}
+ fun methodFactToFactSummaryEdges(
+ methodEntryPoint: MethodEntryPoint,
+ finalFactPattern: FinalFactAp,
+ ): List {
+ val methodStorage = methodSummaryEdges(methodEntryPoint)
+ return methodStorage.factToFactEdges(finalFactPattern)
+ }
+
fun methodFactNDSummaries(
methodEntryPoint: MethodEntryPoint,
finalFactBase: AccessPathBase
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt
index 8563d1702..a72174924 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/SummaryEdgeSubscription.kt
@@ -12,6 +12,7 @@ import org.opentaint.dataflow.ap.ifds.access.ApManager
import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
import org.opentaint.dataflow.ap.ifds.access.MethodAccessPathSubscription
+import org.opentaint.dataflow.ap.ifds.access.baseonly.BaseOnlyApManager
import org.opentaint.dataflow.ap.ifds.serialization.MethodEntryPointSummaries
import org.opentaint.dataflow.util.collectToListWithPostProcess
import org.opentaint.dataflow.util.concurrentReadSafeForEach
@@ -565,9 +566,13 @@ class SummaryEdgeSubscriptionManager(
handleF2F: MethodAnalyzer.(List, List) -> Unit,
handleZ2F: MethodAnalyzer.(List, List) -> Unit,
handleND2F: MethodAnalyzer.(List, List) -> Unit,
+ emptyDeltaRequired: Boolean = false,
) {
- subscriptionStorage.findFactEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) ->
- val summarySubs = subscriptions.mapTo(mutableListOf()) {
+ subscriptionStorage.findFactEdgeSub(summaryInitialFact, emptyDeltaRequired).forEach { (ep, subscriptions) ->
+ val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) {
+ if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ return@mapNotNullTo null
+ }
FactToFactSub(it.callerPathEdge, it.calleeInitialFactBase)
}
@@ -578,7 +583,10 @@ class SummaryEdgeSubscriptionManager(
}
subscriptionStorage.findZeroEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) ->
- val summarySubs = subscriptions.mapTo(mutableListOf()) {
+ val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) {
+ if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ return@mapNotNullTo null
+ }
ZeroToFactSub(it.callerPathEdge, it.calleeInitialFactBase)
}
@@ -588,8 +596,11 @@ class SummaryEdgeSubscriptionManager(
analyzer.handleZ2F(summarySubs, summaries)
}
- subscriptionStorage.findFactNDEdgeSub(summaryInitialFact).forEach { (ep, subscriptions) ->
- val summarySubs = subscriptions.mapTo(mutableListOf()) {
+ subscriptionStorage.findFactNDEdgeSub(summaryInitialFact, emptyDeltaRequired).forEach { (ep, subscriptions) ->
+ val summarySubs = subscriptions.mapNotNullTo(mutableListOf()) {
+ if (emptyDeltaRequired && !it.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ return@mapNotNullTo null
+ }
NDFactToFactSub(it.callerPathEdge, it.calleeInitialFactBase)
}
@@ -611,34 +622,116 @@ class SummaryEdgeSubscriptionManager(
}
}
+ if (manager.apManager !is BaseOnlyApManager) {
+ for ((summaryInitialFact, summaries) in sameInitialFactEdges) {
+ applySummaries(
+ subscriptionStorage, summaryInitialFact, summaries,
+ MethodAnalyzer::handleFactToFactMethodNDSummaryEdge,
+ MethodAnalyzer::handleZeroToFactMethodNDSummaryEdge,
+ MethodAnalyzer::handleNDFactToFactMethodNDSummaryEdge,
+ emptyDeltaRequired = true,
+ )
+ }
+ return
+ }
+
+ val factActivations = linkedMapOf<
+ MethodEntryPoint,
+ MutableMap>,
+ >()
+ val zeroActivations = linkedMapOf<
+ MethodEntryPoint,
+ MutableMap>,
+ >()
+ val ndActivations = linkedMapOf<
+ MethodEntryPoint,
+ MutableMap>,
+ >()
+
for ((summaryInitialFact, summaries) in sameInitialFactEdges) {
- applySummaries(
- subscriptionStorage, summaryInitialFact, summaries,
- MethodAnalyzer::handleFactToFactMethodNDSummaryEdge,
- MethodAnalyzer::handleZeroToFactMethodNDSummaryEdge,
- MethodAnalyzer::handleNDFactToFactMethodNDSummaryEdge,
- )
+ subscriptionStorage.findFactEdgeSub(summaryInitialFact, emptyDeltaRequired = true)
+ .forEach { (ep, subscriptions) ->
+ val bySubscription = factActivations.getOrPut(ep, ::linkedMapOf)
+ subscriptions.forEach { subscription ->
+ if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ val sub = FactToFactSub(
+ subscription.callerPathEdge,
+ subscription.calleeInitialFactBase,
+ )
+ bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries)
+ }
+ }
+ }
+
+ subscriptionStorage.findZeroEdgeSub(summaryInitialFact)
+ .forEach { (ep, subscriptions) ->
+ val bySubscription = zeroActivations.getOrPut(ep, ::linkedMapOf)
+ subscriptions.forEach { subscription ->
+ if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ val sub = ZeroToFactSub(
+ subscription.callerPathEdge,
+ subscription.calleeInitialFactBase,
+ )
+ bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries)
+ }
+ }
+ }
+
+ subscriptionStorage.findFactNDEdgeSub(summaryInitialFact, emptyDeltaRequired = true)
+ .forEach { (ep, subscriptions) ->
+ val bySubscription = ndActivations.getOrPut(ep, ::linkedMapOf)
+ subscriptions.forEach { subscription ->
+ if (subscription.callerPathEdge.factAp.hasEmptyDelta(summaryInitialFact)) {
+ val sub = NDFactToFactSub(
+ subscription.callerPathEdge,
+ subscription.calleeInitialFactBase,
+ )
+ bySubscription.getOrPut(sub, ::linkedSetOf).addAll(summaries)
+ }
+ }
+ }
+ }
+
+ factActivations.forEach { (ep, bySubscription) ->
+ val analyzer = processingCtx.getMethodAnalyzer(ep)
+ bySubscription.forEach { (sub, summaries) ->
+ analyzer.handleFactToFactMethodNDSummaryEdge(listOf(sub), summaries.toList())
+ }
+ }
+ zeroActivations.forEach { (ep, bySubscription) ->
+ val analyzer = processingCtx.getMethodAnalyzer(ep)
+ bySubscription.forEach { (sub, summaries) ->
+ analyzer.handleZeroToFactMethodNDSummaryEdge(listOf(sub), summaries.toList())
+ }
+ }
+ ndActivations.forEach { (ep, bySubscription) ->
+ val analyzer = processingCtx.getMethodAnalyzer(ep)
+ bySubscription.forEach { (sub, summaries) ->
+ analyzer.handleNDFactToFactMethodNDSummaryEdge(listOf(sub), summaries.toList())
+ }
}
}
}
private inner class NewSideEffectRequirementEvent(
private val methodEntryPoint: MethodEntryPoint,
- private val sideEffectRequirements: List
+ private val sideEffectRequirements: List,
) : SummaryEvent {
override fun processMethodSummary() {
val methodSubscriptions = methodSummarySubscriptions[methodEntryPoint] ?: return
sideEffectRequirements.forEach { sideEffectRequirement ->
- methodSubscriptions.findFactEdgeSub(sideEffectRequirement, emptyDeltaRequired = true).forEach { (ep, subscriptions) ->
- val analyzer = processingCtx.getMethodAnalyzer(ep)
- for (subscription in subscriptions) {
- analyzer.handleMethodSideEffectRequirement(
- subscription.callerPathEdge, subscription.calleeInitialFactBase,
- listOf(sideEffectRequirement)
- )
+ methodSubscriptions.findFactEdgeSub(sideEffectRequirement, emptyDeltaRequired = true)
+ .forEach { (ep, subscriptions) ->
+ val analyzer = processingCtx.getMethodAnalyzer(ep)
+ for (subscription in subscriptions) {
+ analyzer.handleMethodSideEffectRequirement(
+ subscription.callerPathEdge,
+ subscription.calleeInitialFactBase,
+ listOf(sideEffectRequirement),
+ )
+ }
}
- }
}
}
}
@@ -737,6 +830,7 @@ class SummaryEdgeSubscriptionManager(
processingCtx.addSummaryEdgeEvent(NewSideEffectSummaryEvent(methodEntryPoint, sideEffects))
}
}
+
}
class SummaryEdgeStorageWithSubscribers(
@@ -789,6 +883,7 @@ class SummaryEdgeStorageWithSubscribers(
addFactToFactEdges(factToFactEdges, addedEdges)
addNDFactToFactEdges(ndFactToFactEdges, addedEdges)
+ if (addedEdges.isEmpty()) return
for (subscriber in subscribers) {
subscriber.newSummaryEdges(addedEdges)
}
@@ -823,6 +918,7 @@ class SummaryEdgeStorageWithSubscribers(
fun sideEffectRequirement(requirements: List) {
val addedRequirements = sideEffectRequirement.add(requirements)
+ if (addedRequirements.isEmpty()) return
for (subscriber in subscribers) {
subscriber.newSideEffectRequirement(methodEntryPoint, addedRequirements)
}
@@ -851,6 +947,7 @@ class SummaryEdgeStorageWithSubscribers(
val addedSideEffects = addedZeroSideEffects + addedFactSideEffects
+ if (addedSideEffects.isEmpty()) return
for (subscriber in subscribers) {
subscriber.newSideEffectSummaries(methodEntryPoint, addedSideEffects)
}
@@ -959,6 +1056,13 @@ class SummaryEdgeStorageWithSubscribers(
it.setEntryPoint(methodEntryPoint).build()
})
+ fun factToFactEdges(finalFactPattern: FinalFactAp): List =
+ collectToListWithPostProcess(mutableListOf(), {
+ taintedFactSummaryEdges.filterEdgesByFinalTo(it, finalFactPattern)
+ }, {
+ it.setEntryPoint(methodEntryPoint).build()
+ })
+
fun factNDEdges(finalFactBase: AccessPathBase): List =
collectToListWithPostProcess(mutableListOf(), {
ndF2FSummaryEdges.filterEdgesTo(it, initialFactPattern = null, finalFactBase)
@@ -993,9 +1097,12 @@ class SummaryEdgeStorageWithSubscribers(
collectAllZeroToFactSummariesTo(sourceEdges)
val sourceSummaries = sourceEdges.sumOf { (it as? Edge.ZeroToFact)?.factAp?.size ?: 0 }
- val passEdges = mutableListOf()
- collectAllFactToFactSummariesTo(passEdges)
- val passSummaries = passEdges.sumOf { it.factAp.size }
+ val passSummaries = taintedFactSummaryEdges.storageStats()?.finalFactSizeSum
+ ?: run {
+ val passEdges = mutableListOf()
+ collectAllFactToFactSummariesTo(passEdges)
+ passEdges.sumOf { it.factAp.size.toLong() }
+ }
stats.stats(methodEntryPoint.method).sourceSummaries += sourceSummaries
stats.stats(methodEntryPoint.method).passSummaries += passSummaries
@@ -1169,6 +1276,12 @@ abstract class MethodSummaryEdgesForExitPoint, Stor
}
}
+ fun forEachStorage(body: (Storage) -> Unit) {
+ exitPointsStorage.concurrentReadSafeMapIndexed { _, storage ->
+ body(storage)
+ }
+ }
+
private inline fun processStorageEdges(dst: MutableList, storageEdges: (Storage, MutableList) -> Unit) {
exitPointsStorage.concurrentReadSafeMapIndexed { idx, storage ->
val exitPoint = exitPoints[idx]
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt
index 69606f10f..3176fa9fa 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/ApManager.kt
@@ -107,7 +107,34 @@ interface MethodEdgesFinalApSet {
}
interface MethodEdgesInitialToFinalApSet {
- fun add(statement: CommonInst, initialAp: InitialFactAp, finalAp: FinalFactAp): Pair?
+ /**
+ * Adds an edge and returns the complete propagation delta. If insertion changes metadata
+ * shared by several stored finals, every affected final must be returned with that metadata.
+ * An empty list means that the represented edge set did not change.
+ */
+ fun add(
+ statement: CommonInst,
+ initialAp: InitialFactAp,
+ finalAp: FinalFactAp,
+ ): List>
+
+ /**
+ * Adds several exact premises with one conclusion without requiring callers to materialize
+ * one path-edge object per premise. The callback is still an exact propagation delta: an
+ * implementation may emit more than one conclusion for a premise when shared metadata changes.
+ */
+ fun addAll(
+ statement: CommonInst,
+ initialAps: Iterable,
+ finalAp: FinalFactAp,
+ emitDelta: (InitialFactAp, FinalFactAp) -> Unit,
+ ) {
+ initialAps.forEach { initialAp ->
+ add(statement, initialAp, finalAp).forEach { (addedInitial, addedFinal) ->
+ emitDelta(addedInitial, addedFinal)
+ }
+ }
+ }
fun collectApAtStatement(collection: MutableList>, statement: CommonInst)
fun collectApAtStatement(collection: MutableList>, statement: CommonInst, finalFactPattern: InitialFactAp)
fun collectApAtStatement(collection: MutableList, statement: CommonInst, initialAp: InitialFactAp, finalFactPattern: InitialFactAp)
@@ -139,8 +166,17 @@ interface MethodFinalApSummariesStorage {
interface MethodInitialToFinalApSummariesStorage {
fun add(edges: List, added: MutableList)
fun filterEdgesTo(dst: MutableList, initialFactPattern: FinalFactAp?, finalFactBase: AccessPathBase?)
+ fun storageStats(): InitialToFinalSummaryStorageStats? = null
+ fun filterEdgesByFinalTo(dst: MutableList, finalFactPattern: FinalFactAp) {
+ filterEdgesTo(dst, initialFactPattern = null, finalFactBase = finalFactPattern.base)
+ }
}
+data class InitialToFinalSummaryStorageStats(
+ val edgeCount: Long,
+ val finalFactSizeSum: Long,
+)
+
interface MethodNDInitialToFinalApSummariesStorage {
fun add(edges: List, added: MutableList)
fun filterEdgesTo(dst: MutableList, initialFactPattern: FinalFactAp?, finalFactBase: AccessPathBase?)
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt
index 63c728e01..6bb5ada28 100644
--- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/automata/MethodEdgesInitialToFinalAutomataApSet.kt
@@ -24,7 +24,7 @@ class MethodEdgesInitialToFinalAutomataApSet(
statement: CommonInst,
initialAp: InitialFactAp,
finalAp: FinalFactAp
- ): Pair? =
+ ): List> =
add(statement, initialAp as AccessGraphInitialFactAp, finalAp as AccessGraphFinalFactAp)
override fun collectApAtStatement(
@@ -73,7 +73,7 @@ class MethodEdgesInitialToFinalAutomataApSet(
statement: CommonInst,
initialAp: AccessGraphInitialFactAp,
finalAp: AccessGraphFinalFactAp
- ): Pair? {
+ ): List> {
check(initialAp.exclusions == finalAp.exclusions)
val storage = this.storage
@@ -81,14 +81,27 @@ class MethodEdgesInitialToFinalAutomataApSet(
.getOrCreate(initialAp.access)
val exclusion = initialAp.exclusions
- val addedExclusion = storage.add(statement, finalAp.base, finalAp.access, exclusion)
-
- if (addedExclusion === exclusion) return initialAp to finalAp
- if (addedExclusion == null) return null
+ val update = storage.add(statement, finalAp.base, finalAp.access, exclusion)
+ ?: return emptyList()
+ val addedInitial = if (update.exclusion === exclusion) {
+ initialAp
+ } else {
+ initialAp.replaceExclusions(update.exclusion)
+ }
+ val addedAccesses = if (update.reemitAll) {
+ mutableListOf().also { storage.collectAccesses(it, statement, finalAp.base) }
+ } else {
+ listOf(finalAp.access)
+ }
- val newInitial = initialAp.replaceExclusions(addedExclusion)
- val newFinal = finalAp.replaceExclusions(addedExclusion)
- return newInitial to newFinal
+ return addedAccesses.map { access ->
+ val addedFinal = if (access === finalAp.access && update.exclusion === exclusion) {
+ finalAp
+ } else {
+ AccessGraphFinalFactAp(finalAp.base, access, update.exclusion)
+ }
+ addedInitial to addedFinal
+ }
}
override fun toString(): String = storage.toString()
@@ -122,15 +135,25 @@ class MethodEdgesInitialToFinalAutomataApSet(
maxInstIdx: Int,
languageManager: LanguageManager
) {
+ data class Update(val exclusion: ExclusionSet, val reemitAll: Boolean)
+
private val factStorage = FinalFactBaseStorage(initialStatement, maxInstIdx, languageManager)
- fun add(statement: CommonInst, finalBase: AccessPathBase, finalAg: AccessGraph, exclusion: ExclusionSet): ExclusionSet? {
+ fun add(
+ statement: CommonInst,
+ finalBase: AccessPathBase,
+ finalAg: AccessGraph,
+ exclusion: ExclusionSet,
+ ): Update? {
val finalFactStorage = factStorage.getOrCreate(finalBase)
val factUpdated = finalFactStorage.addFact(statement, finalAg)
+ val exclusionUpdate = finalFactStorage.addExclusion(statement, exclusion)
+ if (!factUpdated && !exclusionUpdate.changed) return null
+ return Update(exclusionUpdate.exclusion, reemitAll = exclusionUpdate.changed)
+ }
- return finalFactStorage.addExclusion(
- statement, exclusion, returnNullIfNotUpdated = !factUpdated
- )
+ fun collectAccesses(dst: MutableList, statement: CommonInst, finalBase: AccessPathBase) {
+ factStorage.find(finalBase)?.collectTo(dst, statement)
}
fun collectTo(collection: MutableList, statement: CommonInst, finalFactPattern: InitialFactAp?) {
@@ -172,6 +195,8 @@ class MethodEdgesInitialToFinalAutomataApSet(
maxInstIdx: Int,
private val languageManager: LanguageManager
) {
+ data class ExclusionUpdate(val exclusion: ExclusionSet, val changed: Boolean)
+
private val finalFacts = AccessGraphSetArray.create(instructionStorageSize(maxInstIdx))
fun addFact(statement: CommonInst, final: AccessGraph): Boolean {
@@ -195,26 +220,22 @@ class MethodEdgesInitialToFinalAutomataApSet(
private val exclusions = arrayOfNulls(instructionStorageSize(maxInstIdx))
- fun addExclusion(
- statement: CommonInst,
- exclusion: ExclusionSet,
- returnNullIfNotUpdated: Boolean
- ): ExclusionSet? {
+ fun addExclusion(statement: CommonInst, exclusion: ExclusionSet): ExclusionUpdate {
val exclusionIdx = instructionStorageIdx(statement, languageManager)
val currentExclusion = exclusions[exclusionIdx]
if (currentExclusion == null) {
exclusions[exclusionIdx] = exclusion
- return exclusion
+ return ExclusionUpdate(exclusion, changed = true)
}
val merged = currentExclusion.union(exclusion)
if (merged === currentExclusion) {
- return if (returnNullIfNotUpdated) null else merged
+ return ExclusionUpdate(merged, changed = false)
}
exclusions[exclusionIdx] = merged
- return merged
+ return ExclusionUpdate(merged, changed = true)
}
fun exclusion(statement: CommonInst): ExclusionSet? {
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt
new file mode 100644
index 000000000..c3964ecf4
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccess.kt
@@ -0,0 +1,239 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor
+
+typealias BaseOnlyAccess = Long
+
+const val NO_ACCESSOR: AccessorIdx = -1
+const val ABSTRACT_MARK: AccessorIdx = -2
+const val COLLAPSED_MARK: AccessorIdx = -3
+
+const val BASE_ONLY_STATIC_BITS = 16
+const val BASE_ONLY_FIELD_BITS = 24
+const val BASE_ONLY_SUFFIX_BITS = 24
+const val BASE_ONLY_VALUE_ACCESSOR_STATE_BITS = 1
+const val BASE_ONLY_SUFFIX_VALUE_BITS = BASE_ONLY_SUFFIX_BITS - BASE_ONLY_VALUE_ACCESSOR_STATE_BITS
+
+const val BASE_ONLY_SUFFIX_SHIFT = 0
+const val BASE_ONLY_FIELD_SHIFT = BASE_ONLY_SUFFIX_BITS
+const val BASE_ONLY_STATIC_SHIFT = BASE_ONLY_SUFFIX_BITS + BASE_ONLY_FIELD_BITS
+
+const val BASE_ONLY_STATIC_MASK = (1 shl BASE_ONLY_STATIC_BITS) - 1
+const val BASE_ONLY_FIELD_MASK = (1 shl BASE_ONLY_FIELD_BITS) - 1
+const val BASE_ONLY_SUFFIX_MASK = (1 shl BASE_ONLY_SUFFIX_BITS) - 1
+const val BASE_ONLY_SUFFIX_VALUE_MASK = (1 shl BASE_ONLY_SUFFIX_VALUE_BITS) - 1
+const val BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT = BASE_ONLY_SUFFIX_VALUE_BITS
+const val BASE_ONLY_VALUE_ACCESSOR_STATE_MASK = (1 shl BASE_ONLY_VALUE_ACCESSOR_STATE_BITS) - 1
+
+const val BASE_ONLY_BIAS = 3
+
+/**
+ * How the semantic suffix is reached. [Value] encodes a preceding ValueAccessor;
+ * for a type suffix the same bit encodes its analogous TypeInfoGroupAccessor prefix.
+ */
+enum class BaseOnlyValueAccessorState(val encoded: Int) {
+ Normal(0),
+ Value(1);
+
+ companion object {
+ fun decode(encoded: Int): BaseOnlyValueAccessorState =
+ entries.firstOrNull { it.encoded == encoded }
+ ?: throw IllegalArgumentException("Invalid BaseOnly value-accessor state: $encoded")
+ }
+}
+
+fun packBaseOnlyAccess(
+ staticIdx: AccessorIdx,
+ fieldIdx: AccessorIdx,
+ suffixIdx: AccessorIdx,
+ valueAccessorState: BaseOnlyValueAccessorState = BaseOnlyValueAccessorState.Normal,
+): BaseOnlyAccess {
+ require(fieldIdx != ANY_ACCESSOR_IDX) { "AnyAccessor is implicit in BaseOnly and cannot occupy the field slot" }
+ val s = staticIdx + BASE_ONLY_BIAS
+ val f = fieldIdx + BASE_ONLY_BIAS
+ val x = suffixIdx + BASE_ONLY_BIAS
+ require(s in 0..BASE_ONLY_STATIC_MASK) { "BaseOnly static index out of range: $staticIdx" }
+ require(f in 0..BASE_ONLY_FIELD_MASK) { "BaseOnly field index out of range: $fieldIdx" }
+ require(x in 0..BASE_ONLY_SUFFIX_VALUE_MASK) { "BaseOnly suffix index out of range: $suffixIdx" }
+ val encodedSuffix = rawBaseOnlySuffixSlot(suffixIdx, valueAccessorState)
+ return (s.toLong() shl BASE_ONLY_STATIC_SHIFT) or
+ (f.toLong() shl BASE_ONLY_FIELD_SHIFT) or encodedSuffix.toLong()
+}
+
+fun rawBaseOnlySuffixSlot(suffixIdx: AccessorIdx, valueAccessorState: BaseOnlyValueAccessorState): Int {
+ val encodedSuffix = suffixIdx + BASE_ONLY_BIAS
+ require(encodedSuffix in 0..BASE_ONLY_SUFFIX_VALUE_MASK) {
+ "BaseOnly suffix index out of range: $suffixIdx"
+ }
+ return encodedSuffix or (valueAccessorState.encoded shl BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT)
+}
+
+fun packBaseOnlyAccessFromRawSuffix(
+ staticIdx: AccessorIdx,
+ fieldIdx: AccessorIdx,
+ rawSuffixSlot: Int,
+): BaseOnlyAccess {
+ val suffixIdx = (rawSuffixSlot and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS
+ val state = BaseOnlyValueAccessorState.decode(
+ (rawSuffixSlot ushr BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT) and BASE_ONLY_VALUE_ACCESSOR_STATE_MASK
+ )
+ return packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, state)
+}
+
+val EMPTY_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, NO_ACCESSOR)
+val ABSTRACT_EMPTY_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, ABSTRACT_MARK)
+val FINAL_ACCESS: BaseOnlyAccess = packBaseOnlyAccess(NO_ACCESSOR, NO_ACCESSOR, FINAL_ACCESSOR_IDX)
+
+inline fun BaseOnlyAccess.withBaseOnlyAccessUnpacked(
+ body: (staticIdx: AccessorIdx, fieldIdx: AccessorIdx, suffixIdx: AccessorIdx) -> T,
+): T = body(
+ ((this ushr BASE_ONLY_STATIC_SHIFT).toInt() and BASE_ONLY_STATIC_MASK) - BASE_ONLY_BIAS,
+ ((this ushr BASE_ONLY_FIELD_SHIFT).toInt() and BASE_ONLY_FIELD_MASK) - BASE_ONLY_BIAS,
+ (this.toInt() and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS,
+)
+
+val BaseOnlyAccess.staticIdx: AccessorIdx
+ get() = ((this ushr BASE_ONLY_STATIC_SHIFT).toInt() and BASE_ONLY_STATIC_MASK) - BASE_ONLY_BIAS
+
+val BaseOnlyAccess.fieldIdx: AccessorIdx
+ get() = ((this ushr BASE_ONLY_FIELD_SHIFT).toInt() and BASE_ONLY_FIELD_MASK) - BASE_ONLY_BIAS
+
+val BaseOnlyAccess.suffixIdx: AccessorIdx
+ get() = (this.toInt() and BASE_ONLY_SUFFIX_VALUE_MASK) - BASE_ONLY_BIAS
+
+val BaseOnlyAccess.rawSuffixSlot: Int
+ get() = this.toInt() and BASE_ONLY_SUFFIX_MASK
+
+val BaseOnlyAccess.valueAccessorState: BaseOnlyValueAccessorState
+ get() = BaseOnlyValueAccessorState.decode(
+ (rawSuffixSlot ushr BASE_ONLY_VALUE_ACCESSOR_STATE_SHIFT) and BASE_ONLY_VALUE_ACCESSOR_STATE_MASK
+ )
+
+fun BaseOnlyAccess.withValueAccessorState(state: BaseOnlyValueAccessorState): BaseOnlyAccess =
+ packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, state)
+
+val BaseOnlyAccess.isSuffixAbstract: Boolean get() = suffixIdx == ABSTRACT_MARK
+
+val BaseOnlyAccess.isCollapsed: Boolean get() = suffixIdx == COLLAPSED_MARK
+
+val BaseOnlyAccess.apSlot: Int
+ get() = withBaseOnlyAccessUnpacked { s, f, x ->
+ when {
+ s == ABSTRACT_MARK -> 0
+ f == ABSTRACT_MARK -> 1
+ x == ABSTRACT_MARK -> 2
+ else -> -1
+ }
+ }
+
+val BaseOnlyAccess.hasAp: Boolean get() = apSlot >= 0
+
+/** Whether abstract acceptance is available at the current logical node. */
+val BaseOnlyAccess.isRootAbstract: Boolean
+ get() = hasAp && staticIdx < 0 && fieldIdx < 0
+
+val BaseOnlyAccess.hasSemanticMark: Boolean get() = suffixIdx >= 0 && suffixIdx != FINAL_ACCESSOR_IDX
+
+val BaseOnlyAccess.hasTerminalAccessor: Boolean get() = suffixIdx >= 0
+
+val BaseOnlyAccess.hasTypeInfoSuffix: Boolean get() = suffixIdx >= 0 && suffixIdx.isTypeInfoAccessor()
+
+val BaseOnlyAccess.size: Int
+ get() = withBaseOnlyAccessUnpacked { staticIdx, fieldIdx, suffixIdx ->
+ var result = 0
+ if (staticIdx >= 0) result++
+ if (fieldIdx >= 0) result++
+ if (suffixIdx >= 0) result++
+ result
+ }
+
+val BaseOnlyAccess.coreSize: Int
+ get() = withBaseOnlyAccessUnpacked { s, f, x ->
+ var n = 0
+ if (s >= 0) n++
+ if (f >= 0) n++
+ if (x >= 0 && x != FINAL_ACCESSOR_IDX) n++
+ n
+ }
+
+val BaseOnlyAccess.isEmpty: Boolean get() = this == EMPTY_ACCESS
+
+val BaseOnlyAccess.headOrNull: AccessorIdx?
+ get() = withBaseOnlyAccessUnpacked { s, f, x ->
+ when {
+ s >= 0 -> s
+ f >= 0 -> f
+ x >= 0 && x != FINAL_ACCESSOR_IDX -> x
+ x == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX
+ else -> null
+ }
+ }
+
+val BaseOnlyAccess.firstAccessorOrNull: AccessorIdx?
+ get() = withBaseOnlyAccessUnpacked { s, f, x ->
+ when {
+ s >= 0 -> s
+ f >= 0 -> f
+ x < 0 -> null
+ x == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX
+ x.isTypeInfoAccessor() && valueAccessorState == BaseOnlyValueAccessorState.Value -> TYPE_INFO_GROUP_ACCESSOR_IDX
+ else -> x
+ }
+ }
+
+fun BaseOnlyAccess.coreAt(position: Int): AccessorIdx = withBaseOnlyAccessUnpacked { s, f, x ->
+ var k = position
+ if (s >= 0) { if (k == 0) return@withBaseOnlyAccessUnpacked s; k-- }
+ if (f >= 0) { if (k == 0) return@withBaseOnlyAccessUnpacked f; k-- }
+ if (x >= 0 && x != FINAL_ACCESSOR_IDX) { if (k == 0) return@withBaseOnlyAccessUnpacked x; k-- }
+ NO_ACCESSOR
+}
+
+fun BaseOnlyAccess.coreStartsWith(prefix: BaseOnlyAccess, prefixLen: Int): Boolean {
+ if (coreSize < prefixLen) return false
+ for (i in 0 until prefixLen) if (coreAt(i) != prefix.coreAt(i)) return false
+ return true
+}
+
+inline fun BaseOnlyAccess.forEachAccessorIdx(action: (AccessorIdx) -> Unit) {
+ val s = staticIdx
+ val f = fieldIdx
+ val x = suffixIdx
+ if (s >= 0) action(s)
+ if (f >= 0) action(f)
+ if (x >= 0) {
+ if (x != FINAL_ACCESSOR_IDX) {
+ if (x.isTypeInfoAccessor() && valueAccessorState == BaseOnlyValueAccessorState.Value) {
+ action(TYPE_INFO_GROUP_ACCESSOR_IDX)
+ }
+ action(x)
+ }
+ action(FINAL_ACCESSOR_IDX)
+ }
+}
+
+inline fun BaseOnlyAccess.forEachCoreIdx(action: (AccessorIdx) -> Unit) {
+ val s = staticIdx
+ val f = fieldIdx
+ val x = suffixIdx
+ if (s >= 0) action(s)
+ if (f >= 0) action(f)
+ if (x >= 0 && x != FINAL_ACCESSOR_IDX) action(x)
+}
+
+fun AccessorIdx.isAnyIdx(): Boolean = this == ANY_ACCESSOR_IDX
+fun AccessorIdx.isStructuralIdx(): Boolean = isFieldAccessor() || this == ELEMENT_ACCESSOR_IDX
+fun AccessorIdx.isSuffixIdx(): Boolean = !isAnyIdx() && !isStructuralIdx() && !isStaticAccessor()
+
+class BaseOnlyMatch(
+ @JvmField val emptyDelta: Boolean,
+ @JvmField val hasSuffix: Boolean,
+ @JvmField val suffix: BaseOnlyAccess,
+)
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt
new file mode 100644
index 000000000..d16926932
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessOps.kt
@@ -0,0 +1,555 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ANY_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor
+
+class BaseOnlySplit(
+ @JvmField val matched: BaseOnlyAccess,
+ @JvmField val delta: BaseOnlyAccess,
+)
+
+object BaseOnlyAccessOps {
+ val empty: BaseOnlyAccess get() = EMPTY_ACCESS
+ val abstractEmpty: BaseOnlyAccess get() = ABSTRACT_EMPTY_ACCESS
+ val finalAccess: BaseOnlyAccess get() = FINAL_ACCESS
+
+ /** Validate the representation boundary without assigning semantics to malformed packed values. */
+ fun requireCanonical(
+ access: BaseOnlyAccess,
+ allowEmpty: Boolean = false,
+ allowTransientCollapsed: Boolean = false,
+ ): BaseOnlyAccess {
+ val staticIdx = access.staticIdx
+ val fieldIdx = access.fieldIdx
+ val suffixIdx = access.suffixIdx
+ val valueAccessorState = access.valueAccessorState
+
+ require(staticIdx == NO_ACCESSOR || staticIdx == ABSTRACT_MARK || staticIdx.isStaticAccessor()) {
+ "Invalid BaseOnly static slot: $staticIdx"
+ }
+ require(
+ fieldIdx == NO_ACCESSOR || fieldIdx == ABSTRACT_MARK ||
+ fieldIdx.isFieldAccessor() || fieldIdx == ELEMENT_ACCESSOR_IDX
+ ) { "Invalid BaseOnly structural slot: $fieldIdx" }
+ require(
+ suffixIdx == NO_ACCESSOR || suffixIdx == ABSTRACT_MARK ||
+ (allowTransientCollapsed && suffixIdx == COLLAPSED_MARK) || suffixIdx == FINAL_ACCESSOR_IDX ||
+ (suffixIdx >= 0 && !suffixIdx.isStaticAccessor() && !suffixIdx.isFieldAccessor() &&
+ suffixIdx != ELEMENT_ACCESSOR_IDX && suffixIdx != ANY_ACCESSOR_IDX &&
+ suffixIdx != TYPE_INFO_GROUP_ACCESSOR_IDX && suffixIdx != VALUE_ACCESSOR_IDX)
+ ) { "Invalid BaseOnly suffix slot: $suffixIdx" }
+ require(access.hasSemanticMark || valueAccessorState == BaseOnlyValueAccessorState.Normal) {
+ "A value accessor is only valid before a semantic suffix: $valueAccessorState"
+ }
+ require(allowTransientCollapsed || !access.isCollapsed) {
+ "Collapsed BaseOnly access is a transient flow-function value"
+ }
+ if (staticIdx == ABSTRACT_MARK) {
+ require(fieldIdx == NO_ACCESSOR && suffixIdx == NO_ACCESSOR) {
+ "Components after a static abstraction are forbidden"
+ }
+ }
+ if (fieldIdx == ABSTRACT_MARK) {
+ require(staticIdx >= 0 || staticIdx == NO_ACCESSOR) { "Invalid prefix before field abstraction" }
+ require(suffixIdx == NO_ACCESSOR) { "Components after a field abstraction are forbidden" }
+ }
+ if (!access.hasAp && (staticIdx >= 0 || fieldIdx >= 0)) {
+ require(suffixIdx != NO_ACCESSOR) { "A concrete BaseOnly prefix must terminate or abstract" }
+ }
+ if (!allowEmpty) require(!access.isEmpty) { "Empty BaseOnly access is not a fact" }
+ return access
+ }
+
+ fun build(accessors: IntArray, isAbstract: Boolean): BaseOnlyAccess {
+ validateBuildGrammar(accessors)
+ var staticIdx = NO_ACCESSOR
+ var fieldIdx = NO_ACCESSOR
+ var semanticIdx = NO_ACCESSOR
+ var valueAccessorState = BaseOnlyValueAccessorState.Normal
+ var hasFinal = false
+ for (idx in accessors) {
+ when {
+ idx.isStaticAccessor() -> {
+ require(staticIdx == NO_ACCESSOR || staticIdx == idx) {
+ "Multiple static accessors in a BaseOnly path: $staticIdx, $idx"
+ }
+ if (staticIdx == NO_ACCESSOR) staticIdx = idx
+ }
+ idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> {
+ // Canonical BaseOnly retains the outermost structural accessor.
+ if (fieldIdx == NO_ACCESSOR) fieldIdx = idx
+ }
+ idx == ANY_ACCESSOR_IDX -> Unit
+ idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> valueAccessorState = BaseOnlyValueAccessorState.Value
+ idx == VALUE_ACCESSOR_IDX -> valueAccessorState = BaseOnlyValueAccessorState.Value
+ idx == FINAL_ACCESSOR_IDX -> hasFinal = true
+ else -> if (semanticIdx < 0) semanticIdx = idx
+ }
+ }
+ val suffixIdx = when {
+ semanticIdx >= 0 -> semanticIdx
+ hasFinal -> FINAL_ACCESSOR_IDX
+ isAbstract -> ABSTRACT_MARK
+ else -> NO_ACCESSOR
+ }
+ return packNormalized(staticIdx, fieldIdx, suffixIdx, valueAccessorState)
+ }
+
+ /** Validate accessor order before projecting a well-formed linear path into three slots. */
+ private fun validateBuildGrammar(accessors: IntArray) {
+ var staticSeen = false
+ var semanticSeen = false
+ var finalSeen = false
+ var expectType = false
+ var expectMark = false
+ accessors.forEachIndexed { position, idx ->
+ require(!finalSeen) { "Accessor after FinalAccessor at position $position: $idx" }
+ if (semanticSeen) {
+ require(idx == FINAL_ACCESSOR_IDX) { "Accessor after BaseOnly semantic terminal at position $position: $idx" }
+ finalSeen = true
+ return@forEachIndexed
+ }
+ when {
+ expectType -> {
+ require(idx.isTypeInfoAccessor()) { "TypeInfoGroupAccessor must be followed by a type accessor" }
+ expectType = false
+ semanticSeen = true
+ }
+ expectMark -> {
+ require(idx.isTaintMarkAccessor()) { "ValueAccessor must be followed by a taint mark" }
+ expectMark = false
+ semanticSeen = true
+ }
+ idx.isStaticAccessor() -> {
+ require(position == 0 && !staticSeen) { "Static accessor is only valid once at the path root" }
+ staticSeen = true
+ }
+ structural(idx) -> Unit
+ idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> expectType = true
+ idx == VALUE_ACCESSOR_IDX -> expectMark = true
+ idx == FINAL_ACCESSOR_IDX -> finalSeen = true
+ else -> semanticSeen = true // taint mark or compact type residual
+ }
+ }
+ require(!expectType) { "TypeInfoGroupAccessor requires a following type accessor" }
+ require(!expectMark) { "ValueAccessor requires a following taint mark" }
+ }
+
+ fun abstractAt(staticIdx: AccessorIdx, fieldIdx: AccessorIdx, apSlot: Int): BaseOnlyAccess {
+ require(apSlot in 0..2) { "Invalid BaseOnly abstraction slot: $apSlot" }
+ return when (apSlot) {
+ 0 -> packNormalized(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR)
+ 1 -> packNormalized(staticIdx, ABSTRACT_MARK, NO_ACCESSOR)
+ else -> packNormalized(staticIdx, fieldIdx, ABSTRACT_MARK)
+ }
+ }
+
+ fun collapse(access: BaseOnlyAccess): BaseOnlyAccess = when (access.apSlot) {
+ 0 -> packNormalized(NO_ACCESSOR, access.fieldIdx, access.suffixIdx, access.valueAccessorState)
+ 1 -> packNormalized(access.staticIdx, NO_ACCESSOR, access.suffixIdx, access.valueAccessorState)
+ 2 -> packNormalized(access.staticIdx, access.fieldIdx, COLLAPSED_MARK)
+ else -> access
+ }
+
+ fun restoreAbstraction(access: BaseOnlyAccess): BaseOnlyAccess =
+ if (access.suffixIdx == COLLAPSED_MARK) packNormalized(access.staticIdx, access.fieldIdx, ABSTRACT_MARK)
+ else access
+
+ fun prepend(access: BaseOnlyAccess, idx: AccessorIdx, fieldSensitive: Boolean): BaseOnlyAccess = when {
+ idx == TYPE_INFO_GROUP_ACCESSOR_IDX -> {
+ require(access.hasTypeInfoSuffix) { "TypeInfoGroupAccessor requires a compact type suffix" }
+ access.withValueAccessorState(BaseOnlyValueAccessorState.Value)
+ }
+ idx.isStaticAccessor() -> {
+ require(access.staticIdx == NO_ACCESSOR) { "Cannot prepend a second static accessor" }
+ packNormalized(idx, access.fieldIdx, access.suffixIdx, access.valueAccessorState)
+ }
+ idx.isAnyIdx() -> access
+ structural(idx) ->
+ if (!fieldSensitive) access
+ else packNormalized(access.staticIdx, idx, access.suffixIdx, access.valueAccessorState)
+ idx == VALUE_ACCESSOR_IDX -> {
+ require(access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor()) {
+ "ValueAccessor requires a taint-mark suffix"
+ }
+ access.withValueAccessorState(BaseOnlyValueAccessorState.Value)
+ }
+ else -> packNormalized(access.staticIdx, access.fieldIdx, idx, BaseOnlyValueAccessorState.Normal)
+ }
+
+ fun read(access: BaseOnlyAccess, idx: AccessorIdx): BaseOnlyAccess? = when (headRead(access, idx)) {
+ HeadRead.NONE -> null
+ HeadRead.KEEP -> access
+ HeadRead.TAIL -> tail(access)
+ HeadRead.WRAPPER_TAIL -> wrapperTail(access)
+ }
+
+ fun startsWith(access: BaseOnlyAccess, idx: AccessorIdx): Boolean = headRead(access, idx) != HeadRead.NONE
+
+ fun clear(access: BaseOnlyAccess, idx: AccessorIdx): BaseOnlyAccess? {
+ if (access.staticIdx == NO_ACCESSOR && access.fieldIdx == NO_ACCESSOR && access.hasSemanticMark) {
+ // The missing field slot includes the implicit Any self-loop. Clearing a terminal
+ // root can remove the zero-length branch, but the same terminal remains reachable after
+ // one or more structural reads, so the BaseOnly projection is unchanged.
+ return access
+ }
+
+ val head = access.firstAccessorOrNull ?: return access
+ if (head != idx) return access
+
+ return null
+ }
+
+ fun append(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? {
+ if (suffix.isEmpty) return prefix
+ if (prefix.isEmpty) return suffix
+ if (prefix.hasAp) return graftAtAbstraction(prefix, suffix)
+ if (prefix.hasTerminalAccessor) return prefix
+ if (suffix.staticIdx >= 0 && prefix.coreSize > 0) return null
+ val prefixStaticConcrete = if (prefix.staticIdx == ABSTRACT_MARK) NO_ACCESSOR else prefix.staticIdx
+ val prefixFieldConcrete = if (prefix.fieldIdx == ABSTRACT_MARK) NO_ACCESSOR else prefix.fieldIdx
+ val staticIdx = if (suffix.staticIdx >= 0) suffix.staticIdx else prefixStaticConcrete
+ val fieldIdx = when {
+ prefixFieldConcrete >= 0 -> prefixFieldConcrete
+ suffix.fieldIdx != NO_ACCESSOR -> suffix.fieldIdx
+ else -> NO_ACCESSOR
+ }
+ val suffixIdx =
+ if (fieldIdx == ABSTRACT_MARK) NO_ACCESSOR
+ else combineTerminal(prefix, suffix)
+ val valueAccessorState = when {
+ prefix.hasSemanticMark -> prefix.valueAccessorState
+ suffix.hasSemanticMark -> suffix.valueAccessorState
+ else -> BaseOnlyValueAccessorState.Normal
+ }
+ return packNormalized(staticIdx, fieldIdx, suffixIdx, valueAccessorState)
+ }
+
+ fun appendFinal(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? {
+ if (suffix.isEmpty) return prefix
+ if (!prefix.hasAp) return null
+ return graftAtAbstraction(prefix, suffix)
+ }
+
+ /**
+ * Graft [suffix] at [prefix]'s abstract accepting node. A suffix that starts in a later
+ * representational category is valid: loss of an intermediate field is widened with symbolic
+ * Any when an exact or semantic terminal follows. Only a second static is structurally
+ * impossible.
+ */
+ private fun graftAtAbstraction(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): BaseOnlyAccess? {
+ return when (prefix.apSlot) {
+ 0 -> suffix
+ 1 -> {
+ if (suffix.staticIdx != NO_ACCESSOR) return null
+ packNormalized(prefix.staticIdx, suffix.fieldIdx, suffix.suffixIdx, suffix.valueAccessorState)
+ }
+ 2 -> {
+ if (suffix.staticIdx != NO_ACCESSOR) return null
+ // The prefix's suffix abstraction already contains an implicit Any step. It is
+ // the earlier structural step even when no concrete prefix field is retained, so
+ // a structural suffix is absorbed rather than installed into the empty field slot.
+ // Keeping only the incoming semantic terminal covers both the zero-length and
+ // structural branches represented by the prefix.
+ val field = prefix.fieldIdx
+ val terminal = when {
+ suffix.fieldIdx != NO_ACCESSOR && !suffix.hasSemanticMark -> ABSTRACT_MARK
+ else -> suffix.suffixIdx
+ }
+ packNormalized(prefix.staticIdx, field, terminal, suffix.valueAccessorState)
+ }
+ else -> null
+ }
+ }
+
+ private fun slotVal(a: BaseOnlyAccess, slot: Int): AccessorIdx = when (slot) {
+ 0 -> a.staticIdx
+ 1 -> a.fieldIdx
+ else -> a.suffixIdx
+ }
+
+ private fun matchesInitialPrefix(pattern: BaseOnlyAccess, x: BaseOnlyAccess): Boolean {
+ if (pattern == x) return true
+ if (!pattern.hasAp) return false
+ val k = pattern.apSlot
+ for (j in 0 until k) {
+ val patternSlot = slotVal(pattern, j)
+ val factSlot = slotVal(x, j)
+ val matches = if (j == 1) fieldCovers(patternSlot, factSlot, pattern) else patternSlot == factSlot
+ if (!matches) return false
+ }
+ if (slotVal(x, k) == NO_ACCESSOR) return false
+ if (x.hasAp && x.apSlot < k) return false
+ return true
+ }
+
+ fun matchPrefix(final: BaseOnlyAccess, initial: BaseOnlyAccess): BaseOnlyMatch {
+ if (final == initial) return IDENTITY_MATCH
+ if (!matchesInitialPrefix(initial, final)) return NO_MATCH
+ return BaseOnlyMatch(emptyDelta = false, hasSuffix = true, suffix = dropCorePrefix(final, initial.apSlot))
+ }
+
+ fun splitConcreteInitial(final: BaseOnlyAccess, initial: BaseOnlyAccess): BaseOnlySplit? {
+ if (initial.hasAp) return null
+ return when (final.apSlot) {
+ 0 -> BaseOnlySplit(final, initial)
+ 1 -> {
+ if (!staticsCompatible(initial.staticIdx, final.staticIdx)) return null
+ BaseOnlySplit(
+ final,
+ packNormalized(NO_ACCESSOR, initial.fieldIdx, initial.suffixIdx, initial.valueAccessorState),
+ )
+ }
+ 2 -> {
+ if (!staticsCompatible(initial.staticIdx, final.staticIdx)) return null
+ if (!fieldsCompatible(initial.fieldIdx, final.fieldIdx)) return null
+ BaseOnlySplit(
+ final,
+ packNormalized(NO_ACCESSOR, NO_ACCESSOR, initial.suffixIdx, initial.valueAccessorState),
+ )
+ }
+ else -> null
+ }
+ }
+
+ fun splitDelta(
+ fact: BaseOnlyAccess,
+ pattern: BaseOnlyAccess,
+ manager: BaseOnlyApManager,
+ exclusions: ExclusionSet,
+ ): List> {
+ if (fact.hasAp) {
+ if (!containsAccess(pattern, fact)) return emptyList()
+
+ // A suffix-abstract fact matched by a field-abstract summary still has a suffix
+ // beyond the matched field slot. Preserve it so mapping the summary initial and
+ // concatenating the delta reconstructs the caller fact. In particular:
+ // matched against field.* retains field.*; and
+ // matched against .* retains *.
+ if (pattern.apSlot == 1 && fact.apSlot == 2) {
+ val delta = dropCorePrefix(fact, pattern.apSlot)
+ val filtered = manager.applyExclusions(delta, exclusions) ?: return emptyList()
+ return listOf(pattern to BaseOnlyNodeInitialDelta(manager, filtered))
+ }
+
+ return listOf(pattern to BaseOnlyEmptyInitialDelta)
+ }
+
+ if (pattern.hasAp) {
+ val split = splitConcreteInitial(pattern, fact) ?: return emptyList()
+ // A field-lenient match may align `knownField.*` with a root-level suffix after
+ // projection erased one structural side. The summary exclusion is scoped after the
+ // known field and therefore must not be applied to that root-level residual.
+ val erasedStructuralBoundary = pattern.apSlot == 2 &&
+ ((pattern.fieldIdx == NO_ACCESSOR) != (fact.fieldIdx == NO_ACCESSOR))
+ val filtered =
+ if (erasedStructuralBoundary) split.delta
+ else manager.applyExclusions(split.delta, exclusions) ?: return emptyList()
+ return listOf(split.matched to BaseOnlyNodeInitialDelta(manager, filtered))
+ }
+
+ if (containsAccess(pattern, fact)) {
+ return listOf(pattern to BaseOnlyEmptyInitialDelta)
+ }
+ return emptyList()
+ }
+
+ /** Directional logical coverage: every path in [fact] is represented by [pattern]. */
+ fun covers(pattern: BaseOnlyAccess, fact: BaseOnlyAccess): Boolean {
+ if (pattern == fact) return true
+
+ if (pattern.staticIdx == ABSTRACT_MARK) return true
+ if (fact.staticIdx == ABSTRACT_MARK) return false
+ if (!staticsCompatible(pattern.staticIdx, fact.staticIdx)) return false
+
+ if (pattern.fieldIdx == ABSTRACT_MARK) return true
+ if (fact.fieldIdx == ABSTRACT_MARK) return false
+ if (!fieldCovers(pattern.fieldIdx, fact.fieldIdx, pattern)) return false
+
+ if (pattern.suffixIdx == ABSTRACT_MARK) return true
+ if (fact.suffixIdx == ABSTRACT_MARK) return false
+ if (pattern.suffixIdx == NO_ACCESSOR) return false
+ if (pattern.suffixIdx != fact.suffixIdx) return false
+ return !pattern.hasSemanticMark || pattern.valueAccessorState == fact.valueAccessorState
+ }
+
+ /** Symmetric candidate relation. It is deliberately distinct from directional [covers]. */
+ fun mayOverlap(left: BaseOnlyAccess, right: BaseOnlyAccess): Boolean {
+ if (left == right) return true
+ if (left.staticIdx == ABSTRACT_MARK || right.staticIdx == ABSTRACT_MARK) return true
+ if (!staticsCompatible(left.staticIdx, right.staticIdx)) return false
+
+ if (left.fieldIdx == ABSTRACT_MARK || right.fieldIdx == ABSTRACT_MARK) return true
+ if (left.fieldIdx >= 0 && right.fieldIdx >= 0 && left.fieldIdx != right.fieldIdx) return false
+ if (left.fieldIdx >= 0 && right.fieldIdx == NO_ACCESSOR && !hasVirtualStructuralAny(right)
+ ) return false
+ if (right.fieldIdx >= 0 && left.fieldIdx == NO_ACCESSOR && !hasVirtualStructuralAny(left)
+ ) return false
+
+ if (left.suffixIdx == ABSTRACT_MARK || right.suffixIdx == ABSTRACT_MARK) return true
+ if (left.suffixIdx == NO_ACCESSOR || right.suffixIdx == NO_ACCESSOR) return false
+ if (left.suffixIdx != right.suffixIdx) return false
+ return !left.hasSemanticMark || left.valueAccessorState == right.valueAccessorState
+ }
+
+ /**
+ * Projected final-to-initial containment.
+ *
+ * A missing structural slot is compatible with a concrete structural slot here because
+ * BaseOnly projection erases intermediate fields. This relation is intentionally broader
+ * than directional [covers]: it implements the cross-domain `FinalFactAp.contains`
+ * contract, not storage subsumption.
+ */
+ fun containsAccess(final: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean {
+ if (final == initial) return true
+
+ if (final.staticIdx == ABSTRACT_MARK) return true
+ if (!staticsCompatible(final.staticIdx, initial.staticIdx)) return false
+
+ if (final.fieldIdx == ABSTRACT_MARK) return true
+ if (!fieldsCompatible(final.fieldIdx, initial.fieldIdx)) return false
+
+ if (final.suffixIdx == ABSTRACT_MARK) return true
+ if (final.suffixIdx == NO_ACCESSOR) return false
+ if (final.suffixIdx != initial.suffixIdx) return false
+ return !final.hasSemanticMark || final.valueAccessorState == initial.valueAccessorState
+ }
+
+ /**
+ * The first concrete accessor selected by [candidate] after [pattern]'s abstraction point.
+ * A concrete structural slot in the candidate is residual when the suffix-abstract pattern
+ * has no corresponding structural slot: BaseOnly's implicit Any step crosses that boundary.
+ */
+ fun firstAccessorAfterAbstraction(
+ pattern: BaseOnlyAccess,
+ candidate: BaseOnlyAccess,
+ ): AccessorIdx? = when (pattern.apSlot) {
+ 0 -> candidate.staticIdx.takeIf { it >= 0 }
+ ?: candidate.fieldIdx.takeIf { it >= 0 }
+ ?: candidate.suffixIdx.takeIf { it >= 0 }
+
+ 1 -> candidate.fieldIdx.takeIf { it >= 0 }
+ ?: candidate.suffixIdx.takeIf { it >= 0 }
+
+ 2 -> when {
+ pattern.fieldIdx == NO_ACCESSOR && candidate.fieldIdx >= 0 -> candidate.fieldIdx
+ else -> candidate.suffixIdx.takeIf { it >= 0 }
+ }
+
+ else -> null
+ }
+
+ fun equalToInitial(final: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean {
+ if (initial.staticIdx != final.staticIdx) return false
+ if (initial.fieldIdx != final.fieldIdx) return false
+ val initialSemantic = if (initial.hasSemanticMark) initial.suffixIdx else NO_ACCESSOR
+ val finalSemantic = if (final.hasSemanticMark) final.suffixIdx else NO_ACCESSOR
+ if (initialSemantic != finalSemantic) return false
+ if (initialSemantic >= 0 && initial.valueAccessorState != final.valueAccessorState) return false
+ val terminalsAgree =
+ if (initial.hasTerminalAccessor) !final.isSuffixAbstract
+ else final.isSuffixAbstract == initial.isSuffixAbstract
+ return terminalsAgree
+ }
+
+ private enum class HeadRead { NONE, KEEP, TAIL, WRAPPER_TAIL }
+
+ private fun headRead(access: BaseOnlyAccess, idx: AccessorIdx): HeadRead {
+ if (access.staticIdx >= 0) return if (idx == access.staticIdx) HeadRead.TAIL else HeadRead.NONE
+ if (access.staticIdx == ABSTRACT_MARK) return HeadRead.NONE
+ if (access.fieldIdx >= 0) return if (idx == access.fieldIdx) HeadRead.TAIL else HeadRead.NONE
+ if (access.fieldIdx == ABSTRACT_MARK) return HeadRead.NONE
+ return when {
+ access.hasSemanticMark -> when {
+ structural(idx) -> HeadRead.KEEP
+ idx == terminalWrapperIdx(access) && access.valueAccessorState == BaseOnlyValueAccessorState.Value ->
+ HeadRead.WRAPPER_TAIL
+ idx == access.suffixIdx && access.valueAccessorState == BaseOnlyValueAccessorState.Normal -> HeadRead.TAIL
+ else -> HeadRead.NONE
+ }
+ access.suffixIdx == ABSTRACT_MARK -> if (structural(idx)) HeadRead.KEEP else HeadRead.NONE
+ access.isCollapsed -> if (structural(idx)) HeadRead.KEEP else HeadRead.NONE
+ access.suffixIdx == FINAL_ACCESSOR_IDX -> if (idx == FINAL_ACCESSOR_IDX) HeadRead.KEEP else HeadRead.NONE
+ else -> HeadRead.NONE
+ }
+ }
+
+ private fun tail(access: BaseOnlyAccess): BaseOnlyAccess = when {
+ access.staticIdx >= 0 -> packNormalized(
+ NO_ACCESSOR, access.fieldIdx, access.suffixIdx, access.valueAccessorState
+ )
+ access.fieldIdx >= 0 ->
+ packNormalized(NO_ACCESSOR, NO_ACCESSOR, access.suffixIdx, access.valueAccessorState)
+ access.hasSemanticMark -> packNormalized(NO_ACCESSOR, NO_ACCESSOR, FINAL_ACCESSOR_IDX)
+ else -> EMPTY_ACCESS
+ }
+
+ private fun wrapperTail(access: BaseOnlyAccess): BaseOnlyAccess =
+ packNormalized(NO_ACCESSOR, NO_ACCESSOR, access.suffixIdx, BaseOnlyValueAccessorState.Normal)
+
+ private fun combineTerminal(prefix: BaseOnlyAccess, suffix: BaseOnlyAccess): AccessorIdx = when {
+ prefix.hasSemanticMark -> prefix.suffixIdx
+ suffix.hasSemanticMark -> suffix.suffixIdx
+ suffix.suffixIdx == FINAL_ACCESSOR_IDX -> FINAL_ACCESSOR_IDX
+ suffix.suffixIdx == ABSTRACT_MARK -> ABSTRACT_MARK
+ prefix.suffixIdx == ABSTRACT_MARK -> ABSTRACT_MARK
+ else -> NO_ACCESSOR
+ }
+
+ private fun dropCorePrefix(access: BaseOnlyAccess, dropSlots: Int): BaseOnlyAccess {
+ val staticIdx = if (dropSlots <= 0) access.staticIdx else NO_ACCESSOR
+ val fieldIdx = if (dropSlots <= 1) access.fieldIdx else NO_ACCESSOR
+ return packNormalized(staticIdx, fieldIdx, access.suffixIdx, access.valueAccessorState)
+ }
+
+ private fun structural(idx: AccessorIdx): Boolean = idx.isStructuralIdx() || idx.isAnyIdx()
+
+ private fun terminalWrapperIdx(access: BaseOnlyAccess): AccessorIdx = when {
+ access.hasTypeInfoSuffix -> TYPE_INFO_GROUP_ACCESSOR_IDX
+ access.suffixIdx.isTaintMarkAccessor() -> VALUE_ACCESSOR_IDX
+ else -> NO_ACCESSOR
+ }
+
+ private fun hasVirtualStructuralAny(access: BaseOnlyAccess): Boolean =
+ access.fieldIdx == NO_ACCESSOR &&
+ (access.hasSemanticMark || access.isSuffixAbstract || access.isCollapsed)
+
+ private fun fieldCovers(patternField: AccessorIdx, factField: AccessorIdx, pattern: BaseOnlyAccess): Boolean = when {
+ patternField == factField -> true
+ patternField == NO_ACCESSOR -> factField >= 0 && hasVirtualStructuralAny(pattern)
+ else -> false
+ }
+
+ private fun staticsCompatible(a: AccessorIdx, b: AccessorIdx): Boolean = a == b
+
+ private fun fieldsCompatible(a: AccessorIdx, b: AccessorIdx): Boolean =
+ a == NO_ACCESSOR || b == NO_ACCESSOR || a == b
+
+ private fun packNormalized(
+ staticIdx: AccessorIdx,
+ fieldIdx: AccessorIdx,
+ suffixIdx: AccessorIdx,
+ valueAccessorState: BaseOnlyValueAccessorState = BaseOnlyValueAccessorState.Normal,
+ ): BaseOnlyAccess {
+ val apEarlier = staticIdx == ABSTRACT_MARK || fieldIdx == ABSTRACT_MARK
+ val normalizedSuffix =
+ if (suffixIdx == NO_ACCESSOR && !apEarlier && (staticIdx >= 0 || fieldIdx >= 0)) ABSTRACT_MARK
+ else suffixIdx
+ val normalizedState =
+ if (normalizedSuffix >= 0 && normalizedSuffix != FINAL_ACCESSOR_IDX) valueAccessorState
+ else BaseOnlyValueAccessorState.Normal
+ return packBaseOnlyAccess(staticIdx, fieldIdx, normalizedSuffix, normalizedState)
+ }
+
+ private val NO_MATCH = BaseOnlyMatch(emptyDelta = false, hasSuffix = false, suffix = EMPTY_ACCESS)
+ private val IDENTITY_MATCH = BaseOnlyMatch(emptyDelta = true, hasSuffix = false, suffix = EMPTY_ACCESS)
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt
new file mode 100644
index 000000000..dd1e358f1
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyAccessView.kt
@@ -0,0 +1,65 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.AnyAccessor
+import org.opentaint.dataflow.ap.ifds.TypeInfoGroupAccessor
+import org.opentaint.dataflow.ap.ifds.ValueAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor
+
+fun BaseOnlyApManager.startsWithAccessor(access: BaseOnlyAccess, accessor: Accessor): Boolean =
+ BaseOnlyAccessOps.startsWith(access, interner.index(accessor))
+
+fun BaseOnlyApManager.startAccessors(access: BaseOnlyAccess): Set {
+ val staticIdx = access.staticIdx
+ if (staticIdx >= 0) return setOf(accessor(staticIdx))
+ if (staticIdx == ABSTRACT_MARK) return emptySet()
+
+ val fieldIdx = access.fieldIdx
+ if (fieldIdx >= 0) {
+ return setOf(accessor(fieldIdx))
+ }
+ if (fieldIdx == ABSTRACT_MARK) return emptySet()
+
+ return when {
+ access.hasTypeInfoSuffix -> terminalStarts(
+ access,
+ TypeInfoGroupAccessor,
+ accessor(access.suffixIdx),
+ ) + AnyAccessor
+ access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor() -> terminalStarts(
+ access,
+ ValueAccessor,
+ accessor(access.suffixIdx),
+ ) + AnyAccessor
+ access.isSuffixAbstract || access.isCollapsed -> setOf(AnyAccessor)
+ access.hasSemanticMark -> setOf(AnyAccessor, accessor(access.suffixIdx))
+ access.suffixIdx >= 0 -> setOf(accessor(access.suffixIdx))
+ else -> emptySet()
+ }
+}
+
+fun BaseOnlyApManager.allAccessors(access: BaseOnlyAccess): Set =
+ buildSet {
+ if (access.hasSemanticMark && access.suffixIdx.isTaintMarkAccessor() &&
+ access.valueAccessorState == BaseOnlyValueAccessorState.Value
+ ) add(ValueAccessor)
+ access.forEachAccessorIdx { idx ->
+ val accessor = accessor(idx)
+ if (accessor != AnyAccessor) add(accessor)
+ }
+ }
+
+private fun terminalStarts(
+ access: BaseOnlyAccess,
+ wrapper: Accessor,
+ suffix: Accessor,
+): Set = when (access.valueAccessorState) {
+ BaseOnlyValueAccessorState.Normal -> setOf(suffix)
+ BaseOnlyValueAccessorState.Value -> setOf(wrapper)
+}
+
+fun BaseOnlyApManager.readAccess(access: BaseOnlyAccess, accessor: Accessor): BaseOnlyAccess? =
+ BaseOnlyAccessOps.read(access, interner.index(accessor))
+
+private fun BaseOnlyApManager.accessor(idx: Int): Accessor =
+ interner.accessor(idx) ?: error("Accessor not found: $idx")
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt
new file mode 100644
index 000000000..288dd82c1
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApAccess.kt
@@ -0,0 +1,30 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.common.FinalApAccess
+import org.opentaint.dataflow.ap.ifds.access.common.InitialApAccess
+
+interface BaseOnlyFinalApAccess : FinalApAccess {
+ val apManager: BaseOnlyApManager
+
+ override fun getFinalAccess(factAp: FinalFactAp): BaseOnlyAccess =
+ (factAp as BaseOnlyFinalFactAp).access
+
+ override fun createFinal(base: AccessPathBase, ap: BaseOnlyAccess, ex: ExclusionSet): FinalFactAp =
+ BaseOnlyFinalFactAp(apManager, base, ap, ex)
+
+}
+
+interface BaseOnlyInitialApAccess : InitialApAccess {
+ val apManager: BaseOnlyApManager
+
+ override fun getInitialAccess(factAp: InitialFactAp): BaseOnlyAccess =
+ (factAp as BaseOnlyInitialFactAp).access
+
+ override fun createInitial(base: AccessPathBase, ap: BaseOnlyAccess, ex: ExclusionSet): InitialFactAp =
+ BaseOnlyInitialFactAp(apManager, base, ap, ex)
+
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt
new file mode 100644
index 000000000..ef4bfb8fa
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyApManager.kt
@@ -0,0 +1,151 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.ExclusionSet.Empty
+import org.opentaint.dataflow.ap.ifds.LanguageManager
+import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy
+import org.opentaint.dataflow.ap.ifds.access.ApManager
+import org.opentaint.dataflow.ap.ifds.access.FactSideEffectSummariesApStorage
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.FinalFactList
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAbstraction
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.MethodAccessPathSubscription
+import org.opentaint.dataflow.ap.ifds.access.MethodEdgesFinalApSet
+import org.opentaint.dataflow.ap.ifds.access.MethodEdgesInitialToFinalApSet
+import org.opentaint.dataflow.ap.ifds.access.MethodEdgesNDInitialToFinalApSet
+import org.opentaint.dataflow.ap.ifds.access.MethodFinalApSummariesStorage
+import org.opentaint.dataflow.ap.ifds.access.MethodInitialToFinalApSummariesStorage
+import org.opentaint.dataflow.ap.ifds.access.MethodNDInitialToFinalApSummariesStorage
+import org.opentaint.dataflow.ap.ifds.access.SideEffectRequirementApStorage
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor
+import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer
+import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext
+import org.opentaint.dataflow.util.Cancellation
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class BaseOnlyApManager(
+ override val anyAccessorUnrollStrategy: AnyAccessorUnrollStrategy,
+ override val cancellation: Cancellation,
+ val fieldSensitive: Boolean = false,
+ val fieldGeneralizationEnabled: Boolean = false,
+ val summaryStorageFieldGeneralizationEnabled: Boolean = false,
+) : ApManager {
+ val interner = AccessorInterner()
+
+ @Volatile
+ private var traceResolutionMode = false
+
+ /** One-way analyzer phase transition; individual queries capture the phase at entry. */
+ fun enableTraceResolutionMode() {
+ traceResolutionMode = true
+ }
+
+ fun traceResolutionModeEnabled(): Boolean = traceResolutionMode
+
+ val Accessor.idx: AccessorIdx get() = interner.index(this)
+
+ val AccessorIdx.accessor: Accessor
+ get() = interner.accessor(this) ?: error("Accessor not found: $this")
+
+ val finalAccessorAccess: BaseOnlyAccess get() = FINAL_ACCESS
+
+ override fun mostAbstractInitialAp(base: AccessPathBase): InitialFactAp =
+ BaseOnlyInitialFactAp(this, base, ABSTRACT_EMPTY_ACCESS, Empty)
+
+ override fun mostAbstractFinalAp(base: AccessPathBase): FinalFactAp =
+ BaseOnlyFinalFactAp(this, base, ABSTRACT_EMPTY_ACCESS, Empty)
+
+ override fun createFinalAp(base: AccessPathBase, exclusions: ExclusionSet): FinalFactAp =
+ BaseOnlyFinalFactAp(this, base, finalAccessorAccess, exclusions)
+
+ override fun createFinalInitialAp(base: AccessPathBase, exclusions: ExclusionSet): InitialFactAp =
+ BaseOnlyInitialFactAp(this, base, finalAccessorAccess, exclusions)
+
+ fun applyExclusions(suffix: BaseOnlyAccess, exclusions: ExclusionSet): BaseOnlyAccess? =
+ when (exclusions) {
+ ExclusionSet.Universe -> null
+ ExclusionSet.Empty -> suffix
+ is ExclusionSet.Concrete -> {
+ if (suffix.staticIdx == NO_ACCESSOR && suffix.fieldIdx == NO_ACCESSOR && suffix.hasSemanticMark) {
+ // The missing field slot carries the implicit Any self-loop. Exact subtraction
+ // is not representable, so retain the compact cover.
+ suffix
+ } else {
+ val head = suffix.firstAccessorOrNull
+ val accessor = head?.let(interner::accessor)
+ if (accessor == null) suffix else suffix.takeUnless { exclusions.contains(accessor) }
+ }
+ }
+ }
+
+ fun renderAccess(access: BaseOnlyAccess): String {
+ val sb = StringBuilder()
+ access.forEachAccessorIdx { sb.append(idxToText(it)) }
+ if (access.isSuffixAbstract) sb.append(".*")
+ if (access.isCollapsed) sb.append(".^")
+ return sb.toString()
+ }
+
+ private fun idxToText(idx: AccessorIdx): String =
+ interner.accessor(idx)?.toSuffix() ?: when {
+ idx.isAnyIdx() -> ".[any]"
+ idx == FINAL_ACCESSOR_IDX -> ".$"
+ idx.isStaticAccessor() -> ""
+ idx.isStructuralIdx() -> ".f#$idx"
+ else -> ".#$idx"
+ }
+
+ override fun initialFactAbstraction(methodInitialStatement: CommonInst): InitialFactAbstraction =
+ BaseOnlyInitialFactAbstraction(this)
+
+ override fun methodEdgesFinalApSet(
+ methodInitialStatement: CommonInst,
+ maxInstIdx: Int,
+ languageManager: LanguageManager,
+ ): MethodEdgesFinalApSet =
+ MethodEdgesFinalBaseOnlyApSet(methodInitialStatement, maxInstIdx, languageManager, this)
+
+ override fun methodEdgesInitialToFinalApSet(
+ methodInitialStatement: CommonInst,
+ maxInstIdx: Int,
+ languageManager: LanguageManager,
+ ): MethodEdgesInitialToFinalApSet =
+ MethodEdgesInitialToFinalBaseOnlyApSet(methodInitialStatement, maxInstIdx, languageManager, this)
+
+ override fun methodEdgesNDInitialToFinalApSet(
+ methodInitialStatement: CommonInst,
+ maxInstIdx: Int,
+ languageManager: LanguageManager,
+ ): MethodEdgesNDInitialToFinalApSet =
+ MethodEdgesNDInitialToFinalBaseOnlyApSet(methodInitialStatement, languageManager, maxInstIdx, this)
+
+ override fun accessPathSubscription(): MethodAccessPathSubscription =
+ MethodBaseOnlyAccessPathSubscription(this)
+
+ override fun sideEffectRequirementApStorage(): SideEffectRequirementApStorage =
+ BaseOnlySideEffectRequirementApStorage()
+
+ override fun methodFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodFinalApSummariesStorage =
+ MethodFinalBaseOnlyApSummariesStorage(methodInitialStatement, this)
+
+ override fun methodInitialToFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodInitialToFinalApSummariesStorage =
+ MethodInitialToFinalBaseOnlyApSummariesStorage(methodInitialStatement, this)
+
+ override fun methodNDInitialToFinalApSummariesStorage(methodInitialStatement: CommonInst): MethodNDInitialToFinalApSummariesStorage =
+ MethodNDInitialToFinalBaseOnlyApSummariesStorage(methodInitialStatement, this)
+
+ override fun factSideEffectSummariesApStorage(methodInitialStatement: CommonInst): FactSideEffectSummariesApStorage =
+ FactSESummariesBaseOnlyStorage(methodInitialStatement, this)
+
+ override fun finalFactList(): FinalFactList = BaseOnlyFinalFactList(this)
+
+ override fun createSerializer(context: SummarySerializationContext): ApSerializer =
+ BaseOnlySerializer(this, context)
+
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt
new file mode 100644
index 000000000..dc9eb1bd4
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyDelta.kt
@@ -0,0 +1,90 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+
+sealed interface BaseOnlyFinalDelta : FinalFactAp.Delta
+
+data object BaseOnlyEmptyFinalDelta : BaseOnlyFinalDelta {
+ override val isEmpty: Boolean get() = true
+ override fun startsWithAccessor(accessor: Accessor): Boolean = false
+ override fun getStartAccessors(): Set = emptySet()
+ override fun getAllAccessors(): Set = emptySet()
+ override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? = null
+ override fun isAbstract(): Boolean = true
+}
+
+class BaseOnlyNodeFinalDelta(
+ val manager: BaseOnlyApManager,
+ val access: BaseOnlyAccess,
+) : BaseOnlyFinalDelta {
+ override val isEmpty: Boolean get() = false
+
+ override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor)
+
+ override fun getStartAccessors(): Set = manager.startAccessors(access)
+
+ override fun getAllAccessors(): Set = manager.allAccessors(access)
+
+ override fun readAccessor(accessor: Accessor): FinalFactAp.Delta? =
+ manager.readAccess(access, accessor)?.let { BaseOnlyNodeFinalDelta(manager, it) }
+
+ override fun isAbstract(): Boolean = access.isRootAbstract
+
+ override fun equals(other: Any?): Boolean =
+ this === other || (other is BaseOnlyNodeFinalDelta && access == other.access)
+
+ override fun hashCode(): Int = access.hashCode()
+
+ override fun toString(): String = manager.renderAccess(access)
+}
+
+sealed interface BaseOnlyInitialDelta : InitialFactAp.Delta
+
+data object BaseOnlyEmptyInitialDelta : BaseOnlyInitialDelta {
+ override val isEmpty: Boolean get() = true
+ override fun startsWithAccessor(accessor: Accessor): Boolean = false
+ override fun getStartAccessors(): Set = emptySet()
+ override fun getAllAccessors(): Set = emptySet()
+ override fun readAccessor(accessor: Accessor): InitialFactAp.Delta? = null
+ override fun isAbstract(): Boolean = true
+ override fun concat(other: InitialFactAp.Delta): InitialFactAp.Delta = other
+}
+
+class BaseOnlyNodeInitialDelta(
+ val manager: BaseOnlyApManager,
+ val access: BaseOnlyAccess,
+) : BaseOnlyInitialDelta {
+ override val isEmpty: Boolean get() = false
+
+ override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor)
+
+ override fun getStartAccessors(): Set = manager.startAccessors(access)
+
+ override fun getAllAccessors(): Set = manager.allAccessors(access)
+
+ override fun readAccessor(accessor: Accessor): InitialFactAp.Delta? =
+ manager.readAccess(access, accessor)?.let { BaseOnlyNodeInitialDelta(manager, it) }
+
+ override fun isAbstract(): Boolean = access.isRootAbstract
+
+ override fun concat(other: InitialFactAp.Delta): InitialFactAp.Delta = when (other) {
+ BaseOnlyEmptyInitialDelta -> this
+ is BaseOnlyNodeInitialDelta -> {
+ BaseOnlyNodeInitialDelta(
+ manager,
+ BaseOnlyAccessOps.append(access, other.access)
+ ?: error("static-first invariant violated: delta compose")
+ )
+ }
+ else -> error("Unexpected delta: $other")
+ }
+
+ override fun equals(other: Any?): Boolean =
+ this === other || (other is BaseOnlyNodeInitialDelta && access == other.access)
+
+ override fun hashCode(): Int = access.hashCode()
+
+ override fun toString(): String = manager.renderAccess(access)
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt
new file mode 100644
index 000000000..f4d693243
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusion.kt
@@ -0,0 +1,18 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor
+
+fun slotOfIdx(idx: AccessorIdx): Int = when {
+ idx.isStaticAccessor() -> 0
+ idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> 1
+ else -> 2
+}
+
+fun IntOpenHashSet.excludesIdx(idx: AccessorIdx): Boolean =
+ contains(idx) || (idx.isTypeInfoAccessor() && contains(TYPE_INFO_GROUP_ACCESSOR_IDX))
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt
new file mode 100644
index 000000000..bdaf63b3f
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyExclusionSet.kt
@@ -0,0 +1,235 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import kotlinx.collections.immutable.PersistentMap
+import kotlinx.collections.immutable.persistentHashMapOf
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.PersistentAccessorSet
+
+internal fun BaseOnlyApManager.compactExclusions(exclusions: ExclusionSet): ExclusionSet =
+ when (exclusions) {
+ ExclusionSet.Empty, ExclusionSet.Universe -> exclusions
+ is ExclusionSet.Concrete -> {
+ val set = exclusions.set
+ if (set is BaseOnlyExclusionAccessorSet && set.manager === this) {
+ exclusions
+ } else {
+ ExclusionSet.Concrete(BaseOnlyExclusionAccessorSet.from(this, set))
+ }
+ }
+ }
+
+internal class BaseOnlyExclusionAccessorSet private constructor(
+ val manager: BaseOnlyApManager,
+ private val chunks: PersistentMap,
+ override val size: Int,
+ private val cachedHash: Int,
+) : AbstractSet(), PersistentAccessorSet {
+ override fun contains(element: Accessor): Boolean =
+ containsIndex(manager.interner.index(element))
+
+ fun containsIndex(index: Int): Boolean {
+ val mask = chunks[index.chunkIndex()] ?: return false
+ return mask and index.chunkBit() != 0L
+ }
+
+ fun forEachIndex(consume: (Int) -> Unit) {
+ chunks.forEach { (chunkIndex, bits) ->
+ var remaining = bits
+ while (remaining != 0L) {
+ val bit = remaining.countTrailingZeroBits()
+ consume((chunkIndex shl CHUNK_BITS) + bit)
+ remaining = remaining and (remaining - 1)
+ }
+ }
+ }
+
+ fun union(other: BaseOnlyExclusionAccessorSet): BaseOnlyExclusionAccessorSet {
+ require(other.manager === manager)
+ return unionWithAdded(other)?.union ?: this
+ }
+
+ fun unionIfChanged(other: BaseOnlyExclusionAccessorSet): BaseOnlyExclusionAccessorSet? {
+ require(other.manager === manager)
+ return unionWithAdded(other)?.union
+ }
+
+ /**
+ * Adds [other] and returns both the union and the elements that [other] added.
+ * The unchanged case performs no allocation, which is important for repeated
+ * side-effect requirements.
+ */
+ fun unionWithAdded(other: BaseOnlyExclusionAccessorSet): UnionWithAdded? {
+ require(other.manager === manager)
+ if (other.isEmpty()) return null
+
+ var unionChunks = chunks
+ var addedChunks = persistentHashMapOf()
+ var addedSize = 0
+ var addedHash = 0
+ other.chunks.forEach { (chunkIndex, otherBits) ->
+ val currentBits = chunks[chunkIndex] ?: 0L
+ val newBits = otherBits and currentBits.inv()
+ if (newBits == 0L) return@forEach
+
+ unionChunks = unionChunks.put(chunkIndex, currentBits or newBits)
+ addedChunks = addedChunks.put(chunkIndex, newBits)
+ var remaining = newBits
+ while (remaining != 0L) {
+ val bit = remaining.countTrailingZeroBits()
+ addedSize++
+ addedHash += accessorHash((chunkIndex shl CHUNK_BITS) + bit)
+ remaining = remaining and (remaining - 1)
+ }
+ }
+ if (addedSize == 0) return null
+
+ return UnionWithAdded(
+ union = BaseOnlyExclusionAccessorSet(manager, unionChunks, size + addedSize, cachedHash + addedHash),
+ added = BaseOnlyExclusionAccessorSet(manager, addedChunks, addedSize, addedHash),
+ )
+ }
+
+ override fun iterator(): Iterator = object : Iterator {
+ private val chunkIterator = chunks.entries.sortedBy { it.key }.iterator()
+ private var chunkIndex = 0
+ private var remaining = 0L
+
+ init {
+ advanceChunk()
+ }
+
+ override fun hasNext(): Boolean = remaining != 0L
+
+ override fun next(): Accessor {
+ if (!hasNext()) throw NoSuchElementException()
+ val bit = remaining.countTrailingZeroBits()
+ val index = (chunkIndex shl CHUNK_BITS) + bit
+ remaining = remaining and (remaining - 1)
+ if (remaining == 0L) advanceChunk()
+ return manager.interner.accessor(index)
+ ?: error("Accessor not found")
+ }
+
+ private fun advanceChunk() {
+ if (!chunkIterator.hasNext()) return
+ val entry = chunkIterator.next()
+ chunkIndex = entry.key
+ remaining = entry.value
+ }
+ }
+
+ override fun hashCode(): Int = cachedHash
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other is BaseOnlyExclusionAccessorSet) {
+ return manager === other.manager &&
+ size == other.size && cachedHash == other.cachedHash && chunks == other.chunks
+ }
+ return super.equals(other)
+ }
+
+ override fun addPersistent(accessor: Accessor): PersistentAccessorSet {
+ val idx = manager.interner.index(accessor)
+ if (containsIndex(idx)) return this
+ val chunkIndex = idx.chunkIndex()
+ val result = chunks.put(chunkIndex, (chunks[chunkIndex] ?: 0L) or idx.chunkBit())
+ return BaseOnlyExclusionAccessorSet(manager, result, size + 1, cachedHash + accessor.hashCode())
+ }
+
+ override fun addAllPersistent(accessors: Set): PersistentAccessorSet =
+ combine(accessors, SetOperation.Union)
+
+ override fun retainAllPersistent(accessors: Set): PersistentAccessorSet =
+ combine(accessors, SetOperation.Intersection)
+
+ override fun removePersistent(accessor: Accessor): PersistentAccessorSet {
+ val idx = manager.interner.index(accessor)
+ val chunkIndex = idx.chunkIndex()
+ val currentBits = chunks[chunkIndex] ?: return this
+ val bit = idx.chunkBit()
+ if (currentBits and bit == 0L) return this
+ if (size == 1) return empty(manager)
+
+ val newBits = currentBits and bit.inv()
+ val result = if (newBits == 0L) chunks.remove(chunkIndex) else chunks.put(chunkIndex, newBits)
+ return BaseOnlyExclusionAccessorSet(manager, result, size - 1, cachedHash - accessor.hashCode())
+ }
+
+ override fun removeAllPersistent(accessors: Set): PersistentAccessorSet =
+ combine(accessors, SetOperation.Difference)
+
+ private fun combine(
+ accessors: Set,
+ operation: SetOperation,
+ ): BaseOnlyExclusionAccessorSet {
+ val other = from(manager, accessors)
+ return when (operation) {
+ SetOperation.Union -> union(other)
+ SetOperation.Intersection -> filterIndices { other.containsIndex(it) }
+ SetOperation.Difference -> filterIndices { !other.containsIndex(it) }
+ }
+ }
+
+ private inline fun filterIndices(crossinline keep: (Int) -> Boolean): BaseOnlyExclusionAccessorSet {
+ var resultChunks = persistentHashMapOf()
+ var resultSize = 0
+ var resultHash = 0
+ forEachIndex { index ->
+ if (!keep(index)) return@forEachIndex
+ val chunkIndex = index.chunkIndex()
+ resultChunks = resultChunks.put(chunkIndex, (resultChunks[chunkIndex] ?: 0L) or index.chunkBit())
+ resultSize++
+ resultHash += accessorHash(index)
+ }
+ return when (resultSize) {
+ size -> this
+ 0 -> empty(manager)
+ else -> BaseOnlyExclusionAccessorSet(manager, resultChunks, resultSize, resultHash)
+ }
+ }
+
+ private fun accessorHash(index: Int): Int =
+ manager.interner.accessor(index)?.hashCode() ?: error("Accessor not found: $index")
+
+ private enum class SetOperation {
+ Union,
+ Intersection,
+ Difference,
+ }
+
+ companion object {
+ fun from(manager: BaseOnlyApManager, accessors: Set): BaseOnlyExclusionAccessorSet {
+ if (accessors is BaseOnlyExclusionAccessorSet && accessors.manager === manager) return accessors
+
+ var chunks = persistentHashMapOf()
+ var size = 0
+ var hash = 0
+ accessors.forEach { accessor ->
+ val index = manager.interner.index(accessor)
+ val chunkIndex = index.chunkIndex()
+ val bit = index.chunkBit()
+ val currentBits = chunks[chunkIndex] ?: 0L
+ if (currentBits and bit != 0L) return@forEach
+ chunks = chunks.put(chunkIndex, currentBits or bit)
+ size++
+ hash += accessor.hashCode()
+ }
+ return BaseOnlyExclusionAccessorSet(manager, chunks, size, hash)
+ }
+
+ fun empty(manager: BaseOnlyApManager): BaseOnlyExclusionAccessorSet =
+ BaseOnlyExclusionAccessorSet(manager, persistentHashMapOf(), 0, 0)
+
+ private const val CHUNK_BITS = 6
+
+ private fun Int.chunkIndex(): Int = this ushr CHUNK_BITS
+ private fun Int.chunkBit(): Long = 1L shl (this and 63)
+ }
+
+ data class UnionWithAdded(
+ val union: BaseOnlyExclusionAccessorSet,
+ val added: BaseOnlyExclusionAccessorSet,
+ )
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt
new file mode 100644
index 000000000..0f454bcb8
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyF2FFieldGeneralization.kt
@@ -0,0 +1,184 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AnyAccessor
+import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor
+import org.opentaint.dataflow.ap.ifds.ElementAccessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.FieldAccessor
+
+internal const val MAX_FIELD_ENUMERATION_EDGES = 8
+
+internal data class BaseOnlySummaryEdgeAccessKey(
+ val initial: BaseOnlyAccess,
+ val final: BaseOnlyAccess,
+)
+
+internal data class BaseOnlyFieldErasureGroup(
+ val initial: BaseOnlyAccess,
+ val final: BaseOnlyAccess,
+)
+
+internal data class BaseOnlyFieldGeneralizationResult(
+ val summaries: List,
+ val newlyGeneralized: Set,
+)
+
+internal data class BaseOnlyFieldGeneralizationUpdate(
+ val representative: BaseOnlySummaryEdge,
+ val absorbedMembers: Set,
+ val newlyGeneralized: Boolean,
+)
+
+/**
+ * Writer-owned widening state for one initial-base/final-base storage scope.
+ */
+internal class BaseOnlyF2FFieldGeneralizer(
+ private val maxEnumeratedEdges: Int = MAX_FIELD_ENUMERATION_EDGES,
+ private val mergeExclusions: (List) -> ExclusionSet = { exclusions ->
+ exclusions.reduce(ExclusionSet::union)
+ },
+) {
+ private val generalizedGroups = linkedSetOf()
+ private val exclusionsByGroup = linkedMapOf()
+ private val membersByGroup = linkedMapOf<
+ BaseOnlyFieldErasureGroup,
+ LinkedHashMap,
+ >()
+
+ fun groupOf(initial: BaseOnlyAccess, final: BaseOnlyAccess): BaseOnlyFieldErasureGroup? {
+ val erasedInitial = initial.eraseFieldForSummaryGeneralization() ?: return null
+ val erasedFinal = final.eraseFieldForSummaryGeneralization() ?: return null
+ return BaseOnlyFieldErasureGroup(erasedInitial, erasedFinal)
+ }
+
+ fun isGeneralized(initial: BaseOnlyAccess, final: BaseOnlyAccess): Boolean =
+ groupOf(initial, final) in generalizedGroups
+
+ /**
+ * Incrementally observes one canonical edge. Until the group crosses its budget the edge
+ * remains exact. Afterwards each new member only updates the already materialized
+ * representative.
+ */
+ fun observeCanonicalEdge(edge: BaseOnlySummaryEdge): BaseOnlyFieldGeneralizationUpdate? {
+ val group = groupOf(edge.initial, edge.final) ?: return null
+ val currentRepresentativeExclusion = exclusionsByGroup[group]
+ if (group in generalizedGroups) {
+ val mergedExclusion = mergeExclusions(listOf(currentRepresentativeExclusion!!, edge.exclusion))
+ if (mergedExclusion == currentRepresentativeExclusion) return null
+ exclusionsByGroup[group] = mergedExclusion
+ return BaseOnlyFieldGeneralizationUpdate(
+ representative = createRepresentative(group),
+ absorbedMembers = emptySet(),
+ newlyGeneralized = false,
+ )
+ }
+
+ val members = membersByGroup.getOrPut(group) { linkedMapOf() }
+ members[edge.accessKey] = edge.exclusion
+ if (members.size <= maxEnumeratedEdges) return null
+
+ exclusionsByGroup[group] = mergeExclusions(members.values.toList())
+ generalizedGroups += group
+ membersByGroup.remove(group)
+ return BaseOnlyFieldGeneralizationUpdate(
+ representative = createRepresentative(group),
+ absorbedMembers = members.keys.toSet(),
+ newlyGeneralized = true,
+ )
+ }
+
+ fun removeCanonicalEdge(edge: BaseOnlySummaryEdge) {
+ val group = groupOf(edge.initial, edge.final) ?: return
+ if (group in generalizedGroups) return
+ val members = membersByGroup[group] ?: return
+ members.remove(edge.accessKey)
+ if (members.isEmpty()) membersByGroup.remove(group)
+ }
+
+ fun rewrite(summaries: List): BaseOnlyFieldGeneralizationResult {
+ val members = summaries.groupByTo(linkedMapOf()) { edge ->
+ groupOf(edge.initial, edge.final)
+ }
+
+ val newlyGeneralized = linkedSetOf()
+ members.forEach { (group, edges) ->
+ if (group == null) return@forEach
+
+ val observedExclusion = mergeExclusions(edges.map(BaseOnlySummaryEdge::exclusion))
+ exclusionsByGroup[group] = if (group in generalizedGroups) {
+ mergeExclusions(listOf(exclusionsByGroup.getValue(group), observedExclusion))
+ } else {
+ observedExclusion
+ }
+
+ if (group !in generalizedGroups && edges.size > maxEnumeratedEdges) {
+ generalizedGroups += group
+ newlyGeneralized += group
+ }
+ }
+
+ if (generalizedGroups.isEmpty()) {
+ return BaseOnlyFieldGeneralizationResult(summaries, emptySet())
+ }
+
+ val rewritten = summaries.filterTo(arrayListOf()) { edge ->
+ groupOf(edge.initial, edge.final) !in generalizedGroups
+ }
+ generalizedGroups.forEach { group ->
+ rewritten += createRepresentative(group)
+ }
+ rewritten.sortWith(BASE_ONLY_SUMMARY_EDGE_ORDER)
+
+ return BaseOnlyFieldGeneralizationResult(rewritten, newlyGeneralized)
+ }
+
+ fun representative(group: BaseOnlyFieldErasureGroup): BaseOnlySummaryEdge =
+ createRepresentative(group)
+
+ private fun createRepresentative(group: BaseOnlyFieldErasureGroup): BaseOnlySummaryEdge =
+ BaseOnlySummaryEdge(group.initial, group.final, exclusionsByGroup.getValue(group))
+}
+
+internal val BaseOnlySummaryEdge.accessKey: BaseOnlySummaryEdgeAccessKey
+ get() = BaseOnlySummaryEdgeAccessKey(initial, final)
+
+internal fun intersectSummaryFieldGeneralizationExclusions(
+ exclusions: List,
+): ExclusionSet = exclusions
+ .map(ExclusionSet::suffixExclusions)
+ .reduce(ExclusionSet::intersect)
+
+private fun ExclusionSet.suffixExclusions(): ExclusionSet = when (this) {
+ ExclusionSet.Empty,
+ ExclusionSet.Universe,
+ -> this
+
+ is ExclusionSet.Concrete -> set.fold(ExclusionSet.Empty as ExclusionSet) { suffix, accessor ->
+ when (accessor) {
+ AnyAccessor,
+ ElementAccessor,
+ is ClassStaticAccessor,
+ is FieldAccessor,
+ -> suffix
+
+ else -> suffix.add(accessor)
+ }
+ }
+}
+
+internal fun BaseOnlyAccess.eraseFieldForSummaryGeneralization(): BaseOnlyAccess? {
+ if (staticIdx != NO_ACCESSOR || valueAccessorState != BaseOnlyValueAccessorState.Normal) return null
+
+ val eligible = when {
+ fieldIdx == ABSTRACT_MARK && suffixIdx == NO_ACCESSOR -> true
+ fieldIdx.isStructuralIdx() && suffixIdx == ABSTRACT_MARK -> true
+ fieldIdx == NO_ACCESSOR && suffixIdx == ABSTRACT_MARK -> true
+ else -> false
+ }
+ return ABSTRACT_EMPTY_ACCESS.takeIf { eligible }
+}
+
+internal val BASE_ONLY_SUMMARY_EDGE_ORDER = compareBy(
+ { it.initial },
+ { it.final },
+)
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt
new file mode 100644
index 000000000..4f70eebb1
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactAp.kt
@@ -0,0 +1,225 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.FactTypeChecker
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTaintMarkAccessor
+
+class BaseOnlyFinalFactAp(
+ val manager: BaseOnlyApManager,
+ override val base: AccessPathBase,
+ val access: BaseOnlyAccess,
+ exclusions: ExclusionSet,
+) : FinalFactAp {
+ override val exclusions: ExclusionSet = manager.compactExclusions(exclusions)
+
+ init {
+ BaseOnlyAccessOps.requireCanonical(access, allowTransientCollapsed = true)
+ }
+
+ override val size: Int get() = access.size
+ override val depth: Int get() = size
+
+ override fun isAbstract(): Boolean = access.isRootAbstract
+
+ override fun rebase(newBase: AccessPathBase): FinalFactAp =
+ BaseOnlyFinalFactAp(manager, newBase, BaseOnlyAccessOps.restoreAbstraction(access), exclusions)
+
+ override fun exclude(accessor: Accessor): FinalFactAp =
+ BaseOnlyFinalFactAp(manager, base, access, exclusions.add(accessor))
+
+ override fun replaceExclusions(exclusions: ExclusionSet): FinalFactAp =
+ BaseOnlyFinalFactAp(manager, base, access, exclusions)
+
+ private fun rewrap(newAccess: BaseOnlyAccess): BaseOnlyFinalFactAp =
+ BaseOnlyFinalFactAp(manager, base, newAccess, exclusions)
+
+ override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor)
+
+ override fun getStartAccessors(): Set = manager.startAccessors(access)
+
+ override fun getAllAccessors(): Set = manager.allAccessors(access)
+
+ override fun readAccessor(accessor: Accessor): FinalFactAp? = manager.readAccess(access, accessor)?.let(::rewrap)
+
+ override fun prependAccessor(accessor: Accessor): FinalFactAp =
+ rewrap(BaseOnlyAccessOps.prepend(access, manager.interner.index(accessor), manager.fieldSensitive))
+
+ override fun clearAccessor(accessor: Accessor): FinalFactAp? =
+ BaseOnlyAccessOps.clear(access, manager.interner.index(accessor))?.let(::rewrap)
+
+ override fun removeAbstraction(): FinalFactAp? =
+ BaseOnlyAccessOps.collapse(access).takeIf { !it.isEmpty }?.let(::rewrap)
+
+ override fun abstractOnly(): FinalFactAp {
+ val abstractAccess = access.withBaseOnlyAccessUnpacked { staticIdx, fieldIdx, _ ->
+ when {
+ staticIdx == ABSTRACT_MARK -> packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR)
+ fieldIdx == ABSTRACT_MARK -> packBaseOnlyAccess(NO_ACCESSOR, ABSTRACT_MARK, NO_ACCESSOR)
+ else -> ABSTRACT_EMPTY_ACCESS
+ }
+ }
+ return rewrap(abstractAccess)
+ }
+
+ override fun filterFact(filter: FactTypeChecker.FactApFilter): FinalFactAp? =
+ filterAccess(filter)?.let { filtered -> if (filtered == access) this else rewrap(filtered) }
+
+ override fun filterFact(filter: FactTypeChecker.FactCompatibilityFilter): FinalFactAp? {
+ if (filter is FactTypeChecker.AlwaysCompatibleFilter) return this
+ if (!access.hasAp) return this
+
+ // Tree checks only an edge whose child node has direct abstract acceptance.
+ // Ancestor nodes that merely contain an abstract descendant are not checked.
+ val predecessor = when (access.apSlot) {
+ 0 -> NO_ACCESSOR
+ 1 -> access.staticIdx
+ 2 -> if (access.fieldIdx >= 0) access.fieldIdx else access.staticIdx
+ else -> error("Canonical abstract fact has no abstraction slot: $access")
+ }
+ if (predecessor < 0) return this
+ val accessor = manager.interner.accessor(predecessor)
+ ?: error("Accessor not found: $predecessor")
+ return when (filter.check(accessor)) {
+ FactTypeChecker.CompatibilityFilterResult.Compatible -> this
+ FactTypeChecker.CompatibilityFilterResult.NotCompatible -> null
+ }
+ }
+
+ private fun filterAccess(
+ filter: FactTypeChecker.FactApFilter,
+ candidate: BaseOnlyAccess = access,
+ ): BaseOnlyAccess? {
+ if (!candidate.hasSemanticMark) {
+ return candidate.takeIf { logicalPaths(candidate).any { path -> pathAccepted(filter, path) } }
+ }
+ val common = logicalPrefix(candidate)
+ val path = when (candidate.valueAccessorState) {
+ BaseOnlyValueAccessorState.Normal -> common + intArrayOf(candidate.suffixIdx, FINAL_ACCESSOR_IDX)
+ BaseOnlyValueAccessorState.Value -> {
+ val valueAccessor =
+ if (candidate.hasTypeInfoSuffix) TYPE_INFO_GROUP_ACCESSOR_IDX else VALUE_ACCESSOR_IDX
+ common + intArrayOf(valueAccessor, candidate.suffixIdx, FINAL_ACCESSOR_IDX)
+ }
+ }
+ return candidate.takeIf { pathAccepted(filter, path) }
+ }
+
+ private fun pathAccepted(filter: FactTypeChecker.FactApFilter, path: IntArray): Boolean {
+ var current = filter
+ path.forEach { idx ->
+ val accessor = manager.interner.accessor(idx) ?: error("Accessor not found: $idx")
+ when (val result = current.check(accessor)) {
+ FactTypeChecker.FilterResult.Accept -> return true
+ FactTypeChecker.FilterResult.Reject -> return false
+ is FactTypeChecker.FilterResult.FilterNext -> current = result.filter
+ }
+ }
+ return true
+ }
+
+ /** The single logical path represented by one compact access. */
+ private fun logicalPaths(candidate: BaseOnlyAccess): List {
+ val common = logicalPrefix(candidate)
+ val suffix = candidate.suffixIdx
+ if (suffix < 0) return listOf(common)
+ if (suffix == FINAL_ACCESSOR_IDX) return listOf(common + FINAL_ACCESSOR_IDX)
+ if (candidate.hasTypeInfoSuffix) {
+ return listOf(terminalLogicalPath(candidate, common, TYPE_INFO_GROUP_ACCESSOR_IDX))
+ }
+ if (suffix.isTaintMarkAccessor()) {
+ return listOf(terminalLogicalPath(candidate, common, VALUE_ACCESSOR_IDX))
+ }
+ return listOf(common + intArrayOf(suffix, FINAL_ACCESSOR_IDX))
+ }
+
+ private fun logicalPrefix(candidate: BaseOnlyAccess): IntArray = buildList {
+ if (candidate.staticIdx >= 0) add(candidate.staticIdx)
+ if (candidate.fieldIdx >= 0) add(candidate.fieldIdx)
+ }.toIntArray()
+
+ private fun terminalLogicalPath(
+ candidate: BaseOnlyAccess,
+ common: IntArray,
+ valueAccessor: Int,
+ ): IntArray = when (candidate.valueAccessorState) {
+ BaseOnlyValueAccessorState.Normal -> common + intArrayOf(candidate.suffixIdx, FINAL_ACCESSOR_IDX)
+ BaseOnlyValueAccessorState.Value ->
+ common + intArrayOf(valueAccessor, candidate.suffixIdx, FINAL_ACCESSOR_IDX)
+ }
+
+ override fun contains(factAp: InitialFactAp): Boolean {
+ factAp as BaseOnlyInitialFactAp
+ if (base != factAp.base) return false
+ if (!BaseOnlyAccessOps.containsAccess(access, factAp.access)) return false
+ val residualHead = BaseOnlyAccessOps.firstAccessorAfterAbstraction(access, factAp.access)
+ ?: return true
+ val accessor = manager.interner.accessor(residualHead) ?: return true
+ return accessor !in exclusions
+ }
+
+ override fun equalTo(factAp: InitialFactAp): Boolean {
+ factAp as BaseOnlyInitialFactAp
+ if (base != factAp.base) return false
+ return BaseOnlyAccessOps.equalToInitial(access, factAp.access)
+ }
+
+ override fun delta(other: InitialFactAp): List {
+ other as BaseOnlyInitialFactAp
+ if (base != other.base) return emptyList()
+ val match = BaseOnlyAccessOps.matchPrefix(access, other.access)
+ val result = ArrayList(2)
+ if (match.emptyDelta) result.add(BaseOnlyEmptyFinalDelta)
+ if (match.hasSuffix) {
+ manager.applyExclusions(match.suffix, other.exclusions)?.let { suffix ->
+ result.add(BaseOnlyNodeFinalDelta(manager, suffix))
+ }
+ }
+ return result
+ }
+
+ override fun hasEmptyDelta(other: InitialFactAp): Boolean {
+ other as BaseOnlyInitialFactAp
+ return base == other.base && access == other.access
+ }
+
+ override fun concat(typeChecker: FactTypeChecker, delta: FinalFactAp.Delta): FinalFactAp? {
+ return when (val d = delta as BaseOnlyFinalDelta) {
+ BaseOnlyEmptyFinalDelta -> this
+ is BaseOnlyNodeFinalDelta -> {
+ val filteredDelta = filterDelta(typeChecker, d.access) ?: return null
+ BaseOnlyAccessOps.appendFinal(access, filteredDelta)?.let(::rewrap)
+ }
+ }
+ }
+
+ private fun filterDelta(typeChecker: FactTypeChecker, delta: BaseOnlyAccess): BaseOnlyAccess? {
+ val prefix = buildList {
+ access.forEachCoreIdx { idx ->
+ add(manager.interner.accessor(idx) ?: error("Accessor not found: $idx"))
+ }
+ }
+ return filterAccess(typeChecker.accessPathFilter(prefix), delta)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BaseOnlyFinalFactAp) return false
+ return base == other.base && access == other.access && exclusions == other.exclusions
+ }
+
+ override fun hashCode(): Int {
+ var result = base.hashCode()
+ result = 31 * result + access.hashCode()
+ result = 31 * result + exclusions.hashCode()
+ return result
+ }
+
+ override fun toString(): String = "$base${manager.renderAccess(access)}/$exclusions"
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt
new file mode 100644
index 000000000..5e3491ad4
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyFinalFactList.kt
@@ -0,0 +1,29 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.LongArrayList
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.common.CommonFinalFactList
+
+class BaseOnlyFinalFactList(
+ override val apManager: BaseOnlyApManager,
+) : CommonFinalFactList(), BaseOnlyFinalApAccess {
+ override val storage: AccessStorage = LongAccessStorage()
+
+ override fun add(fact: FinalFactAp) {
+ fact as BaseOnlyFinalFactAp
+ if (fact.access.isCollapsed) return
+ super.add(fact)
+ }
+
+ private class LongAccessStorage : AccessStorage {
+ private val storage = LongArrayList()
+
+ override fun add(fact: BaseOnlyAccess) {
+ storage.add(fact)
+ }
+
+ override fun get(idx: Int): BaseOnlyAccess = storage.getLong(idx)
+
+ override fun removeLast(): BaseOnlyAccess = storage.removeLong(storage.size - 1)
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt
new file mode 100644
index 000000000..4ebbd44fb
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialAccessIndex.kt
@@ -0,0 +1,199 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.util.ConcurrentReadSafeInt2ObjectMap
+import org.opentaint.dataflow.util.forEachEntry
+import org.opentaint.dataflow.util.getOrCreateNullable
+import org.opentaint.dataflow.util.int2ObjectMap
+
+/**
+ * A single-writer/multiple-reader index over the three packed BaseOnly access slots.
+ *
+ * Most summary and subscription indexes contain only a handful of accesses. Keeping those entries
+ * in an immutable flat list avoids allocating three hash tables per index. Once an index grows past
+ * [SMALL_INDEX_LIMIT], the writer atomically publishes the slot hierarchy used for indexed lookup.
+ */
+internal class BaseOnlyInitialAccessIndex {
+ private data class Entry(val access: BaseOnlyAccess, val value: V)
+
+ private sealed interface State {
+ class Small(val entries: List>) : State
+ class Indexed(val hierarchy: Hierarchy) : State
+ }
+
+ private class FieldNode {
+ val fields: ConcurrentReadSafeInt2ObjectMap?> = int2ObjectMap()
+ }
+
+ private class SuffixNode {
+ val suffixes: ConcurrentReadSafeInt2ObjectMap = int2ObjectMap()
+ }
+
+ private class Hierarchy {
+ private val statics: ConcurrentReadSafeInt2ObjectMap?> = int2ObjectMap()
+
+ fun getOrCreate(access: BaseOnlyAccess, create: () -> V): V {
+ val fieldNode = statics.getOrCreateNullable(access.staticIdx) { FieldNode() }
+ val suffixNode = fieldNode.fields.getOrCreateNullable(access.fieldIdx) { SuffixNode() }
+ return suffixNode.suffixes.getOrCreateNullable(access.rawSuffixSlot, create)
+ }
+
+ fun get(access: BaseOnlyAccess): V? =
+ statics.get(access.staticIdx)
+ ?.fields?.get(access.fieldIdx)
+ ?.suffixes?.get(access.rawSuffixSlot)
+
+ fun collectAll(consume: (BaseOnlyAccess, V) -> Unit) {
+ statics.forEachEntry { staticIdx, fieldNode ->
+ fieldNode?.collectAll(staticIdx, consume)
+ }
+ }
+
+ fun collectCandidates(pattern: BaseOnlyAccess, consume: (BaseOnlyAccess, V) -> Unit) {
+ if (pattern.staticIdx == ABSTRACT_MARK) {
+ collectAll(consume)
+ return
+ }
+
+ statics.get(ABSTRACT_MARK)?.collectAll(ABSTRACT_MARK, consume)
+ val fieldNode = statics.get(pattern.staticIdx) ?: return
+ if (pattern.fieldIdx == ABSTRACT_MARK) {
+ fieldNode.collectAll(pattern.staticIdx, consume)
+ return
+ }
+
+ fieldNode.fields.get(ABSTRACT_MARK)?.collectAll(pattern.staticIdx, ABSTRACT_MARK, consume)
+ when (pattern.fieldIdx) {
+ NO_ACCESSOR -> fieldNode.fields.forEachEntry { fieldIdx, suffixNode ->
+ suffixNode?.collectCandidates(pattern.staticIdx, fieldIdx, pattern, consume)
+ }
+
+ else -> {
+ fieldNode.fields.get(pattern.fieldIdx)?.collectCandidates(
+ pattern.staticIdx,
+ pattern.fieldIdx,
+ pattern,
+ consume,
+ )
+ fieldNode.fields.get(NO_ACCESSOR)?.collectCandidates(
+ pattern.staticIdx,
+ NO_ACCESSOR,
+ pattern,
+ consume,
+ )
+ }
+ }
+ }
+
+ private fun FieldNode.collectAll(staticIdx: Int, consume: (BaseOnlyAccess, V) -> Unit) {
+ fields.forEachEntry { fieldIdx, suffixNode ->
+ suffixNode?.collectAll(staticIdx, fieldIdx, consume)
+ }
+ }
+
+ private fun SuffixNode.collectCandidates(
+ staticIdx: Int,
+ fieldIdx: Int,
+ pattern: BaseOnlyAccess,
+ consume: (BaseOnlyAccess, V) -> Unit,
+ ) {
+ if (pattern.suffixIdx == ABSTRACT_MARK) {
+ collectAll(staticIdx, fieldIdx, consume)
+ return
+ }
+
+ val abstractSuffix = rawBaseOnlySuffixSlot(ABSTRACT_MARK, BaseOnlyValueAccessorState.Normal)
+ suffixes.get(abstractSuffix)?.let { value ->
+ consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, abstractSuffix), value)
+ }
+
+ val states =
+ if (pattern.hasSemanticMark) BaseOnlyValueAccessorState.entries
+ else listOf(BaseOnlyValueAccessorState.Normal)
+ for (state in states) {
+ val rawSuffix = rawBaseOnlySuffixSlot(pattern.suffixIdx, state)
+ suffixes.get(rawSuffix)?.let { value ->
+ consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, rawSuffix), value)
+ }
+ }
+ }
+
+ private fun SuffixNode.collectAll(
+ staticIdx: Int,
+ fieldIdx: Int,
+ consume: (BaseOnlyAccess, V) -> Unit,
+ ) {
+ suffixes.forEachEntry { rawSuffix, value ->
+ value?.let { consume(packBaseOnlyAccessFromRawSuffix(staticIdx, fieldIdx, rawSuffix), it) }
+ }
+ }
+ }
+
+ @Volatile
+ private var state: State = State.Small(emptyList())
+
+ fun getOrCreate(access: BaseOnlyAccess, create: () -> V): V {
+ return when (val current = state) {
+ is State.Indexed -> current.hierarchy.getOrCreate(access, create)
+ is State.Small -> {
+ current.entries.firstOrNull { it.access == access }?.value?.let { return it }
+ val value = create()
+ if (current.entries.size < SMALL_INDEX_LIMIT) {
+ state = State.Small(current.entries + Entry(access, value))
+ } else {
+ val hierarchy = Hierarchy()
+ current.entries.forEach { entry ->
+ hierarchy.getOrCreate(entry.access) { entry.value }
+ }
+ hierarchy.getOrCreate(access) { value }
+ state = State.Indexed(hierarchy)
+ }
+ value
+ }
+ }
+ }
+
+ fun get(access: BaseOnlyAccess): V? = when (val current = state) {
+ is State.Indexed -> current.hierarchy.get(access)
+ is State.Small -> current.entries.firstOrNull { it.access == access }?.value
+ }
+
+ fun collectAll(consume: (BaseOnlyAccess, V) -> Unit) {
+ when (val current = state) {
+ is State.Indexed -> current.hierarchy.collectAll(consume)
+ is State.Small -> current.entries.forEach { consume(it.access, it.value) }
+ }
+ }
+
+ fun collectCandidates(pattern: BaseOnlyAccess, consume: (BaseOnlyAccess, V) -> Unit) {
+ when (val current = state) {
+ is State.Indexed -> current.hierarchy.collectCandidates(pattern, consume)
+ is State.Small -> current.entries.forEach { entry ->
+ if (isCandidate(pattern, entry.access)) consume(entry.access, entry.value)
+ }
+ }
+ }
+
+ private fun isCandidate(pattern: BaseOnlyAccess, candidate: BaseOnlyAccess): Boolean {
+ if (pattern.staticIdx == ABSTRACT_MARK || candidate.staticIdx == ABSTRACT_MARK) return true
+ if (pattern.staticIdx != candidate.staticIdx) return false
+
+ if (pattern.fieldIdx == ABSTRACT_MARK || candidate.fieldIdx == ABSTRACT_MARK) return true
+ val fieldMatches = when (pattern.fieldIdx) {
+ NO_ACCESSOR -> true
+ else -> candidate.fieldIdx == pattern.fieldIdx || candidate.fieldIdx == NO_ACCESSOR
+ }
+ if (!fieldMatches) return false
+
+ if (pattern.suffixIdx == ABSTRACT_MARK || candidate.suffixIdx == ABSTRACT_MARK) return true
+ if (pattern.suffixIdx != candidate.suffixIdx) return false
+ return pattern.hasSemanticMark || candidate.valueAccessorState == BaseOnlyValueAccessorState.Normal
+ }
+
+ private companion object {
+ const val SMALL_INDEX_LIMIT = 32
+ }
+}
+
+/** Tree's filterContains is a symmetric applicability query, not directional containment. */
+internal fun baseOnlySummaryInitialMatches(pattern: BaseOnlyAccess, initial: BaseOnlyAccess): Boolean =
+ BaseOnlyAccessOps.mayOverlap(pattern, initial)
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt
new file mode 100644
index 000000000..433ecab02
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAbstraction.kt
@@ -0,0 +1,287 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
+import it.unimi.dsi.fastutil.ints.IntArrayList
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap
+import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.FactTypeChecker
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAbstraction
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.ELEMENT_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.FINAL_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.TYPE_INFO_GROUP_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.VALUE_ACCESSOR_IDX
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isFieldAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isStaticAccessor
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorInterner.Companion.isTypeInfoAccessor
+
+class BaseOnlyInitialFactAbstraction(
+ private val manager: BaseOnlyApManager,
+) : InitialFactAbstraction {
+ private val perBase = Object2ObjectOpenHashMap()
+
+ private inner class BaseState {
+ val added = LongOpenHashSet()
+ val emitted = LongOpenHashSet()
+ val knownExclusionsByPattern = Long2ObjectOpenHashMap()
+ val factsByExclusion = Int2ObjectOpenHashMap()
+ val blockedAtByFact = Long2LongOpenHashMap()
+ val concreteTypeBlockerByFact = Long2IntOpenHashMap().apply { defaultReturnValue(NO_ACCESSOR) }
+
+ fun addExclusionsAndFindUnblockedAccessors(
+ pattern: BaseOnlyAccess,
+ exclusions: Set,
+ ): IntArrayList? {
+ val compactExclusions = BaseOnlyExclusionAccessorSet.from(manager, exclusions)
+ val knownExclusions = knownExclusionsByPattern[pattern]
+
+ if (knownExclusions == null) {
+ if (compactExclusions.isEmpty()) return null
+
+ knownExclusionsByPattern[pattern] = compactExclusions
+ return newlyExcludedBlockedAccessors(compactExclusions, previouslyExcluded = null)
+ }
+
+ val union = knownExclusions.unionIfChanged(compactExclusions) ?: return null
+
+ knownExclusionsByPattern[pattern] = union
+ return newlyExcludedBlockedAccessors(compactExclusions, knownExclusions)
+ }
+
+ fun excludes(blockedAt: BaseOnlyAccess, accessor: AccessorIdx): Boolean {
+ val typeGroupMatches = accessor.isTypeInfoAccessor()
+ val iterator = knownExclusionsByPattern.long2ObjectEntrySet().fastIterator()
+ while (iterator.hasNext()) {
+ val entry = iterator.next()
+ if (!exclusionPatternCovers(entry.longKey, blockedAt)) continue
+ val exclusions = entry.value
+ if (exclusions.containsIndex(accessor)) return true
+ if (typeGroupMatches && exclusions.containsIndex(TYPE_INFO_GROUP_ACCESSOR_IDX)) return true
+ }
+ return false
+ }
+
+ fun registerBlockedFact(access: BaseOnlyAccess, blockedAt: BaseOnlyAccess, accessor: AccessorIdx) {
+ check(!blockedAtByFact.containsKey(access))
+ blockedAtByFact.put(access, blockedAt)
+ check(factsByExclusion.computeIfAbsent(accessor) { LongOpenHashSet() }.add(access))
+ if (accessor.isTypeInfoAccessor() && accessor != TYPE_INFO_GROUP_ACCESSOR_IDX) {
+ check(concreteTypeBlockerByFact.put(access, accessor) == NO_ACCESSOR)
+ check(
+ factsByExclusion
+ .computeIfAbsent(TYPE_INFO_GROUP_ACCESSOR_IDX) { LongOpenHashSet() }
+ .add(access)
+ )
+ }
+ }
+
+ fun takeFactsUnblockedBy(accessor: AccessorIdx, pattern: BaseOnlyAccess): LongOpenHashSet? {
+ val candidates = factsByExclusion[accessor] ?: return null
+ val unblocked = LongOpenHashSet()
+ val candidateIterator = candidates.iterator()
+ while (candidateIterator.hasNext()) {
+ val access = candidateIterator.nextLong()
+ val blockedAt = blockedAtByFact.get(access)
+ if (!exclusionPatternCovers(pattern, blockedAt)) continue
+
+ candidateIterator.remove()
+ unblocked.add(access)
+ blockedAtByFact.remove(access)
+ val concreteTypeBlocker = concreteTypeBlockerByFact.remove(access)
+ if (accessor == TYPE_INFO_GROUP_ACCESSOR_IDX && concreteTypeBlocker != NO_ACCESSOR) {
+ factsByExclusion[concreteTypeBlocker]?.remove(access)
+ } else if (concreteTypeBlocker != NO_ACCESSOR) {
+ factsByExclusion[TYPE_INFO_GROUP_ACCESSOR_IDX]?.remove(access)
+ }
+ }
+ if (candidates.isEmpty()) factsByExclusion.remove(accessor)
+ return unblocked.takeUnless { it.isEmpty() }
+ }
+
+ private fun newlyExcludedBlockedAccessors(
+ exclusions: BaseOnlyExclusionAccessorSet,
+ previouslyExcluded: BaseOnlyExclusionAccessorSet?,
+ ): IntArrayList? {
+ var result: IntArrayList? = null
+ val iterator = factsByExclusion.keys.iterator()
+ while (iterator.hasNext()) {
+ val accessor = iterator.nextInt()
+ if (exclusions.containsIndex(accessor) && previouslyExcluded?.containsIndex(accessor) != true) {
+ val matches = result ?: IntArrayList().also { result = it }
+ matches.add(accessor)
+ }
+ }
+ return result
+ }
+
+ private fun exclusionPatternCovers(pattern: BaseOnlyAccess, blockedAt: BaseOnlyAccess): Boolean =
+ pattern == ABSTRACT_EMPTY_ACCESS || BaseOnlyAccessOps.containsAccess(pattern, blockedAt)
+ }
+
+ private data class Blocker(val accessor: AccessorIdx, val blockedAt: BaseOnlyAccess)
+
+ override fun addAbstractedInitialFact(
+ factAp: FinalFactAp,
+ typeChecker: FactTypeChecker,
+ ): List> {
+ factAp as BaseOnlyFinalFactAp
+ val state = perBase.getOrPut(factAp.base) { BaseState() }
+ if (!state.added.add(factAp.access)) return emptyList()
+
+ val out = ArrayList>()
+ abstractAndIndex(factAp.base, factAp.access, state, out)
+ return out
+ }
+
+ override fun registerNewInitialFact(
+ factAp: InitialFactAp,
+ typeChecker: FactTypeChecker,
+ ): List> {
+ factAp as BaseOnlyInitialFactAp
+ val state = perBase.getOrPut(factAp.base) { BaseState() }
+
+ val unblockedAccessors = when (val ex = factAp.exclusions) {
+ is ExclusionSet.Concrete -> state.addExclusionsAndFindUnblockedAccessors(
+ factAp.access,
+ ex.set,
+ )
+ ExclusionSet.Empty -> null
+ ExclusionSet.Universe -> error("Unexpected universe exclusion")
+ }
+ if (unblockedAccessors == null) return emptyList()
+
+ val out = ArrayList>()
+ val exclusionIterator = unblockedAccessors.iterator()
+ while (exclusionIterator.hasNext()) {
+ val accessor = exclusionIterator.nextInt()
+ val unblocked = state.takeFactsUnblockedBy(accessor, factAp.access) ?: continue
+ val unblockedIterator = unblocked.iterator()
+ while (unblockedIterator.hasNext()) {
+ abstractAndIndex(factAp.base, unblockedIterator.nextLong(), state, out)
+ }
+ }
+ return out
+ }
+
+ private fun abstractAndIndex(
+ base: AccessPathBase,
+ added: BaseOnlyAccess,
+ state: BaseState,
+ out: MutableList>,
+ ) {
+ val blocker = abstractOneBranch(base, added, state, out)
+ if (blocker != null) state.registerBlockedFact(added, blocker.blockedAt, blocker.accessor)
+ }
+
+ private fun abstractOneBranch(
+ base: AccessPathBase,
+ added: BaseOnlyAccess,
+ state: BaseState,
+ out: MutableList>,
+ ): Blocker? {
+ val prefix = ArrayList(3)
+ var stopped = false
+ val core = buildList {
+ if (added.staticIdx >= 0) add(added.staticIdx)
+ if (added.fieldIdx >= 0) add(added.fieldIdx)
+ if (added.hasSemanticMark && added.valueAccessorState == BaseOnlyValueAccessorState.Value) {
+ add(if (added.hasTypeInfoSuffix) TYPE_INFO_GROUP_ACCESSOR_IDX else VALUE_ACCESSOR_IDX)
+ }
+ if (added.suffixIdx >= 0 && added.suffixIdx != FINAL_ACCESSOR_IDX) add(added.suffixIdx)
+ }
+ var blocker: Blocker? = null
+ core.forEach { accessor ->
+ if (!stopped) {
+ val blockedAt = abstractAccess(prefix, slotOfIdx(accessor))
+ emit(
+ base, prefix, slotOfIdx(accessor), isAbstract = true, exact = false,
+ valueAccessorState = BaseOnlyValueAccessorState.Normal, state, out,
+ )
+ if (state.excludes(blockedAt, accessor)) {
+ prefix.add(accessor)
+ } else {
+ stopped = true
+ blocker = Blocker(accessor, blockedAt)
+ }
+ }
+ }
+ if (!stopped) {
+ if (added.hasAp) {
+ emit(
+ base, prefix, apSlot = added.apSlot, isAbstract = true, exact = false,
+ valueAccessorState = BaseOnlyValueAccessorState.Normal, state, out,
+ )
+ } else {
+ emit(
+ base, prefix, apSlot = 2, isAbstract = false, exact = true,
+ valueAccessorState = added.valueAccessorState, state, out,
+ )
+ }
+ }
+ return blocker
+ }
+
+ private fun abstractAccess(prefix: List, apSlot: Int): BaseOnlyAccess {
+ var committedStatic = NO_ACCESSOR
+ var committedField = NO_ACCESSOR
+ for (idx in prefix) {
+ when {
+ idx.isStaticAccessor() -> committedStatic = idx
+ idx.isFieldAccessor() || idx == ELEMENT_ACCESSOR_IDX -> committedField = idx
+ }
+ }
+ return BaseOnlyAccessOps.abstractAt(committedStatic, committedField, apSlot)
+ }
+
+ private fun emit(
+ base: AccessPathBase,
+ prefix: List,
+ apSlot: Int,
+ isAbstract: Boolean,
+ exact: Boolean,
+ valueAccessorState: BaseOnlyValueAccessorState,
+ state: BaseState,
+ out: MutableList>,
+ ) {
+ if (exact) {
+ val abstractAccess = abstractAccess(prefix, apSlot)
+ if (state.emitted.add(abstractAccess)) {
+ out.add(
+ BaseOnlyInitialFactAp(manager, base, abstractAccess, ExclusionSet.Empty)
+ to BaseOnlyFinalFactAp(manager, base, abstractAccess, ExclusionSet.Empty)
+ )
+ }
+ var concreteAccess = BaseOnlyAccessOps.build(
+ (prefix + FINAL_ACCESSOR_IDX).toIntArray(),
+ isAbstract = false,
+ )
+ if (concreteAccess.hasSemanticMark) concreteAccess = concreteAccess.withValueAccessorState(valueAccessorState)
+ if (state.emitted.add(concreteAccess)) {
+ out.add(
+ BaseOnlyInitialFactAp(manager, base, concreteAccess, ExclusionSet.Empty)
+ to BaseOnlyFinalFactAp(manager, base, concreteAccess, ExclusionSet.Empty)
+ )
+ }
+ return
+ }
+
+ val initialAccess: BaseOnlyAccess
+ val finalAccess: BaseOnlyAccess
+ val apAccess = abstractAccess(prefix, apSlot)
+ if (!state.emitted.add(apAccess)) return
+ initialAccess = apAccess
+ finalAccess = apAccess
+
+ val initial = BaseOnlyInitialFactAp(manager, base, initialAccess, ExclusionSet.Empty)
+ val final = BaseOnlyFinalFactAp(manager, base, finalAccess, ExclusionSet.Empty)
+ out.add(initial to final)
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt
new file mode 100644
index 000000000..b4b27fd9d
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlyInitialFactAp.kt
@@ -0,0 +1,97 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.Accessor
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.FactTypeChecker
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+
+class BaseOnlyInitialFactAp(
+ val manager: BaseOnlyApManager,
+ override val base: AccessPathBase,
+ val access: BaseOnlyAccess,
+ exclusions: ExclusionSet,
+) : InitialFactAp {
+ override val exclusions: ExclusionSet = manager.compactExclusions(exclusions)
+
+ init {
+ BaseOnlyAccessOps.requireCanonical(access)
+ }
+
+ override val size: Int get() = access.size
+ override val depth: Int get() = access.size
+
+ override fun isAbstract(): Boolean = access.isRootAbstract
+
+ override fun rebase(newBase: AccessPathBase): InitialFactAp =
+ BaseOnlyInitialFactAp(manager, newBase, access, exclusions)
+
+ override fun exclude(accessor: Accessor): InitialFactAp =
+ BaseOnlyInitialFactAp(manager, base, access, exclusions.add(accessor))
+
+ override fun replaceExclusions(exclusions: ExclusionSet): InitialFactAp =
+ BaseOnlyInitialFactAp(manager, base, access, exclusions)
+
+ private fun rewrap(newAccess: BaseOnlyAccess): BaseOnlyInitialFactAp =
+ BaseOnlyInitialFactAp(manager, base, newAccess, exclusions)
+
+ override fun startsWithAccessor(accessor: Accessor): Boolean = manager.startsWithAccessor(access, accessor)
+
+ override fun getStartAccessors(): Set = manager.startAccessors(access)
+
+ override fun getAllAccessors(): Set = manager.allAccessors(access)
+
+ override fun readAccessor(accessor: Accessor): InitialFactAp? = manager.readAccess(access, accessor)?.let(::rewrap)
+
+ override fun prependAccessor(accessor: Accessor): InitialFactAp =
+ rewrap(BaseOnlyAccessOps.prepend(access, manager.interner.index(accessor), manager.fieldSensitive))
+
+ override fun clearAccessor(accessor: Accessor): InitialFactAp? =
+ BaseOnlyAccessOps.clear(access, manager.interner.index(accessor))?.let(::rewrap)
+
+ override fun compatibilityFilter(typeChecker: FactTypeChecker): FactTypeChecker.FactCompatibilityFilter =
+ typeChecker.accessPathCompatibilityFilter(
+ buildList { access.forEachAccessorIdx { add(manager.interner.accessor(it) ?: error("Accessor not found: $it")) } }
+ )
+
+ override fun splitDelta(other: FinalFactAp): List> {
+ other as BaseOnlyFinalFactAp
+ if (base != other.base) return emptyList()
+
+ return BaseOnlyAccessOps.splitDelta(access, other.access, manager, other.exclusions)
+ .map { (f, delta) -> rewrap(f) to delta }
+ }
+
+ override fun concat(delta: InitialFactAp.Delta): InitialFactAp =
+ when (val d = delta as BaseOnlyInitialDelta) {
+ BaseOnlyEmptyInitialDelta -> this
+ is BaseOnlyNodeInitialDelta -> {
+ rewrap(
+ BaseOnlyAccessOps.append(access, d.access)
+ ?: error("static-first invariant violated: initial concat")
+ )
+ }
+ }
+
+ override fun contains(factAp: InitialFactAp): Boolean {
+ factAp as BaseOnlyInitialFactAp
+ if (base != factAp.base) return false
+ return BaseOnlyAccessOps.matchPrefix(access, factAp.access).emptyDelta
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BaseOnlyInitialFactAp) return false
+ return base == other.base && access == other.access && exclusions == other.exclusions
+ }
+
+ override fun hashCode(): Int {
+ var result = base.hashCode()
+ result = 31 * result + access.hashCode()
+ result = 31 * result + exclusions.hashCode()
+ return result
+ }
+
+ override fun toString(): String = "$base${manager.renderAccess(access)}/$exclusions"
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt
new file mode 100644
index 000000000..0c632204d
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySerializer.kt
@@ -0,0 +1,90 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.util.AccessorIdx
+import org.opentaint.dataflow.ap.ifds.serialization.AccessPathBaseSerializer
+import org.opentaint.dataflow.ap.ifds.serialization.ApSerializer
+import org.opentaint.dataflow.ap.ifds.serialization.ExclusionSetSerializer
+import org.opentaint.dataflow.ap.ifds.serialization.SummarySerializationContext
+import java.io.DataInputStream
+import java.io.DataOutputStream
+
+internal class BaseOnlySerializer(
+ private val manager: BaseOnlyApManager,
+ private val context: SummarySerializationContext,
+) : ApSerializer {
+ private val exclusionSetSerializer = ExclusionSetSerializer(context)
+
+ override fun DataOutputStream.writeFinalAp(ap: FinalFactAp) {
+ ap as BaseOnlyFinalFactAp
+ writeFact(ap.base, ap.exclusions, ap.access)
+ }
+
+ override fun DataOutputStream.writeInitialAp(ap: InitialFactAp) {
+ ap as BaseOnlyInitialFactAp
+ writeFact(ap.base, ap.exclusions, ap.access)
+ }
+
+ override fun DataInputStream.readFinalAp(): FinalFactAp {
+ val fact = readFact()
+ return BaseOnlyFinalFactAp(manager, fact.base, fact.access, fact.exclusions)
+ }
+
+ override fun DataInputStream.readInitialAp(): InitialFactAp {
+ val fact = readFact()
+ return BaseOnlyInitialFactAp(manager, fact.base, fact.access, fact.exclusions)
+ }
+
+ private fun DataOutputStream.writeFact(base: AccessPathBase, exclusions: ExclusionSet, access: BaseOnlyAccess) {
+ BaseOnlyAccessOps.requireCanonical(access)
+ with(AccessPathBaseSerializer) { writeAccessPathBase(base) }
+ with(exclusionSetSerializer) { writeExclusionSet(exclusions) }
+ writeSlot(access.staticIdx)
+ writeSlot(access.fieldIdx)
+ writeSlot(access.suffixIdx)
+ writeByte(access.valueAccessorState.encoded)
+ }
+
+ private fun DataInputStream.readFact(): DeserializedFact {
+ val base = with(AccessPathBaseSerializer) { readAccessPathBase() }
+ val exclusions = with(exclusionSetSerializer) { readExclusionSet() }
+ val staticIdx = readSlot()
+ val fieldIdx = readSlot()
+ val suffixIdx = readSlot()
+ val valueAccessorState = BaseOnlyValueAccessorState.decode(readUnsignedByte())
+ val access = BaseOnlyAccessOps.requireCanonical(
+ packBaseOnlyAccess(staticIdx, fieldIdx, suffixIdx, valueAccessorState)
+ )
+ return DeserializedFact(base, exclusions, access)
+ }
+
+ private fun DataOutputStream.writeSlot(idx: AccessorIdx) {
+ when (idx) {
+ NO_ACCESSOR, ABSTRACT_MARK -> writeByte(idx)
+ else -> {
+ writeByte(ACCESSOR_SLOT)
+ val accessor = manager.interner.accessor(idx) ?: error("Accessor not found: $idx")
+ writeLong(context.getIdByAccessor(accessor))
+ }
+ }
+ }
+
+ private fun DataInputStream.readSlot(): AccessorIdx = when (val tag = readByte().toInt()) {
+ NO_ACCESSOR, ABSTRACT_MARK -> tag
+ ACCESSOR_SLOT -> manager.interner.index(context.getAccessorById(readLong()))
+ else -> error("Unexpected BaseOnly access slot tag: $tag")
+ }
+
+ private class DeserializedFact(
+ val base: AccessPathBase,
+ val exclusions: ExclusionSet,
+ val access: BaseOnlyAccess,
+ )
+
+ private companion object {
+ const val ACCESSOR_SLOT = 0
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt
new file mode 100644
index 000000000..21e8e9dc1
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementApStorage.kt
@@ -0,0 +1,124 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.SideEffectRequirementApStorage
+import java.util.concurrent.ConcurrentHashMap
+
+class BaseOnlySideEffectRequirementApStorage : SideEffectRequirementApStorage {
+ private val based = ConcurrentHashMap()
+
+ override fun add(requirements: List): List {
+ val modified = mutableListOf()
+
+ for (requirement in requirements) {
+ requirement as BaseOnlyInitialFactAp
+ if (requirement.access.isCollapsed) continue
+ val storage = based.computeIfAbsent(requirement.base) { RequirementStorage() }
+ if (storage.mergeAdd(requirement) != null) modified += storage
+ }
+
+ val result = mutableListOf()
+ modified.forEach { it.getAndResetDelta(result) }
+ return result
+ }
+
+
+ override fun filterTo(dst: MutableList, fact: FinalFactAp) {
+ fact as BaseOnlyFinalFactAp
+ val storage = based[fact.base] ?: return
+ storage.filterTo(dst, fact.access)
+ }
+
+ override fun collectAllRequirementsTo(dst: MutableList) {
+ based.values.forEach { storage ->
+ storage.collectAllTo(dst)
+ }
+ }
+
+ private class RequirementStorage {
+ private class RequirementNode(initial: BaseOnlyInitialFactAp) {
+ @Volatile
+ var requirement: BaseOnlyInitialFactAp = initial
+ }
+
+ private val requirements = BaseOnlyInitialAccessIndex()
+ private val delta = Long2ObjectOpenHashMap()
+
+ fun mergeAdd(requirement: BaseOnlyInitialFactAp): BaseOnlyInitialFactAp? {
+ var added = false
+ val node = requirements.getOrCreate(requirement.access) {
+ added = true
+ RequirementNode(requirement)
+ }
+ if (added) {
+ delta.put(requirement.access, requirement)
+ return requirement
+ }
+
+ val previous = node.requirement
+ val update = previous.mergeWithAdded(requirement) ?: return null
+ val merged = update.merged
+ node.requirement = merged
+
+ val addedRequirement = requirement.replaceExclusions(update.added) as BaseOnlyInitialFactAp
+ val previousDelta = delta[requirement.access]
+ val mergedDelta = checkNotNull(previousDelta.mergeAdd(addedRequirement))
+ delta.put(requirement.access, mergedDelta)
+ return addedRequirement
+ }
+
+ fun getAndResetDelta(dst: MutableList) {
+ dst.addAll(delta.values)
+ delta.clear()
+ }
+
+ fun filterTo(dst: MutableList, fact: BaseOnlyAccess) {
+ requirements.collectCandidates(fact) { _, node ->
+ val requirement = node.requirement
+ if (baseOnlySummaryInitialMatches(fact, requirement.access)) {
+ dst.add(requirement)
+ }
+ }
+ }
+
+ fun collectAllTo(dst: MutableList) {
+ requirements.collectAll { _, node -> dst.add(node.requirement) }
+ }
+ }
+}
+
+private data class ExclusionMerge(
+ val merged: BaseOnlyInitialFactAp,
+ val added: ExclusionSet,
+)
+
+private fun BaseOnlyInitialFactAp.mergeWithAdded(requirement: BaseOnlyInitialFactAp): ExclusionMerge? {
+ val previousExclusions = exclusions
+ val incomingExclusions = requirement.exclusions
+ if (incomingExclusions is ExclusionSet.Empty) return null
+ if (previousExclusions is ExclusionSet.Empty) {
+ return ExclusionMerge(requirement, incomingExclusions)
+ }
+ check(previousExclusions is ExclusionSet.Concrete && incomingExclusions is ExclusionSet.Concrete)
+
+ val previousSet = previousExclusions.set as BaseOnlyExclusionAccessorSet
+ val incomingSet = incomingExclusions.set as BaseOnlyExclusionAccessorSet
+ val update = previousSet.unionWithAdded(incomingSet) ?: return null
+ val mergedExclusions = ExclusionSet.Concrete(update.union)
+ val addedExclusions = ExclusionSet.Concrete(update.added)
+ return ExclusionMerge(
+ BaseOnlyInitialFactAp(requirement.manager, requirement.base, requirement.access, mergedExclusions),
+ addedExclusions,
+ )
+}
+
+private fun BaseOnlyInitialFactAp?.mergeAdd(requirement: BaseOnlyInitialFactAp): BaseOnlyInitialFactAp? {
+ if (this == null) return requirement
+ val mergedExclusion = exclusions.union(requirement.exclusions)
+ if (mergedExclusion === exclusions) return null
+ return BaseOnlyInitialFactAp(requirement.manager, requirement.base, requirement.access, mergedExclusion)
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt
new file mode 100644
index 000000000..fb3eb45a1
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySideEffectRequirementDeltaTracker.kt
@@ -0,0 +1,63 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+
+/**
+ * Tracks the exclusion information already applied for one side-effect requirement.
+ *
+ * Exclusions grow monotonically. The access paths identify the operation while the returned
+ * requirement contains only the exclusion delta that has not been applied for that operation.
+ */
+internal class BaseOnlySideEffectRequirementDeltaTracker {
+ private data class Key(
+ val currentBase: AccessPathBase,
+ val currentAccess: BaseOnlyAccess,
+ val requirementBase: AccessPathBase,
+ val requirementAccess: BaseOnlyAccess,
+ )
+
+ private val appliedExclusions = hashMapOf()
+
+ fun add(
+ currentInitial: InitialFactAp,
+ requirement: InitialFactAp,
+ ): InitialFactAp? {
+ currentInitial as BaseOnlyInitialFactAp
+ requirement as BaseOnlyInitialFactAp
+
+ val key = Key(currentInitial.base, currentInitial.access, requirement.base, requirement.access)
+ val previous = appliedExclusions[key]
+ if (previous == null) {
+ appliedExclusions[key] = requirement.exclusions
+ return requirement
+ }
+
+ val incoming = requirement.exclusions
+ val update = when {
+ incoming is ExclusionSet.Empty || previous is ExclusionSet.Universe -> null
+ incoming is ExclusionSet.Universe -> ExclusionUpdate(incoming, incoming)
+ previous is ExclusionSet.Empty -> ExclusionUpdate(incoming, incoming)
+ else -> {
+ check(previous is ExclusionSet.Concrete && incoming is ExclusionSet.Concrete)
+ val previousSet = previous.set as BaseOnlyExclusionAccessorSet
+ val incomingSet = incoming.set as BaseOnlyExclusionAccessorSet
+ previousSet.unionWithAdded(incomingSet)?.let {
+ ExclusionUpdate(
+ merged = ExclusionSet.Concrete(it.union),
+ added = ExclusionSet.Concrete(it.added),
+ )
+ }
+ }
+ } ?: return null
+
+ appliedExclusions[key] = update.merged
+ return requirement.replaceExclusions(update.added)
+ }
+
+ private data class ExclusionUpdate(
+ val merged: ExclusionSet,
+ val added: ExclusionSet,
+ )
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt
new file mode 100644
index 000000000..6c9a9d3f0
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/BaseOnlySummaryEdgeOps.kt
@@ -0,0 +1,140 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+
+internal data class BaseOnlySummaryEdge(
+ val initial: BaseOnlyAccess,
+ val final: BaseOnlyAccess,
+ val exclusion: ExclusionSet,
+)
+
+/**
+ * Semantic operations on a single BaseOnly fact-to-fact summary edge.
+ *
+ * An edge is a correlated transformation: the residual consumed after [BaseOnlySummaryEdge.initial]
+ * must be grafted after [BaseOnlySummaryEdge.final]. When premises differ, independently comparing
+ * the two access paths is therefore not a valid subsumption test. When premises are identical,
+ * correlation is already fixed and directional conclusion coverage is sufficient.
+ */
+internal object BaseOnlySummaryEdgeOps {
+ fun canonicallyCovers(
+ manager: BaseOnlyApManager,
+ cover: BaseOnlySummaryEdge,
+ covered: BaseOnlySummaryEdge,
+ ): Boolean {
+ if (!subsumes(manager, cover, covered)) return false
+ if (!subsumes(manager, covered, cover)) return true
+ return BASE_ONLY_SUMMARY_EDGE_ORDER.compare(cover, covered) < 0
+ }
+
+ fun subsumes(
+ manager: BaseOnlyApManager,
+ general: BaseOnlySummaryEdge,
+ specific: BaseOnlySummaryEdge,
+ ): Boolean {
+ if (general.initial == specific.initial) {
+ val effectiveExclusion = specific.exclusion.union(general.exclusion)
+ return effectiveExclusion == specific.exclusion &&
+ BaseOnlyAccessOps.covers(general.final, specific.final)
+ }
+
+ val specificInitial = SummaryFact(specific.initial, specific.exclusion)
+ val specificFinal = SummaryFact(specific.final, specific.exclusion)
+ val application = applyEdge(manager, general, specificInitial) ?: return false
+ if (application.result != specificFinal) return false
+
+ return reconstructInitials(
+ manager = manager,
+ edge = general,
+ final = specificFinal,
+ residual = application.residual,
+ ).any { it == specificInitial.access }
+ }
+
+ private fun applyEdge(
+ manager: BaseOnlyApManager,
+ edge: BaseOnlySummaryEdge,
+ initial: SummaryFact,
+ ): SummaryApplication? {
+ val match = BaseOnlyAccessOps.matchPrefix(initial.access, edge.initial)
+ if (match.emptyDelta) {
+ return SummaryApplication(
+ residual = SummaryResidual.Empty,
+ result = SummaryFact(edge.final, initial.exclusion.union(edge.exclusion)),
+ )
+ }
+ if (!match.hasSuffix) return null
+
+ val residualAccess = retainResidual(manager, match.suffix, edge.exclusion) ?: return null
+ val resultAccess = BaseOnlyAccessOps.appendFinal(edge.final, residualAccess) ?: return null
+ return SummaryApplication(
+ residual = SummaryResidual.Access(residualAccess),
+ result = SummaryFact(resultAccess, initial.exclusion),
+ )
+ }
+
+ private fun reconstructInitials(
+ manager: BaseOnlyApManager,
+ edge: BaseOnlySummaryEdge,
+ final: SummaryFact,
+ residual: SummaryResidual,
+ ): Sequence {
+ return BaseOnlyAccessOps.splitDelta(
+ fact = final.access,
+ pattern = edge.final,
+ manager = manager,
+ exclusions = edge.exclusion,
+ ).asSequence().mapNotNull { (_, delta) ->
+ val reconstructedResidual = delta.toSummaryResidual(manager, edge.exclusion) ?: return@mapNotNull null
+ if (reconstructedResidual != residual) return@mapNotNull null
+
+ when (reconstructedResidual) {
+ SummaryResidual.Empty -> edge.initial
+ is SummaryResidual.Access -> BaseOnlyAccessOps.append(edge.initial, reconstructedResidual.access)
+ }
+ }
+ }
+
+ private fun BaseOnlyInitialDelta.toSummaryResidual(
+ manager: BaseOnlyApManager,
+ exclusions: ExclusionSet,
+ ): SummaryResidual? = when (this) {
+ BaseOnlyEmptyInitialDelta -> SummaryResidual.Empty
+ is BaseOnlyNodeInitialDelta ->
+ retainResidual(manager, access, exclusions)?.let(SummaryResidual::Access)
+ }
+
+ /**
+ * Storage subsumption needs exact evidence that the residual branch survives. The ordinary
+ * BaseOnly exclusion operation may retain an excluded root terminal as a sound cover of its
+ * implicit-Any continuations; that widening must not be used to delete the explicit terminal
+ * edge itself.
+ */
+ private fun retainResidual(
+ manager: BaseOnlyApManager,
+ residual: BaseOnlyAccess,
+ exclusions: ExclusionSet,
+ ): BaseOnlyAccess? = when (exclusions) {
+ ExclusionSet.Empty -> residual
+ ExclusionSet.Universe -> null
+ is ExclusionSet.Concrete -> {
+ val accessor = residual.headOrNull?.let(manager.interner::accessor)
+ residual.takeUnless { accessor != null && exclusions.contains(accessor) }
+ }
+ }
+
+ private data class SummaryApplication(
+ val residual: SummaryResidual,
+ val result: SummaryFact,
+ )
+
+ private data class SummaryFact(
+ val access: BaseOnlyAccess,
+ val exclusion: ExclusionSet,
+ )
+
+ private sealed interface SummaryResidual {
+ data object Empty : SummaryResidual
+ data class Access(val access: BaseOnlyAccess) : SummaryResidual
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt
new file mode 100644
index 000000000..da1134454
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/FactSESummariesBaseOnlyStorage.kt
@@ -0,0 +1,53 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.SideEffectKind
+import org.opentaint.dataflow.ap.ifds.access.common.CommonFactSideEffectSummary
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class FactSESummariesBaseOnlyStorage(
+ methodInitialInst: CommonInst,
+ override val apManager: BaseOnlyApManager,
+) : CommonFactSideEffectSummary(methodInitialInst),
+ BaseOnlyInitialApAccess, BaseOnlyFinalApAccess {
+ override fun createStorage(): Storage = SEStorage(apManager)
+
+ private class SEStorage(private val manager: BaseOnlyApManager) : Storage {
+ private val perInitial = BaseOnlyInitialAccessIndex()
+
+ override fun add(
+ iap: BaseOnlyAccess,
+ se: Map,
+ added: MutableList>,
+ ) {
+ val storageNode = perInitial.getOrCreate(iap) { MergeStorage(manager, iap) }
+ for ((kind, exclusion) in se) {
+ storageNode.add(kind, exclusion)?.let { added += it }
+ }
+ }
+
+ override fun collectSummariesTo(
+ dst: MutableList>,
+ initialFactPattern: BaseOnlyAccess?,
+ ) {
+ val collect: (BaseOnlyAccess, MergeStorage) -> Unit = { _, storage -> dst += storage.summaries() }
+ if (initialFactPattern == null) {
+ perInitial.collectAll(collect)
+ } else {
+ perInitial.collectCandidates(initialFactPattern) { initial, storage ->
+ if (baseOnlySummaryInitialMatches(initialFactPattern, initial)) collect(initial, storage)
+ }
+ }
+ }
+ }
+
+ private class MergeStorage(private val manager: BaseOnlyApManager, private val initialAccess: BaseOnlyAccess) :
+ SideEffectExclusionMergingStorage() {
+ override fun createBuilder(): FactSEBuilder = Builder(manager).setInitialAp(initialAccess)
+ }
+
+ private class Builder(override val apManager: BaseOnlyApManager) :
+ FactSEBuilder(), BaseOnlyInitialApAccess {
+ override fun nonNullIAP(iap: BaseOnlyAccess?): BaseOnlyAccess = iap ?: ABSTRACT_EMPTY_ACCESS
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt
new file mode 100644
index 000000000..47c2e095e
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodBaseOnlyAccessPathSubscription.kt
@@ -0,0 +1,156 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.common.CommonAPSub
+import org.opentaint.dataflow.ap.ifds.access.common.CommonFactEdgeSubBuilder
+import org.opentaint.dataflow.ap.ifds.access.common.CommonFactNDEdgeSubBuilder
+import org.opentaint.dataflow.ap.ifds.access.common.CommonZeroEdgeSubBuilder
+import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSubStorageWithAp
+import org.opentaint.ir.api.common.cfg.CommonInst
+import java.util.BitSet
+
+class MethodBaseOnlyAccessPathSubscription(
+ override val apManager: BaseOnlyApManager,
+) : CommonAPSub(), BaseOnlyInitialApAccess, BaseOnlyFinalApAccess {
+
+ override fun createZ2FSubStorage(callerEp: CommonInst): Z2FSubStorage =
+ Z2FSub(apManager)
+
+ override fun createF2FSubStorage(callerEp: CommonInst): F2FSubStorage =
+ F2FSub(apManager)
+
+ override fun createNDF2FSubStorage(callerEp: CommonInst): NDF2FSubStorage =
+ NDSub(callerEp, apManager)
+
+ private class Z2FSub(private val manager: BaseOnlyApManager) :
+ CommonAPSub.Z2FSubStorage {
+ private val edges = LongOpenHashSet()
+ private val edgeIndex = BaseOnlyInitialAccessIndex()
+
+ override fun add(callerExitAp: BaseOnlyAccess): CommonZeroEdgeSubBuilder? {
+ if (!edges.add(callerExitAp)) return null
+ edgeIndex.getOrCreate(callerExitAp) { Unit }
+ return ZeroBuilder(manager).setNode(callerExitAp)
+ }
+
+ override fun find(dst: MutableList>, summaryInitialFact: BaseOnlyAccess) {
+ edgeIndex.collectCandidates(summaryInitialFact) { exit, _ ->
+ val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact)
+ if (match.emptyDelta || match.hasSuffix) {
+ dst += ZeroBuilder(manager).setNode(exit)
+ }
+ }
+ }
+ }
+
+ private class F2FSub(private val manager: BaseOnlyApManager) :
+ CommonAPSub.F2FSubStorage {
+ private val initialFactsByExit =
+ BaseOnlyInitialAccessIndex>()
+
+ override fun add(
+ callerInitialAp: InitialFactAp,
+ callerExitAp: BaseOnlyAccess,
+ ): CommonFactEdgeSubBuilder? {
+ callerInitialAp as BaseOnlyInitialFactAp
+ val initialFacts = initialFactsByExit.getOrCreate(callerExitAp, ::hashSetOf)
+ if (!initialFacts.add(callerInitialAp)) return null
+ return FactBuilder(manager)
+ .setCallerNode(callerExitAp)
+ .setCallerInitialAp(callerInitialAp)
+ .setCallerExclusion(callerInitialAp.exclusions)
+ }
+
+ override fun find(
+ dst: MutableList>,
+ summaryInitialFact: BaseOnlyAccess,
+ emptyDeltaRequired: Boolean,
+ ) {
+ initialFactsByExit.collectCandidates(summaryInitialFact) { exit, initialFacts ->
+ val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact)
+ if (!match.emptyDelta && !match.hasSuffix) return@collectCandidates
+ collectExit(dst, exit, initialFacts)
+ }
+ }
+
+ private fun collectExit(
+ dst: MutableList>,
+ exit: BaseOnlyAccess,
+ initialFacts: Set,
+ ) {
+ initialFacts.forEach { initial ->
+ dst += FactBuilder(manager)
+ .setCallerNode(exit)
+ .setCallerInitialAp(initial)
+ .setCallerExclusion(initial.exclusions)
+ }
+ }
+ }
+
+ private class NDSub(callerEp: CommonInst, private val manager: BaseOnlyApManager) :
+ DefaultNDF2FSubStorageWithAp(callerEp), BaseOnlyInitialApAccess {
+ override val apManager: BaseOnlyApManager get() = manager
+
+ private val storageIndicesByExit =
+ BaseOnlyInitialAccessIndex>()
+
+ override fun createBuilder(): CommonFactNDEdgeSubBuilder = NDBuilder(manager)
+
+ override fun add(
+ callerInitial: Set,
+ callerExitAp: BaseOnlyAccess,
+ ): CommonFactNDEdgeSubBuilder? = super.add(
+ callerInitial.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) },
+ callerExitAp,
+ )
+
+ override fun createStorage(idx: Int): Storage {
+ return FactStorage(idx)
+ }
+
+ override fun relevantStorageIndices(summaryInitialFact: BaseOnlyAccess): BitSet {
+ val result = BitSet()
+ storageIndicesByExit.collectCandidates(summaryInitialFact) { exit, storageIndices ->
+ val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact)
+ if (match.emptyDelta || match.hasSuffix) {
+ storageIndices.forEach(result::set)
+ }
+ }
+ return result
+ }
+
+ private inner class FactStorage(
+ private val storageIdx: Int,
+ ) : Storage {
+ private val edges = LongOpenHashSet()
+
+ override fun add(element: BaseOnlyAccess): BaseOnlyAccess? {
+ if (!edges.add(element)) return null
+ storageIndicesByExit.getOrCreate(element, ::hashSetOf).add(storageIdx)
+ return element
+ }
+
+ override fun collect(dst: MutableList) {
+ dst.addAll(edges)
+ }
+
+ override fun collect(dst: MutableList, summaryInitialFact: BaseOnlyAccess) {
+ edges.forEach { exit ->
+ val match = BaseOnlyAccessOps.matchPrefix(exit, summaryInitialFact)
+ if (match.emptyDelta || match.hasSuffix) dst.add(exit)
+ }
+ }
+ }
+ }
+
+ private class ZeroBuilder(override val apManager: BaseOnlyApManager) :
+ CommonZeroEdgeSubBuilder(), BaseOnlyFinalApAccess
+
+ private class FactBuilder(override val apManager: BaseOnlyApManager) :
+ CommonFactEdgeSubBuilder(), BaseOnlyFinalApAccess
+
+ private class NDBuilder(override val apManager: BaseOnlyApManager) :
+ CommonFactNDEdgeSubBuilder(), BaseOnlyFinalApAccess
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt
new file mode 100644
index 000000000..13c40c6cf
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesFinalBaseOnlyApSet.kt
@@ -0,0 +1,37 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet
+import org.opentaint.dataflow.ap.ifds.LanguageManager
+import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageIdx
+import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageSize
+import org.opentaint.dataflow.ap.ifds.access.common.CommonZ2FSet
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class MethodEdgesFinalBaseOnlyApSet(
+ methodInitialStatement: CommonInst,
+ private val maxInstIdx: Int,
+ private val languageManager: LanguageManager,
+ override val apManager: BaseOnlyApManager,
+) : CommonZ2FSet(methodInitialStatement), BaseOnlyFinalApAccess {
+ override fun createApStorage(): ApStorage =
+ ZeroInitialFactEdges(maxInstIdx, languageManager)
+
+ private class ZeroInitialFactEdges(
+ maxInstIdx: Int,
+ private val languageManager: LanguageManager,
+ ) : ApStorage {
+ private val edges = arrayOfNulls(instructionStorageSize(maxInstIdx))
+
+ override fun addEdge(statement: CommonInst, accessPath: BaseOnlyAccess): BaseOnlyAccess? {
+ if (accessPath.isCollapsed) return null
+ val idx = instructionStorageIdx(statement, languageManager)
+ val set = edges[idx] ?: LongOpenHashSet().also { edges[idx] = it }
+ if (!set.add(accessPath)) return null
+ return accessPath
+ }
+
+ override fun collectApAtStatement(statement: CommonInst, dst: MutableList) {
+ edges[instructionStorageIdx(statement, languageManager)]?.let { dst.addAll(it) }
+ }
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt
new file mode 100644
index 000000000..fa9fd83bd
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesInitialToFinalBaseOnlyApSet.kt
@@ -0,0 +1,338 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.LongArrayList
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.LanguageManager
+import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageIdx
+import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges.Companion.instructionStorageSize
+import org.opentaint.dataflow.ap.ifds.access.common.CommonF2FSet
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class MethodEdgesInitialToFinalBaseOnlyApSet(
+ methodInitialStatement: CommonInst,
+ private val maxInstIdx: Int,
+ private val languageManager: LanguageManager,
+ override val apManager: BaseOnlyApManager,
+) : CommonF2FSet(methodInitialStatement),
+ BaseOnlyInitialApAccess, BaseOnlyFinalApAccess {
+
+ override fun mostAbstractPattern(base: AccessPathBase): BaseOnlyAccess = ABSTRACT_EMPTY_ACCESS
+
+ override fun createApStorage(): ApStorage = Storage()
+
+ private inner class Storage : ApStorage {
+ private val statements = arrayOfNulls(instructionStorageSize(maxInstIdx))
+
+ override fun add(
+ statement: CommonInst,
+ initial: BaseOnlyAccess,
+ final: AccessWithExclusion,
+ ): List> {
+ if (initial.isCollapsed || final.access.isCollapsed) return emptyList()
+ return statementState(statement, create = true)!!.add(initial, final)
+ }
+
+ override fun filter(
+ dst: MutableList>>,
+ statement: CommonInst,
+ finalPattern: BaseOnlyAccess,
+ ) {
+ statementState(statement, create = false)?.collect(finalPattern) { initial, final ->
+ dst.add(initial to final)
+ }
+
+ traceGeneralizationAt(statement)?.let { edge ->
+ val generalized = edge.initial to AccessWithExclusion(edge.final, edge.exclusion)
+ dst += generalized
+ }
+ }
+
+ override fun filter(
+ dst: MutableList>,
+ statement: CommonInst,
+ initial: BaseOnlyAccess,
+ finalPattern: BaseOnlyAccess,
+ ) {
+ statementState(statement, create = false)?.collect(initial, finalPattern) { dst.add(it) }
+
+ if (!apManager.traceResolutionModeEnabled()) return
+
+ // Trace-time summary normalization exposes a field-abstract initial as a
+ // suffix-abstract alias. Resolve that view back to the primary intraprocedural
+ // key; the alias itself is never stored.
+ if (initial.apSlot == 2 && finalPattern.apSlot == 2) {
+ val primary = packBaseOnlyAccess(initial.staticIdx, ABSTRACT_MARK, NO_ACCESSOR)
+ statementState(statement, create = false)?.collect(primary, finalPattern) { dst.addDistinct(it) }
+ }
+
+ traceGeneralizationAt(statement)
+ ?.takeIf { baseOnlySummaryInitialMatches(initial, it.initial) }
+ ?.let { dst.addDistinct(AccessWithExclusion(it.final, it.exclusion)) }
+ }
+
+ private fun traceGeneralizationAt(statement: CommonInst): BaseOnlySummaryEdge? {
+ if (!apManager.traceResolutionModeEnabled() || !apManager.fieldGeneralizationEnabled) return null
+
+ val exact = arrayListOf()
+ statementState(statement, create = false)?.collect(finalPattern = null) { initial, final ->
+ exact += BaseOnlySummaryEdge(initial, final.access, final.exclusion)
+ }
+ if (exact.isEmpty()) return null
+
+ val generalizer = BaseOnlyF2FFieldGeneralizer(maxEnumeratedEdges = 0)
+ val result = generalizer.rewrite(exact)
+ val group = result.newlyGeneralized.singleOrNull() ?: return null
+ return generalizer.representative(group)
+ }
+
+ private fun statementState(statement: CommonInst, create: Boolean): StatementState? {
+ val idx = instructionStorageIdx(statement, languageManager)
+ val current = statements[idx]
+ if (current != null || !create) return current
+ return StatementState().also { statements[idx] = it }
+ }
+ }
+
+ private class StatementState {
+ private val initials = Long2ObjectOpenHashMap()
+ private val conclusions = BaseOnlyInitialAccessIndex()
+
+ fun add(
+ initial: BaseOnlyAccess,
+ final: AccessWithExclusion,
+ ): List> {
+ val state = initials[initial]
+ if (state == null) {
+ initials.put(initial, InitialState(final))
+ conclusion(final.access).add(initial)
+ return listOf(final)
+ }
+
+ val update = state.add(final)
+ if (!update.changed) return emptyList()
+ update.removedFinals.forEach { removed ->
+ conclusions.get(removed)?.remove(initial)
+ }
+ if (update.finalAdded) conclusion(final.access).add(initial)
+ return update.delta
+ }
+
+ fun collect(
+ finalPattern: BaseOnlyAccess?,
+ out: (BaseOnlyAccess, AccessWithExclusion) -> Unit,
+ ) {
+ val collectSupport: (BaseOnlyAccess, InitialSupport) -> Unit = collectSupport@{ final, support ->
+ if (support.isEmpty ||
+ finalPattern != null && !baseOnlySummaryInitialMatches(finalPattern, final)
+ ) return@collectSupport
+ support.forEach { initial ->
+ val state = initials[initial] ?: error("Missing initial support")
+ out(initial, AccessWithExclusion(final, state.exclusion))
+ }
+ }
+ if (finalPattern == null) {
+ conclusions.collectAll(collectSupport)
+ } else {
+ conclusions.collectCandidates(finalPattern, collectSupport)
+ }
+ }
+
+ fun collect(
+ initial: BaseOnlyAccess,
+ finalPattern: BaseOnlyAccess,
+ out: (AccessWithExclusion) -> Unit,
+ ) {
+ initials[initial]?.collect(finalPattern, out)
+ }
+
+ private fun conclusion(final: BaseOnlyAccess): InitialSupport =
+ conclusions.getOrCreate(final, ::InitialSupport)
+ }
+
+ private class InitialState(first: AccessWithExclusion) {
+ private var firstFinal = first.access
+ private var multipleFinals: LongOpenHashSet? = null
+ var exclusion: ExclusionSet = first.exclusion
+ private set
+
+ fun add(final: AccessWithExclusion): InitialUpdate {
+ val accessUpdate = addAccess(final.access)
+ val mergedExclusion = exclusion.union(final.exclusion)
+ val exclusionChanged = mergedExclusion != exclusion
+ if (!accessUpdate.changed && !exclusionChanged) return InitialUpdate.Unchanged
+
+ exclusion = mergedExclusion
+ val delta = if (exclusionChanged) {
+ buildList { collect(finalPattern = null) { add(it) } }
+ } else {
+ listOf(AccessWithExclusion(final.access, exclusion))
+ }
+ return InitialUpdate(
+ changed = true,
+ finalAdded = accessUpdate.changed,
+ removedFinals = accessUpdate.removed,
+ delta = delta,
+ )
+ }
+
+ fun collect(
+ finalPattern: BaseOnlyAccess?,
+ out: (AccessWithExclusion) -> Unit,
+ ) {
+ val finals = multipleFinals
+ if (finals == null) {
+ if (finalPattern == null || baseOnlySummaryInitialMatches(finalPattern, firstFinal)) {
+ out(AccessWithExclusion(firstFinal, exclusion))
+ }
+ return
+ }
+ finals.forEach { access ->
+ if (finalPattern == null || baseOnlySummaryInitialMatches(finalPattern, access)) {
+ out(AccessWithExclusion(access, exclusion))
+ }
+ }
+ }
+
+ private fun addAccess(access: BaseOnlyAccess): AccessUpdate {
+ val finals = multipleFinals
+ if (finals != null) {
+ if (finals.containsCoverOf(access)) return AccessUpdate.Unchanged
+
+ val removed = LongArrayList()
+ if (access.mayCoverDistinctAccess()) {
+ val covered = finals.iterator()
+ while (covered.hasNext()) {
+ val candidate = covered.nextLong()
+ if (BaseOnlyAccessOps.covers(access, candidate)) {
+ covered.remove()
+ removed.add(candidate)
+ }
+ }
+ }
+
+ val added = finals.add(access)
+ if (!added) return AccessUpdate.Unchanged
+ if (finals.size == 1) {
+ firstFinal = access
+ multipleFinals = null
+ }
+ return AccessUpdate(true, removed)
+ }
+ if (BaseOnlyAccessOps.covers(firstFinal, access)) return AccessUpdate.Unchanged
+ if (BaseOnlyAccessOps.covers(access, firstFinal)) {
+ val removed = LongArrayList(1).also { it.add(firstFinal) }
+ firstFinal = access
+ return AccessUpdate(true, removed)
+ }
+
+ multipleFinals = LongOpenHashSet(2).also {
+ it.add(firstFinal)
+ it.add(access)
+ }
+ return AccessUpdate(true, LongArrayList())
+ }
+ }
+
+ private class InitialSupport {
+ private var first: BaseOnlyAccess = NO_SUPPORT
+ private var multiple: LongOpenHashSet? = null
+
+ val isEmpty: Boolean get() = first == NO_SUPPORT
+
+ fun add(initial: BaseOnlyAccess) {
+ val supports = multiple
+ if (supports != null) {
+ supports.add(initial)
+ return
+ }
+ if (first == NO_SUPPORT) {
+ first = initial
+ } else if (first != initial) {
+ multiple = LongOpenHashSet(2).also {
+ it.add(first)
+ it.add(initial)
+ }
+ }
+ }
+
+ fun remove(initial: BaseOnlyAccess) {
+ val supports = multiple
+ if (supports == null) {
+ if (first == initial) first = NO_SUPPORT
+ return
+ }
+ if (!supports.remove(initial)) return
+ if (supports.size == 1) {
+ first = supports.iterator().nextLong()
+ multiple = null
+ }
+ }
+
+ fun forEach(action: (BaseOnlyAccess) -> Unit) {
+ multiple?.forEach(action) ?: first.takeUnless { it == NO_SUPPORT }?.let(action)
+ }
+ }
+
+ private data class InitialUpdate(
+ val changed: Boolean,
+ val finalAdded: Boolean,
+ val removedFinals: LongArrayList,
+ val delta: List>,
+ ) {
+ companion object {
+ val Unchanged = InitialUpdate(false, false, LongArrayList(), emptyList())
+ }
+ }
+
+ private data class AccessUpdate(
+ val changed: Boolean,
+ val removed: LongArrayList,
+ ) {
+ companion object {
+ val Unchanged = AccessUpdate(false, LongArrayList())
+ }
+ }
+
+ private fun MutableList>.addDistinct(
+ value: AccessWithExclusion,
+ ) {
+ if (value !in this) add(value)
+ }
+
+ private companion object {
+ const val NO_SUPPORT: BaseOnlyAccess = Long.MIN_VALUE
+ }
+}
+
+private fun LongOpenHashSet.containsCoverOf(access: BaseOnlyAccess): Boolean {
+ if (contains(access)) return true
+ if (contains(packBaseOnlyAccess(ABSTRACT_MARK, NO_ACCESSOR, NO_ACCESSOR))) return true
+ if (access.staticIdx == ABSTRACT_MARK) return false
+
+ if (contains(packBaseOnlyAccess(access.staticIdx, ABSTRACT_MARK, NO_ACCESSOR))) return true
+ if (access.fieldIdx == ABSTRACT_MARK) return false
+
+ if (contains(packBaseOnlyAccess(access.staticIdx, access.fieldIdx, ABSTRACT_MARK))) return true
+ if (access.fieldIdx < 0) return false
+
+ if (contains(packBaseOnlyAccess(access.staticIdx, NO_ACCESSOR, ABSTRACT_MARK))) return true
+ if (!access.hasSemanticMark) return false
+
+ return contains(
+ packBaseOnlyAccess(
+ access.staticIdx,
+ NO_ACCESSOR,
+ access.suffixIdx,
+ access.valueAccessorState,
+ )
+ )
+}
+
+private fun BaseOnlyAccess.mayCoverDistinctAccess(): Boolean =
+ staticIdx == ABSTRACT_MARK ||
+ fieldIdx == ABSTRACT_MARK ||
+ suffixIdx == ABSTRACT_MARK ||
+ (fieldIdx == NO_ACCESSOR && hasSemanticMark)
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt
new file mode 100644
index 000000000..12a0e5d92
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodEdgesNDInitialToFinalBaseOnlyApSet.kt
@@ -0,0 +1,52 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet
+import org.opentaint.dataflow.ap.ifds.AccessPathBase
+import org.opentaint.dataflow.ap.ifds.ExclusionSet
+import org.opentaint.dataflow.ap.ifds.LanguageManager
+import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
+import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
+import org.opentaint.dataflow.ap.ifds.access.common.CommonNDF2FSet
+import org.opentaint.dataflow.ap.ifds.access.common.ndf2f.DefaultNDF2FSetStorage
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class MethodEdgesNDInitialToFinalBaseOnlyApSet(
+ initialStatement: CommonInst,
+ languageManager: LanguageManager,
+ maxInstIdx: Int,
+ override val apManager: BaseOnlyApManager,
+) : CommonNDF2FSet(initialStatement, languageManager, maxInstIdx),
+ BaseOnlyFinalApAccess, BaseOnlyInitialApAccess {
+
+ override fun mostAbstractPattern(base: AccessPathBase): BaseOnlyAccess = ABSTRACT_EMPTY_ACCESS
+
+ override fun add(
+ statement: CommonInst,
+ initial: Set,
+ finalAp: FinalFactAp,
+ ): Pair, FinalFactAp>? =
+ super.add(
+ statement,
+ initial.mapTo(hashSetOf()) { it.replaceExclusions(ExclusionSet.Universe) },
+ finalAp,
+ )
+
+ override fun createApStorage(): ApStorage =
+ object : DefaultNDF2FSetStorage() {
+ override fun createStorage(): Storage = SetStorage(apManager)
+ }
+
+ private class SetStorage(private val manager: BaseOnlyApManager) : DefaultNDF2FSetStorage.Storage {
+ private val set = LongOpenHashSet()
+
+ override fun add(element: BaseOnlyAccess): BaseOnlyAccess? {
+ if (element.isCollapsed) return null
+ if (!set.add(element)) return null
+ return element
+ }
+
+ override fun collect(dst: MutableList) {
+ dst.addAll(set)
+ }
+ }
+}
diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt
new file mode 100644
index 000000000..1532d2508
--- /dev/null
+++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/ap/ifds/access/baseonly/MethodFinalBaseOnlyApSummariesStorage.kt
@@ -0,0 +1,31 @@
+package org.opentaint.dataflow.ap.ifds.access.baseonly
+
+import org.opentaint.dataflow.ap.ifds.access.common.CommonZ2FSummary
+import org.opentaint.dataflow.util.forEachLong
+import org.opentaint.dataflow.util.longSet
+import org.opentaint.ir.api.common.cfg.CommonInst
+
+class MethodFinalBaseOnlyApSummariesStorage(
+ methodInitialStatement: CommonInst,
+ override val apManager: BaseOnlyApManager,
+) : CommonZ2FSummary