diff --git a/.gitignore b/.gitignore index 1433d695556d..0f358c7c2384 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ visualizer/C1Visualizer/*/target/ *.interp *.tokens /vm/tests/gh_workflows/CDTInspectorTest/target/ +/abstint-tests/out diff --git a/absint-tests/src/bounds/inter/BoundsAllContextSafe.java b/absint-tests/src/bounds/inter/BoundsAllContextSafe.java new file mode 100644 index 000000000000..d0f3faf4d141 --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsAllContextSafe.java @@ -0,0 +1,14 @@ +public class BoundsAllContextSafe { + static final int[] arr = new int[100]; + + static void safeStore(int idx, int value) { + arr[idx] = value; + } + + public static void main(String[] args) { + for (int i = 0; i < 100; i++) { + safeStore(i, i * 2); + } + System.out.println(arr[0]); + } +} diff --git a/absint-tests/src/bounds/inter/BoundsCalleeAlwaysSafe.java b/absint-tests/src/bounds/inter/BoundsCalleeAlwaysSafe.java new file mode 100644 index 000000000000..0278788395b8 --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsCalleeAlwaysSafe.java @@ -0,0 +1,21 @@ +public class BoundsCalleeAlwaysSafe { + static int safeGet(int[] a, int i) { + if (a == null) return -1; + if (i >= 0 && i < a.length) { + return a[i]; + } + return -1; + } + + public static void main(String[] args) { + int[] a = new int[4]; + for (int i = 0; i < a.length; i++) a[i] = i + 1; + + int v1 = safeGet(a, 2); + int v2 = safeGet(a, -1); + int v3 = safeGet(a, 99); + + if (v1 + v2 + v3 == 123456) System.out.println("unlikely"); + } +} + diff --git a/absint-tests/src/bounds/inter/BoundsCalleeGuarded.java b/absint-tests/src/bounds/inter/BoundsCalleeGuarded.java new file mode 100644 index 000000000000..ed3e561564f1 --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsCalleeGuarded.java @@ -0,0 +1,18 @@ +public class BoundsCalleeGuarded { + static int getOrDefault(int[] a, int i) { + if (a == null) return -1; + if (i >= 0 && i < a.length) { + return a[i]; + } + return -1; + } + + public static void main(String[] args) { + int[] a = new int[4]; + for (int i = 0; i < a.length; i++) a[i] = i + 1; + int v1 = getOrDefault(a, 2); + int v2 = getOrDefault(a, -1); + int v3 = getOrDefault(a, 99); + if (v1 + v2 + v3 == 123456) System.out.println("unlikely"); + } +} diff --git a/absint-tests/src/bounds/inter/BoundsCalleeUnsafeCallerSafe.java b/absint-tests/src/bounds/inter/BoundsCalleeUnsafeCallerSafe.java new file mode 100644 index 000000000000..e13e19924e69 --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsCalleeUnsafeCallerSafe.java @@ -0,0 +1,27 @@ +public class BoundsCalleeUnsafeCallerSafe { + static int uncheckedGet(int[] a, int i) { + // No internal guard: safety fully depends on caller. + return a[i]; + } + + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) a[i] = i; + + int sum = 0; + + // Safe usage: caller guards i before calling. + for (int i = -2; i < a.length + 2; i++) { + if (i >= 0 && i < a.length) { + // On this path, i is definitely in [0, a.length-1], so the call is safe. + sum += uncheckedGet(a, i); + } + } + + // Unsafe usage: caller does not guard the index. + int bad = uncheckedGet(a, a.length); // always out-of-bounds + + if (sum + bad == 123456) System.out.println("unlikely"); + } +} + diff --git a/absint-tests/src/bounds/inter/BoundsCallerGuards.java b/absint-tests/src/bounds/inter/BoundsCallerGuards.java new file mode 100644 index 000000000000..f841727e676b --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsCallerGuards.java @@ -0,0 +1,15 @@ +public class BoundsCallerGuards { + static int uncheckedAt(int[] a, int i) { + return a[i]; + } + + public static void main(String[] args) { + int[] a = new int[6]; + for (int i = 0; i < a.length; i++) a[i] = i * 2; + int sum = 0; + for (int i = 0; i < a.length; i++) { + sum += uncheckedAt(a, i); + } + if (sum == -1) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/inter/BoundsClampIndex.java b/absint-tests/src/bounds/inter/BoundsClampIndex.java new file mode 100644 index 000000000000..667c35ab9f1a --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsClampIndex.java @@ -0,0 +1,17 @@ +public class BoundsClampIndex { + static int clamp(int i, int len) { + if (i < 0) return 0; + if (i >= len) return len - 1; + return i; + } + + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) a[i] = i + 10; + int idx = clamp(100, a.length); + int v1 = a[idx]; + idx = clamp(-7, a.length); + int v2 = a[idx]; + if (v1 + v2 == -1) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/inter/BoundsLengthPropagationInterproc.java b/absint-tests/src/bounds/inter/BoundsLengthPropagationInterproc.java new file mode 100644 index 000000000000..54f7ff3026de --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsLengthPropagationInterproc.java @@ -0,0 +1,22 @@ +public class BoundsLengthPropagationInterproc { + static int mid(int[] a) { + if (a == null || a.length == 0) return -1; + return a.length / 2; + } + + public static void main(String[] args) { + int[] a = new int[args.length + 1]; + + int m = mid(a); + if (m >= 0) { + a[m] = 42; + + if (m >= a.length) { + System.out.println("unreachable"); + } + } + + if (a[m] == 123456) System.out.println("unlikely"); + } +} + diff --git a/absint-tests/src/bounds/inter/BoundsProducerConsumer.java b/absint-tests/src/bounds/inter/BoundsProducerConsumer.java new file mode 100644 index 000000000000..f49c8fcaf712 --- /dev/null +++ b/absint-tests/src/bounds/inter/BoundsProducerConsumer.java @@ -0,0 +1,19 @@ +public class BoundsProducerConsumer { + static int consumeLast(int[] a) { + if (a.length == 0) return 0; + int i = a.length - 1; + return a[i]; + } + + static int[] produce(int n) { + int[] a = new int[n]; + for (int i = 0; i < a.length; i++) a[i] = i + 1; + return a; + } + + public static void main(String[] args) { + int[] a = produce(4); + int v = consumeLast(a); // safe access + if (v == -1) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/intra/ArrayIndexing.java b/absint-tests/src/bounds/intra/ArrayIndexing.java new file mode 100644 index 000000000000..216760695184 --- /dev/null +++ b/absint-tests/src/bounds/intra/ArrayIndexing.java @@ -0,0 +1,21 @@ +public class ArrayIndexing { + private static int val; + private static int computed; + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) { + a[i] = i * 2; + } + + int idx = 2; + val = a[idx]; + + if (val > 3) { + idx = 4; + } else { + idx = 0; + } + + computed = (a[1] / 2) + 1; + } +} diff --git a/absint-tests/src/bounds/intra/Bounds2D.java b/absint-tests/src/bounds/intra/Bounds2D.java new file mode 100644 index 000000000000..2ebc60ab0c24 --- /dev/null +++ b/absint-tests/src/bounds/intra/Bounds2D.java @@ -0,0 +1,16 @@ +// Intra-procedural: 2D array with per-row lengths, classic nested loops +public class Bounds2D { + public static void main(String[] args) { + int rows = 3; + int cols = 4; + int[][] m = new int[rows][cols]; + int sum = 0; + for (int i = 0; i < m.length; i++) { + for (int j = 0; j < m[i].length; j++) { + m[i][j] = i + j; + sum += m[i][j]; + } + } + if (sum == -1) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/intra/BoundsAlwaysOffByOne.java b/absint-tests/src/bounds/intra/BoundsAlwaysOffByOne.java new file mode 100644 index 000000000000..6961d0f6fd9c --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsAlwaysOffByOne.java @@ -0,0 +1,13 @@ +public class BoundsAlwaysOffByOne { + public static void main(String[] args) { + int[] a = new int[5]; + + for (int i = 0; i <= a.length; i++) { + if (i < a.length) { + a[i] = i; + } else { + } + } + } +} + diff --git a/absint-tests/src/bounds/intra/BoundsAlwaysSafeSimple.java b/absint-tests/src/bounds/intra/BoundsAlwaysSafeSimple.java new file mode 100644 index 000000000000..9f87c1877a46 --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsAlwaysSafeSimple.java @@ -0,0 +1,26 @@ +public class BoundsAlwaysSafeSimple { + public static void main(String[] args) { + int[] a = new int[4]; + for (int i = 0; i < a.length; i++) { + // At this point, an interval analysis should infer i in [0, a.length-1] + // so the following access is always in-bounds. + a[i] = i; + if (!(0 <= i && i < a.length)) { + // This branch should be unreachable (condition always false). + System.out.println("unreachable"); + } + if (i >= a.length) { + // Also unreachable: i >= a.length is always false in the loop body. + System.out.println("also unreachable"); + } + } + + int sum = 0; + for (int i = 0; i < a.length; i++) { + // Again, always in-bounds. + sum += a[i]; + } + if (sum == 123456) System.out.println("unlikely"); + } +} + diff --git a/absint-tests/src/bounds/intra/BoundsConstantIndex.java b/absint-tests/src/bounds/intra/BoundsConstantIndex.java new file mode 100644 index 000000000000..5f919d853ef7 --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsConstantIndex.java @@ -0,0 +1,10 @@ +public class BoundsConstantIndex { + private static int v; + public static void main(String[] args) { + int[] a = new int[6]; + a[2] = 7; + a[4] = 9; + v = a[2] + a[4]; + if (v == 0) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/intra/BoundsConstantOOB.java b/absint-tests/src/bounds/intra/BoundsConstantOOB.java new file mode 100644 index 000000000000..c95907068773 --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsConstantOOB.java @@ -0,0 +1,31 @@ +public class BoundsConstantOOB { + public static void main(String[] args) { + int[] a = new int[5]; + + int in = 3; // always in-bounds + int outNeg = -1; // always out-of-bounds (negative) + int outHigh = 5; // always out-of-bounds (== length) + + // Definitely safe + a[in] = 1; + + // The following two uses are definitely out-of-bounds. + // An interval analysis should classify these as always unsafe. + // (They may still run and throw at runtime if executed.) + if (outNeg >= 0 && outNeg < a.length) { + // Guard is always false; body is unreachable. + a[outNeg] = 2; + } + + if (outHigh >= 0 && outHigh < a.length) { + // Guard is always false; body is unreachable. + a[outHigh] = 3; + } + + // Unguarded OOB access for analysis to detect. + // a[outHigh] = 4; // uncomment to create a concrete runtime error + + if (a[in] == 123456) System.out.println("unlikely"); + } +} + diff --git a/absint-tests/src/bounds/intra/BoundsGuardedIndex.java b/absint-tests/src/bounds/intra/BoundsGuardedIndex.java new file mode 100644 index 000000000000..0dce253e44c2 --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsGuardedIndex.java @@ -0,0 +1,18 @@ +// Intra-procedural: access guarded by explicit 0 <= idx < length checks +public class BoundsGuardedIndex { + private static int unknown() { return (int)(System.currentTimeMillis() & 7); } + + public static void main(String[] args) { + int[] a = new int[8]; + for (int i = 0; i < a.length; i++) a[i] = i * 3; + + int idx = unknown(); // some unknown in [0,7] + int v; + if (idx >= 0 && idx < a.length) { + v = a[idx]; // should be recognized as safe after guard + } else { + v = -1; + } + if (v == 123456) System.out.println("unlikely"); + } +} diff --git a/absint-tests/src/bounds/intra/BoundsMinCopy.java b/absint-tests/src/bounds/intra/BoundsMinCopy.java new file mode 100644 index 000000000000..703253e06abc --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsMinCopy.java @@ -0,0 +1,15 @@ +public class BoundsMinCopy { + + public static void main(String[] args) { + int[] src = new int[7]; + int[] dst = new int[5]; + for (int i = 0; i < src.length; i++) { + src[i] = i * 2; + } + + int n = Math.min(src.length, dst.length); + for (int i = 0; i < n; i++) { + dst[i] = src[i]; + } + } +} diff --git a/absint-tests/src/bounds/intra/BoundsOffByOne.java b/absint-tests/src/bounds/intra/BoundsOffByOne.java new file mode 100644 index 000000000000..3dcaedcdf78a --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsOffByOne.java @@ -0,0 +1,11 @@ +public class BoundsOffByOne { + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) a[i] = i; + + int last = 0; + if (a.length > 0) { + last = a[a.length - 1]; + } + } +} diff --git a/absint-tests/src/bounds/intra/BoundsPathSensitiveIfElse.java b/absint-tests/src/bounds/intra/BoundsPathSensitiveIfElse.java new file mode 100644 index 000000000000..77d690cae18b --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsPathSensitiveIfElse.java @@ -0,0 +1,25 @@ +public class BoundsPathSensitiveIfElse { + public static void main(String[] args) { + int[] a = new int[8]; + int i = args.length - 1; // abstractly, could be negative or up to some small number + + if (i >= 0 && i < a.length) { + // On this path, index is definitely within [0, a.length-1]. + a[i] = 1; // always safe on the then-branch. + } else { + // Here, we only know that !(0 <= i < a.length), so i < 0 or i >= a.length. + // This access is potentially unsafe and should be flagged. + // Depending on analysis precision, it might be classified as definitely out-of-bounds + // or at least unknown. + a[i] = 2; + } + + if (i >= 0 && i < a.length) { + // This condition is not always true at this point (because we have both branches), + // but it is exactly the same as the guard above. + // Interval analysis may classify this as unknown in general. + System.out.println("maybe"); + } + } +} + diff --git a/absint-tests/src/bounds/intra/BoundsSafeLoop.java b/absint-tests/src/bounds/intra/BoundsSafeLoop.java new file mode 100644 index 000000000000..92eede0ab9cc --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsSafeLoop.java @@ -0,0 +1,14 @@ +public class BoundsSafeLoop { + public static void main(String[] args) { + int n = 10; + int[] a = new int[n]; + int sum = 0; + for (int i = 0; i < a.length; i++) { + a[i] = i; + } + for (int i = 0; i < a.length; i++) { + sum += a[i]; + } + if (sum == -1) System.out.println("impossible"); + } +} diff --git a/absint-tests/src/bounds/intra/BoundsUnsafeAccess.java b/absint-tests/src/bounds/intra/BoundsUnsafeAccess.java new file mode 100644 index 000000000000..b911e627c267 --- /dev/null +++ b/absint-tests/src/bounds/intra/BoundsUnsafeAccess.java @@ -0,0 +1,10 @@ +public class BoundsUnsafeAccess { + public static void main(String[] args) { + int[] a = new int[5]; + int x = 0; + int i = 5; + if (args.length == 42) { + x = a[i]; + } + } +} diff --git a/absint-tests/src/bounds/intra/UnknownCycleLength.java b/absint-tests/src/bounds/intra/UnknownCycleLength.java new file mode 100644 index 000000000000..739015421b53 --- /dev/null +++ b/absint-tests/src/bounds/intra/UnknownCycleLength.java @@ -0,0 +1,8 @@ +public class UnknownCycleLength { + private static int res = 0; + public static void main(String[] args) { + for (String arg : args) { + res += arg.length(); + } + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/inter/AliasedObjectFieldBounds.java b/absint-tests/src/numerical/inter/AliasedObjectFieldBounds.java new file mode 100644 index 000000000000..33682f8b20e7 --- /dev/null +++ b/absint-tests/src/numerical/inter/AliasedObjectFieldBounds.java @@ -0,0 +1,49 @@ +class Buffer { + int[] data; + Buffer(int n) { data = new int[n]; } +} + +class BufferHolder { + Buffer buf; +} + +public class AliasedObjectFieldBounds { + private static BufferHolder init(int size) { + BufferHolder h = new BufferHolder(); + if (size < 0) size = 0; + if (size > 12) size = 12; + h.buf = new Buffer(size); + return h; + } + + private static BufferHolder alias(BufferHolder h) { + // create an alias path through another object + BufferHolder g = new BufferHolder(); + g.buf = h.buf; + return g; + } + + private static int selectIndex(BufferHolder h) { + int len = h.buf.data.length; + int i = (len * 5) / 7; // not trivially folded to boundary + if (i < 0) i = 0; + if (i >= len) i = len - 1; + return i; + } + + public static void main(String[] args) { + BufferHolder h = init(20); + BufferHolder g = alias(h); + int i = selectIndex(g); + if (i >= 0 && i < h.buf.data.length) { + System.out.println("ALWAYS_TRUE: i in bounds via alias"); + h.buf.data[i] = 1; + } + if (h.buf.data.length <= 12) { + System.out.println("ALWAYS_TRUE: clamped length<=12"); + } else { + System.out.println("UNREACHABLE"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/AliasingExample.java b/absint-tests/src/numerical/inter/AliasingExample.java new file mode 100644 index 000000000000..cb6d3a28c869 --- /dev/null +++ b/absint-tests/src/numerical/inter/AliasingExample.java @@ -0,0 +1,10 @@ +class Box { int v; } + +public class AliasingExample { + public static void main(String[] args) { + Box a = new Box(); + Box b = a; + a.v = 10; + int x = b.v; + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/inter/ArrayAliasLengthPropagation.java b/absint-tests/src/numerical/inter/ArrayAliasLengthPropagation.java new file mode 100644 index 000000000000..639b60fa8ddf --- /dev/null +++ b/absint-tests/src/numerical/inter/ArrayAliasLengthPropagation.java @@ -0,0 +1,19 @@ +public class ArrayAliasLengthPropagation { + private static int[] makeArray(int n) { return new int[n]; } + + public static void main(String[] args) { + int[] a = makeArray(3); + int[] b = a; // alias + if (b.length == a.length) { + System.out.println("ALWAYS_TRUE: lengths equal under alias"); + } else { + System.out.println("UNREACHABLE"); + } + int i = 2; + if (i < b.length) { + a[i] = 99; // safe due to shared length + System.out.println("ALWAYS_TRUE: i 3 ? 0 : 1); // 3 or 4; here 4 + } + + public static void main(String[] args) { + int ret = provide(); + if (ret >= 2 && ret <= 4) { + System.out.println("ALWAYS_TRUE: 2<=ret<=4"); + } else { + System.out.println("UNREACHABLE"); + } + + if (ret > 4) { + System.out.println("UNREACHABLE: ret>4"); + } else { + System.out.println("ALWAYS_TRUE: ret<=4"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/Factorial.java b/absint-tests/src/numerical/inter/Factorial.java new file mode 100644 index 000000000000..f7ac9c188aed --- /dev/null +++ b/absint-tests/src/numerical/inter/Factorial.java @@ -0,0 +1,14 @@ +public class Factorial { + private static int y = 0; + + private static int fact(int n) { + if (n <= 1) return 1; + return n * fact(n - 1); + } + + public static void main(String[] args) { + y = fact(5); + System.out.println(y); + } + +} diff --git a/absint-tests/src/numerical/inter/FieldCopyAndAliasing.java b/absint-tests/src/numerical/inter/FieldCopyAndAliasing.java new file mode 100644 index 000000000000..ba67c5cfe22f --- /dev/null +++ b/absint-tests/src/numerical/inter/FieldCopyAndAliasing.java @@ -0,0 +1,12 @@ +class Node { Node next; int val; } + +public class FieldCopyAndAliasing { + public static void main(String[] args) { + Node a = new Node(); + Node b = new Node(); + a.val = 1; + b.val = 2; + a.next = b; + int x = a.next.val; + } +} diff --git a/absint-tests/src/numerical/inter/GuardedByCalleeContract.java b/absint-tests/src/numerical/inter/GuardedByCalleeContract.java new file mode 100644 index 000000000000..3da7fcfe5547 --- /dev/null +++ b/absint-tests/src/numerical/inter/GuardedByCalleeContract.java @@ -0,0 +1,21 @@ +public class GuardedByCalleeContract { + private static int clampNonNegative(int x) { + return x < 0 ? 0 : x; + } + + public static void main(String[] args) { + int a = clampNonNegative(-5); + int b = clampNonNegative(10); + if (a < 0) { + System.out.println("UNREACHABLE: a<0"); + } else { + System.out.println("ALWAYS_TRUE: a>=0"); + } + if (b < 0) { + System.out.println("UNREACHABLE: b<0"); + } else { + System.out.println("ALWAYS_TRUE: b>=0"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/HeapLoopSummary.java b/absint-tests/src/numerical/inter/HeapLoopSummary.java new file mode 100644 index 000000000000..fa31dae2cad8 --- /dev/null +++ b/absint-tests/src/numerical/inter/HeapLoopSummary.java @@ -0,0 +1,13 @@ +public class HeapLoopSummary { + static class Node { Node next; int val; } + + public static void main(String[] args) { + Node n = head(); // unknown linked list head + while (n != null) { + n.val = n.val + 1; + n = n.next; + } + } + + static Node head() { return new Node(); } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/inter/InterConditionExample.java b/absint-tests/src/numerical/inter/InterConditionExample.java new file mode 100644 index 000000000000..4e6637013ff9 --- /dev/null +++ b/absint-tests/src/numerical/inter/InterConditionExample.java @@ -0,0 +1,20 @@ +public class InterConditionExample { + + enum Parity { + ODD, + EVEN + } + + private static Parity getParity(int value) { + return value % 2 == 0 ? Parity.EVEN : Parity.ODD; + } + + public static void main(String[] args) { + Parity parity = getParity(Integer.parseInt(args[0])); + if (parity != Parity.ODD) { + System.out.println("it is even"); + } else { + System.out.println("it is odd"); + } + } +} diff --git a/absint-tests/src/numerical/inter/InterprocContractsVirtualDispatch.java b/absint-tests/src/numerical/inter/InterprocContractsVirtualDispatch.java new file mode 100644 index 000000000000..6857872e8fef --- /dev/null +++ b/absint-tests/src/numerical/inter/InterprocContractsVirtualDispatch.java @@ -0,0 +1,54 @@ +interface SizeProvider { + int size(); +} + +class FixedSize implements SizeProvider { + private final int n; + FixedSize(int n) { this.n = n; } + public int size() { return n; } +} + +class ClampedSize implements SizeProvider { + private final int n; + ClampedSize(int n) { this.n = n; } + public int size() { + int s = n; + if (s < 0) s = 0; + if (s > 10) s = 10; + return s; + } +} + +public class InterprocContractsVirtualDispatch { + private static int[] make(SizeProvider sp) { + return new int[sp.size()]; + } + + private static int safeIndex(int guess, int len) { + // non-trivial arithmetic to avoid folding + int g = (guess * 3) - (guess / 2); + if (g < 0) return 0; + if (g >= len) return len - 1; + return g; + } + + public static void main(String[] args) { + SizeProvider sp = args.length > 0 ? new ClampedSize(15) : new FixedSize(8); + int[] arr = make(sp); + int idx = safeIndex(arr.length - 1, arr.length); + if (idx >= 0 && idx < arr.length) { + // should be always true regardless of dispatch due to clamping + arr[idx] = 1; + System.out.println("ALWAYS_TRUE: idx within length via dispatch and clamp"); + } else { + System.out.println("UNREACHABLE"); + } + // demonstrate branch on upper bound of size + if (arr.length > 10) { + System.out.println("UNREACHABLE: arr.length>10"); + } else { + System.out.println("ALWAYS_TRUE: arr.length<=10"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/InterprocLoopContracts.java b/absint-tests/src/numerical/inter/InterprocLoopContracts.java new file mode 100644 index 000000000000..847de0d48ca8 --- /dev/null +++ b/absint-tests/src/numerical/inter/InterprocLoopContracts.java @@ -0,0 +1,30 @@ +public class InterprocLoopContracts { + private static int contractBound(int n) { + // clamp into [0, 9] + if (n < 0) return 0; + if (n > 9) return 9; + return n; + } + + private static int sumPrefix(int[] a, int upto) { + int u = contractBound(upto); + int s = 0; + for (int i = 0; i < u; i++) { + if (i < a.length) { // always true if u<=a.length + s += a[i]; + } else { + // unreachable due to contractBound and a.length==10 below + s -= 1; + } + } + return s; + } + + public static void main(String[] args) { + int[] arr = new int[10]; + for (int i = 0; i < arr.length; i++) arr[i] = i; + int res = sumPrefix(arr, 100); + System.out.println(res); + } +} + diff --git a/absint-tests/src/numerical/inter/Main.java b/absint-tests/src/numerical/inter/Main.java new file mode 100644 index 000000000000..1c2786840107 --- /dev/null +++ b/absint-tests/src/numerical/inter/Main.java @@ -0,0 +1,14 @@ +public class Main { + + public static int getMax(int a, int b) { + if (a < b) { + return b; + } + return a; + } + + public static void main(String[] args) { + System.out.println(getMax(1, 2)); + System.out.println(getMax(2, 3)); + } +} diff --git a/absint-tests/src/numerical/inter/MutualRecursion.java b/absint-tests/src/numerical/inter/MutualRecursion.java new file mode 100644 index 000000000000..c781cae8ce36 --- /dev/null +++ b/absint-tests/src/numerical/inter/MutualRecursion.java @@ -0,0 +1,18 @@ +public class MutualRecursion { + static int even(int n) { + if (n == 0) return 1; + else return odd(n - 1); + } + + static int odd(int n) { + if (n == 0) return 0; + else return even(n - 1); + } + + static int input() { return 5; } + + public static void main(String[] args) { + int x = input(); + int y = even(x); + } +} diff --git a/absint-tests/src/numerical/inter/NestedConditionals.java b/absint-tests/src/numerical/inter/NestedConditionals.java new file mode 100644 index 000000000000..22433be5fe76 --- /dev/null +++ b/absint-tests/src/numerical/inter/NestedConditionals.java @@ -0,0 +1,13 @@ +public class NestedConditionals { + static boolean input() { return true; } + + public static void main(String[] args) { + int x = 0; + int y = 0; + if (input()) { + x = 1; + if (input()) y = 2; + else y = 3; + } + } +} diff --git a/absint-tests/src/numerical/inter/NestedFields.java b/absint-tests/src/numerical/inter/NestedFields.java new file mode 100644 index 000000000000..6c5e99de3257 --- /dev/null +++ b/absint-tests/src/numerical/inter/NestedFields.java @@ -0,0 +1,11 @@ +public class NestedFields { + static class Node { Node next; int val; } + + public static void main(String[] args) { + Node n = new Node(); + n.val = 0; + n.next = new Node(); + n.next.val = 42; + int a = n.next.val; + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/inter/NestedInterprocGuards.java b/absint-tests/src/numerical/inter/NestedInterprocGuards.java new file mode 100644 index 000000000000..4b1c24563eee --- /dev/null +++ b/absint-tests/src/numerical/inter/NestedInterprocGuards.java @@ -0,0 +1,42 @@ +class Range { + final int lo; + final int hi; + Range(int lo, int hi) { + this.lo = lo; + this.hi = hi; + } +} + +public class NestedInterprocGuards { + private static Range mkRange(int base) { + int lo = base - 2; + int hi = base + 2; + if (lo < 0) lo = 0; + if (hi > 15) hi = 15; + return new Range(lo, hi); + } + + private static int choose(Range r, int[] a) { + int guess = (r.lo + r.hi) / 2; + int idx = guess; + if (idx < 0) idx = 0; + if (idx >= a.length) idx = a.length - 1; + return idx; + } + + public static void main(String[] args) { + int[] a = new int[12]; + Range r = mkRange(20); + int idx = choose(r, a); + if (idx >= 0 && idx < a.length) { + System.out.println("ALWAYS_TRUE: nested guards produce safe idx"); + } else { + System.out.println("UNREACHABLE"); + } + // derived branch about r bounds + if (r.hi <= 15 && r.lo >= 0) { + System.out.println("ALWAYS_TRUE: range clamped"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/ObjectFieldAliasingClamp.java b/absint-tests/src/numerical/inter/ObjectFieldAliasingClamp.java new file mode 100644 index 000000000000..f8bfcb281e70 --- /dev/null +++ b/absint-tests/src/numerical/inter/ObjectFieldAliasingClamp.java @@ -0,0 +1,26 @@ +class Holder { + int[] arr; +} + +public class ObjectFieldAliasingClamp { + private static Holder makeHolder(int size) { + Holder h = new Holder(); + if (size < 0) size = 0; // clamp + h.arr = new int[size]; + return h; + } + + public static void main(String[] args) { + Holder h = makeHolder(-5); + if (h.arr.length >= 0) { + System.out.println("ALWAYS_TRUE: non-negative length"); + } + int idx = 0; + if (idx >= 0 && idx < h.arr.length) { + System.out.println("UNREACHABLE: idx in empty arr"); + } else { + System.out.println("ALWAYS_TRUE: idx out of range for empty"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/RecursiveAliasing.java b/absint-tests/src/numerical/inter/RecursiveAliasing.java new file mode 100644 index 000000000000..0d9ed8bdccfe --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveAliasing.java @@ -0,0 +1,16 @@ +public class RecursiveAliasing { + static class Node { Node next; int val; } + + static void set(Node n, int v) { + if (n == null) return; + n.val = v; + set(n.next, v); + } + + public static void main(String[] args) { + Node x = new Node(); + Node y = x; + x.next = new Node(); + set(y, 5); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveArraySum.java b/absint-tests/src/numerical/inter/RecursiveArraySum.java new file mode 100644 index 000000000000..efd7e4b106fa --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveArraySum.java @@ -0,0 +1,11 @@ +public class RecursiveArraySum { + static int sum(int[] A, int i) { + if (i >= A.length) return 0; + return A[i] + sum(A, i + 1); + } + + public static void main(String[] args) { + int[] arr = {1, 2, 3}; + int s = sum(arr, 0); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveDecrement.java b/absint-tests/src/numerical/inter/RecursiveDecrement.java new file mode 100644 index 000000000000..65383df08ab4 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveDecrement.java @@ -0,0 +1,13 @@ +public class RecursiveDecrement { + static int dec(int n) { + if (n <= 0) return 0; + return dec(n - 1); + } + + static int input() { return 5; } + + public static void main(String[] args) { + int x = input(); + int y = dec(x); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveFactoryBounds.java b/absint-tests/src/numerical/inter/RecursiveFactoryBounds.java new file mode 100644 index 000000000000..105c54363af5 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveFactoryBounds.java @@ -0,0 +1,41 @@ +public class RecursiveFactoryBounds { + private static int clampDepth(int n) { + if (n < 0) return 0; + if (n > 6) return 6; + return n; + } + + private static int computeSize(int n) { + // mutual recursion-like chain via decreasing n + if (n <= 1) return 1; + return computeSize(n - 2) + 1; + } + + private static int[] makeArray(int n) { + int c = clampDepth(n); + int s = computeSize(c); + return new int[s]; + } + + private static int chooseIndex(int s) { + int a = (s * 2) - 1; + int b = a / 3 + (s % 2); + return Math.max(0, Math.min(b, s - 1)); + } + + public static void main(String[] args) { + int[] arr = makeArray(10); + int idx = chooseIndex(arr.length); + if (idx >= 0 && idx < arr.length) { + System.out.println("ALWAYS_TRUE: idx in bounds after recursion"); + } else { + System.out.println("UNREACHABLE"); + } + if (arr.length >= 1 && arr.length <= 4) { + System.out.println("ALWAYS_TRUE: size in [1,4]"); + } else { + System.out.println("UNREACHABLE"); + } + } +} + diff --git a/absint-tests/src/numerical/inter/RecursiveFibonacci.java b/absint-tests/src/numerical/inter/RecursiveFibonacci.java new file mode 100644 index 000000000000..93dba46cbc8b --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveFibonacci.java @@ -0,0 +1,11 @@ +class RecursiveFibonacci { + static void main(String[] args) { + System.out.println(getFibonacci(40)); + } + static int getFibonacci(int idx) { + if (idx < 2) { + return 1; + } + return getFibonacci(idx - 1) + getFibonacci(idx - 2); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveListSum.java b/absint-tests/src/numerical/inter/RecursiveListSum.java new file mode 100644 index 000000000000..ecc0590644e3 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveListSum.java @@ -0,0 +1,18 @@ +public class RecursiveListSum { + static class Node { int val; Node next; } + + static int sum(Node n) { + if (n == null) return 0; + return n.val + sum(n.next); + } + + public static void main(String[] args) { + Node a = new Node(); + Node b = new Node(); + a.val = 1; + b.val = 2; + a.next = b; + b.next = null; + int s = sum(a); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveMin.java b/absint-tests/src/numerical/inter/RecursiveMin.java new file mode 100644 index 000000000000..609d51146150 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveMin.java @@ -0,0 +1,12 @@ +public class RecursiveMin { + static int min(int[] A, int i) { + if (i == A.length - 1) return A[i]; + int m = min(A, i + 1); + return (A[i] < m) ? A[i] : m; + } + + public static void main(String[] args) { + int[] arr = {3, 1, 4}; + int m = min(arr, 0); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveObjectUpdate.java b/absint-tests/src/numerical/inter/RecursiveObjectUpdate.java new file mode 100644 index 000000000000..7451bb932670 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveObjectUpdate.java @@ -0,0 +1,18 @@ +public class RecursiveObjectUpdate { + static class Node { int val; Node next; } + + static void inc(Node n) { + if (n == null) return; + n.val = n.val + 1; + inc(n.next); + } + + public static void main(String[] args) { + Node a = new Node(); + Node b = new Node(); + a.val = 1; + b.val = 2; + a.next = b; + inc(a); + } +} diff --git a/absint-tests/src/numerical/inter/RecursiveSum.java b/absint-tests/src/numerical/inter/RecursiveSum.java new file mode 100644 index 000000000000..5f572eb64301 --- /dev/null +++ b/absint-tests/src/numerical/inter/RecursiveSum.java @@ -0,0 +1,10 @@ +public class RecursiveSum { + static int sumToN(int n) { + if (n == 0) return 0; + return n + sumToN(n - 1); + } + + public static void main(String[] args) { + int s = sumToN(5); + } +} diff --git a/absint-tests/src/numerical/inter/SimpleObjectField.java b/absint-tests/src/numerical/inter/SimpleObjectField.java new file mode 100644 index 000000000000..e357c9d563b1 --- /dev/null +++ b/absint-tests/src/numerical/inter/SimpleObjectField.java @@ -0,0 +1,9 @@ +public class SimpleObjectField { + static class Box { int v; } + + public static void main(String[] args) { + Box b = new Box(); + b.v = 5; + int x = b.v + 2; + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/inter/SizeFromFactory.java b/absint-tests/src/numerical/inter/SizeFromFactory.java new file mode 100644 index 000000000000..27786e844007 --- /dev/null +++ b/absint-tests/src/numerical/inter/SizeFromFactory.java @@ -0,0 +1,22 @@ +public class SizeFromFactory { + private static int[] mk(int size) { + return new int[size]; + } + + public static void main(String[] args) { + int[] a = mk(5); + int i = 3; + if (i < a.length && i >= 0) { + System.out.println("ALWAYS_TRUE: 0<=i writes through a reflect in reads through b +*/ +public class AliasArrayTest { + private static int res = 0; + public static void main(String[] args) { + int[] a = new int[3]; + int[] b = a; // alias + a[0] = 42; + res = b[0]; // expect 42 + } +} + diff --git a/absint-tests/src/numerical/intra/ArrayBoundsSafe.java b/absint-tests/src/numerical/intra/ArrayBoundsSafe.java new file mode 100644 index 000000000000..1257d2c4802b --- /dev/null +++ b/absint-tests/src/numerical/intra/ArrayBoundsSafe.java @@ -0,0 +1,28 @@ +public class ArrayBoundsSafe { + + private static int x; + private static int y; + + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) { + a[i] = i * 2; // 0,2,4,6,8 + } + + int idx = 2; + x = a[idx]; + // Infer that x == [4, 4] always holds here + + if (x > 3) { + x = 4; + } else { + x = 0; + } + + // Infer that x == [0, 0] always holds here + + y = (a[1] / 2) + 1; + // Infer that y == [2, 2] always holds here + // System.out.println(x); + } +} diff --git a/absint-tests/src/numerical/intra/ArrayConstantIndex.java b/absint-tests/src/numerical/intra/ArrayConstantIndex.java new file mode 100644 index 000000000000..5ffcc9119b12 --- /dev/null +++ b/absint-tests/src/numerical/intra/ArrayConstantIndex.java @@ -0,0 +1,12 @@ +/* + Expected invariants: + - reading arr[2] should be exact if arr constant initialized +*/ +public class ArrayConstantIndex { + private static int res; + public static void main(String[] args) { + int[] arr = new int[] {10,20,30,40}; + res = arr[2]; + } +} + diff --git a/absint-tests/src/numerical/intra/ArrayUnknownIndex.java b/absint-tests/src/numerical/intra/ArrayUnknownIndex.java new file mode 100644 index 000000000000..0fe49f07978d --- /dev/null +++ b/absint-tests/src/numerical/intra/ArrayUnknownIndex.java @@ -0,0 +1,14 @@ +/* + Expected invariants: + - reading arr[i] when i unknown -> wildcard, result probably TOP unless array elements initialized to constants in store +*/ +public class ArrayUnknownIndex { + public static int main(String[] args) { + int[] arr = new int[10]; + int i = 3; // set to a number or leave unknown for test + arr[1] = 5; + int v = arr[i]; + return v; + } +} + diff --git a/absint-tests/src/numerical/intra/BranchingArithmetic.java b/absint-tests/src/numerical/intra/BranchingArithmetic.java new file mode 100644 index 000000000000..4b178f1c72a0 --- /dev/null +++ b/absint-tests/src/numerical/intra/BranchingArithmetic.java @@ -0,0 +1,21 @@ +public class BranchingArithmetic { + private static int y; + private static int z; + public static void main(String[] args) { + int x = 7; + + if (x % 2 == 0) { + y = x / 2; + } else { + y = 3 * x + 1; + } + + if (x > 10) { + z = x - 10; + } else if (x > 5) { + z = x + 5; + } else { + z = x * 2; + } + } +} diff --git a/absint-tests/src/numerical/intra/ConditionalNarrowing.java b/absint-tests/src/numerical/intra/ConditionalNarrowing.java new file mode 100644 index 000000000000..f013cd2e0ea1 --- /dev/null +++ b/absint-tests/src/numerical/intra/ConditionalNarrowing.java @@ -0,0 +1,16 @@ +/* + Expected invariants: + - after if (x < 10) { ... } else { ... } edges should be narrowed: true-branch x in [-inf,9], false-branch x in [10, +inf] +*/ +public class ConditionalNarrowing { + public static int main(String[] args) { + int x = 15; + if (x < 10) { + x = 1; + } else { + x = 20; + } + return x; + } +} + diff --git a/absint-tests/src/numerical/intra/ConstPropagation.java b/absint-tests/src/numerical/intra/ConstPropagation.java new file mode 100644 index 000000000000..e59c309f6d72 --- /dev/null +++ b/absint-tests/src/numerical/intra/ConstPropagation.java @@ -0,0 +1,14 @@ +/* + Expected invariants: + - constant result: local 'x' should be exactly 42 + - arithmetic folding: x = 40 + 2 => [42,42] +*/ +public class ConstPropagation { + public static int main(String[] args) { + int a = 40; + int b = 2; + int x = a + b; + return x; + } +} + diff --git a/absint-tests/src/numerical/intra/DivisionByConstant.java b/absint-tests/src/numerical/intra/DivisionByConstant.java new file mode 100644 index 000000000000..e9cd91c75690 --- /dev/null +++ b/absint-tests/src/numerical/intra/DivisionByConstant.java @@ -0,0 +1,13 @@ +/* + Expected invariants: + - Division by constant -> produce interval accordingly, division by zero handled conservatively +*/ +public class DivisionByConstant { + public static int main(String[] args) { + int x = 20; + int d = 4; + int r = x / d; // expect 5 + return r; + } +} + diff --git a/absint-tests/src/numerical/intra/FieldInit.java b/absint-tests/src/numerical/intra/FieldInit.java new file mode 100644 index 000000000000..ab4add440764 --- /dev/null +++ b/absint-tests/src/numerical/intra/FieldInit.java @@ -0,0 +1,17 @@ +/* + Expected invariants: + - static field S = 7 after initialization + - instance field f for new object at alloc site is unknown unless written +*/ +public class FieldInit { + static int S; + int f; + public static void main(String[] args) { + FieldInit x = new FieldInit(); + S = 7; + x.f = 3; + int a = S; // expect [7,7] + int b = x.f; // expect [3,3] + } +} + diff --git a/absint-tests/src/numerical/intra/FixedBoundSummationSafe.java b/absint-tests/src/numerical/intra/FixedBoundSummationSafe.java new file mode 100644 index 000000000000..36eb2ddeeb0c --- /dev/null +++ b/absint-tests/src/numerical/intra/FixedBoundSummationSafe.java @@ -0,0 +1,17 @@ +public class FixedBoundSummationSafe { + public static void main(String[] args) { + int[] arr = new int[10]; + int sum = 0; + for (int i = 0; i < 10; i++) { + arr[i] = i; + } + for (int i = 0; i < 10; i++) { + if (i < arr.length) { + // always true inside this loop + sum += arr[i]; + } + } + System.out.println("sum=" + sum); + } +} + diff --git a/absint-tests/src/numerical/intra/HelloWorld.java b/absint-tests/src/numerical/intra/HelloWorld.java new file mode 100644 index 000000000000..cf1c7fde0127 --- /dev/null +++ b/absint-tests/src/numerical/intra/HelloWorld.java @@ -0,0 +1,5 @@ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello world"); + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/intra/InterprocCallee.java b/absint-tests/src/numerical/intra/InterprocCallee.java new file mode 100644 index 000000000000..2af5caa3cf6c --- /dev/null +++ b/absint-tests/src/numerical/intra/InterprocCallee.java @@ -0,0 +1,8 @@ +public class InterprocCallee { + public static int sumToN(int n) { + int s = 0; + for (int i = 0; i < n; i++) s += i; + return s; + } +} + diff --git a/absint-tests/src/numerical/intra/InterprocCaller.java b/absint-tests/src/numerical/intra/InterprocCaller.java new file mode 100644 index 000000000000..02b6ebba37dd --- /dev/null +++ b/absint-tests/src/numerical/intra/InterprocCaller.java @@ -0,0 +1,6 @@ +public class InterprocCaller { + public static int main(String[] args) { + return InterprocCallee.sumToN(4); + } +} + diff --git a/absint-tests/src/numerical/intra/LoopSumFixed.java b/absint-tests/src/numerical/intra/LoopSumFixed.java new file mode 100644 index 000000000000..d7019acd7632 --- /dev/null +++ b/absint-tests/src/numerical/intra/LoopSumFixed.java @@ -0,0 +1,16 @@ +/* + Expected invariants: + - loop index i in [0,4] + - sum s in [0, 10] if add 0..2 each iteration (example uses res[i] constant 1) +*/ +public class LoopSumFixed { + public static int main(String[] args) { + int s = 0; + int[] res = new int[] {1,1,1,1,1}; + for (int i = 0; i < 5; i++) { + s = s + res[i]; + } + return s; + } +} + diff --git a/absint-tests/src/numerical/intra/LoopSumVariable.java b/absint-tests/src/numerical/intra/LoopSumVariable.java new file mode 100644 index 000000000000..0bed8f3f558e --- /dev/null +++ b/absint-tests/src/numerical/intra/LoopSumVariable.java @@ -0,0 +1,18 @@ +/* + Expected invariants: + - loop index i in [0,n-1] + - sum s in [0, n*5] if res[i] in [0,5] +*/ +public class LoopSumVariable { + public static int main(String[] args) { + int s = 0; + int n = 10; + int[] res = new int[n]; + for (int i = 0; i < n; i++) { + res[i] = 1; // or unknown value + s += res[i]; + } + return s; + } +} + diff --git a/absint-tests/src/numerical/intra/LoopSummation.java b/absint-tests/src/numerical/intra/LoopSummation.java new file mode 100644 index 000000000000..ad1a56f4f456 --- /dev/null +++ b/absint-tests/src/numerical/intra/LoopSummation.java @@ -0,0 +1,14 @@ +public class LoopSummation { + private static int res = 0; + + public static void main(String[] args) { + int n = 5; + int sum = 0; + + // simple loop with incremental update + for (int i = 0; i < n; i++) { + sum += i; + } + res = sum; + } +} diff --git a/absint-tests/src/numerical/intra/MatrixSum.java b/absint-tests/src/numerical/intra/MatrixSum.java new file mode 100644 index 000000000000..eb2702d682cc --- /dev/null +++ b/absint-tests/src/numerical/intra/MatrixSum.java @@ -0,0 +1,14 @@ +public class MatrixSum { + private static int val; + public static void main(String[] args) { + int[][] m = new int[3][4]; + int sum = 0; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 4; j++) { + m[i][j] = i + j; + sum += m[i][j]; + } + } + val = sum; + } +} diff --git a/absint-tests/src/numerical/intra/NestedConditionalsTest.java b/absint-tests/src/numerical/intra/NestedConditionalsTest.java new file mode 100644 index 000000000000..a1f9f4ca7e1b --- /dev/null +++ b/absint-tests/src/numerical/intra/NestedConditionalsTest.java @@ -0,0 +1,33 @@ +public class NestedConditionalsTest { + private static int res = 0; + private static int x = 0; + + public static void main(String[] args) { + int a = 3; + int b = 4; + + if (a > 0) { + if (b > 0) { + res = a + b; // 7 + } else { + res = a - b; + } + } else { + if (b > 10) { + res = a * b; + } else { + res = b - a; + } + } + + // nested condition with reassignment + if (res % 2 == 0) { + x = res / 2; + if (x > 3) { + x = x + 10; + } + } else { + x = -res; + } + } +} diff --git a/absint-tests/src/numerical/intra/OffByOneUnsafe.java b/absint-tests/src/numerical/intra/OffByOneUnsafe.java new file mode 100644 index 000000000000..e3d4bcfc5b8e --- /dev/null +++ b/absint-tests/src/numerical/intra/OffByOneUnsafe.java @@ -0,0 +1,17 @@ +public class OffByOneUnsafe { + public static void main(String[] args) { + int[] arr = new int[5]; + // Intentional off-by-one: i<=arr.length + for (int i = 0; i <= arr.length; i++) { + if (i < arr.length) { + // true for i=0..4 + arr[i] = i; + } else { + // false only when i==arr.length + System.out.println("ALWAYS_TRUE: i==len triggers else"); + } + } + System.out.println("done"); + } +} + diff --git a/absint-tests/src/numerical/intra/SimpleConstants.java b/absint-tests/src/numerical/intra/SimpleConstants.java new file mode 100644 index 000000000000..1c6efd454801 --- /dev/null +++ b/absint-tests/src/numerical/intra/SimpleConstants.java @@ -0,0 +1,16 @@ +public class SimpleConstants { + private static int field = 0; + public static void main(String[] args) { + int a = 5; + int b = -3; + int c = a + b; // 2 + for (int i = 0; i < c; i++) { + b += i; + } + field = b; + int d = 0; + d = (a > 0) ? 1 : 2; // d should be 1 + + int e = 10 * 2 - 5; // 15 + } +} diff --git a/absint-tests/src/numerical/intra/UnguardedOOB.java b/absint-tests/src/numerical/intra/UnguardedOOB.java new file mode 100644 index 000000000000..fab7f26b5ff7 --- /dev/null +++ b/absint-tests/src/numerical/intra/UnguardedOOB.java @@ -0,0 +1,14 @@ +public class UnguardedOOB { + public static void main(String[] args) { + int[] arr = new int[5]; + int idx = 5; + if (idx < arr.length) { + System.out.println("UNREACHABLE: idx=arr.length"); + } + // Intentional unsafe access to model OOB + // Do not execute arr[idx] to avoid runtime error; just show branch + } +} + diff --git a/absint-tests/src/numerical/intra/UnsafeArrayAccess.java b/absint-tests/src/numerical/intra/UnsafeArrayAccess.java new file mode 100644 index 000000000000..641529f77d99 --- /dev/null +++ b/absint-tests/src/numerical/intra/UnsafeArrayAccess.java @@ -0,0 +1,9 @@ +public class UnsafeArrayAccess { + private static int val; + public static void main(String[] args) { + int[] a = new int[2]; + a[0] = 0; + a[1] = 1; + val = a[1]; + } +} \ No newline at end of file diff --git a/absint-tests/src/numerical/intra/VariableBoundClamped.java b/absint-tests/src/numerical/intra/VariableBoundClamped.java new file mode 100644 index 000000000000..729a4512053f --- /dev/null +++ b/absint-tests/src/numerical/intra/VariableBoundClamped.java @@ -0,0 +1,20 @@ +public class VariableBoundClamped { + private static int min(int a, int b) { return a < b ? a : b; } + + public static void main(String[] args) { + int len = 7; + int n = 100; // could be large + int[] arr = new int[len]; + int upto = min(n, arr.length); // clamp + for (int i = 0; i < upto; i++) { + if (i >= arr.length) { + System.out.println("UNREACHABLE: i>=len"); + } else { + // always true path + arr[i] = i; + } + } + System.out.println("done"); + } +} + diff --git a/docs/ai-absint/ConstantPropagationChecker.java b/docs/ai-absint/ConstantPropagationChecker.java new file mode 100644 index 000000000000..9c3b3e3495f8 --- /dev/null +++ b/docs/ai-absint/ConstantPropagationChecker.java @@ -0,0 +1,25 @@ +// Prototype ConstantPropagationChecker that consumes AbstractState-like maps and produces ConstantFacts. +// This is a sketch and should be adapted to your real Checker interface in substratevm. +package docs.ai.absint; + +import java.util.*; +import jdk.graal.compiler.nodes.Node; +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +public class ConstantPropagationChecker { + private final FactAPI.FactAggregator agg = new FactAPI.FactAggregator(); + + public void run(AnalysisMethod method, Map invariants) { + // invariants is a map from Node -> interval or boxed long if constant + for (var e : invariants.entrySet()) { + Node node = e.getKey(); + Object v = e.getValue(); + if (v instanceof Long l) { + agg.add(new FactAPI.ConstantFact(method, node, l)); + } + } + } + + public FactAPI.FactAggregator facts() { return agg; } +} + diff --git a/docs/ai-absint/tests/ArrayIndexing.java b/docs/ai-absint/tests/ArrayIndexing.java new file mode 100644 index 000000000000..cba4884d8a07 --- /dev/null +++ b/docs/ai-absint/tests/ArrayIndexing.java @@ -0,0 +1,25 @@ +public class ArrayIndexing { + private static int val; + private static int computed; + + public static void main(String[] args) { + int[] a = new int[5]; + for (int i = 0; i < a.length; i++) { + a[i] = i * 2; // 0,2,4,6,8 + } + + int idx = 2; + val = a[idx]; + + // index changed by branch + if (val > 3) { + idx = 4; + } else { + idx = 0; + } + + // boundary condition: use computed index + computed = (a[1] / 2) + 1; // (2/2)+1 = 2 + } +} + diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java index acb0be088563..030b020ae5d9 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java @@ -24,16 +24,18 @@ */ package com.oracle.graal.pointsto.util; -import com.oracle.graal.pointsto.reports.StatisticsPrinter; -import com.oracle.svm.util.ImageBuildStatistics; -import jdk.graal.compiler.debug.GraalError; -import org.graalvm.nativeimage.ImageSingletons; - import java.io.PrintWriter; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.graal.pointsto.reports.StatisticsPrinter; +import com.oracle.svm.util.ImageBuildStatistics; + +import jdk.graal.compiler.debug.GraalError; + public class TimerCollection implements ImageBuildStatistics.TimerCollectionPrinter { public static TimerCollection singleton() { @@ -51,6 +53,7 @@ public enum Registry { FEATURES("(features)"), VERIFY_HEAP("(verify)"), ANALYSIS("analysis"), + ABSTRACT_INTERPRETATION("abstract_interpretation"), UNIVERSE("universe"), COMPILE_TOTAL("compile"), PARSE("(parse)"), diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanAndDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanAndDomainTest.java new file mode 100644 index 000000000000..a862b1279b50 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanAndDomainTest.java @@ -0,0 +1,133 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.BooleanAndDomain; +import org.junit.Assert; +import org.junit.Test; + +public class BooleanAndDomainTest { + + @Test + public void testDefaultConstructor() { + BooleanAndDomain domain = new BooleanAndDomain(); + Assert.assertTrue(domain.getValue()); + } + + @Test + public void testValueConstructor() { + BooleanAndDomain domain = new BooleanAndDomain(false); + Assert.assertFalse(domain.getValue()); + } + + @Test + public void testSetToBot() { + BooleanAndDomain domain = new BooleanAndDomain(true); + domain.setToBot(); + Assert.assertFalse(domain.getValue()); + } + + @Test + public void testSetToTop() { + BooleanAndDomain domain = new BooleanAndDomain(false); + domain.setToTop(); + Assert.assertTrue(domain.getValue()); + } + + @Test + public void testJoinWith() { + BooleanAndDomain domain1 = new BooleanAndDomain(true); + BooleanAndDomain domain2 = new BooleanAndDomain(true); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(true); + domain1.joinWith(domain2); + Assert.assertFalse(domain1.getValue()); + + domain1 = new BooleanAndDomain(true); + domain2 = new BooleanAndDomain(false); + domain1.joinWith(domain2); + Assert.assertFalse(domain1.getValue()); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(false); + domain1.joinWith(domain2); + Assert.assertFalse(domain1.getValue()); + } + + @Test + public void testWidenWith() { + BooleanAndDomain domain1 = new BooleanAndDomain(true); + BooleanAndDomain domain2 = new BooleanAndDomain(false); + domain1.widenWith(domain2); + Assert.assertFalse(domain1.getValue()); + } + + @Test + public void testMeetWith() { + BooleanAndDomain domain1 = new BooleanAndDomain(true); + BooleanAndDomain domain2 = new BooleanAndDomain(false); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(true); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanAndDomain(true); + domain2 = new BooleanAndDomain(true); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(false); + domain1.meetWith(domain2); + Assert.assertFalse(domain1.getValue()); + } + + @Test + public void testEquals() { + BooleanAndDomain domain1 = new BooleanAndDomain(true); + BooleanAndDomain domain2 = new BooleanAndDomain(true); + Assert.assertEquals(domain1, domain2); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(false); + Assert.assertEquals(domain1, domain2); + + domain1 = new BooleanAndDomain(true); + domain2 = new BooleanAndDomain(false); + Assert.assertNotEquals(domain1, domain2); + + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(true); + Assert.assertNotEquals(domain1, domain2); + } + + @Test + public void testLeq() { + /* false > true, + * this holds because of the way join is defined on BooleanAndDomain + * false.joinWith(true) = false, therefore false >= true + */ + BooleanAndDomain domain1 = new BooleanAndDomain(false); + BooleanAndDomain domain2 = new BooleanAndDomain(true); + Assert.assertFalse(domain1.leq(domain2)); + + /* true < false */ + domain1 = new BooleanAndDomain(true); + domain2 = new BooleanAndDomain(false); + Assert.assertTrue(domain1.leq(domain2)); + + /* true <= true */ + domain1 = new BooleanAndDomain(true); + domain2 = new BooleanAndDomain(true); + Assert.assertTrue(domain1.leq(domain2)); + + /* false <= false */ + domain1 = new BooleanAndDomain(false); + domain2 = new BooleanAndDomain(false); + Assert.assertTrue(domain1.leq(domain2)); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanOrDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanOrDomainTest.java new file mode 100644 index 000000000000..45ed5429b234 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/BooleanOrDomainTest.java @@ -0,0 +1,131 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.BooleanOrDomain; +import org.junit.Assert; +import org.junit.Test; + +public class BooleanOrDomainTest { + + @Test + public void testDefaultConstructor() { + BooleanOrDomain domain = new BooleanOrDomain(); + Assert.assertFalse(domain.getValue()); + } + + @Test + public void testValueConstructor() { + BooleanOrDomain domain = new BooleanOrDomain(true); + Assert.assertTrue(domain.getValue()); + } + + @Test + public void testSetToBot() { + BooleanOrDomain domain = new BooleanOrDomain(true); + domain.setToBot(); + Assert.assertFalse(domain.getValue()); + } + + @Test + public void testSetToTop() { + BooleanOrDomain domain = new BooleanOrDomain(false); + domain.setToTop(); + Assert.assertTrue(domain.getValue()); + } + + @Test + public void testJoinWith() { + BooleanOrDomain domain1 = new BooleanOrDomain(true); + BooleanOrDomain domain2 = new BooleanOrDomain(true); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(true); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanOrDomain(true); + domain2 = new BooleanOrDomain(false); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(false); + domain1.joinWith(domain2); + Assert.assertFalse(domain1.getValue()); + } + + @Test + public void testWidenWith() { + BooleanOrDomain domain1 = new BooleanOrDomain(false); + BooleanOrDomain domain2 = new BooleanOrDomain(true); + domain1.widenWith(domain2); + Assert.assertTrue(domain1.getValue()); + } + + + @Test + public void testMeetWith() { + BooleanOrDomain domain1 = new BooleanOrDomain(true); + BooleanOrDomain domain2 = new BooleanOrDomain(false); + domain1.meetWith(domain2); + Assert.assertFalse(domain1.getValue()); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(true); + domain1.meetWith(domain2); + Assert.assertFalse(domain1.getValue()); + + domain1 = new BooleanOrDomain(true); + domain2 = new BooleanOrDomain(true); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.getValue()); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(false); + domain1.meetWith(domain2); + Assert.assertFalse(domain1.getValue()); + } + + @Test + public void testEquals() { + BooleanOrDomain domain1 = new BooleanOrDomain(true); + BooleanOrDomain domain2 = new BooleanOrDomain(true); + Assert.assertTrue(domain1.equals(domain2)); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(false); + Assert.assertTrue(domain1.equals(domain2)); + + domain1 = new BooleanOrDomain(true); + domain2 = new BooleanOrDomain(false); + Assert.assertNotEquals(domain1, domain2); + + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(true); + Assert.assertNotEquals(domain1, domain2); + } + + @Test + public void testLeq() { + /* false < true */ + BooleanOrDomain domain1 = new BooleanOrDomain(false); + BooleanOrDomain domain2 = new BooleanOrDomain(true); + Assert.assertTrue(domain1.leq(domain2)); + + /* true > false */ + domain1 = new BooleanOrDomain(true); + domain2 = new BooleanOrDomain(false); + Assert.assertFalse(domain1.leq(domain2)); + + /* true <= true */ + domain1 = new BooleanOrDomain(true); + domain2 = new BooleanOrDomain(true); + Assert.assertTrue(domain1.leq(domain2)); + + /* false <= false */ + domain1 = new BooleanOrDomain(false); + domain2 = new BooleanOrDomain(false); + Assert.assertTrue(domain1.leq(domain2)); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ConstantDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ConstantDomainTest.java new file mode 100644 index 000000000000..f7484b1dc29b --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ConstantDomainTest.java @@ -0,0 +1,114 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.ConstantDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; +import org.junit.Assert; +import org.junit.Test; + +public class ConstantDomainTest { + + @Test + public void testDefaultConstructor() { + ConstantDomain domain = new ConstantDomain<>(); + Assert.assertTrue(domain.isBot()); + } + + @Test + public void testValueConstructor() { + ConstantDomain domain = new ConstantDomain<>(5); + Assert.assertTrue(domain.isValue()); + Assert.assertEquals(Integer.valueOf(5), domain.getValue()); + } + + @Test + public void testKindConstructor() { + ConstantDomain domain = new ConstantDomain<>(AbstractValueKind.BOT); + Assert.assertTrue(domain.isBot()); + } + + @Test + public void testSetToBot() { + ConstantDomain domain = new ConstantDomain<>(5); + domain.setToBot(); + Assert.assertTrue(domain.isBot()); + } + + @Test + public void testSetToTop() { + ConstantDomain domain = new ConstantDomain<>(5); + domain.setToTop(); + Assert.assertTrue(domain.isTop()); + } + + @Test + public void testLeq() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain domain2 = new ConstantDomain<>(10); + Assert.assertFalse(domain1.leq(domain2)); + + domain2 = new ConstantDomain<>(5); + Assert.assertTrue(domain1.leq(domain2)); + + domain2 = new ConstantDomain<>(AbstractValueKind.TOP); + Assert.assertTrue(domain1.leq(domain2)); + + domain2 = new ConstantDomain<>(AbstractValueKind.BOT); + Assert.assertFalse(domain1.leq(domain2)); + } + + @Test + public void testEquals() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain domain2 = new ConstantDomain<>(5); + Assert.assertEquals(domain1, domain2); + + domain2 = new ConstantDomain<>(10); + Assert.assertNotEquals(domain1, domain2); + + domain2 = new ConstantDomain<>(AbstractValueKind.TOP); + Assert.assertNotEquals(domain1, domain2); + } + + @Test + public void testJoinWith() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain domain2 = new ConstantDomain<>(10); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.isTop()); + + domain1 = new ConstantDomain<>(5); + domain2 = new ConstantDomain<>(5); + domain1.joinWith(domain2); + Assert.assertTrue(domain1.isValue()); + Assert.assertEquals(Integer.valueOf(5), domain1.getValue()); + } + + @Test + public void testWidenWith() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain domain2 = new ConstantDomain<>(10); + domain1.widenWith(domain2); + Assert.assertTrue(domain1.isTop()); + } + + @Test + public void testMeetWith() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain domain2 = new ConstantDomain<>(10); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.isBot()); + + domain1 = new ConstantDomain<>(5); + domain2 = new ConstantDomain<>(5); + domain1.meetWith(domain2); + Assert.assertTrue(domain1.isValue()); + Assert.assertEquals(Integer.valueOf(5), domain1.getValue()); + } + + @Test + public void testCopyOf() { + ConstantDomain domain1 = new ConstantDomain<>(5); + ConstantDomain copy = domain1.copyOf(); + Assert.assertEquals(domain1, copy); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/IntIntervalDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/IntIntervalDomainTest.java new file mode 100644 index 000000000000..2fd26a546fed --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/IntIntervalDomainTest.java @@ -0,0 +1,211 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import org.junit.Assert; +import org.junit.Test; + +public class IntIntervalDomainTest { + + @Test + public void testDefaultConstructor() { + IntInterval interval = new IntInterval(); + Assert.assertTrue(interval.isBot()); + interval.setToTop(); + Assert.assertTrue(interval.isTop()); + } + + @Test + public void testConstantConstructor() { + IntInterval interval = new IntInterval(5); + Assert.assertEquals(5, interval.getLower()); + Assert.assertEquals(5, interval.getUpper()); + } + + @Test + public void testRangeConstructor() { + IntInterval interval = new IntInterval(3, 7); + Assert.assertEquals(3, interval.getLower()); + Assert.assertEquals(7, interval.getUpper()); + } + + @Test + public void testCopyConstructor() { + IntInterval original = new IntInterval(3, 7); + IntInterval copy = new IntInterval(original); + Assert.assertEquals(original.getLower(), copy.getLower()); + Assert.assertEquals(original.getUpper(), copy.getUpper()); + } + + @Test + public void testLeq() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + IntInterval interval3 = new IntInterval(1, 10); + Assert.assertFalse(interval1.leq(interval2)); + Assert.assertTrue(interval1.leq(interval3)); + Assert.assertTrue(interval2.leq(interval3)); + } + + @Test + public void testCopyOf() { + IntInterval interval = new IntInterval(1, 5); + IntInterval copy = interval.copyOf(); + Assert.assertEquals(interval, copy); + Assert.assertNotSame(interval, copy); + } + + @Test + public void testEquals() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(1, 5); + IntInterval interval3 = new IntInterval(2, 6); + Assert.assertEquals(interval1, interval2); + Assert.assertNotEquals(interval1, interval3); + } + + @Test + public void testJoin() { + /* Classic joining of two intervals */ + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + interval1.joinWith(interval2); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(7, interval1.getUpper()); + + /* Joining an interval with a BOT should not change the interval */ + interval1 = new IntInterval(1, 5); + IntInterval bottom = new IntInterval(); + interval1.joinWith(bottom); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(5, interval1.getUpper()); + + /* Joining an interval with a TOP should result in a TOP interval */ + interval1 = new IntInterval(1, 5); + IntInterval top = new IntInterval(); + top.setToTop(); + interval1.joinWith(top); + Assert.assertTrue(interval1.isTop()); + + /* Joining a bot with non-bot interval should result in the non-bot interval */ + interval1 = new IntInterval(); + interval2 = new IntInterval(1, 5); + interval1.joinWith(interval2); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(5, interval1.getUpper()); + } + + @Test + public void testWiden() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + interval1.widenWith(interval2); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(IntInterval.POS_INF, interval1.getUpper()); + + /* Widen with a BOT should not change the interval */ + interval1 = new IntInterval(1, 5); + IntInterval bottom = new IntInterval(); + interval1.widenWith(bottom); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(5, interval1.getUpper()); + + /* Widen with a TOP should result in a TOP interval */ + interval1 = new IntInterval(1, 5); + IntInterval top = new IntInterval(); + top.setToTop(); + interval1.widenWith(top); + Assert.assertTrue(interval1.isTop()); + + /* Widen a bot with non-bot interval should result in the non-bot interval */ + interval1 = new IntInterval(); + interval2 = new IntInterval(1, 5); + interval1.widenWith(interval2); + Assert.assertEquals(1, interval1.getLower()); + Assert.assertEquals(5, interval1.getUpper()); + } + + @Test + public void testMeet() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + interval1.meetWith(interval2); + Assert.assertEquals(3, interval1.getLower()); + Assert.assertEquals(5, interval1.getUpper()); + + /* Meet with a BOT should result in a BOT interval */ + interval1 = new IntInterval(1, 5); + IntInterval bottom = new IntInterval(); + bottom.setToBot(); + interval1.meetWith(bottom); + Assert.assertTrue(interval1.isBot()); + } + + @Test + public void testAdd() { + IntInterval interval1 = new IntInterval(1, 2); + IntInterval interval2 = new IntInterval(3, 4); + IntInterval result = interval1.add(interval2); + Assert.assertEquals(4, result.getLower()); + Assert.assertEquals(6, result.getUpper()); + + IntInterval interval3 = new IntInterval(-7, 4); + IntInterval interval4 = new IntInterval(-3, 6); + IntInterval expected = interval3.add(interval4); + Assert.assertEquals(-10, expected.getLower()); + Assert.assertEquals(10, expected.getUpper()); + } + + @Test + public void testSub() { + IntInterval interval1 = new IntInterval(5, 7); + IntInterval interval2 = new IntInterval(2, 3); + IntInterval result = interval1.sub(interval2); + Assert.assertEquals(3, result.getLower()); + Assert.assertEquals(4, result.getUpper()); + } + + @Test + public void testMul() { + IntInterval interval1 = new IntInterval(2, 3); + IntInterval interval2 = new IntInterval(4, 5); + IntInterval result = interval1.mul(interval2); + Assert.assertEquals(8, result.getLower()); + Assert.assertEquals(15, result.getUpper()); + } + + @Test + public void testDiv() { + IntInterval interval1 = new IntInterval(8, 10); + IntInterval interval2 = new IntInterval(2, 2); + IntInterval result = interval1.div(interval2); + Assert.assertEquals(4, result.getLower()); + Assert.assertEquals(5, result.getUpper()); + } + + @Test + public void testRem() { + IntInterval interval1 = new IntInterval(8, 10); + IntInterval interval2 = new IntInterval(3, 3); + IntInterval result = interval1.rem(interval2); + Assert.assertEquals(1, result.getLower()); + Assert.assertEquals(2, result.getUpper()); + } + + @Test + public void testGetLowerInterval() { + IntInterval interval = new IntInterval(4, 6); + IntInterval result = IntInterval.getLowerInterval(interval); + Assert.assertEquals(IntInterval.NEG_INF, result.getLower()); + Assert.assertEquals(3, result.getUpper()); + } + + @Test + public void testGetHigherInterval() { + IntInterval interval = new IntInterval(4, 6); + IntInterval result = IntInterval.getHigherInterval(interval); + Assert.assertEquals(7, result.getLower()); + Assert.assertEquals(IntInterval.POS_INF, result.getUpper()); + } + + +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/InvertedDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/InvertedDomainTest.java new file mode 100644 index 000000000000..143459c826ed --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/InvertedDomainTest.java @@ -0,0 +1,91 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.InvertedDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import org.junit.Assert; +import org.junit.Test; + +public class InvertedDomainTest { + + @Test + public void testDefaultConstructor() { + /* IntInterval is BOT when created with default constructor */ + IntInterval interval = new IntInterval(); + InvertedDomain invertedDomain = new InvertedDomain<>(interval); + Assert.assertTrue(invertedDomain.isTop()); + } + + @Test + public void testValueConstructor() { + IntInterval interval = new IntInterval(5, 10); + InvertedDomain invertedDomain = new InvertedDomain<>(interval); + Assert.assertEquals(interval, invertedDomain.domain()); + } + + @Test + public void testSetToBot() { + IntInterval interval = new IntInterval(5, 10); + InvertedDomain invertedDomain = new InvertedDomain<>(interval); + invertedDomain.setToBot(); + Assert.assertTrue(invertedDomain.isBot()); + } + + @Test + public void testSetToTop() { + IntInterval interval = new IntInterval(5, 10); + InvertedDomain invertedDomain = new InvertedDomain<>(interval); + invertedDomain.setToTop(); + Assert.assertTrue(invertedDomain.isTop()); + } + + @Test + public void testJoinWith() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + InvertedDomain invertedDomain1 = new InvertedDomain<>(interval1); + InvertedDomain invertedDomain2 = new InvertedDomain<>(interval2); + invertedDomain1.joinWith(invertedDomain2); + Assert.assertEquals(3, invertedDomain1.domain().getLower()); + Assert.assertEquals(5, invertedDomain1.domain().getUpper()); + } + + @Test + public void testMeetWith() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + InvertedDomain invertedDomain1 = new InvertedDomain<>(interval1); + InvertedDomain invertedDomain2 = new InvertedDomain<>(interval2); + invertedDomain1.meetWith(invertedDomain2); + Assert.assertEquals(1, invertedDomain1.domain().getLower()); + Assert.assertEquals(7, invertedDomain1.domain().getUpper()); + } + + @Test + public void testCopyOf() { + IntInterval interval = new IntInterval(5, 10); + InvertedDomain invertedDomain = new InvertedDomain<>(interval); + InvertedDomain copy = invertedDomain.copyOf(); + Assert.assertEquals(5, copy.domain().getLower()); + Assert.assertEquals(10, copy.domain().getUpper()); + } + + @Test + public void testEquals() { + IntInterval interval1 = new IntInterval(5, 10); + IntInterval interval2 = new IntInterval(5, 10); + InvertedDomain invertedDomain1 = new InvertedDomain<>(interval1); + InvertedDomain invertedDomain2 = new InvertedDomain<>(interval2); + Assert.assertEquals(invertedDomain1, invertedDomain2); + } + + @Test + public void testLeq() { + /* interval2 <= interval1, so in inverted domain, invertedDomain1 <= invertedDomain2 */ + IntInterval interval1 = new IntInterval(1, 10); + IntInterval interval2 = new IntInterval(3, 7); + InvertedDomain invertedDomain1 = new InvertedDomain<>(interval1); + InvertedDomain invertedDomain2 = new InvertedDomain<>(interval2); + Assert.assertTrue(invertedDomain1.leq(invertedDomain2)); + Assert.assertFalse(invertedDomain2.leq(invertedDomain1)); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/MapDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/MapDomainTest.java new file mode 100644 index 000000000000..df1222a9c9bc --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/MapDomainTest.java @@ -0,0 +1,208 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.util.BooleanAndDomain; +import com.oracle.svm.hosted.analysis.ai.domain.util.MapDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +class TestMapDomain> + extends MapDomain> { + + public TestMapDomain(Domain initialDomain) { + super(initialDomain); + } + + public TestMapDomain(Map map, Domain initialDomain) { + super(map, initialDomain); + } + + public TestMapDomain(TestMapDomain other) { + super(other); + } + + @Override + public TestMapDomain copyOf() { + return new TestMapDomain<>(this); + } +} + +public class MapDomainTest { + + @Test + public void testDefaultConstructor() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + Assert.assertTrue(mapDomain.isBot()); + Assert.assertTrue(mapDomain.get("x").isTop()); + Assert.assertEquals(0, mapDomain.getSize()); + } + + @Test + public void testPutAndGet() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.put("x", new IntInterval(1, 5)); + Assert.assertEquals(new IntInterval(1, 5), mapDomain.get("x")); + } + + @Test + public void testSetToBot() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.put("x", new IntInterval(1, 5)); + mapDomain.setToBot(); + Assert.assertTrue(mapDomain.isBot()); + Assert.assertEquals(0, mapDomain.getSize()); + + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain2.setToTop(); + mapDomain2.setToBot(); + Assert.assertTrue(mapDomain2.isBot()); + } + + @Test + public void testSetToTop() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.setToTop(); + Assert.assertTrue(mapDomain.isTop()); + } + + @Test + public void testJoinWith() { + TestMapDomain mapDomain1 = new TestMapDomain<>(new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain1.put("x", new IntInterval(1, 5)); + mapDomain2.put("x", new IntInterval(3, 7)); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain1.getKind()); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain2.getKind()); + mapDomain1.joinWith(mapDomain2); + Assert.assertEquals(new IntInterval(1, 7), mapDomain1.get("x")); + } + + @Test + public void testJoinWithNonEmptyDifference() { + TestMapDomain mapDomain1 = new TestMapDomain<>(new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain1.put("x", new IntInterval(1, 5)); + mapDomain2.put("y", new IntInterval(3, 7)); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain1.getKind()); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain2.getKind()); + mapDomain1.joinWith(mapDomain2); + Assert.assertEquals(new IntInterval(1, 5), mapDomain1.get("x")); + Assert.assertEquals(new IntInterval(3, 7), mapDomain1.get("y")); + } + + @Test + public void testWidenWith() { + TestMapDomain mapDomain1 = new TestMapDomain<>(new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain1.put("x", new IntInterval(1, 5)); + mapDomain2.put("x", new IntInterval(3, 7)); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain1.getKind()); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain2.getKind()); + mapDomain1.widenWith(mapDomain2); + Assert.assertEquals(new IntInterval(1, IntInterval.POS_INF), mapDomain1.get("x")); + } + + @Test + public void testMeetWith() { + TestMapDomain mapDomain1 = new TestMapDomain<>(new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain1.put("x", new IntInterval(1, 5)); + mapDomain2.put("x", new IntInterval(3, 7)); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain1.getKind()); + Assert.assertEquals(AbstractValueKind.VAL, mapDomain2.getKind()); + mapDomain1.meetWith(mapDomain2); + Assert.assertEquals(new IntInterval(3, 5), mapDomain1.get("x")); + } + + @Test + public void testEquals() { + Map map1 = new HashMap<>(); + map1.put("x", new IntInterval(1, 5)); + Map map2 = new HashMap<>(); + map2.put("x", new IntInterval(1, 5)); + TestMapDomain mapDomain1 = new TestMapDomain<>(map1, new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(map2, new IntInterval()); + Assert.assertEquals(mapDomain1, mapDomain2); + } + + @Test + public void testLeq() { + TestMapDomain mapDomain1 = new TestMapDomain<>(new IntInterval()); + TestMapDomain mapDomain2 = new TestMapDomain<>(new IntInterval()); + mapDomain1.put("x", new IntInterval(1, 5)); + mapDomain2.put("x", new IntInterval(1, 10)); + Assert.assertTrue(mapDomain1.leq(mapDomain2)); + Assert.assertFalse(mapDomain2.leq(mapDomain1)); + } + + @Test + public void testCopyOf() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.put("x", new IntInterval(1, 5)); + TestMapDomain copy = mapDomain.copyOf(); + Assert.assertEquals(mapDomain, copy); + Assert.assertNotSame(mapDomain, copy); + + TestMapDomain tmp = new TestMapDomain<>(new IntInterval()); + tmp.setToTop(); + TestMapDomain copy2 = tmp.copyOf(); + Assert.assertTrue(copy2.isTop()); + } + + @Test + public void testGetDomainAtKey() { + Map map = new HashMap<>(); + map.put("key1", new BooleanAndDomain(true)); + TestMapDomain mapDomain = new TestMapDomain<>(map, new BooleanAndDomain(false)); + + Assert.assertTrue(mapDomain.get("key1").getValue()); + BooleanAndDomain result = mapDomain.get("key2"); + Assert.assertTrue(result.isTop()); + } + + @Test + public void testRemove() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.put("x", new IntInterval(1, 5)); + mapDomain.remove("x"); + Assert.assertTrue(mapDomain.isBot()); + } + + @Test + public void testUpdate() { + TestMapDomain mapDomain = new TestMapDomain<>(new IntInterval()); + mapDomain.put("x", new IntInterval(1, 5)); + mapDomain.update(interval -> new IntInterval(interval.getLower() + 1, interval.getUpper() + 1), "x"); + Assert.assertEquals(new IntInterval(2, 6), mapDomain.get("x")); + } + + @Test + public void testTransform() { + Map map = new HashMap<>(); + map.put("key1", new BooleanAndDomain(true)); + map.put("key2", new BooleanAndDomain(false)); + TestMapDomain mapDomain = new TestMapDomain<>(map, new BooleanAndDomain(false)); + + mapDomain.transform(BooleanAndDomain::getNegated); + + Assert.assertFalse(mapDomain.get("key1").getValue()); + Assert.assertTrue(mapDomain.get("key2").getValue()); + } + + @Test + public void testRemoveIf() { + Map map = new HashMap<>(); + map.put("key1", new BooleanAndDomain(true)); + map.put("key2", new BooleanAndDomain(false)); + TestMapDomain mapDomain = new TestMapDomain<>(map, new BooleanAndDomain(false)); + + mapDomain.removeIf((entry) -> entry.getValue().getValue()); + Assert.assertTrue(mapDomain.get("key1").getValue()); + Assert.assertEquals(1, mapDomain.getSize()); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PairDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PairDomainTest.java new file mode 100644 index 000000000000..d446530d2170 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PairDomainTest.java @@ -0,0 +1,108 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.BooleanAndDomain; +import com.oracle.svm.hosted.analysis.ai.domain.composite.PairDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import org.junit.Assert; +import org.junit.Test; + +public class PairDomainTest { + + @Test + public void testDefaultConstructor() { + IntInterval interval = new IntInterval(); + BooleanAndDomain booleanDomain = new BooleanAndDomain(); + interval.setToBot(); + booleanDomain.setToBot(); + PairDomain pairDomain = new PairDomain<>(interval, booleanDomain); + Assert.assertTrue(pairDomain.isBot()); + } + + @Test + public void testValueConstructor() { + IntInterval interval = new IntInterval(5, 10); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + PairDomain pairDomain = new PairDomain<>(interval, booleanDomain); + Assert.assertEquals(interval, pairDomain.first()); + Assert.assertEquals(booleanDomain, pairDomain.second()); + } + + @Test + public void testSetToBot() { + IntInterval interval = new IntInterval(5, 10); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + PairDomain pairDomain = new PairDomain<>(interval, booleanDomain); + pairDomain.setToBot(); + Assert.assertTrue(pairDomain.isBot()); + } + + @Test + public void testSetToTop() { + IntInterval interval = new IntInterval(5, 10); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + PairDomain pairDomain = new PairDomain<>(interval, booleanDomain); + pairDomain.setToTop(); + Assert.assertTrue(pairDomain.isTop()); + } + + @Test + public void testJoinWith() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(false); + PairDomain pairDomain1 = new PairDomain<>(interval1, booleanDomain1); + PairDomain pairDomain2 = new PairDomain<>(interval2, booleanDomain2); + pairDomain1.joinWith(pairDomain2); + Assert.assertEquals(1, pairDomain1.first().getLower()); + Assert.assertEquals(7, pairDomain1.first().getUpper()); + Assert.assertFalse(pairDomain1.second().getValue()); + } + + @Test + public void testMeetWith() { + IntInterval interval1 = new IntInterval(1, 5); + IntInterval interval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(false); + PairDomain pairDomain1 = new PairDomain<>(interval1, booleanDomain1); + PairDomain pairDomain2 = new PairDomain<>(interval2, booleanDomain2); + pairDomain1.meetWith(pairDomain2); + Assert.assertEquals(3, pairDomain1.first().getLower()); + Assert.assertEquals(5, pairDomain1.first().getUpper()); + Assert.assertTrue(pairDomain1.second().getValue()); + } + + @Test + public void testCopyOf() { + IntInterval interval = new IntInterval(5, 10); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + PairDomain pairDomain = new PairDomain<>(interval, booleanDomain); + PairDomain copy = pairDomain.copyOf(); + Assert.assertEquals(pairDomain.first(), copy.first()); + Assert.assertEquals(pairDomain.second(), copy.second()); + } + + @Test + public void testEquals() { + IntInterval interval1 = new IntInterval(5, 10); + IntInterval interval2 = new IntInterval(5, 10); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(true); + PairDomain pairDomain1 = new PairDomain<>(interval1, booleanDomain1); + PairDomain pairDomain2 = new PairDomain<>(interval2, booleanDomain2); + Assert.assertEquals(pairDomain1, pairDomain2); + } + + @Test + public void testLeq() { + IntInterval interval1 = new IntInterval(1, 10); + IntInterval interval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(false); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(true); + PairDomain pairDomain1 = new PairDomain<>(interval1, booleanDomain1); + PairDomain pairDomain2 = new PairDomain<>(interval2, booleanDomain2); + Assert.assertFalse(pairDomain1.leq(pairDomain2)); + Assert.assertTrue(pairDomain2.leq(pairDomain1)); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ParityDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ParityDomainTest.java new file mode 100644 index 000000000000..4517eebf1caa --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ParityDomainTest.java @@ -0,0 +1,100 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.ParityDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.Parity; +import org.junit.Assert; +import org.junit.Test; + +public class ParityDomainTest { + + @Test + public void testDefaultConstructor() { + ParityDomain parityDomain = new ParityDomain(); + Assert.assertTrue(parityDomain.isBot()); + } + + @Test + public void testParityConstructor() { + ParityDomain parityDomain = new ParityDomain(Parity.ODD); + Assert.assertEquals(Parity.ODD, parityDomain.getState()); + + ParityDomain botDomain = new ParityDomain(Parity.BOT); + Assert.assertTrue(botDomain.isBot()); + + ParityDomain topDomain = new ParityDomain(Parity.TOP); + Assert.assertTrue(topDomain.isTop()); + } + + @Test + public void testCopyConstructor() { + ParityDomain original = new ParityDomain(Parity.EVEN); + ParityDomain copy = new ParityDomain(original); + Assert.assertEquals(original.getState(), copy.getState()); + } + + @Test + public void testSetToBot() { + ParityDomain parityDomain = new ParityDomain(Parity.ODD); + parityDomain.setToBot(); + Assert.assertTrue(parityDomain.isBot()); + } + + @Test + public void testSetToTop() { + ParityDomain parityDomain = new ParityDomain(Parity.EVEN); + parityDomain.setToTop(); + Assert.assertTrue(parityDomain.isTop()); + } + + @Test + public void testJoinWith() { + ParityDomain parityDomain1 = new ParityDomain(Parity.ODD); + ParityDomain parityDomain2 = new ParityDomain(Parity.EVEN); + parityDomain1.joinWith(parityDomain2); + Assert.assertTrue(parityDomain1.isTop()); + } + + @Test + public void testWidenWith() { + ParityDomain parityDomain1 = new ParityDomain(Parity.ODD); + ParityDomain parityDomain2 = new ParityDomain(Parity.EVEN); + parityDomain1.widenWith(parityDomain2); + Assert.assertTrue(parityDomain1.isTop()); + } + + @Test + public void testMeetWith() { + ParityDomain parityDomain1 = new ParityDomain(Parity.ODD); + ParityDomain parityDomain2 = new ParityDomain(Parity.ODD); + parityDomain1.meetWith(parityDomain2); + Assert.assertEquals(Parity.ODD, parityDomain1.getState()); + + parityDomain1 = new ParityDomain(Parity.ODD); + parityDomain2 = new ParityDomain(Parity.EVEN); + parityDomain1.meetWith(parityDomain2); + Assert.assertTrue(parityDomain1.isBot()); + } + + @Test + public void testEquals() { + ParityDomain parityDomain1 = new ParityDomain(Parity.ODD); + ParityDomain parityDomain2 = new ParityDomain(Parity.ODD); + Assert.assertEquals(parityDomain1, parityDomain2); + + parityDomain2 = new ParityDomain(Parity.EVEN); + Assert.assertNotEquals(parityDomain1, parityDomain2); + } + + @Test + public void testLeq() { + ParityDomain parityDomain1 = new ParityDomain(Parity.ODD); + ParityDomain parityDomain2 = new ParityDomain(Parity.TOP); + Assert.assertTrue(parityDomain1.leq(parityDomain2)); + + parityDomain2 = new ParityDomain(Parity.BOT); + Assert.assertFalse(parityDomain1.leq(parityDomain2)); + + parityDomain2 = new ParityDomain(Parity.ODD); + Assert.assertTrue(parityDomain1.leq(parityDomain2)); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PentagonDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PentagonDomainTest.java new file mode 100644 index 000000000000..bba5289d6533 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/PentagonDomainTest.java @@ -0,0 +1,133 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.PentagonDomain; +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for the Pentagon domain implementation. + */ +public class PentagonDomainTest { + + @Test + public void testBasicOperations() { + // Create two pentagon domains + PentagonDomain domain1 = new PentagonDomain<>(); + PentagonDomain domain2 = new PentagonDomain<>(); + + // Set up intervals + domain1.setInterval("x", new IntInterval(0, 10)); + domain1.setInterval("y", new IntInterval(5, 15)); + + domain2.setInterval("x", new IntInterval(5, 15)); + domain2.setInterval("y", new IntInterval(10, 20)); + + // Set up less-than relations + // y should be in the range [11, 15] because of this relation + domain1.addLessThanRelation("x", "y"); + Assert.assertEquals(new IntInterval(11, 15), domain1.getInterval("y")); + + // Test join + PentagonDomain joined = domain1.join(domain2); + Assert.assertEquals(new IntInterval(0, 15), joined.getInterval("x")); + // y is refined + Assert.assertEquals(new IntInterval(10, 20), joined.getInterval("y")); + // Less-than relation is maintained only if present in both domains + Assert.assertFalse(joined.lessThan("x", "y")); + + // Test meet + PentagonDomain met = domain1.meet(domain2); + Assert.assertEquals(new IntInterval(), met.getInterval("x")); + Assert.assertEquals(new IntInterval(11, 15), met.getInterval("y")); + Assert.assertFalse(met.lessThan("x", "y")); + + // Test reduction + PentagonDomain reduced = new PentagonDomain<>(); + reduced.setInterval("x", new IntInterval(0, 100)); + reduced.setInterval("y", new IntInterval(0, 100)); + reduced.addLessThanRelation("x", "y"); + + // After reduction, x interval should be refined to [0, 99] due to x < y + Assert.assertTrue(reduced.getInterval("x").isBot()); + // After reduction, y interval should be refined to [1, 100] due to x < y + Assert.assertEquals(new IntInterval(0, 100), reduced.getInterval("y")); + } + + @Test + public void testWidening() { + PentagonDomain domain1 = new PentagonDomain<>(); + PentagonDomain domain2 = new PentagonDomain<>(); + + domain1.setInterval("i", new IntInterval(0, 10)); + domain2.setInterval("i", new IntInterval(0, 20)); + + // Widening should accelerate convergence + PentagonDomain widened = domain1.widen(domain2); + Assert.assertEquals(new IntInterval(0, IntInterval.POS_INF), widened.getInterval("i")); + } + + @Test + public void testTransitivity() { + PentagonDomain domain = new PentagonDomain<>(); + + domain.setInterval("x", new IntInterval(0, 100)); + domain.setInterval("y", new IntInterval(0, 100)); + domain.setInterval("z", new IntInterval(0, 100)); + + /* These two relations make x and y the BOT */ + domain.addLessThanRelation("x", "y"); + domain.addLessThanRelation("y", "z"); + + // Transitivity should be applied automatically during reduction + Assert.assertTrue(domain.lessThan("x", "z")); + + // Check interval refinement through transitive relations + Assert.assertTrue(domain.getInterval("x").isBot()); + Assert.assertTrue(domain.getInterval("y").isBot()); + Assert.assertEquals(new IntInterval(0, 100), domain.getInterval("z")); + } + + @Test + public void testContradictoryJoinRemovesRelation() { + PentagonDomain d1 = new PentagonDomain<>(); + d1.setInterval("x", new IntInterval(0, 10)); + d1.setInterval("y", new IntInterval(20, 30)); + d1.addLessThanRelation("x", "y"); + + PentagonDomain d2 = new PentagonDomain<>(); + d2.setInterval("x", new IntInterval(25, 35)); + d2.setInterval("y", new IntInterval(0, 5)); + d2.addLessThanRelation("y", "x"); + + d1.joinWith(d2); + + Assert.assertFalse(d1.lessThan("x", "y")); + Assert.assertFalse(d1.lessThan("y", "x")); + } + + @Test + public void testMeetWithBot() { + PentagonDomain domain1 = new PentagonDomain<>(); + PentagonDomain domain2 = new PentagonDomain<>(); + + domain1.setInterval("a", new IntInterval(0, 50)); + domain2.setInterval("a", new IntInterval(60, 100)); + domain1.addLessThanRelation("a", "a"); // force an unsatisfiable constraint + + domain1.meetWith(domain2); + Assert.assertTrue(domain1.getInterval("a").isBot()); + } + + @Test + public void testSetToTopAndBot() { + PentagonDomain domain = new PentagonDomain<>(); + domain.setInterval("b", new IntInterval(10, 20)); + domain.addLessThanRelation("b", "b"); // a contradictory relation + Assert.assertTrue(domain.isBot()); + domain.setToTop(); + Assert.assertTrue(domain.isTop()); + domain.setToBot(); + Assert.assertTrue(domain.isBot()); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ProductDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ProductDomainTest.java new file mode 100644 index 000000000000..d54d56c0cea1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ProductDomainTest.java @@ -0,0 +1,127 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.BooleanAndDomain; +import com.oracle.svm.hosted.analysis.ai.domain.composite.ProductDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class ProductDomainTest { + + @Test + public void testDefaultConstructor() { + ProductDomain productDomain = new ProductDomain(); + Assert.assertTrue(productDomain.isBot()); + } + + @Test + public void testConstructorWithDomains() { + IntInterval intInterval = new IntInterval(1, 5); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + ProductDomain productDomain = new ProductDomain(Arrays.asList(intInterval, booleanDomain)); + Assert.assertFalse(productDomain.isBot()); + } + + @Test + public void testCopyConstructor() { + IntInterval intInterval = new IntInterval(1, 5); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + ProductDomain productDomain = new ProductDomain(Arrays.asList(intInterval, booleanDomain)); + ProductDomain copy = new ProductDomain(productDomain); + Assert.assertTrue(productDomain.equals(copy)); + Assert.assertNotSame(productDomain, copy); + } + + @Test + public void testLeq() { + Assert.assertEquals(new IntInterval(1, 2), new IntInterval(1, 2)); + IntInterval intInterval1 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + ProductDomain productDomain1 = new ProductDomain(Arrays.asList(intInterval1, booleanDomain1)); + + IntInterval intInterval2 = new IntInterval(1, 10); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(true); + ProductDomain productDomain2 = new ProductDomain(Arrays.asList(intInterval2, booleanDomain2)); + + Assert.assertTrue(productDomain1.leq(productDomain2)); + Assert.assertFalse(productDomain2.leq(productDomain1)); + } + + @Test + public void testEquals() { + IntInterval intInterval1 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + ProductDomain productDomain1 = new ProductDomain(Arrays.asList(intInterval1, booleanDomain1)); + + IntInterval intInterval2 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(true); + ProductDomain productDomain2 = new ProductDomain(Arrays.asList(intInterval2, booleanDomain2)); + Assert.assertEquals(productDomain1, productDomain2); + Assert.assertTrue(productDomain1.equals(productDomain2)); + } + + @Test + public void testSetToBot() { + IntInterval intInterval = new IntInterval(1, 5); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + ProductDomain productDomain = new ProductDomain(Arrays.asList(intInterval, booleanDomain)); + productDomain.setToBot(); + Assert.assertTrue(productDomain.isBot()); + } + + @Test + public void testSetToTop() { + IntInterval intInterval = new IntInterval(1, 5); + BooleanAndDomain booleanDomain = new BooleanAndDomain(true); + ProductDomain productDomain = new ProductDomain(Arrays.asList(intInterval, booleanDomain)); + productDomain.setToTop(); + Assert.assertTrue(productDomain.isTop()); + } + + @Test + public void testJoinWith() { + IntInterval intInterval1 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + ProductDomain productDomain1 = new ProductDomain(Arrays.asList(intInterval1, booleanDomain1)); + + IntInterval intInterval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(false); + ProductDomain productDomain2 = new ProductDomain(Arrays.asList(intInterval2, booleanDomain2)); + + productDomain1.joinWith(productDomain2); + Assert.assertEquals(new IntInterval(1, 7), productDomain1.getDomains().get(0)); + Assert.assertFalse(((BooleanAndDomain) productDomain1.getDomains().get(1)).getValue()); + } + + @Test + public void testWidenWith() { + IntInterval intInterval1 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + ProductDomain productDomain1 = new ProductDomain(Arrays.asList(intInterval1, booleanDomain1)); + + IntInterval intInterval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(false); + ProductDomain productDomain2 = new ProductDomain(Arrays.asList(intInterval2, booleanDomain2)); + + productDomain1.widenWith(productDomain2); + Assert.assertEquals(new IntInterval(1, IntInterval.POS_INF), productDomain1.getDomains().get(0)); + Assert.assertFalse(((BooleanAndDomain) productDomain1.getDomains().get(1)).getValue()); + } + + @Test + public void testMeetWith() { + IntInterval intInterval1 = new IntInterval(1, 5); + BooleanAndDomain booleanDomain1 = new BooleanAndDomain(true); + ProductDomain productDomain1 = new ProductDomain(Arrays.asList(intInterval1, booleanDomain1)); + + IntInterval intInterval2 = new IntInterval(3, 7); + BooleanAndDomain booleanDomain2 = new BooleanAndDomain(false); + ProductDomain productDomain2 = new ProductDomain(Arrays.asList(intInterval2, booleanDomain2)); + + productDomain1.meetWith(productDomain2); + Assert.assertEquals(new IntInterval(3, 5), productDomain1.getDomains().get(0)); + Assert.assertTrue(((BooleanAndDomain) productDomain1.getDomains().get(1)).getValue()); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ReducedProductDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ReducedProductDomainTest.java new file mode 100644 index 000000000000..aae0171d5fae --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/ReducedProductDomainTest.java @@ -0,0 +1,170 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.composite.reducedproduct.ReducedProductDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.SignDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.Sign; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class ReducedProductDomainTest { + + /** + * Implementation of ReducedProductDomain that combines interval and sign domains. + */ + static class IntervalSignDomain extends ReducedProductDomain { + + private IntervalSignDomain(List> domains, List reducers) { + super(domains, reducers); + } + + private IntervalSignDomain(IntervalSignDomain other) { + super(other); + } + + @Override + public IntervalSignDomain copyOf() { + return new IntervalSignDomain(this); + } + + public IntInterval getInterval() { + return getDomain(0); + } + + public SignDomain getSign() { + return (SignDomain) getDomain(1); + } + + /** + * Creates an IntervalSignReducedProductDomain with the given interval and sign domains. + */ + public static IntervalSignDomain create(IntInterval interval, SignDomain sign) { + return new Builder<>(IntervalSignDomain::new) + .withDomains(interval, sign) + .withReducer(domains -> { + IntInterval intervalDomain = (IntInterval) domains.get(0); + SignDomain signDomain = (SignDomain) domains.get(1); + + // Refine interval based on sign + long lowerBound = intervalDomain.getLower(); + long upperBound = intervalDomain.getUpper(); + boolean changed = false; + + if (signDomain.getState() == Sign.POS) { + if (lowerBound < 1) { + lowerBound = 1; + changed = true; + } + } else if (signDomain.getState() == Sign.NEG) { + if (upperBound > -1) { + upperBound = -1; + changed = true; + } + } else if (signDomain.getState() == Sign.ZERO) { + if (lowerBound != 0 || upperBound != 0) { + lowerBound = 0; + upperBound = 0; + changed = true; + } + } + + // Create new interval if bounds changed + if (changed) { + domains.set(0, new IntInterval(lowerBound, upperBound)); + intervalDomain = (IntInterval) domains.getFirst(); + } + + // Refine sign based on interval + if (intervalDomain.getLower() > 0) { + signDomain.setState(Sign.POS); + } else if (intervalDomain.getUpper() < 0) { + signDomain.setState(Sign.NEG); + } else { + if (intervalDomain.getLower() == 0) { + if (intervalDomain.getUpper() == 0) { + signDomain.setState(Sign.ZERO); + } + } + } + }) + .build(); + } + } + + @Test + public void testIntervalRefinedBySign() { + IntInterval interval = new IntInterval(-10, 10); + SignDomain sign = new SignDomain(Sign.POS); + + IntervalSignDomain domain = IntervalSignDomain.create(interval, sign); + + // Since sign is positive, interval should be refined to [1, 10] + Assert.assertEquals(1, domain.getInterval().getLower()); + Assert.assertEquals(10, domain.getInterval().getUpper()); + } + + @Test + public void testSignRefinedByInterval() { + IntInterval interval = new IntInterval(5, 10); + SignDomain sign = new SignDomain(Sign.TOP); + + IntervalSignDomain domain = IntervalSignDomain.create(interval, sign); + + // Since interval is [5, 10], sign should be refined to positive + Assert.assertEquals(Sign.POS, domain.getSign().getState()); + } +// +// @Test +// public void testJoinWith() { +// IntervalSignReducedProductDomain domain1 = IntervalSignReducedProductDomain.create( +// new IntInterval(1, 5), new SignDomain(Sign.POS)); +// IntervalSignReducedProductDomain domain2 = IntervalSignReducedProductDomain.create( +// new IntInterval(-3, -1), new SignDomain(Sign.NEG)); +// +// domain1.joinWith(domain2); +// +// +// Assert.assertEquals(5, domain1.getInterval().getUpperBound()); +// Assert.assertEquals(Sign.TOP, domain1.getSign().getState()); +// } + + @Test + public void testMeetWith() { + IntervalSignDomain domain1 = IntervalSignDomain.create( + new IntInterval(-5, 10), new SignDomain(Sign.TOP)); + IntervalSignDomain domain2 = IntervalSignDomain.create( + new IntInterval(-10, 7), new SignDomain(Sign.POS)); + + domain1.meetWith(domain2); + + // Meeting [-5,10] and [-10,7] should result in [-5,7] + Assert.assertEquals(1, domain1.getInterval().getLower()); + Assert.assertEquals(7, domain1.getInterval().getUpper()); + + // Meeting TOP and POS should result in POS + // Additionally, the reducer should refine the interval to [1,7] + Assert.assertEquals(Sign.POS, domain1.getSign().getState()); + Assert.assertEquals(1, domain1.getInterval().getLower()); + } + + @Test + public void testZeroHandling() { + IntInterval interval = new IntInterval(0, 0); + SignDomain sign = new SignDomain(Sign.TOP); + + IntervalSignDomain domain = IntervalSignDomain.create(interval, sign); + + // Interval [0,0] should refine sign to ZERO + Assert.assertEquals(Sign.ZERO, domain.getSign().getState()); + + // Test the opposite direction + domain = IntervalSignDomain.create(new IntInterval(-10, 10), new SignDomain(Sign.ZERO)); + + // Sign ZERO should refine interval to [0,0] + Assert.assertEquals(0, domain.getInterval().getLower()); + Assert.assertEquals(0, domain.getInterval().getUpper()); + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SetDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SetDomainTest.java new file mode 100644 index 000000000000..489d95a05258 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SetDomainTest.java @@ -0,0 +1,175 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.util.SetDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; +import org.junit.Assert; +import org.junit.Test; + +public class SetDomainTest { + + @Test + public void testDefaultConstructor() { + SetDomain setDomain = new SetDomain<>(); + Assert.assertTrue(setDomain.empty()); + Assert.assertEquals(AbstractValueKind.BOT, setDomain.getKind()); + SetDomain setDomain2 = new SetDomain<>(setDomain); + Assert.assertTrue(setDomain.leq(setDomain2)); + } + + @Test + public void testAdd() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("element"); + Assert.assertFalse(setDomain.empty()); + Assert.assertTrue(setDomain.getSet().contains("element")); + Assert.assertEquals(AbstractValueKind.VAL, setDomain.getKind()); + } + + @Test + public void testRemove() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("element"); + setDomain.remove("element"); + Assert.assertTrue(setDomain.empty()); + Assert.assertFalse(setDomain.getSet().contains("element")); + Assert.assertEquals(AbstractValueKind.BOT, setDomain.getKind()); + } + + @Test + public void testClear() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("a"); + setDomain.add("b"); + setDomain.clear(); + Assert.assertTrue(setDomain.empty()); + Assert.assertFalse(setDomain.getSet().contains("element")); + Assert.assertEquals(AbstractValueKind.BOT, setDomain.getKind()); + } + + @Test + public void testClearThenAdd() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("a"); + setDomain.add("b"); + setDomain.clear(); + setDomain.add("c"); + Assert.assertFalse(setDomain.empty()); + Assert.assertTrue(setDomain.getSet().contains("c")); + Assert.assertEquals(AbstractValueKind.VAL, setDomain.getKind()); + } + + @Test + public void testUnionWith() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.unionWith(setDomain2); + Assert.assertTrue(setDomain1.getSet().contains("element1")); + Assert.assertTrue(setDomain1.getSet().contains("element2")); + } + + @Test + public void testIntersectionWith() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + setDomain1.add("element2"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.intersectionWith(setDomain2); + Assert.assertFalse(setDomain1.getSet().contains("element1")); + Assert.assertTrue(setDomain1.getSet().contains("element2")); + } + + @Test + public void testDifferenceWith() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + setDomain1.add("element2"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.differenceWith(setDomain2); + Assert.assertTrue(setDomain1.getSet().contains("element1")); + Assert.assertFalse(setDomain1.getSet().contains("element2")); + } + + @Test + public void testFilter() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("element1"); + setDomain.add("element2"); + setDomain.filter(e -> e.equals("element1")); + Assert.assertTrue(setDomain.getSet().contains("element1")); + Assert.assertFalse(setDomain.getSet().contains("element2")); + } + + @Test + public void testCopyOf() { + SetDomain setDomain = new SetDomain<>(); + setDomain.add("element"); + SetDomain copy = setDomain.copyOf(); + Assert.assertEquals(setDomain, copy); + Assert.assertNotSame(setDomain, copy); + } + + @Test + public void testEquals() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element"); + Assert.assertEquals(setDomain1, setDomain2); + } + + @Test + public void testSetToTop() { + SetDomain setDomain = new SetDomain<>(); + setDomain.setToTop(); + Assert.assertTrue(setDomain.isTop()); + } + + @Test + public void testLeq() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element1"); + setDomain2.add("element2"); + Assert.assertTrue(setDomain1.leq(setDomain2)); + Assert.assertFalse(setDomain2.leq(setDomain1)); + } + + @Test + public void testJoin() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.joinWith(setDomain2); + Assert.assertTrue(setDomain1.getSet().contains("element1")); + Assert.assertTrue(setDomain1.getSet().contains("element2")); + } + + @Test + public void testWiden() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.widenWith(setDomain2); + Assert.assertTrue(setDomain1.getSet().contains("element1")); + Assert.assertTrue(setDomain1.getSet().contains("element2")); + } + + @Test + public void testMeet() { + SetDomain setDomain1 = new SetDomain<>(); + setDomain1.add("element1"); + setDomain1.add("element2"); + SetDomain setDomain2 = new SetDomain<>(); + setDomain2.add("element2"); + setDomain1.meetWith(setDomain2); + Assert.assertFalse(setDomain1.getSet().contains("element1")); + Assert.assertTrue(setDomain1.getSet().contains("element2")); + } +} diff --git a/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SignDomainTest.java b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SignDomainTest.java new file mode 100644 index 000000000000..93b4dae79ac9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.graal.test/src/com/oracle/svm/graal/test/ai/domain/SignDomainTest.java @@ -0,0 +1,101 @@ +package com.oracle.svm.graal.test.ai.domain; + +import com.oracle.svm.hosted.analysis.ai.domain.numerical.SignDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.Sign; +import org.junit.Assert; +import org.junit.Test; + +public class SignDomainTest { + + @Test + public void testDefaultConstructor() { + SignDomain signDomain = new SignDomain(); + Assert.assertTrue(signDomain.isBot()); + } + + @Test + public void testSignConstructor() { + SignDomain signDomain = new SignDomain(Sign.POS); + Assert.assertEquals(Sign.POS, signDomain.getState()); + + SignDomain botDomain = new SignDomain(Sign.BOT); + Assert.assertTrue(botDomain.isBot()); + + SignDomain topDomain = new SignDomain(Sign.TOP); + Assert.assertTrue(topDomain.isTop()); + } + + @Test + public void testCopyConstructor() { + SignDomain original = new SignDomain(Sign.NEG); + SignDomain copy = new SignDomain(original); + Assert.assertEquals(original.getState(), copy.getState()); + } + + @Test + public void testSetToBot() { + SignDomain signDomain = new SignDomain(Sign.POS); + signDomain.setToBot(); + Assert.assertTrue(signDomain.isBot()); + } + + @Test + public void testSetToTop() { + SignDomain signDomain = new SignDomain(Sign.NEG); + signDomain.setToTop(); + Assert.assertTrue(signDomain.isTop()); + } + + @Test + public void testJoinWith() { + SignDomain signDomain1 = new SignDomain(Sign.POS); + SignDomain signDomain2 = new SignDomain(Sign.NEG); + signDomain1.joinWith(signDomain2); + Assert.assertTrue(signDomain1.isTop()); + } + + @Test + public void testWidenWith() { + SignDomain signDomain1 = new SignDomain(Sign.POS); + SignDomain signDomain2 = new SignDomain(Sign.NEG); + signDomain1.widenWith(signDomain2); + Assert.assertTrue(signDomain1.isTop()); + } + + @Test + public void testMeetWith() { + SignDomain signDomain1 = new SignDomain(Sign.POS); + SignDomain signDomain2 = new SignDomain(Sign.POS); + signDomain1.meetWith(signDomain2); + Assert.assertEquals(Sign.POS, signDomain1.getState()); + + /* Meet with a different sign results in BOT */ + signDomain1 = new SignDomain(Sign.POS); + signDomain2 = new SignDomain(Sign.NEG); + signDomain1.meetWith(signDomain2); + Assert.assertTrue(signDomain1.isBot()); + } + + @Test + public void testEquals() { + SignDomain signDomain1 = new SignDomain(Sign.POS); + SignDomain signDomain2 = new SignDomain(Sign.POS); + Assert.assertEquals(signDomain1, signDomain2); + + signDomain2 = new SignDomain(Sign.NEG); + Assert.assertNotEquals(signDomain1, signDomain2); + } + + @Test + public void testLeq() { + SignDomain signDomain1 = new SignDomain(Sign.POS); + SignDomain signDomain2 = new SignDomain(Sign.TOP); + Assert.assertTrue(signDomain1.leq(signDomain2)); + + signDomain2 = new SignDomain(Sign.BOT); + Assert.assertFalse(signDomain1.leq(signDomain2)); + + signDomain2 = new SignDomain(Sign.POS); + Assert.assertTrue(signDomain1.leq(signDomain2)); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index efb7a0a1e459..fdb142b19f52 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -55,6 +55,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import com.oracle.svm.hosted.analysis.ai.AbstractInterpretationDriver; import org.graalvm.collections.EconomicSet; import org.graalvm.nativeimage.ImageInfo; import org.graalvm.nativeimage.ImageSingletons; @@ -80,6 +81,7 @@ import org.graalvm.nativeimage.impl.SizeOfSupport; import org.graalvm.word.PointerBase; +import com.oracle.svm.hosted.analysis.ai.AIFOptions; import com.oracle.graal.pointsto.AnalysisObjectScanningObserver; import com.oracle.graal.pointsto.AnalysisPolicy; import com.oracle.graal.pointsto.BigBang; @@ -420,7 +422,7 @@ public static Platform getTargetPlatform(ClassLoader classLoader) { return loadPlatform(classLoader, platformClassName); } catch (ClassNotFoundException ex) { throw UserError.abort("Could not find platform class %s that was specified explicitly on the command line using the system property %s", - platformClassName, Platform.PLATFORM_PROPERTY_NAME); + platformClassName, Platform.PLATFORM_PROPERTY_NAME); } } @@ -557,7 +559,7 @@ public void run(Map entryPoints, ResolvedJavaType.class.getDeclaredMethod("link"); } catch (ReflectiveOperationException ex) { throw UserError.abort("JVMCI version provided %s is missing the 'ResolvedJavaType.link()' method added in jvmci-20.2-b01. " + - "Please use the latest JVMCI JDK from %s.", System.getProperty("java.home"), OPEN_LABSJDK_RELEASE_URL_PATTERN); + "Please use the latest JVMCI JDK from %s.", System.getProperty("java.home"), OPEN_LABSJDK_RELEASE_URL_PATTERN); } var hostedOptionValues = new HostedOptionValues(optionProvider.getHostedValues()); @@ -600,7 +602,7 @@ public void run(Map entryPoints, protected static void setSystemPropertiesForImageEarly() { VMError.guarantee(ImageInfo.inImageBuildtimeCode(), "Expected ImageInfo.inImageBuildtimeCode() to return true"); VMError.guarantee(NativeImageSupport.inBuildtimeCode(), - "ImageInfo.inImageBuildtimeCode() and NativeImageSupport.inBuildtimeCode() are not in sync"); + "ImageInfo.inImageBuildtimeCode() and NativeImageSupport.inBuildtimeCode() are not in sync"); } private static void setSystemPropertiesForImageLate(NativeImageKind imageKind) { @@ -630,7 +632,7 @@ protected void doRun(Map entryPoints, JavaM OptionValues options = HostedOptionValues.singleton(); try (DebugContext debug = new Builder(options, new GraalDebugHandlersFactory(GuestAccess.get().getSnippetReflection())).build(); - DebugCloseable _ = () -> featureHandler.forEachFeature(Feature::cleanup)) { + DebugCloseable _ = () -> featureHandler.forEachFeature(Feature::cleanup)) { setupNativeImage(options, entryPoints, javaMainSupport, imageName, harnessSubstitutions, debug); boolean returnAfterAnalysis = runPointsToAnalysis(imageName, options, debug); @@ -657,19 +659,19 @@ protected void doRun(Map entryPoints, JavaM featureHandler.forEachFeature(feature -> feature.beforeUniverseBuilding(beforeUniverseBuildingConfig)); new UniverseBuilder(aUniverse, bb.getMetaAccess(), hUniverse, hMetaAccess, HostedConfiguration.instance().createStrengthenGraphs(bb, hUniverse), - bb.getUnsupportedFeatures()).build(debug); + bb.getUnsupportedFeatures()).build(debug); BuildPhaseProvider.markHostedUniverseBuilt(); ClassInitializationSupport classInitializationSupport = bb.getHostVM().getClassInitializationSupport(); SubstratePlatformConfigurationProvider platformConfig = getPlatformConfig(hMetaAccess); runtimeConfiguration = new HostedRuntimeConfigurationBuilder(options, bb.getHostVM(), hUniverse, hMetaAccess, - bb.getProviders(MethodVariant.ORIGINAL_METHOD), classInitializationSupport, platformConfig, - bb.getSnippetReflectionProvider()).build(); + bb.getProviders(MethodVariant.ORIGINAL_METHOD), classInitializationSupport, platformConfig, + bb.getSnippetReflectionProvider()).build(); registerGraphBuilderPlugins(featureHandler, runtimeConfiguration, (HostedProviders) runtimeConfiguration.getProviders(), bb.getMetaAccess(), aUniverse, - nativeLibraries, loader, ParsingReason.AOTCompilation, bb.getAnnotationSubstitutionProcessor(), - new SubstrateClassInitializationPlugin((SVMHost) aUniverse.hostVM()), - ConfigurationValues.getTarget(), this.isStubBasedPluginsSupported()); + nativeLibraries, loader, ParsingReason.AOTCompilation, bb.getAnnotationSubstitutionProcessor(), + new SubstrateClassInitializationPlugin((SVMHost) aUniverse.hostVM()), + ConfigurationValues.getTarget(), this.isStubBasedPluginsSupported()); if (NativeImageOptions.PrintUniverse.getValue()) { hUniverse.printTypes(); @@ -703,6 +705,8 @@ protected void doRun(Map entryPoints, JavaM throw UserError.abort(ufe, "%s", ufe.getMessage()); } + runAbstractInterpretation(debug, options); + var hConstantReflection = (HostedConstantReflectionProvider) runtimeConfiguration.getProviders().getConstantReflection(); heap = new NativeImageHeap(aUniverse, hUniverse, hMetaAccess, hConstantReflection, ImageSingletons.lookup(ImageHeapLayouter.class)); @@ -730,14 +734,14 @@ protected void doRun(Map entryPoints, JavaM try (ProgressReporter.ReporterClosable _ = reporter.printLayouting()) { codeCache = NativeImageCodeCacheFactory.get().newCodeCache(compileQueue, heap, loader.platform, - ImageSingletons.lookup(TemporaryBuildDirectoryProvider.class).getTemporaryBuildDirectory()); + ImageSingletons.lookup(TemporaryBuildDirectoryProvider.class).getTemporaryBuildDirectory()); codeCache.layoutConstants(); codeCache.layoutMethods(debug, bb); codeCache.buildRuntimeMetadata(debug, bb.getSnippetReflectionProvider()); } AfterCompilationAccessImpl config = new AfterCompilationAccessImpl(featureHandler, loader, aUniverse, hUniverse, compileQueue.getCompilations(), codeCache, heap, debug, - runtimeConfiguration, nativeLibraries); + runtimeConfiguration, nativeLibraries); featureHandler.forEachFeature(feature -> feature.afterCompilation(config)); BuildPhaseProvider.markCompilationFinished(); } @@ -754,7 +758,7 @@ protected void doRun(Map entryPoints, JavaM loader.watchdog.recordActivity(); BeforeHeapLayoutAccessImpl beforeLayoutConfig = new BeforeHeapLayoutAccessImpl(featureHandler, loader, aUniverse, hUniverse, heap, debug, runtimeConfiguration, - nativeLibraries); + nativeLibraries); featureHandler.forEachFeature(feature -> feature.beforeHeapLayout(beforeLayoutConfig)); verifyAndSealShadowHeap(codeCache, debug, heap); @@ -768,7 +772,7 @@ protected void doRun(Map entryPoints, JavaM createAbstractImage(k, hostedEntryPoints, heap, heapLayout, hMetaAccess, codeCache); FeatureImpl.AfterAbstractImageCreationAccessImpl access = new FeatureImpl.AfterAbstractImageCreationAccessImpl(featureHandler, loader, hMetaAccess, debug, image, - runtimeConfiguration.getBackendForNormalMethod()); + runtimeConfiguration.getBackendForNormalMethod()); featureHandler.forEachGraalFeature(feature -> feature.afterAbstractImageCreation(access)); image.build(imageName, debug); @@ -800,7 +804,7 @@ protected void doRun(Map entryPoints, JavaM try (StopTimer _ = TimerCollection.createTimerAndStart(TimerCollection.Registry.WRITE)) { loader.watchdog.recordActivity(); BeforeImageWriteAccessImpl beforeConfig = new BeforeImageWriteAccessImpl(featureHandler, loader, imageName, image, - runtimeConfiguration, aUniverse, hUniverse, optionProvider, hMetaAccess, debug); + runtimeConfiguration, aUniverse, hUniverse, optionProvider, hMetaAccess, debug); featureHandler.forEachFeature(feature -> feature.beforeImageWrite(beforeConfig)); /* @@ -831,10 +835,23 @@ protected void doRun(Map entryPoints, JavaM } } reporter.printCreationEnd(image.getImageFileSize(), heap.getCurrentLayerObjectCount(), image.getImageHeapSize(), codeCache.getCodeAreaSize(), numCompilations, image.getDebugInfoSize(), - imageDiskFileSize); + imageDiskFileSize); } } + private void runAbstractInterpretation(DebugContext debug, OptionValues options) { + if (!AIFOptions.RunAbstractInterpretation.getValue(options)) { + return; + } + + AnalysisMethod root = null; + if (mainEntryPoint != null && mainEntryPoint.method() != null) { + root = bb.getUniverse().getMethod(mainEntryPoint.method()); + } + AbstractInterpretationDriver driver = new AbstractInterpretationDriver(debug, root, bb, options); + driver.run(); + } + /* * Re-run shadow heap verification before heap layout. This time the verification uses the * embedded roots discovered after compilation. @@ -875,7 +892,7 @@ protected ImageHeapLayoutInfo layoutNativeImageHeap(NativeImageHeap heap) { } protected void createAbstractImage(NativeImageKind k, List hostedEntryPoints, NativeImageHeap heap, ImageHeapLayoutInfo heapLayout, - HostedMetaAccess hMetaAccess, NativeImageCodeCache codeCache) { + HostedMetaAccess hMetaAccess, NativeImageCodeCache codeCache) { this.image = AbstractImage.create(k, hUniverse, hMetaAccess, nativeLibraries, heap, heapLayout, codeCache, hostedEntryPoints, loader.getClassLoader()); } @@ -998,7 +1015,7 @@ protected boolean verifyAssignableTypes() { } protected void setupNativeImage(OptionValues options, Map entryPoints, JavaMainSupport javaMainSupport, - String imageName, SubstitutionProcessor harnessSubstitutions, DebugContext debug) { + String imageName, SubstitutionProcessor harnessSubstitutions, DebugContext debug) { try (Indent _ = debug.logAndIndent("setup native-image builder")) { try (StopTimer _ = TimerCollection.createTimerAndStart(TimerCollection.Registry.SETUP)) { installDefaultExceptionHandler(options, imageName); @@ -1008,7 +1025,7 @@ protected void setupNativeImage(OptionValues options, Map feature.afterRegistration(access)); @@ -1095,7 +1112,7 @@ protected void setupNativeImage(OptionValues options, Map additionalSubstitutions, MissingRegistrationSupport missingRegistrationSupport) { + AnnotationSubstitutionProcessor annotationSubstitutions, SubstitutionProcessor cEnumProcessor, + ClassInitializationSupport classInitializationSupport, List additionalSubstitutions, MissingRegistrationSupport missingRegistrationSupport) { SubstitutionProcessor aSubstitutions = createAnalysisSubstitutionProcessor(cEnumProcessor, annotationSubstitutions, additionalSubstitutions); SVMHost hostVM = HostedConfiguration.instance().createHostVM(options, loader, classInitializationSupport, annotationSubstitutions, missingRegistrationSupport); AnalysisPolicy analysisPolicy = PointstoOptions.AllocationSiteSensitiveHeap.getValue(options) ? new BytecodeSensitiveAnalysisPolicy(options) - : new DefaultAnalysisPolicy(options); + : new DefaultAnalysisPolicy(options); AnalysisFactory analysisFactory; if (UseExperimentalReachabilityAnalysis.getValue(options)) { analysisFactory = new ReachabilityAnalysisFactory(); @@ -1317,7 +1334,7 @@ public static AnalysisUniverse createAnalysisUniverse(OptionValues options, Targ } public static AnnotationSubstitutionProcessor createAnnotationSubstitutionProcessor(MetaAccessProvider originalMetaAccess, ImageClassLoader loader, - ClassInitializationSupport classInitializationSupport) { + ClassInitializationSupport classInitializationSupport) { AnnotationSubstitutionProcessor annotationSubstitutions = new AnnotationSubstitutionProcessor(loader, originalMetaAccess, classInitializationSupport); var fieldValueInterceptionSupport = new FieldValueInterceptionSupport(annotationSubstitutions); ImageSingletons.add(FieldValueInterceptionSupport.class, fieldValueInterceptionSupport); @@ -1326,8 +1343,8 @@ public static AnnotationSubstitutionProcessor createAnnotationSubstitutionProces } public static SubstitutionProcessor createAnalysisSubstitutionProcessor( - SubstitutionProcessor cEnumProcessor, SubstitutionProcessor annotationSubstitutions, - List additionalSubstitutionProcessors) { + SubstitutionProcessor cEnumProcessor, SubstitutionProcessor annotationSubstitutions, + List additionalSubstitutionProcessors) { List allProcessors = new ArrayList<>(); SubstitutionProcessor cFunctionSubstitutions = new CFunctionSubstitutionProcessor(); SubstitutionProcessor proxySubstitutionProcessor = new ProxyRenamingSubstitutionProcessor(); @@ -1337,8 +1354,8 @@ public static SubstitutionProcessor createAnalysisSubstitutionProcessor( } public static void initializeBigBang(Inflation bb, OptionValues options, FeatureHandler featureHandler, NativeLibraries nativeLibraries, DebugContext debug, - AnalysisMetaAccess aMetaAccess, SubstitutionProcessor substitutions, ImageClassLoader loader, boolean initForeignCalls, ClassInitializationPlugin classInitializationPlugin, - boolean supportsStubBasedPlugins, HostedProviders aProviders) { + AnalysisMetaAccess aMetaAccess, SubstitutionProcessor substitutions, ImageClassLoader loader, boolean initForeignCalls, ClassInitializationPlugin classInitializationPlugin, + boolean supportsStubBasedPlugins, HostedProviders aProviders) { SubstrateReplacements aReplacements = (SubstrateReplacements) bb.getProviders(MethodVariant.ORIGINAL_METHOD).getReplacements(); AnalysisUniverse aUniverse = bb.getUniverse(); @@ -1370,7 +1387,7 @@ public static void initializeBigBang(Inflation bb, OptionValues options, Feature registerRootElements(bb); NativeImageGenerator.registerGraphBuilderPlugins(featureHandler, null, aProviders, aMetaAccess, aUniverse, nativeLibraries, loader, ParsingReason.PointsToAnalysis, - bb.getAnnotationSubstitutionProcessor(), classInitializationPlugin, ConfigurationValues.getTarget(), supportsStubBasedPlugins); + bb.getAnnotationSubstitutionProcessor(), classInitializationPlugin, ConfigurationValues.getTarget(), supportsStubBasedPlugins); registerReplacements(debug, featureHandler, null, aProviders, true, initForeignCalls, new GraphEncoder(ConfigurationValues.getTarget().arch)); performSnippetGraphAnalysis(bb, aReplacements, options, Function.identity()); @@ -1411,7 +1428,7 @@ private static void registerRootElements(Inflation bb) { } bb.addRootMethod(ReflectionUtil.lookupMethod(SubstrateArraycopySnippets.class, "doArraycopy", - Object.class, int.class, Object.class, int.class, int.class), true, rootMethodReason); + Object.class, int.class, Object.class, int.class, int.class), true, rootMethodReason); bb.addRootMethod(ReflectionUtil.lookupMethod(Object.class, "getClass"), true, rootMethodReason); for (JavaKind kind : JavaKind.values()) { @@ -1468,7 +1485,7 @@ public static void performSnippetGraphAnalysis(BigBang bb, SubstrateReplacements } private static HostedProviders createHostedProviders(TargetDescription target, AnalysisUniverse aUniverse, - Providers originalProviders, SubstratePlatformConfigurationProvider platformConfig, AnalysisMetaAccess aMetaAccess, ClassInitializationSupport classInitializationSupport) { + Providers originalProviders, SubstratePlatformConfigurationProvider platformConfig, AnalysisMetaAccess aMetaAccess, ClassInitializationSupport classInitializationSupport) { ForeignCallsProvider aForeignCalls = new SubstrateForeignCallsProvider(aMetaAccess, null); AnalysisConstantFieldProvider aConstantFieldProvider = new AnalysisConstantFieldProvider(aMetaAccess, (SVMHost) aUniverse.hostVM()); @@ -1486,7 +1503,7 @@ private static HostedProviders createHostedProviders(TargetDescription target, A StampProvider aStampProvider = new SubstrateStampProvider(aMetaAccess); HostedProviders aProviders = new HostedProviders(aMetaAccess, null, aConstantReflection, aConstantFieldProvider, aForeignCalls, aLoweringProvider, null, aStampProvider, aSnippetReflection, - aWordTypes, platformConfig, aMetaAccessExtensionProvider, originalProviders.getLoopsDataProvider()); + aWordTypes, platformConfig, aMetaAccessExtensionProvider, originalProviders.getLoopsDataProvider()); BytecodeProvider bytecodeProvider = new ResolvedJavaMethodBytecodeProvider(); SubstrateReplacements aReplacements = new SubstrateReplacements(aProviders, bytecodeProvider, target, new SubstrateGraphMakerFactory()); @@ -1497,7 +1514,7 @@ private static HostedProviders createHostedProviders(TargetDescription target, A } private static Inflation createBigBang(DebugContext debug, OptionValues options, AnalysisUniverse aUniverse, AnalysisMetaAccess aMetaAccess, HostedProviders aProviders, - AnnotationSubstitutionProcessor annotationSubstitutionProcessor) { + AnnotationSubstitutionProcessor annotationSubstitutionProcessor) { SnippetReflectionProvider snippetReflectionProvider = aProviders.getSnippetReflection(); ConstantReflectionProvider constantReflectionProvider = aProviders.getConstantReflection(); WordTypes wordTypes = aProviders.getWordTypes(); @@ -1513,17 +1530,17 @@ private static Inflation createBigBang(DebugContext debug, OptionValues options, reachabilityMethodProcessingHandler = new DirectMethodProcessingHandler(); } return new NativeImageReachabilityAnalysisEngine(options, aUniverse, aMetaAccess, snippetReflectionProvider, constantReflectionProvider, wordTypes, annotationSubstitutionProcessor, debug, - ImageSingletons.lookup(TimerCollection.class), reachabilityMethodProcessingHandler, classInclusionPolicy); + ImageSingletons.lookup(TimerCollection.class), reachabilityMethodProcessingHandler, classInclusionPolicy); } return new NativeImagePointsToAnalysis(options, aUniverse, aMetaAccess, snippetReflectionProvider, constantReflectionProvider, wordTypes, annotationSubstitutionProcessor, - new SubstrateUnsupportedFeatures(), debug, ImageSingletons.lookup(TimerCollection.class), classInclusionPolicy); + new SubstrateUnsupportedFeatures(), debug, ImageSingletons.lookup(TimerCollection.class), classInclusionPolicy); } protected NativeLibraries setupNativeLibraries(HostedProviders providers, CEnumCallWrapperSubstitutionProcessor cEnumProcessor, ClassInitializationSupport classInitializationSupport, - DebugContext debug) { + DebugContext debug) { try (StopTimer _ = TimerCollection.createTimerAndStart("(cap)")) { NativeLibraries nativeLibs = new NativeLibraries(providers, ConfigurationValues.getTarget(), classInitializationSupport, - ImageSingletons.lookup(TemporaryBuildDirectoryProvider.class).getTemporaryBuildDirectory(), debug); + ImageSingletons.lookup(TemporaryBuildDirectoryProvider.class).getTemporaryBuildDirectory(), debug); cEnumProcessor.setNativeLibraries(nativeLibs); processNativeLibraryImports(nativeLibs, classInitializationSupport); @@ -1569,16 +1586,16 @@ protected boolean isStubBasedPluginsSupported() { } public static void registerGraphBuilderPlugins(FeatureHandler featureHandler, RuntimeConfiguration runtimeConfig, HostedProviders providers, AnalysisMetaAccess aMetaAccess, - AnalysisUniverse aUniverse, NativeLibraries nativeLibs, ImageClassLoader loader, ParsingReason reason, - AnnotationSubstitutionProcessor annotationSubstitutionProcessor, ClassInitializationPlugin classInitializationPlugin, - TargetDescription target, boolean supportsStubBasedPlugins) { + AnalysisUniverse aUniverse, NativeLibraries nativeLibs, ImageClassLoader loader, ParsingReason reason, + AnnotationSubstitutionProcessor annotationSubstitutionProcessor, ClassInitializationPlugin classInitializationPlugin, + TargetDescription target, boolean supportsStubBasedPlugins) { GraphBuilderConfiguration.Plugins plugins = new GraphBuilderConfiguration.Plugins(new SubstitutionInvocationPlugins(annotationSubstitutionProcessor)); HostedSnippetReflectionProvider hostedSnippetReflection = new HostedSnippetReflectionProvider(aUniverse.getHeapScanner(), - new SubstrateWordTypes(aMetaAccess, ConfigurationValues.getWordKind())); + new SubstrateWordTypes(aMetaAccess, ConfigurationValues.getWordKind())); WordOperationPlugin wordOperationPlugin = new SubstrateWordOperationPlugins(hostedSnippetReflection, providers.getConstantReflection(), providers.getWordTypes(), - providers.getPlatformConfigurationProvider().getBarrierSet()); + providers.getPlatformConfigurationProvider().getBarrierSet()); SubstrateReplacements replacements = (SubstrateReplacements) providers.getReplacements(); plugins.appendInlineInvokePlugin(replacements); @@ -1603,7 +1620,7 @@ public static void registerGraphBuilderPlugins(FeatureHandler featureHandler, Ru featureHandler.forEachGraalFeature(feature -> feature.registerGraphBuilderPlugins(providers, plugins, reason)); NodeIntrinsificationProvider nodeIntrinsificationProvider = new NodeIntrinsificationProvider(providers.getMetaAccess(), - hostedSnippetReflection, providers.getForeignCalls(), providers.getWordTypes(), target); + hostedSnippetReflection, providers.getForeignCalls(), providers.getWordTypes(), target); for (Class factoryClass : loader.findSubclasses(GeneratedPluginFactory.class, true)) { if (!Modifier.isAbstract(factoryClass.getModifiers()) && !factoryClass.getName().contains("hotspot")) { GeneratedPluginFactory factory; @@ -1623,10 +1640,10 @@ public static void registerGraphBuilderPlugins(FeatureHandler featureHandler, Ru ImageSingletons.lookup(TargetGraphBuilderPlugins.class).registerPlugins(plugins, options); SubstrateGraphBuilderPlugins.registerInvocationPlugins(annotationSubstitutionProcessor, - loader, - plugins.getInvocationPlugins(), - reason, - supportsStubBasedPlugins); + loader, + plugins.getInvocationPlugins(), + reason, + supportsStubBasedPlugins); featureHandler.forEachGraalFeature(feature -> feature.registerInvocationPlugins(providers, plugins, reason)); @@ -1641,7 +1658,7 @@ public static void registerGraphBuilderPlugins(FeatureHandler featureHandler, Ru } public static void registerReplacements(DebugContext debug, FeatureHandler featureHandler, RuntimeConfiguration runtimeConfig, Providers providers, - boolean hosted, boolean initForeignCalls, GraphEncoder encoder) { + boolean hosted, boolean initForeignCalls, GraphEncoder encoder) { OptionValues options = hosted ? HostedOptionValues.singleton() : RuntimeOptionValues.singleton(); SubstrateForeignCallsProvider foreignCallsProvider = (SubstrateForeignCallsProvider) providers.getForeignCalls(); @@ -1677,11 +1694,11 @@ private static boolean checkInvocationPluginMethods(SubstrateReplacements replac if (!runtimeDescriptor.equals(hostedDescriptor)) { String name = method.format("%H.%n"); throw new AssertionError( - String.format("Cannot have invocation plugin for a method whose runtime signature is different from its hosted signature:%n" + - " method: %s%n" + - " hosted signature: %s%n" + - " runtime signature: %s", - name, runtimeDescriptor, hostedDescriptor)); + String.format("Cannot have invocation plugin for a method whose runtime signature is different from its hosted signature:%n" + + " method: %s%n" + + " hosted signature: %s%n" + + " runtime signature: %s", + name, runtimeDescriptor, hostedDescriptor)); } } assert method.equals(unwrapped) || method.getSignature().toMethodDescriptor().equals(unwrapped.getSignature().toMethodDescriptor()); @@ -1732,7 +1749,7 @@ public static Suites createFallbackSuites(FeatureHandler featureHandler, Runtime } private static Suites modifySuites(SubstrateBackend backend, Suites suites, FeatureHandler featureHandler, - boolean hosted, boolean firstTier, boolean fallback) { + boolean hosted, boolean firstTier, boolean fallback) { Providers runtimeCallProviders = backend.getProviders(); PhaseSuite highTier = suites.getHighTier(); @@ -1957,10 +1974,10 @@ public static void checkName(BigBang bb, ResolvedJavaType type) { * These are legit elements from the JDK that have hotspot in their name. */ private static final Set CHECK_NAMING_EXCEPTIONS = Set.of( - "java.awt.Cursor.DOT_HOTSPOT_SUFFIX", - "sun.lwawt.macosx.CCustomCursor.fHotspot", - "sun.lwawt.macosx.CCustomCursor.getHotSpot()", - "sun.awt.shell.Win32ShellFolder2.ATTRIB_GHOSTED"); + "java.awt.Cursor.DOT_HOTSPOT_SUFFIX", + "sun.lwawt.macosx.CCustomCursor.fHotspot", + "sun.lwawt.macosx.CCustomCursor.getHotSpot()", + "sun.awt.shell.Win32ShellFolder2.ATTRIB_GHOSTED"); private static void checkName(BigBang bb, AnalysisMethod method, String name) { /* @@ -1981,9 +1998,9 @@ private static void checkName(BigBang bb, AnalysisMethod method, String name) { private static String namingConventionsErrorMessageSuffix(String elementType) { return """ - - If this is a regular JDK value, and not a %s element that was accidentally included, you can add it to the NativeImageGenerator.CHECK_NAMING_EXCEPTIONS - If this is a %s element that was accidentally included, find a way to exclude it from the image.""".formatted(elementType, elementType); + + If this is a regular JDK value, and not a %s element that was accidentally included, you can add it to the NativeImageGenerator.CHECK_NAMING_EXCEPTIONS + If this is a %s element that was accidentally included, find a way to exclude it from the image.""".formatted(elementType, elementType); } private static void report(BigBang bb, String key, AnalysisMethod method, String message) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java index 9a69e23ebc85..9fb166da4736 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporter.java @@ -53,6 +53,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.impl.ImageSingletonsSupport; @@ -155,8 +156,9 @@ public class ProgressReporter { */ private enum BuildStage { INITIALIZING("Initializing"), - ANALYSIS("Performing analysis", true, false), + ANALYSIS("Performing points-to analysis", true, false), UNIVERSE("Building universe"), + ABSTRACT_INTERPRETATION("Performing abstract interpretation", true, false), PARSING("Parsing methods", true, true), INLINING("Inlining methods", true, false), COMPILING("Compiling methods", true, true), @@ -225,7 +227,7 @@ public void printStart(String imageName, NativeImageKind imageKind) { recordJsonMetric(GeneralInfo.NAME, outputFilename); String imageKindName = imageKind.name().toLowerCase(Locale.ROOT).replace('_', ' '); l().blueBold().link("GraalVM Native Image", "https://www.graalvm.org/native-image/").reset() - .a(": Generating '").bold().a(outputFilename).reset().a("' (").doclink(imageKindName, "#glossary-imagekind").a(")...").println(); + .a(": Generating '").bold().a(outputFilename).reset().a("' (").doclink(imageKindName, "#glossary-imagekind").a(")...").println(); l().printHeadlineSeparator(); if (!linkStrategy.isTerminalSupported()) { l().a("For detailed information and explanations on the build output, visit:").println(); @@ -270,8 +272,8 @@ public void printInitializeEnd(List features, ImageClassLoader classLoa String maxHeapValue = maxHeapSize == 0 ? Heap.getHeap().getGC().getDefaultMaxHeapSize() : ByteFormattingUtil.bytesToHuman(maxHeapSize); l().a(" - ").doclink("Assertions", "#glossary-builder-assertions").a(": ").a(SubstrateUtil.assertionsEnabled() ? "enabled" : "disabled").a(", system assertions: ") - .a(getSystemAssertionStatus() ? "enabled" : "disabled") - .println(); + .a(getSystemAssertionStatus() ? "enabled" : "disabled") + .println(); printFeatures(features); @@ -279,8 +281,8 @@ public void printInitializeEnd(List features, ImageClassLoader classLoa l().a(" ").a("Image configuration:").println(); l().a(" - ").doclink("Garbage collector", "#glossary-gc").a(": ").a(gcName).a(" (").doclink("max heap size", "#glossary-gc-max-heap-size").a(": ").a(maxHeapValue).a(")").println(); l().a(" - ").doclink("Assertions", "#glossary-image-assertions").a(": ").a(RuntimeAssertionsSupport.singleton().getDefaultAssertionStatus() ? "enabled" : "disabled") - .a(" (class-specific config may apply), system assertions: ") - .a(RuntimeAssertionsSupport.singleton().getDefaultSystemAssertionStatus() ? "enabled" : "disabled").println(); + .a(" (class-specific config may apply), system assertions: ") + .a(RuntimeAssertionsSupport.singleton().getDefaultSystemAssertionStatus() ? "enabled" : "disabled").println(); printExperimentalOptions(classLoader); printResourceInfo(); @@ -392,8 +394,8 @@ private void printExperimentalOptions(ImageClassLoader classLoader) { } else { origins = lmov.getValuesWithOrigins().filter(p -> !isStableOrInternalOrigin(p.origin())).map(p -> p.origin().toString()).distinct().collect(Collectors.joining(", ")); alternatives = lmov.getValuesWithOrigins().map(p -> SubstrateOptionsParser.commandArgument(option, p.value().toString())) - .filter(c -> !c.startsWith(CommonOptionParser.HOSTED_OPTION_PREFIX)) - .collect(Collectors.joining(", ")); + .filter(c -> !c.startsWith(CommonOptionParser.HOSTED_OPTION_PREFIX)) + .collect(Collectors.joining(", ")); } } else { OptionOrigin origin = experimentalBuilderOptionsAndOrigins.get(prefixedOptionName); @@ -457,13 +459,22 @@ private void printResourceInfo() { l().yellowBold().doclink("Build resources", "#glossary-build-resources").a(":").reset().println(); l().a(" - %s", memoryUsageReason).println(); l().a(" - %s thread(s) (%.1f%% of %s available processor(s), %s)", - maxNumberOfThreads, ProgressReporterUtils.toPercentage(maxNumberOfThreads, availableProcessors), availableProcessors, maxNumberOfThreadsSuffix).println(); + maxNumberOfThreads, ProgressReporterUtils.toPercentage(maxNumberOfThreads, availableProcessors), availableProcessors, maxNumberOfThreadsSuffix).println(); } public ReporterClosable printAnalysis(AnalysisUniverse universe, Collection libraries) { return print(TimerCollection.Registry.ANALYSIS, BuildStage.ANALYSIS, () -> printAnalysisStatistics(universe, libraries)); } + public ReporterClosable printAbstractInterpretation() { + return print(TimerCollection.Registry.ABSTRACT_INTERPRETATION, BuildStage.ABSTRACT_INTERPRETATION, this::printAbstractInterpretationStatistics); + } + + private void printAbstractInterpretationStatistics() { + var stats = AbstractInterpretationServices.getInstance().getStats(); + l().a(stats.toMultilineReport()).println(); + } + private ReporterClosable print(TimerCollection.Registry registry, BuildStage buildStage) { return print(registry, buildStage, null); } @@ -501,7 +512,7 @@ private void printAnalysisStatistics(AnalysisUniverse universe, Collection= 0 ? numJNIMethods : UNAVAILABLE_METRIC)); recordJsonMetric(AnalysisResults.TYPES_JNI, (numJNIClasses >= 0 ? numJNIClasses : UNAVAILABLE_METRIC)); recordJsonMetric(AnalysisResults.FIELD_JNI, (numJNIFields >= 0 ? numJNIFields : UNAVAILABLE_METRIC)); if (numJNIClasses >= 0) { l().a(typesFieldsMethodFormat, numJNIClasses, numJNIFields, numJNIMethods) - .doclink("registered for JNI access", "#glossary-jni-access-registrations").println(); + .doclink("registered for JNI access", "#glossary-jni-access-registrations").println(); } String stubsFormat = "%,9d downcalls and %,d upcalls "; recordJsonMetric(AnalysisResults.FOREIGN_DOWNCALLS, (numForeignDowncalls >= 0 ? numForeignDowncalls : UNAVAILABLE_METRIC)); recordJsonMetric(AnalysisResults.FOREIGN_UPCALLS, (numForeignUpcalls >= 0 ? numForeignUpcalls : UNAVAILABLE_METRIC)); if (numForeignDowncalls > 0 || numForeignUpcalls > 0) { l().a(stubsFormat, numForeignDowncalls, numForeignUpcalls) - .doclink("registered for foreign access", "#glossary-foreign-downcall-and-upcall-registrations").println(); + .doclink("registered for foreign access", "#glossary-foreign-downcall-and-upcall-registrations").println(); } int resourceCount = Resources.currentLayer().resources().size(); long totalResourceSize = 0; @@ -545,7 +556,7 @@ private void printAnalysisStatistics(AnalysisUniverse universe, Collection= 0) { recordJsonMetric(ImageDetailKey.RUNTIME_COMPILED_METHODS_COUNT, numRuntimeCompiledMethods); l().a("%,9d ", numRuntimeCompiledMethods).doclink("runtime compiled methods", "#glossary-runtime-methods") - .a(" (%.1f%% of all reachable methods)", ProgressReporterUtils.toPercentage(numRuntimeCompiledMethods, reachableMethods), reachableMethods).println(); + .a(" (%.1f%% of all reachable methods)", ProgressReporterUtils.toPercentage(numRuntimeCompiledMethods, reachableMethods), reachableMethods).println(); } } @@ -604,7 +615,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH creationStageEndCompleted = true; String format = BYTES_TO_HUMAN_FORMAT + " (%5.2f%%) for "; l().a(format, ByteFormattingUtil.bytesToHuman(codeAreaSize), ProgressReporterUtils.toPercentage(codeAreaSize, imageFileSize)) - .doclink("code area", "#glossary-code-area").a(":%,10d compilation units", numCompilations).println(); + .doclink("code area", "#glossary-code-area").a(":%,10d compilation units", numCompilations).println(); int numResources = 0; for (ConditionalRuntimeValue entry : Resources.currentLayer().resources().getValues()) { if (entry.getValueUnconditionally() != Resources.NEGATIVE_QUERY_MARKER && entry.getValueUnconditionally() != Resources.MISSING_METADATA_MARKER) { @@ -613,13 +624,13 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH } recordJsonMetric(ImageDetailKey.IMAGE_HEAP_RESOURCE_COUNT, numResources); l().a(format, ByteFormattingUtil.bytesToHuman(imageHeapSize), ProgressReporterUtils.toPercentage(imageHeapSize, imageFileSize)) - .doclink("image heap", "#glossary-image-heap").a(":%,9d objects and %,d resource%s", heapObjectCount, numResources, numResources == 1 ? "" : "s").println(); + .doclink("image heap", "#glossary-image-heap").a(":%,9d objects and %,d resource%s", heapObjectCount, numResources, numResources == 1 ? "" : "s").println(); long otherBytes = imageFileSize - codeAreaSize - imageHeapSize; if (debugInfoSize > 0) { recordJsonMetric(ImageDetailKey.DEBUG_INFO_SIZE, debugInfoSize); // Optional metric DirectPrinter l = l().a(format, ByteFormattingUtil.bytesToHuman(debugInfoSize), ProgressReporterUtils.toPercentage(debugInfoSize, imageFileSize)) - .doclink("debug info", "#glossary-debug-info"); + .doclink("debug info", "#glossary-debug-info"); if (debugInfoTimer != null) { l.a(" generated in %.1fs", ProgressReporterUtils.millisToSeconds(debugInfoTimer.getTotalTime())); } @@ -635,7 +646,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH recordJsonMetric(ImageDetailKey.CODE_AREA_SIZE, codeAreaSize); recordJsonMetric(ImageDetailKey.NUM_COMP_UNITS, numCompilations); l().a(format, ByteFormattingUtil.bytesToHuman(otherBytes), ProgressReporterUtils.toPercentage(otherBytes, imageFileSize)) - .doclink("other data", "#glossary-other-data").println(); + .doclink("other data", "#glossary-other-data").println(); l().a(BYTES_TO_HUMAN_FORMAT + " in total image size", ByteFormattingUtil.bytesToHuman(imageFileSize)); if (imageDiskFileSize >= 0) { l().a(", %s in total file size", ByteFormattingUtil.bytesToHuman(imageDiskFileSize)); @@ -654,12 +665,12 @@ public void ensureCreationStageEndCompleted() { private void printBreakdowns() { Map codeBreakdown = CodeBreakdownProvider.getAndClear().entrySet().stream() - .collect(Collectors.groupingBy( - entry -> ProgressReporterUtils.BreakDownClassifier.of(entry.getKey()), - Collectors.summingLong(Entry::getValue))); + .collect(Collectors.groupingBy( + entry -> ProgressReporterUtils.BreakDownClassifier.of(entry.getKey()), + Collectors.summingLong(Entry::getValue))); List> sortedBreakdownData = codeBreakdown.entrySet().stream() - .sorted(Entry.comparingByValue(Comparator.reverseOrder())).toList(); + .sorted(Entry.comparingByValue(Comparator.reverseOrder())).toList(); if (SubstrateOptions.BuildOutputCodeBreakdownFile.getValue()) { String valueSeparator = ","; @@ -684,8 +695,8 @@ private void printBreakdowns() { final TwoColumnPrinter p = new TwoColumnPrinter(); l().printLineSeparator(); p.l().yellowBold().a(String.format("Top %d ", MAX_NUM_BREAKDOWN)).doclink("origins", "#glossary-code-area-origins").a(" of code area:") - .jumpToMiddle() - .a(String.format("Top %d object types in image heap:", MAX_NUM_BREAKDOWN)).reset().flushln(); + .jumpToMiddle() + .a(String.format("Top %d object types in image heap:", MAX_NUM_BREAKDOWN)).reset().flushln(); long printedCodeBytes = 0; long printedHeapBytes = 0; @@ -728,10 +739,10 @@ private void printBreakdowns() { long totalCodeBytes = codeBreakdown.values().stream().mapToLong(Long::longValue).sum(); p.l().a(String.format(BYTES_TO_HUMAN_FORMAT + " for %s more packages", ByteFormattingUtil.bytesToHuman(totalCodeBytes - printedCodeBytes), numCodeItems - printedCodeItems)) - .jumpToMiddle() - .a(String.format(BYTES_TO_HUMAN_FORMAT + " for %s more object types", ByteFormattingUtil.bytesToHuman(heapBreakdown.getTotalHeapSize() - printedHeapBytes), - numHeapItems - printedHeapItems)) - .flushln(); + .jumpToMiddle() + .a(String.format(BYTES_TO_HUMAN_FORMAT + " for %s more object types", ByteFormattingUtil.bytesToHuman(heapBreakdown.getTotalHeapSize() - printedHeapBytes), + numHeapItems - printedHeapItems)) + .flushln(); } private static String getBreakdownSizeString(long sizeInBytes) { @@ -756,7 +767,7 @@ private void printRecommendations() { } public void printEpilog(Optional optionalImageName, Optional optionalGenerator, ImageClassLoader classLoader, NativeImageGeneratorRunner.BuildOutcome buildOutcome, - Optional optionalUnhandledThrowable, OptionValues parsedHostedOptions) { + Optional optionalUnhandledThrowable, OptionValues parsedHostedOptions) { executor.shutdown(); boolean singletonSupportAvailable = ImageSingletonsSupport.isInstalled() && ImageSingletons.contains(BuildArtifacts.class) && ImageSingletons.contains(TimerCollection.class); @@ -764,8 +775,8 @@ public void printEpilog(Optional optionalImageName, Optional featureHandler = optionalGenerator.map(nativeImageGenerator -> nativeImageGenerator.featureHandler); ReportUtils.report("GraalVM Native Image Error Report", errorReportPath, - p -> VMErrorReporter.generateErrorReport(p, buildOutputLog, classLoader, featureHandler, optionalUnhandledThrowable.get()), - false); + p -> VMErrorReporter.generateErrorReport(p, buildOutputLog, classLoader, featureHandler, optionalUnhandledThrowable.get()), + false); if (!singletonSupportAvailable) { printErrorMessage(optionalUnhandledThrowable, parsedHostedOptions); return; @@ -798,7 +809,7 @@ public void printEpilog(Optional optionalImageName, Optional= 0) { p.a(" | ").doclink("Peak RSS", "#glossary-peak-rss").a(": ").a(ByteFormattingUtil.bytesToHuman(peakRSS)); @@ -985,9 +996,9 @@ private void checkForExcessiveGarbageCollection() { double ratio = gcTimeDeltaMillis / (double) timeDeltaMillis; if (gcTimeDeltaMillis > EXCESSIVE_GC_MIN_THRESHOLD_MILLIS && ratio > EXCESSIVE_GC_RATIO) { l().redBold().a("GC warning").reset() - .a(": %.1fs spent in %d GCs during the last stage, taking up %.2f%% of the time.", - ProgressReporterUtils.millisToSeconds(gcTimeDeltaMillis), currentGCStats.totalCount - lastGCStats.totalCount, ratio * 100) - .println(); + .a(": %.1fs spent in %d GCs during the last stage, taking up %.2f%% of the time.", + ProgressReporterUtils.millisToSeconds(gcTimeDeltaMillis), currentGCStats.totalCount - lastGCStats.totalCount, ratio * 100) + .println(); l().a(" Please ensure more than %s of memory is available for Native Image", ByteFormattingUtil.bytesToHuman(ProgressReporterCHelper.getPeakRSS())).println(); l().a(" to reduce GC overhead and improve image build time.").println(); } @@ -1239,7 +1250,7 @@ public void beforeNextStdioWrite() { } abstract class StagePrinter> extends LinePrinter { - private static final int PROGRESS_BAR_START = 30; + private static final int PROGRESS_BAR_START = 50; private BuildStage activeBuildStage = null; private ScheduledFuture periodicPrintingTask; @@ -1278,7 +1289,7 @@ public void run() { private void appendStageStart() { blue().a(String.format("[%s/%s] ", 1 + activeBuildStage.ordinal(), BuildStage.NUM_STAGES)).reset() - .blueBold().doclink(activeBuildStage.message, "#stage-" + activeBuildStage.name().toLowerCase(Locale.ROOT)).a("...").reset(); + .blueBold().doclink(activeBuildStage.message, "#stage-" + activeBuildStage.name().toLowerCase(Locale.ROOT)).a("...").reset(); } final String progressBarStartPadding() { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AIFOptions.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AIFOptions.java new file mode 100644 index 000000000000..e03cb8c8c2fa --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AIFOptions.java @@ -0,0 +1,122 @@ +package com.oracle.svm.hosted.analysis.ai; + +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionKey; +import jdk.graal.compiler.options.OptionType; + +/** + * Configuration options for the Abstract Interpretation Framework (AIF). + */ +public class AIFOptions { + + @Option(help = "Enable the Abstract Interpretation Framework for program analysis and optimization.") + public static final OptionKey RunAbstractInterpretation = new OptionKey<>(false); + + @Option(help = "Enable interprocedural abstract interpretation analysis.") + public static final OptionKey InterproceduralAnalysis = new OptionKey<>(false); + + @Option(help = "Enable intraprocedural abstract interpretation analysis.") + public static final OptionKey IntraproceduralAnalysis = new OptionKey<>(true); + + @Option(help = "Maximum recursion depth for interprocedural analysis.") + public static final OptionKey MaxRecursionDepth = new OptionKey<>(5); + + @Option(help = "Maximum call stack depth for interprocedural analysis.") + public static final OptionKey MaxCallStackDepth = new OptionKey<>(10); + + @Option(help = "Maximum number of iterations before applying widening in fixpoint computation.") + public static final OptionKey MaxJoinIterations = new OptionKey<>(10); + + @Option(help = "Maximum number of widening iterations before forcing convergence in fixpoint computation.") + public static final OptionKey MaxWidenIterations = new OptionKey<>(5); + + @Option(help = "K value for K-CFA (k-call-site sensitivity). Higher values increase precision but also analysis time.") + public static final OptionKey KCFADepth = new OptionKey<>(2); + + @Option(help = "Enable bounds check elimination based on abstract interpretation results.") + public static final OptionKey EnableBoundsCheckElimination = new OptionKey<>(true); + + @Option(help = "Enable constant propagation and folding based on abstract interpretation results.") + public static final OptionKey EnableConstantPropagation = new OptionKey<>(true); + + @Option(help = "Enable dead branch elimination when conditions are proven to be always true or false.") + public static final OptionKey EnableDeadBranchElimination = new OptionKey<>(true); + + @Option(help = "Enable method inlining when return values can be statically determined.") + public static final OptionKey EnableConstantMethodInlining = new OptionKey<>(true); + + @Option(help = "Run graph cleanup and dead code elimination after applying analysis results.") + public static final OptionKey EnableGraphCleanup = new OptionKey<>(true); + + @Option(help = "Run canonicalizer phase after abstract interpretation transformations.") + public static final OptionKey RunCanonicalizerAfterAI = new OptionKey<>(true); + + @Option(help = "Skip analysis of java.lang.* methods to improve performance.") + public static final OptionKey SkipJavaLangMethods = new OptionKey<>(true); + + @Option(help = "Skip analysis of JNI methods.") + public static final OptionKey SkipJNIMethods = new OptionKey<>(true); + + @Option(help = "Skip analysis of Spring framework methods.") + public static final OptionKey SkipSpringMethods = new OptionKey<>(true); + + @Option(help = "Skip analysis of Micronaut framework methods.") + public static final OptionKey SkipMicronautMethods = new OptionKey<>(true); + + @Option(help = "Regex pattern for method names to exclude from analysis (e.g., '.*\\.internal\\..*').") + public static final OptionKey ExcludeMethodPattern = new OptionKey<>(""); + + @Option(help = "Regex pattern for method names to include in analysis.") + public static final OptionKey IncludeMethodPattern = new OptionKey<>(""); + + @Option(help = "Set verbosity level for abstract interpretation logging: SILENT, ERROR, WARN, INFO, DEBUG.") + public static final OptionKey AILogLevel = new OptionKey<>("SILENT"); + + @Option(help = "Enable logging to console for abstract interpretation.") + public static final OptionKey AILogToConsole = new OptionKey<>(false); + + @Option(help = "Enable logging to file for abstract interpretation.") + public static final OptionKey AILogToFile = new OptionKey<>(true); + + @Option(help = "File path for abstract interpretation log output.") + public static final OptionKey AILogFilePath = new OptionKey<>("ai_analysis.log"); + + @Option(help = "Enable IGV dumps for abstract interpretation. Works in conjunction with -Dump filter to control which methods are dumped.") + public static final OptionKey AIEnableIGVDump = new OptionKey<>(true); + + @Option(help = "Export analyzed graphs to JSON format for inspection.") + public static final OptionKey AIExportGraphToJSON = new OptionKey<>(false); + + @Option(help = "Directory path for JSON graph exports.") + public static final OptionKey AIJSONExportPath = new OptionKey<>("ai_graphs"); + + @Option(help = "Print detailed statistics about abstract interpretation analysis and optimizations.") + public static final OptionKey PrintAIStatistics = new OptionKey<>(false); + + @Option(help = "Print summary of optimizations performed per method.") + public static final OptionKey PrintOptimizationSummary = new OptionKey<>(false); + + @Option(help = "Print list of most-optimized methods.") + public static final OptionKey PrintTopOptimizedMethods = new OptionKey<>(false); + + @Option(help = "Number of top-optimized methods to display in statistics.") + public static final OptionKey TopOptimizedMethodsCount = new OptionKey<>(10); + + @Option(help = "Enable parallel execution of interprocedural analysis from multiple root methods.", type = OptionType.Expert) + public static final OptionKey ParallelInterproceduralAnalysis = new OptionKey<>(false); + + @Option(help = "Number of worker threads for parallel analysis. -1 uses available processors.", type = OptionType.Expert) + public static final OptionKey AnalysisThreadCount = new OptionKey<>(-1); + + @Option(help = "Enable early summary creation for recursive methods.", type = OptionType.Expert) + public static final OptionKey EnableEarlySummaries = new OptionKey<>(true); + + @Option(help = "Maximum size of interval domain before widening to infinity.", type = OptionType.Expert) + public static final OptionKey IntervalWideningThreshold = new OptionKey<>(1000L); + + @Option(help = "Enable array length tracking in dataflow analysis for better bounds check elimination.", type = OptionType.Expert) + public static final OptionKey TrackArrayLengths = new OptionKey<>(true); + + @Option(help = "Fail analysis on errors instead of continuing with conservative approximations.", type = OptionType.Expert) + public static final OptionKey FailOnAnalysisError = new OptionKey<>(false); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationDriver.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationDriver.java new file mode 100644 index 000000000000..6c60000811b1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationDriver.java @@ -0,0 +1,178 @@ +package com.oracle.svm.hosted.analysis.ai; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.ProgressReporter; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.analysis.ai.aif.dataflow.DataFlowIntervalAbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.aif.dataflow.inter.DataFlowIntervalAnalysisSummaryFactory; +import com.oracle.svm.hosted.analysis.ai.analysis.AnalyzerManager; +import com.oracle.svm.hosted.analysis.ai.analysis.InterProceduralAnalyzer; +import com.oracle.svm.hosted.analysis.ai.analysis.IntraProceduralAnalyzer; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.SkipJavaLangAnalysisMethodFilter; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.SkipMicronautMethodFilter; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.SkipSpringMethodFilter; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.InterAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.IntraAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.checker.checkers.IfConditionChecker; +import com.oracle.svm.hosted.analysis.ai.checker.checkers.ConstantValueChecker; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.exception.AbstractInterpretationException; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryFactory; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.options.OptionValues; + +/** + * The entry point of the abstract interpretation framework. + * This class is responsible for all the necessary setup and configuration of the framework, which will then be executed + * The most important component of the framework is the {@link AnalyzerManager}, which manages all registered analyzers + * and coordinates their execution. + * The actual analysis is performed by the {@link AbstractInterpretationEngine}, + * which uses the registered analyzers to analyze the program. + */ +public class AbstractInterpretationDriver { + private final DebugContext debug; + private final AnalyzerManager analyzerManager; + private final AbstractInterpretationEngine engine; + private final OptionValues options; + + public AbstractInterpretationDriver(DebugContext debug, AnalysisMethod mainEntryPoint, Inflation bb, OptionValues options) { + this.debug = debug; + this.options = options; + this.analyzerManager = new AnalyzerManager(); + this.engine = new AbstractInterpretationEngine(analyzerManager, mainEntryPoint, bb, debug); + } + + /* To see the output of the abstract interpretation, run with -H:Log=AbstractInterpretation */ + @SuppressWarnings("try") + public void run() { + try (ProgressReporter.ReporterClosable _ = ProgressReporter.singleton().printAbstractInterpretation()) { + /* Creating a new scope for logging, run with -H:Log=AbstractInterpretation to activate it */ + try (var _ = debug.scope("AbstractInterpretation")) { + prepareAnalyses(); + engine.executeAbstractInterpretation(); + } catch (AbstractInterpretationException e) { + if (AIFOptions.FailOnAnalysisError.getValue(options)) { + throw new RuntimeException("Abstract interpretation failed", e); + } + debug.log("Abstract interpretation encountered a runtime error: ", e); + } + } + } + + /** + * This is the entry method for setting up analyses in GraalAF. + * Configuration is driven by {@link AIFOptions}. + */ + private void prepareAnalyses() { + LoggerVerbosity verbosity = parseLogLevel(AIFOptions.AILogLevel.getValue(options)); + String logFilePath = AIFOptions.AILogToFile.getValue(options) + ? AIFOptions.AILogFilePath.getValue(options) + : "GraalAF"; + + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(logFilePath, verbosity) + .setConsoleEnabled(AIFOptions.AILogToConsole.getValue(options)) + .setFileEnabled(AIFOptions.AILogToFile.getValue(options)) + .setGraphIgvDumpEnabled(AIFOptions.AIEnableIGVDump.getValue(options)) + .setFileThreshold(verbosity); + + /* 1. Define the abstract domain */ + AbstractMemory initialDomain = new AbstractMemory(); + + /* 2. Create an interpreter */ + DataFlowIntervalAbstractInterpreter interpreter = new DataFlowIntervalAbstractInterpreter(); + + /* 3. Register checkers based on enabled optimizations */ + + // Build intraprocedural analyzer if enabled + if (AIFOptions.IntraproceduralAnalysis.getValue(options)) { + var intraBuilder = new IntraProceduralAnalyzer.Builder<>( + initialDomain, + interpreter, + IntraAnalyzerMode.ANALYZE_ALL_INVOKED_METHODS + ); + + configureCheckers(intraBuilder); + configureFilters(intraBuilder); + + analyzerManager.registerAnalyzer(intraBuilder.build()); + } + + // Build interprocedural analyzer if enabled + if (AIFOptions.InterproceduralAnalysis.getValue(options)) { + SummaryFactory summaryFactory = new DataFlowIntervalAnalysisSummaryFactory(); + + var interBuilder = new InterProceduralAnalyzer.Builder<>( + initialDomain, + interpreter, + summaryFactory, + InterAnalyzerMode.ANALYZE_FROM_ALL_ROOTS + ) + .maxRecursionDepth(AIFOptions.MaxRecursionDepth.getValue(options)) + .maxCallStackDepth(AIFOptions.MaxCallStackDepth.getValue(options)); + + configureCheckers(interBuilder); + configureFilters(interBuilder); + + analyzerManager.registerAnalyzer(interBuilder.build()); + } + } + + private void configureCheckers(T builder) { + if (AIFOptions.EnableConstantPropagation.getValue(options)) { + if (builder instanceof IntraProceduralAnalyzer.Builder) { + ((IntraProceduralAnalyzer.Builder) builder).registerChecker(new ConstantValueChecker()); + } else if (builder instanceof InterProceduralAnalyzer.Builder) { + ((InterProceduralAnalyzer.Builder) builder).registerChecker(new ConstantValueChecker()); + } + } + + if (AIFOptions.EnableDeadBranchElimination.getValue(options)) { + if (builder instanceof IntraProceduralAnalyzer.Builder) { + ((IntraProceduralAnalyzer.Builder) builder).registerChecker(new IfConditionChecker()); + } else if (builder instanceof InterProceduralAnalyzer.Builder) { + ((InterProceduralAnalyzer.Builder) builder).registerChecker(new IfConditionChecker()); + } + } + } + + private void configureFilters(T builder) { + if (AIFOptions.SkipJavaLangMethods.getValue(options)) { + if (builder instanceof IntraProceduralAnalyzer.Builder) { + ((IntraProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipJavaLangAnalysisMethodFilter()); + } else if (builder instanceof InterProceduralAnalyzer.Builder) { + ((InterProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipJavaLangAnalysisMethodFilter()); + } + } + + if (AIFOptions.SkipMicronautMethods.getValue(options)) { + if (builder instanceof IntraProceduralAnalyzer.Builder) { + ((IntraProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipMicronautMethodFilter()); + } else if (builder instanceof InterProceduralAnalyzer.Builder) { + ((InterProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipMicronautMethodFilter()); + } + } + + if (AIFOptions.SkipSpringMethods.getValue(options)) { + if (builder instanceof IntraProceduralAnalyzer.Builder) { + ((IntraProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipSpringMethodFilter()); + } else if (builder instanceof InterProceduralAnalyzer.Builder) { + ((InterProceduralAnalyzer.Builder) builder).addMethodFilter(new SkipSpringMethodFilter()); + } + } + } + + /** + * Parse log level string to LoggerVerbosity enum + */ + private LoggerVerbosity parseLogLevel(String level) { + return switch (level.toUpperCase()) { + case "SILENT" -> LoggerVerbosity.SILENT; + case "ERROR" -> LoggerVerbosity.ERROR; + case "WARN", "WARNING" -> LoggerVerbosity.WARN; + case "DEBUG" -> LoggerVerbosity.DEBUG; + default -> LoggerVerbosity.INFO; + }; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationEngine.java new file mode 100644 index 000000000000..2939a4fb9ddb --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/AbstractInterpretationEngine.java @@ -0,0 +1,136 @@ +package com.oracle.svm.hosted.analysis.ai; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.analysis.ai.analysis.Analyzer; +import com.oracle.svm.hosted.analysis.ai.analysis.AnalyzerManager; +import com.oracle.svm.hosted.analysis.ai.analysis.InterProceduralAnalyzer; +import com.oracle.svm.hosted.analysis.ai.analysis.IntraProceduralAnalyzer; +import com.oracle.svm.hosted.analysis.ai.analysis.context.MethodGraphCache; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.InterAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.IntraAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryManager; +import jdk.graal.compiler.debug.DebugContext; + +import java.util.List; + +/** + * This class is responsible for running the abstract interpretation analyses, + * that were configured by {@link AbstractInterpretationDriver}. + */ +public class AbstractInterpretationEngine { + + private final AnalyzerManager analyzerManager; + private final List rootMethods; + private final List invokedMethods; + private AnalysisMethod mainMethod; + + public AbstractInterpretationEngine(AnalyzerManager analyzerManager, AnalysisMethod mainEntryPoint, Inflation bb, DebugContext debug) { + var analysisServices = AbstractInterpretationServices.getInstance(bb, debug); + this.analyzerManager = analyzerManager; + this.rootMethods = AnalysisUniverse.getCallTreeRoots(bb.getUniverse()); + this.invokedMethods = analysisServices.getInvokedMethods(); + + // The mainEntryPoint is not required, but we may need it for some debugging purposes, such as analyzing only from it + this.mainMethod = null; +// for (AnalysisMethod m : invokedMethods) { +// if (m.getName().endsWith("main")) { +// this.mainMethod = m; +// break; +// } +// } + } + + public void executeAbstractInterpretation() { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + for (var analyzer : analyzerManager.getAnalyzers()) { + executeAnalyzer(analyzer); + } + logger.close(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void executeAnalyzer(Analyzer analyzer) { + if (analyzer instanceof InterProceduralAnalyzer interProceduralAnalyzer) { + executeInterProceduralAnalysis((InterProceduralAnalyzer) interProceduralAnalyzer); + return; + } + if (analyzer instanceof IntraProceduralAnalyzer intraProceduralAnalyzer) { + executeIntraProceduralAnalysis(intraProceduralAnalyzer); + return; + } + throw new RuntimeException("The provided abstract interpretation analyzer is neither an Intra nor Inter procedural analyzer"); + } + + private void executeIntraProceduralAnalysis(IntraProceduralAnalyzer analyzer) { + var logger = AbstractInterpretationLogger.getInstance(); + IntraAnalyzerMode mode = analyzer.getAnalyzerMode(); + if (mode == IntraAnalyzerMode.ANALYZE_MAIN_ENTRYPOINT_ONLY) { + logger.log("Running intraprocedural analyzer on the main entry point only", LoggerVerbosity.INFO); + analyzer.runAnalysis(mainMethod); + return; + } + + logger.log("Running intraprocedural analyzer on all trivially invoked methods", LoggerVerbosity.INFO); + invokedMethods.parallelStream().filter(method -> !analyzer.getMethodFilterManager().shouldSkipMethod(method)).forEach(analyzer::runAnalysis); + } + + private > void executeInterProceduralAnalysis(InterProceduralAnalyzer analyzer) { + var logger = AbstractInterpretationLogger.getInstance(); + InterAnalyzerMode mode = analyzer.getAnalyzerMode(); + + if (mode == InterAnalyzerMode.ANALYZE_FROM_MAIN_ENTRYPOINT) { + logger.log("Running interprocedural analyzer from the main entry point only", LoggerVerbosity.INFO); + analyzer.runAnalysis(mainMethod); + var methodGraphCache = analyzer.getAnalysisContext().getMethodGraphCache().getMethodGraphMap(); + var methodSummaryMap = analyzer.getSummaryManager().getSummaryRepository().getMethodSummaryMap(); + analyzer.getAnalysisContext().getCheckerManager().runCheckersOnMethodSummaries(methodSummaryMap, methodGraphCache); + return; + } + + // Compute per-root analyses in parallel, then merge results sequentially to avoid races. + logger.log("Running interprocedural analyzer from all root methods", LoggerVerbosity.INFO); + for (var root : rootMethods) { + logger.log("Running analysis of root:" + root.getQualifiedName(), LoggerVerbosity.INFO); + analyzer.runAnalysis(root); + } + + var methodGraphCache = analyzer.getAnalysisContext().getMethodGraphCache().getMethodGraphMap(); + var methodSummaryMap = analyzer.getSummaryManager().getSummaryRepository().getMethodSummaryMap(); + + // debugging purposes only additionally run the root manually if it wasn't found bt the interproc analysis + // todo: think how to make this more parallel + if (mainMethod != null && !methodGraphCache.containsKey(mainMethod)) { + analyzer.runAnalysis(mainMethod); + } + analyzer.getAnalysisContext().getCheckerManager().runCheckersOnMethodSummaries(methodSummaryMap, methodGraphCache); + } + + private static final class InterResult> { + private final SummaryManager summaries; + private final MethodGraphCache graphs; + + InterResult(SummaryManager s, MethodGraphCache g) { + this.summaries = s; + this.graphs = g; + } + + void merge(InterResult other) { + this.summaries.mergeFrom(other.summaries); + this.graphs.joinWith(other.graphs); + } + + public MethodGraphCache getMethodGraphCache() { + return graphs; + } + + public SummaryManager getSummaryManager() { + return summaries; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/DataFlowIntervalAbstractInterpreter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/DataFlowIntervalAbstractInterpreter.java new file mode 100644 index 000000000000..71b6af422c4d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/DataFlowIntervalAbstractInterpreter.java @@ -0,0 +1,1128 @@ +package com.oracle.svm.hosted.analysis.ai.aif.dataflow; + +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.InvokeCallBack; +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.InvokeInput; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AccessPath; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AliasSet; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.NodeState; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.summary.Summary; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import jdk.graal.compiler.core.common.type.IntegerStamp; +import jdk.graal.compiler.core.common.type.Stamp; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.AbstractMergeNode; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.FixedNode; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.NodeView; +import jdk.graal.compiler.nodes.PhiNode; +import jdk.graal.compiler.nodes.PiNode; +import jdk.graal.compiler.nodes.ReturnNode; +import jdk.graal.compiler.nodes.ParameterNode; +import jdk.graal.compiler.nodes.calc.CompareNode; +import jdk.graal.compiler.nodes.calc.IsNullNode; +import jdk.graal.compiler.nodes.spi.ArrayLengthProvider; +import jdk.graal.compiler.nodes.spi.ValueProxy; +import jdk.graal.compiler.nodes.java.LoadFieldNode; +import jdk.graal.compiler.nodes.java.StoreFieldNode; +import jdk.graal.compiler.nodes.java.LoadIndexedNode; +import jdk.graal.compiler.nodes.java.StoreIndexedNode; +import jdk.graal.compiler.nodes.java.ArrayLengthNode; +import jdk.graal.compiler.nodes.calc.AddNode; +import jdk.graal.compiler.nodes.calc.SubNode; +import jdk.graal.compiler.nodes.calc.MulNode; +import jdk.graal.compiler.nodes.calc.FloatDivNode; +import jdk.graal.compiler.nodes.calc.BinaryArithmeticNode; +import jdk.graal.compiler.nodes.calc.IntegerLessThanNode; +import jdk.graal.compiler.nodes.calc.IntegerEqualsNode; +import jdk.graal.compiler.nodes.calc.IntegerBelowNode; +import jdk.graal.compiler.nodes.calc.RemNode; +import jdk.graal.compiler.nodes.calc.SignedFloatingIntegerDivNode; +import jdk.graal.compiler.nodes.calc.LeftShiftNode; +import jdk.graal.compiler.nodes.calc.RightShiftNode; +import jdk.graal.compiler.nodes.calc.UnsignedRightShiftNode; +import jdk.graal.compiler.nodes.cfg.HIRBlock; +import jdk.graal.compiler.nodes.LoopBeginNode; +import jdk.graal.compiler.nodes.java.NewInstanceNode; +import jdk.graal.compiler.nodes.java.NewArrayNode; +import jdk.graal.compiler.nodes.virtual.AllocatedObjectNode; +import jdk.graal.compiler.nodes.virtual.VirtualArrayNode; +import jdk.vm.ci.meta.ResolvedJavaField; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public class DataFlowIntervalAbstractInterpreter implements AbstractInterpreter { + + private static final String NODE_PREFIX = "n"; + private static final int MAX_INDEX_EXPANSION = 16; + private static final int MAX_TRACE_DEPTH = 64; + + private static String nodeId(Node n) { + return NODE_PREFIX + Integer.toHexString(System.identityHashCode(n)); + } + + @Override + public void execEdge(Node source, Node target, AbstractState abstractState, IteratorContext iteratorContext) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + AbstractMemory sourcePost = abstractState.getPostCondition(source); + AbstractMemory destPre = abstractState.getPreCondition(target); + if (source instanceof IfNode ifNode) { + Node cond = ifNode.condition(); + IntInterval res = getNodeResultInterval(ifNode, sourcePost); + boolean defTrue = !res.isBot() && !res.isTop() && res.getLower() == 1 && res.getUpper() == 1; + boolean defFalse = !res.isBot() && !res.isTop() && res.getLower() == 0 && res.getUpper() == 0; + + boolean toTrue = target.equals(ifNode.trueSuccessor()); + boolean toFalse = target.equals(ifNode.falseSuccessor()); + + if ((toTrue && defFalse) || (toFalse && defTrue)) { + abstractState.getNodeState(target).setMark(NodeState.NodeMark.UNREACHABLE); + logger.log("[EdgePrune] Skipping " + (toTrue ? "true" : "false") + " edge (condition is definitely " + (defTrue ? "true" : "false") + "): " + source + " -> " + target, + LoggerVerbosity.DEBUG); + return; + } + + // Narrowing on some conditions + AbstractMemory edgeState = sourcePost.copyOf(); + if (cond instanceof IntegerLessThanNode itn) { + if (toTrue) { + narrowOnLessThan(edgeState, itn, true); + } else if (toFalse) { + narrowOnLessThan(edgeState, itn, false); + } + destPre.joinWith(edgeState); + return; + } + + if (cond instanceof IntegerBelowNode ib) { + if (toTrue) { + narrowOnBelow(edgeState, ib, true); + } else if (toFalse) { + narrowOnBelow(edgeState, ib, false); + } + destPre.joinWith(edgeState); + return; + } + + if (cond instanceof IntegerEqualsNode eq) { + if (toTrue) { + narrowOnEquals(edgeState, eq, true); + } else if (toFalse) { + narrowOnEquals(edgeState, eq, false); + } + destPre.joinWith(edgeState); + return; + } + + if (cond instanceof IsNullNode isNull) { + IntInterval bool = toTrue ? new IntInterval(1, 1) : new IntInterval(0, 0); + bindNodeResult(isNull, bool, edgeState); + destPre.joinWith(edgeState); + return; + } + + return; + } + + /* Non-If edges: standard propagation */ + destPre.joinWith(sourcePost); + } + + private void narrowOnLessThan(AbstractMemory base, IntegerLessThanNode itn, boolean isTrue) { + Node x = itn.getX(); + Node y = itn.getY(); + IntInterval ix = getNodeResultInterval(x, base); + IntInterval iy = getNodeResultInterval(y, base); + if (ix == null || iy == null) return; + AccessPath px = base.lookupTempByName(nodeId(x)); + if (px == null) return; + + if (isTrue) { + // (x < y) => x.upper < y.lower + ix.meetWith(new IntInterval(IntInterval.NEG_INF, iy.getLower() - 1)); + } else { + // !(x < y) => x >= y => x.lower >= y.lower and y.upper <= x.upper + ix.meetWith(new IntInterval(iy.getUpper(), ix.getUpper())); + } + + base.writeStoreStrong(px, ix); + } + + private void narrowOnEquals(AbstractMemory base, IntegerEqualsNode eq, boolean isTrue) { + Node x = eq.getX(); + Node y = eq.getY(); + IntInterval ix = getNodeResultInterval(x, base); + IntInterval iy = getNodeResultInterval(y, base); + if (ix == null || iy == null) return; + AccessPath px = base.lookupTempByName(nodeId(x)); + if (px == null) return; + + if (isTrue && isPoint(iy)) { + ix.meetWith(iy); + base.writeStoreStrong(px, ix); + } + } + + private static boolean isNonNegative(IntInterval v) { + return v != null && !v.isBot() && !v.isTop() && !v.isLowerInfinite() && v.getLower() >= 0; + } + + private void narrowOnBelow(AbstractMemory base, IntegerBelowNode ib, boolean isTrue) { + Node x = ib.getX(); + Node y = ib.getY(); + IntInterval ix = getNodeResultInterval(x, base); + IntInterval iy = getNodeResultInterval(y, base); + if (ix == null || iy == null) return; + if (!isNonNegative(ix) || !isNonNegative(iy)) return; + AccessPath px = base.lookupTempByName(nodeId(x)); + if (px == null) return; + + if (isTrue) { + ix.meetWith(new IntInterval(0, iy.getLower() - 1)); + } else { + ix.meetWith(new IntInterval(iy.getUpper(), ix.getUpper())); + } + + base.writeStoreStrong(px, ix); + } + + @Override + public void execNode(Node node, AbstractState abstractState, InvokeCallBack invokeCallBack, IteratorContext iteratorContext) { + AbstractMemory pre = abstractState.getPreCondition(node); + var logger = AbstractInterpretationLogger.getInstance(); + logger.log("Executing node: " + node + " with pre-condition: " + pre, LoggerVerbosity.DEBUG); + + AbstractMemory post = pre.copyOf(); + if (node instanceof StoreFieldNode sfn) { + AbstractMemory afterVal = evalNode(sfn.value(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + IntInterval val = getNodeResultInterval(sfn.value(), afterVal); + AliasSet bases = resolveFieldBaseSet(sfn.object(), sfn.field(), afterVal); + post = afterVal.copyOf(); + post.writeTo(bases, p -> p.appendField(sfn.field().getName()), val); + } else if (node instanceof StoreIndexedNode sin) { + AbstractMemory afterArr = evalNode(sin.array(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + AbstractMemory afterIdx = evalNode(sin.index(), afterArr, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + AbstractMemory afterVal = evalNode(sin.value(), afterIdx, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + IntInterval val = getNodeResultInterval(sin.value(), afterVal); + AliasSet bases = accessBaseSetForNodeEval(sin.array(), afterVal); + Function idxTransform = indexTransform(sin.index(), afterVal); + post = afterVal.copyOf(); + post.writeTo(bases, idxTransform, val); + + IntInterval idxIv = getNodeResultInterval(sin.index(), afterVal); + boolean finiteBounds = idxIv != null && !idxIv.isTop() && !idxIv.isBot() && !idxIv.isLowerInfinite() && !idxIv.isUpperInfinite(); + if (finiteBounds) { + long lo = Math.max(0, idxIv.getLower()); + long hi = idxIv.getUpper(); + long span = hi - lo; + if (span >= 0 && span <= MAX_INDEX_EXPANSION && hi <= Integer.MAX_VALUE) { + for (long k = lo; k <= hi; k++) { + AbstractMemory memK = afterIdx.copyOf(); + String idxId = nodeId(sin.index()); + AccessPath ip = AccessPath.forLocal(idxId); + memK.bindTempByName(idxId, ip); + memK.writeStoreStrong(ip, new IntInterval(k, k)); + memK = evalNode(sin.value(), memK, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + IntInterval vk = getNodeResultInterval(sin.value(), memK); + long finalK = k; + post.writeTo(bases, p -> p.appendArrayIndex((int) finalK), vk); + } + } + } + } else if (node instanceof LoadFieldNode lfn) { + ResolvedJavaField field = lfn.field(); + if (lfn.object() == null || field.isStatic()) { + AliasSet bases = resolveFieldBaseSet(null, field, post); + IntInterval val = post.readFrom(bases, p -> p.appendField(field.getName())); + if (val.isTop() && field.getType().getJavaKind().isNumericInteger()) { + val = new IntInterval(0, 0); + logger.log("LoadField of uninitialized static field " + field.getName() + + ", using default value [0, 0]", LoggerVerbosity.DEBUG); + } + bindNodeResult(node, val, post); + } else { + AbstractMemory afterObj = evalNode(lfn.object(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + AliasSet bases = resolveFieldBaseSet(lfn.object(), field, afterObj); + IntInterval val = afterObj.readFrom(bases, p -> p.appendField(field.getName())); + bindNodeResult(node, val, afterObj); + post = afterObj; + } + } else if (node instanceof LoadIndexedNode lin) { + AbstractMemory afterArr = evalNode(lin.array(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + AbstractMemory afterIdx = evalNode(lin.index(), afterArr, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + AliasSet bases = accessBaseSetForNodeEval(lin.array(), afterIdx); + Function idxTransform = indexTransform(lin.index(), afterIdx); + + IntInterval idxIv = getNodeResultInterval(lin.index(), afterIdx); + boolean preciseIndex = isPoint(idxIv); + + IntInterval val; + if (preciseIndex) { + val = afterIdx.readFrom(bases, idxTransform).copyOf(); + } else { + IntInterval precisePart = afterIdx.readFrom(bases, idxTransform); + IntInterval summaryPart = afterIdx.readFrom(bases, AccessPath::appendArrayWildcard); + val = precisePart.copyOf(); + val.joinWith(summaryPart); + } + + bindNodeResult(node, val, afterIdx); + + if (!lin.stamp(NodeView.DEFAULT).isIntegerStamp()) { + String nestedArrayId = nodeId(lin); + AliasSet nestedArrayAliases = AliasSet.of(); + + for (AccessPath base : bases.paths()) { + AccessPath nestedPath = idxTransform.apply(base); + nestedArrayAliases = nestedArrayAliases.union(AliasSet.of(nestedPath)); + } + + if (!nestedArrayAliases.isEmpty()) { + afterIdx.bindTempSetByName(nestedArrayId, nestedArrayAliases); + } + } + + post = afterIdx; + } else if (node instanceof ArrayLengthNode aln) { + AbstractMemory afterArr = evalNode(aln.array(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + Node arr = aln.array(); + + IntInterval lenFromArrayVal = getNodeResultInterval(arr, afterArr); + if (lenFromArrayVal != null && !lenFromArrayVal.isBot() && !lenFromArrayVal.isTop()) { + bindNodeResult(aln, lenFromArrayVal, afterArr); + post = afterArr; + } else { + NewArrayNode newArr = resolveNewArray(arr); + if (newArr != null) { + IntInterval lenIv = getNodeResultInterval(newArr.length(), afterArr); + if (lenIv == null || lenIv.isTop() || lenIv.isBot()) { + IntInterval len = new IntInterval(0, IntInterval.POS_INF); + bindNodeResult(node, len, afterArr); + } else { + bindNodeResult(node, lenIv, afterArr); + } + } + else if (arr instanceof LoadIndexedNode) { + AliasSet nestedArrays = accessBaseSetForNodeEval(arr, afterArr); + if (!nestedArrays.isEmpty()) { + IntInterval arrayLenInterval = new IntInterval(); + arrayLenInterval.setToBot(); + for (AccessPath nestedPath : nestedArrays.paths()) { + IntInterval pathLen = afterArr.readFrom(AliasSet.of(nestedPath), path -> path); + if (pathLen != null && !pathLen.isBot()) { + arrayLenInterval.joinWith(pathLen); + } + } + if (!arrayLenInterval.isBot()) { + bindNodeResult(aln, arrayLenInterval, afterArr); + } else { + IntInterval len = new IntInterval(0, IntInterval.POS_INF); + bindNodeResult(node, len, afterArr); + } + post = afterArr; + } else { + IntInterval len = new IntInterval(0, IntInterval.POS_INF); + bindNodeResult(node, len, afterArr); + post = afterArr; + } + } + else { + // Standard fallback for unknown array sources + IntInterval len = new IntInterval(0, IntInterval.POS_INF); + bindNodeResult(node, len, afterArr); + post = afterArr; + } + } + IntInterval len = new IntInterval(0, IntInterval.POS_INF); + bindNodeResult(node, len, afterArr); + post = afterArr; + } else if (node instanceof NewInstanceNode nii) { + String aid = "alloc" + Integer.toHexString(System.identityHashCode(nii)); + AccessPath root = AccessPath.forAllocSite(aid); + post.bindTempByName(nodeId(nii), root); + } else if (node instanceof NewArrayNode nan) { + String aid = "alloc" + Integer.toHexString(System.identityHashCode(nan)); + AccessPath root = AccessPath.forAllocSite(aid); + post.bindTempByName(nodeId(nan), root); + IntInterval size = getNodeResultInterval(nan.length(), post); + bindNodeResult(node, size, post); + + } else if (node instanceof + Invoke inv) { + var invokeLogger = AbstractInterpretationLogger.getInstance(); + invokeLogger.log("[InvokeEval] Enter invoke node: " + inv + ", targetMethod=" + (inv.callTarget() != null ? inv.callTarget().targetMethod() : "") + + ", preMemory=" + post, LoggerVerbosity.DEBUG); + + if (inv.callTarget() == null) { + IntInterval top = new IntInterval(); + top.setToTop(); + bindNodeResult(inv.asNode(), top, post); + abstractState.setPostCondition(node, post); + return; + } + + List args = new ArrayList<>(inv.callTarget().arguments()); + AbstractMemory afterArgs = post.copyOf(); + List argDomains = new ArrayList<>(args.size()); + List argIntervals = new ArrayList<>(args.size()); + + for (Node arg : args) { + afterArgs = evalNode(arg, afterArgs, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + abstractState.setPostCondition(arg, afterArgs.copyOf()); + argDomains.add(afterArgs.copyOf()); + IntInterval ai = getNodeResultInterval(arg, afterArgs); + argIntervals.add(ai); + } + + invokeLogger.log("[InvokeEval] Collected argument intervals: " + argIntervals, LoggerVerbosity.DEBUG); + var callerMethod = iteratorContext.getCurrentAnalysisMethod(); + InvokeInput input = InvokeInput.of(callerMethod, abstractState, inv, args, argDomains); + var outcome = invokeCallBack.handleInvoke(input); + + if (outcome == null) { + invokeLogger.log("[InvokeEval] Invoke outcome is null, binding TOP.", LoggerVerbosity.DEBUG); + IntInterval top = new IntInterval(); + top.setToTop(); + bindNodeResult(inv.asNode(), top, post); + } else if (!outcome.isOk()) { + invokeLogger.log("[InvokeEval] Invoke outcome indicates ERROR: " + outcome + ", setting whole memory TOP.", LoggerVerbosity.DEBUG); + post.setToTop(); + IntInterval top = new IntInterval(); + top.setToTop(); + bindNodeResult(inv.asNode(), top, post); + } else if (outcome.summary() != null) { + Summary sum = outcome.summary(); + AbstractMemory calleePost = sum.getPostCondition(); + IntInterval retVal = calleePost.readStore(AccessPath.forLocal("ret")); + + if (retVal == null) { + retVal = new IntInterval(); + retVal.setToTop(); + invokeLogger.log("[InvokeEval] Summary found but return value missing; using TOP.", LoggerVerbosity.DEBUG); + } else { + invokeLogger.log("[InvokeEval] Summary return interval: " + retVal, LoggerVerbosity.DEBUG); + } + bindNodeResult(inv.asNode(), retVal, post); + } else { + invokeLogger.log("[InvokeEval] Outcome without summary; binding TOP.", LoggerVerbosity.DEBUG); + IntInterval top = new IntInterval(); + top.setToTop(); + bindNodeResult(inv.asNode(), top, post); + } + } else if (node instanceof + ReturnNode rn) { + if (rn.result() != null) { + AbstractMemory after = evalNode(rn.result(), post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + IntInterval v = getNodeResultInterval(rn.result(), after); + post = after.copyOf(); + post.bindLocalByName("ret", AccessPath.forLocal("ret")); + post.writeStore(AccessPath.forLocal("ret"), v); + } + } else if (node instanceof + AbstractMergeNode merge) { + Set evalStack = new HashSet<>(); + for (Node usage : merge.usages()) { + if (usage instanceof PhiNode phi) { + post.joinWith(evalNode(phi, post, abstractState, invokeCallBack, evalStack, iteratorContext)); + } + } + } else if (node instanceof + IfNode ifNode) { + Node cond = ifNode.condition(); + post = evalNode(cond, post, abstractState, invokeCallBack, new HashSet<>(), iteratorContext); + IntInterval ix = getNodeResultInterval(cond, post); + bindNodeResult(ifNode, ix, post); + } + + logger.log("Computed post-condition: " + post + " for node: " + node, LoggerVerbosity.DEBUG); + abstractState.setPostCondition(node, post); + } + + private AbstractMemory evalNode(Node node, AbstractMemory in, AbstractState abstractState, + InvokeCallBack invokeCallBack, Set evalStack, IteratorContext iteratorContext) { + if (node == null) { + return in; + } + if (node instanceof FixedNode || evalStack.contains(node)) { + return in; + } + + var logger = AbstractInterpretationLogger.getInstance(); + logger.log("Evaluating node: " + node + " with input memory: " + in, LoggerVerbosity.DEBUG); + evalStack.add(node); + AbstractMemory mem = in.copyOf(); + + if (!(node instanceof PhiNode)) { + for (Node input : node.inputs()) { + mem = evalNode(input, mem, abstractState, invokeCallBack, evalStack, iteratorContext); + } + } + + mem = evaluateNodeSemantics(node, mem, in, abstractState, invokeCallBack, evalStack, iteratorContext); + evalStack.remove(node); + return mem; + } + + private AbstractMemory evaluateNodeSemantics(Node node, AbstractMemory mem, AbstractMemory originalIn, + AbstractState abstractState, + InvokeCallBack invokeCallBack, + Set evalStack, IteratorContext iteratorContext) { + var logger = AbstractInterpretationLogger.getInstance(); + switch (node) { + case ConstantNode constantNode -> { + return evalConstantNode(constantNode, mem); + } + case ParameterNode parameterNode -> { + return evalParameterNode(parameterNode, mem); + } + case PhiNode phiNode -> { + return evalPhiNode(phiNode, mem, originalIn, abstractState, invokeCallBack, evalStack, iteratorContext); + } + case BinaryArithmeticNode binaryArithmeticNode -> { + return evalBinaryArithmeticNode(binaryArithmeticNode, mem); + } + case CompareNode cmpNode -> { + return evalCompareNode(cmpNode, mem); + } + case UnsignedRightShiftNode unsignedRightShiftNode -> { + return evalUnsignedRightShiftNode(unsignedRightShiftNode, mem); + } + case RightShiftNode rightShiftNode -> { + return evalRightShiftNode(rightShiftNode, mem); + } + case LeftShiftNode leftShiftNode -> { + return evalLeftShiftNode(leftShiftNode, mem); + } + case IsNullNode isNullNode -> { + IntInterval res = new IntInterval(0, 1); + bindNodeResult(isNullNode, res, mem); + return mem; + } + case PiNode piNode -> { + return evalPiNode(piNode, mem); + } + case ValueProxy valueProxy -> { + Node original = valueProxy.getOriginalNode(); + IntInterval v = getNodeResultInterval(original, mem); + bindNodeResult(node, v, mem); + return mem; + } + case AllocatedObjectNode allocatedObjectNode -> { + return evalAllocatedObjectNode(allocatedObjectNode, mem); + } + default -> { + logger.log("Unknown node type for evaluation: " + node.getClass().getSimpleName(), LoggerVerbosity.DEBUG); + IntInterval top = new IntInterval(); + top.setToTop(); + bindNodeResult(node, top, mem); + return mem; + } + } + } + + private AbstractMemory evalConstantNode(ConstantNode cn, AbstractMemory mem) { + var logger = AbstractInterpretationLogger.getInstance(); + if (cn.asJavaConstant() != null && cn.asJavaConstant().getJavaKind().isNumericInteger()) { + long v = cn.asJavaConstant().asLong(); + IntInterval iv = new IntInterval(v, v); + logger.log(" Constant node " + cn + " evaluated to interval: " + iv, LoggerVerbosity.DEBUG); + bindNodeResult(cn, iv, mem); + } else { + IntInterval top = new IntInterval(); + top.setToTop(); + logger.log(" Constant node " + cn + " is non-integer, using top", LoggerVerbosity.DEBUG); + bindNodeResult(cn, top, mem); + } + return mem; + } + + private AbstractMemory evalParameterNode(ParameterNode pn, AbstractMemory mem) { + String pname = "param" + pn.index(); + AccessPath root = AccessPath.forLocal(pname); + mem.bindParamByName(pname, root); + mem.bindTempByName(nodeId(pn), root); + if (!mem.hasStoreEntry(root)) { + IntInterval top = new IntInterval(); + top.setToTop(); + mem.writeStoreStrong(root, top); + } + return mem; + } + + private AbstractMemory evalPhiNode(PhiNode phi, AbstractMemory mem, AbstractMemory originalIn, + AbstractState abstractState, + InvokeCallBack invokeCallBack, + Set evalStack, IteratorContext iteratorContext) { + var logger = AbstractInterpretationLogger.getInstance(); + AbstractMergeNode merge = phi.merge(); + HIRBlock phiBlock = (iteratorContext != null) ? iteratorContext.getBlockForNode(merge) : null; + HIRBlock currentBlock = (iteratorContext != null) ? iteratorContext.getCurrentBlock() : null; + + boolean analyzingPhiBlock = (phiBlock != null && phiBlock.equals(currentBlock)); + + if (!analyzingPhiBlock && phiBlock != null) { + AccessPath existingPath = mem.lookupTempByName(nodeId(phi)); + if (existingPath != null) { + IntInterval existingValue = mem.readStore(existingPath); + logger.log(" PhiNode cached value: " + existingValue, LoggerVerbosity.DEBUG); + return mem; + } + + AbstractMemory mergePost = abstractState.getPostCondition(merge); + AccessPath mergePath = mergePost.lookupTempByName(nodeId(phi)); + if (mergePath != null) { + IntInterval mergeValue = mergePost.readStore(mergePath); + bindNodeResult(phi, mergeValue, mem); + logger.log(" PhiNode value from merge post-condition: " + mergeValue, LoggerVerbosity.DEBUG); + return mem; + } + + } + boolean isLoopHeader = (merge instanceof LoopBeginNode); + boolean isFirstVisit = false; + + if (isLoopHeader && iteratorContext != null) { + isFirstVisit = iteratorContext.isFirstVisit(merge); + } + + IntInterval acc = new IntInterval(); + acc.setToBot(); + + int numInputs = phi.valueCount(); + for (int i = 0; i < numInputs; i++) { + Node input = phi.valueAt(i); + if (isLoopHeader && isFirstVisit && i == numInputs - 1) { + continue; + } + + IntInterval v; + if (isLoopHeader && !isFirstVisit && i == numInputs - 1) { + AbstractMemory mergePost = abstractState.getPostCondition(merge); + if (mergePost != null) { + v = evalBackEdgeValue(input, mergePost); + } else { + v = evalBackEdgeValue(input, originalIn); + } + logger.log(" Input[" + i + "]: " + input + " = " + v + " (computed from previous iteration)", LoggerVerbosity.DEBUG); + } else { + v = getNodeResultInterval(input, originalIn); + logger.log(" Input[" + i + "]: " + input + " = " + v, LoggerVerbosity.DEBUG); + } + + acc.joinWith(v); + } + + if (isLoopHeader && !isFirstVisit) { + IntInterval narrowed = narrowPhiWithLoopBounds(phi, acc, merge, originalIn); + if (narrowed != null) { + logger.log(" PhiNode narrowed using loop bounds: " + narrowed, LoggerVerbosity.DEBUG); + acc = narrowed; + } + } + + bindNodeResult(phi, acc, mem); + logger.log(" PhiNode result: " + acc, LoggerVerbosity.DEBUG); + return mem; + } + + private IntInterval narrowPhiWithLoopBounds(PhiNode phi, IntInterval current, AbstractMergeNode merge, AbstractMemory mem) { + if (current.isBot() || current.isTop()) { + return null; + } + + // Look for loop exit conditions that compare this phi with an upper bound + for (Node usage : phi.usages()) { + if (usage instanceof IntegerLessThanNode lessThan) { + Node x = lessThan.getX(); + Node y = lessThan.getY(); + + // Check if phi is compared with array length or another bound + if (x == phi || (x instanceof ValueProxy vp && vp.getOriginalNode() == phi)) { + // phi < y, so phi upper bound is y - 1 + IntInterval yInterval = getNodeResultInterval(y, mem); + if (yInterval != null && !yInterval.isTop() && !yInterval.isBot() && !yInterval.isUpperInfinite()) { + long upperBound = yInterval.getUpper() - 1; + if (!current.isUpperInfinite() && current.getUpper() > upperBound) { + IntInterval narrowed = current.copyOf(); + narrowed.setUpper(upperBound); + return narrowed; + } else if (current.isUpperInfinite()) { + IntInterval narrowed = current.copyOf(); + narrowed.setUpper(upperBound); + return narrowed; + } + } + } else if (y == phi || (y instanceof ValueProxy vp && vp.getOriginalNode() == phi)) { + // x < phi, so phi lower bound is x + 1 + IntInterval xInterval = getNodeResultInterval(x, mem); + if (xInterval != null && !xInterval.isTop() && !xInterval.isBot() && !xInterval.isLowerInfinite()) { + long lowerBound = xInterval.getLower() + 1; + if (!current.isLowerInfinite() && current.getLower() < lowerBound) { + IntInterval narrowed = current.copyOf(); + narrowed.setLower(lowerBound); + return narrowed; + } + } + } + } else if (usage instanceof IntegerBelowNode below) { + Node x = below.getX(); + Node y = below.getY(); + + // For unsigned comparison: phi |<| y means phi < y (unsigned) + if (x == phi || (x instanceof ValueProxy vp && vp.getOriginalNode() == phi)) { + IntInterval yInterval = getNodeResultInterval(y, mem); + if (yInterval != null && !yInterval.isTop() && !yInterval.isBot() && !yInterval.isUpperInfinite()) { + // Ensure we're in non-negative range for unsigned comparison + if (!current.isLowerInfinite() && current.getLower() >= 0) { + long upperBound = yInterval.getUpper() - 1; + if (upperBound >= 0) { + IntInterval narrowed = current.copyOf(); + narrowed.setUpper(Math.min(narrowed.getUpper(), upperBound)); + return narrowed; + } + } + } + } + } + } + + return null; + } + + private boolean isArrayLengthOfParamOrPiParam(Node node) { + if (!(node instanceof ArrayLengthNode aln)) return false; + Node arr = aln.array(); + if (arr instanceof ParameterNode) return true; + if (arr instanceof PiNode pi) { + Node obj = pi.object(); + return obj instanceof ParameterNode; + } + return false; + } + + private IntInterval evalBackEdgeValue(Node node, AbstractMemory fromMem) { + var logger = AbstractInterpretationLogger.getInstance(); + logger.log(" evalBackEdgeValue for node: " + node, LoggerVerbosity.DEBUG); + + if (node instanceof jdk.graal.compiler.nodes.calc.BinaryArithmeticNode bin) { + Node x = bin.getX(); + Node y = bin.getY(); + + logger.log(" X operand: " + x, LoggerVerbosity.DEBUG); + logger.log(" Y operand: " + y, LoggerVerbosity.DEBUG); + + IntInterval ix = getNodeResultInterval(x, fromMem); + IntInterval iy = getNodeResultInterval(y, fromMem); + + logger.log(" X interval: " + ix, LoggerVerbosity.DEBUG); + logger.log(" Y interval: " + iy, LoggerVerbosity.DEBUG); + + if (bin instanceof AddNode) { + IntInterval result = ix.add(iy); + logger.log(" Add result: " + result, LoggerVerbosity.DEBUG); + return result; + } else if (bin instanceof SubNode) { + return ix.sub(iy); + } else if (bin instanceof MulNode) { + return ix.mul(iy); + } else if (bin instanceof SignedFloatingIntegerDivNode) { + return ix.div(iy); + } else if (bin instanceof RemNode) { + return ix.rem(iy); + } else if (bin instanceof FloatDivNode) { + IntInterval top = new IntInterval(); + top.setToTop(); + return top; + } + } + + logger.log(" Returning top (unsupported node type)", LoggerVerbosity.DEBUG); + IntInterval top = new IntInterval(); + top.setToTop(); + return top; + } + + private AbstractMemory evalBinaryArithmeticNode(BinaryArithmeticNode bin, AbstractMemory mem) { + Node x = bin.getX(); + Node y = bin.getY(); + IntInterval ix = getNodeResultInterval(x, mem); + IntInterval iy = getNodeResultInterval(y, mem); + + IntInterval res; + if (bin instanceof AddNode) { + res = ix.add(iy); + } else if (bin instanceof SubNode) { + res = ix.sub(iy); + } else if (bin instanceof MulNode) { + res = ix.mul(iy); + } else if (bin instanceof SignedFloatingIntegerDivNode) { + res = ix.div(iy); + } else if (bin instanceof RemNode) { + res = ix.rem(iy); + } else if (bin instanceof FloatDivNode) { + res = new IntInterval(); + res.setToTop(); + } else { + res = new IntInterval(); + res.setToTop(); + } + + bindNodeResult(bin, res, mem); + return mem; + } + + private AbstractMemory evalCompareNode(CompareNode cmpNode, AbstractMemory mem) { + IntInterval ix = getNodeResultInterval(cmpNode.getX(), mem); + IntInterval iy = getNodeResultInterval(cmpNode.getY(), mem); + IntInterval res = new IntInterval(); + if (ix.isBot() || iy.isBot()) { + res.setToBot(); + } else if (ix.isTop() || iy.isTop()) { + res.setToTop(); + } + + switch (cmpNode) { + case IntegerLessThanNode iltn -> res = getResultIntegerLessThanNode(ix, iy); + case IntegerBelowNode ibn -> res = getResultIntegerBelowNode(ix, iy); + case IntegerEqualsNode ien -> res = getResultIntegerEqualsNode(ix, iy); + default -> { + } + } + bindNodeResult(cmpNode, res, mem); + return mem; + } + + private IntInterval getResultIntegerLessThanNode(IntInterval ix, IntInterval iy) { + assert !ix.isTop() && !ix.isBot() && !iy.isTop() && !iy.isBot(); + if (!ix.isUpperInfinite() && !iy.isLowerInfinite() && ix.getUpper() < iy.getLower()) { + return new IntInterval(1, 1); + } + if (!ix.isLowerInfinite() && !iy.isUpperInfinite() && ix.getLower() >= iy.getUpper()) { + return new IntInterval(0, 0); + } + return new IntInterval(0, 1); + } + + private IntInterval getResultIntegerBelowNode(IntInterval ix, IntInterval iy) { + assert !ix.isTop() && !ix.isBot() && !iy.isTop() && !iy.isBot(); + if (isNonNegative(ix) && isNonNegative(iy) && !ix.isUpperInfinite() && !iy.isLowerInfinite() && ix.getUpper() < iy.getLower()) { + return new IntInterval(1, 1); + } + if (isNonNegative(ix) && isNonNegative(iy) && !ix.isLowerInfinite() && !iy.isUpperInfinite() && ix.getLower() >= iy.getUpper()) { + return new IntInterval(0, 0); + } + return new IntInterval(0, 1); + } + + private IntInterval getResultIntegerEqualsNode(IntInterval ix, IntInterval iy) { + assert !ix.isTop() && !ix.isBot() && !iy.isTop() && !iy.isBot(); + if (isPoint(ix) && isPoint(iy) && ix.getUpper() == iy.getUpper() && ix.getLower() == iy.getLower()) { + return new IntInterval(1, 1); + } + + IntInterval meetInt = ix.meet(iy); + if (meetInt.isBot()) { + return new IntInterval(0, 0); + } + + return new IntInterval(0, 1); + } + + private AbstractMemory evalLeftShiftNode(LeftShiftNode node, AbstractMemory mem) { + IntInterval x = getNodeResultInterval(node.getX(), mem); + IntInterval s = getNodeResultInterval(node.getY(), mem); + IntInterval res = new IntInterval(); + if (x.isBot() || s.isBot()) { + res.setToBot(); + } else if (x.isTop() || s.isTop()) { + res.setToTop(); + } else if (s.isConstantValue() && s.getLower() >= 0 && s.getLower() <= 63) { + long sh = s.getLower(); + long factor = 1L << sh; + IntInterval f = new IntInterval(factor, factor); + res = x.mul(f); + } else { + res.setToTop(); + } + bindNodeResult(node, res, mem); + return mem; + } + + private AbstractMemory evalRightShiftNode(RightShiftNode node, AbstractMemory mem) { + IntInterval x = getNodeResultInterval(node.getX(), mem); + IntInterval s = getNodeResultInterval(node.getY(), mem); + IntInterval res = new IntInterval(); + if (x.isBot() || s.isBot()) { + res.setToBot(); + } else if (x.isTop() || s.isTop()) { + res.setToTop(); + } else if (s.isConstantValue() && s.getLower() >= 0 && s.getLower() <= 63) { + long sh = s.getLower(); + long div = 1L << sh; + if (!x.isLowerInfinite() && x.getLower() >= 0) { + long lo = x.getLower() / div; + long hi = x.getUpper() / div; + res = new IntInterval(lo, hi); + } else { + res.setToTop(); + } + } else { + res.setToTop(); + } + bindNodeResult(node, res, mem); + return mem; + } + + private AbstractMemory evalUnsignedRightShiftNode(UnsignedRightShiftNode node, AbstractMemory mem) { + IntInterval x = getNodeResultInterval(node.getX(), mem); + IntInterval s = getNodeResultInterval(node.getY(), mem); + IntInterval res = new IntInterval(); + if (x.isBot() || s.isBot()) { + res.setToBot(); + } else if (x.isTop() || s.isTop()) { + res.setToTop(); + } else if (s.isConstantValue() && s.getLower() >= 0 && s.getLower() <= 63) { + long sh = s.getLower(); + long div = 1L << sh; + if (!x.isLowerInfinite() && x.getLower() >= 0) { + long lo = x.getLower() / div; + long hi = x.getUpper() / div; + res = new IntInterval(lo, hi); + } else { + res.setToTop(); + } + } else { + res.setToTop(); + } + bindNodeResult(node, res, mem); + return mem; + } + + private AbstractMemory evalVirtualArrayNode(VirtualArrayNode node, AbstractMemory mem) { + String aid = "allocVArr" + Integer.toHexString(System.identityHashCode(node)); + AccessPath root = AccessPath.forAllocSite(aid); + mem.bindTempByName(nodeId(node), root); + return mem; + } + + private AbstractMemory evalAllocatedObjectNode(AllocatedObjectNode node, AbstractMemory mem) { + String aid = "allocObj" + Integer.toHexString(System.identityHashCode(node)); + AccessPath root = AccessPath.forAllocSite(aid); + mem.bindTempByName(nodeId(node), root); + + if (node.getVirtualObject() instanceof VirtualArrayNode vArr) { + var bb = AbstractInterpretationServices.getInstance().getInflation(); + var constRef = bb.getConstantReflectionProvider(); + var lenNode = vArr.findLength(ArrayLengthProvider.FindLengthMode.SEARCH_ONLY, constRef); + if (lenNode instanceof ConstantNode cn && + cn.asJavaConstant() != null && + cn.asJavaConstant().getJavaKind().isNumericInteger()) { + long len = cn.asJavaConstant().asLong(); + if (len >= 0) { + IntInterval sizeIv = new IntInterval(len, len); + bindNodeResult(node, sizeIv, mem); + mem.writeStoreStrong(root, sizeIv); + } + } + } + + return mem; + } + + private AbstractMemory evalPiNode(PiNode piNode, AbstractMemory mem) { + IntInterval base = getNodeResultInterval(piNode.object(), mem); + IntInterval refined = (base != null) ? base.copyOf() : new IntInterval(); + if (base == null) { + refined.setToTop(); + } + + Node obj = piNode.object(); + if (obj != null) { + String objId = nodeId(obj); + AliasSet objAliases = mem.lookupTempSetByName(objId); + if (objAliases != null && !objAliases.isEmpty()) { + mem.bindTempSetByName(nodeId(piNode), objAliases); + } + } + + Stamp s = piNode.piStamp(); + if (s instanceof IntegerStamp integerStamp && !refined.isBot() && !refined.isTop()) { + if (integerStamp.isStrictlyPositive()) { + if (refined.isLowerInfinite() || refined.getLower() < 0) { + refined.setLower(0); + } + } + if (integerStamp.isStrictlyPositive() || integerStamp.isStrictlyNegative()) { + if (refined.getLower() == 0 && refined.getUpper() == 0) { + refined.setToTop(); + } else if (refined.getLower() == 0) { + refined.setLower(1); + } else if (refined.getUpper() == 0) { + refined.setToTop(); + } + } + } + bindNodeResult(piNode, refined, mem); + return mem; + } + + private static void bindNodeResult(Node node, IntInterval val, AbstractMemory mem) { + if (node == null) { + AbstractInterpretationLogger.getInstance().log("bindNodeResult called with null node, skipping", LoggerVerbosity.DEBUG); + return; + } + String id = nodeId(node); + AccessPath p = AccessPath.forLocal(id); + mem.bindTempByName(id, p); + mem.writeStoreStrong(p, val.copyOf()); + } + + private static IntInterval getNodeResultInterval(Node node, AbstractMemory mem) { + var logger = AbstractInterpretationLogger.getInstance(); + + if (node == null) { + IntInterval top = new IntInterval(); + top.setToTop(); + return top; + } + + String nid = nodeId(node); + if (node instanceof ConstantNode cn) { + if (cn.asJavaConstant() != null && cn.asJavaConstant().getJavaKind().isNumericInteger()) { + long v = cn.asJavaConstant().asLong(); + IntInterval result = new IntInterval(v, v); + logger.log(" getNodeResultInterval: constant " + node + " = " + result, LoggerVerbosity.DEBUG); + return result; + } + } + + AccessPath mapped = mem.lookupTempByName(nid); + IntInterval result; + if (mapped != null) { + result = mem.readStore(mapped); + } else { + AccessPath p = AccessPath.forLocal(nid); + result = mem.readStore(p); + } + + logger.log(" getNodeResultInterval: node " + node + " (id=" + nid + ") = " + result, LoggerVerbosity.DEBUG); + return result; + } + + private static AliasSet accessBaseSetForNodeEval(Node objNode, AbstractMemory mem) { + switch (objNode) { + case null -> { + return AliasSet.of(); + } + case ParameterNode pn -> { + String pname = "param" + pn.index(); + return mem.lookupParamSetByName(pname); + } + case NewInstanceNode nii -> { + String aid = "alloc" + Integer.toHexString(System.identityHashCode(nii)); + return AliasSet.of(AccessPath.forAllocSite(aid)); + } + case NewArrayNode nan -> { + String aid = "alloc" + Integer.toHexString(System.identityHashCode(nan)); + return AliasSet.of(AccessPath.forAllocSite(aid)); + } + default -> { + } + } + AliasSet s = mem.lookupTempSetByName(nodeId(objNode)); + if (s != null) return s; + return AliasSet.of(); + } + + private static AliasSet resolveFieldBaseSet(Node objNode, ResolvedJavaField field, AbstractMemory mem) { + if (objNode == null) { + String className = field.getDeclaringClass().getName(); + return AliasSet.of(AccessPath.forStaticClass(className)); + } + return accessBaseSetForNodeEval(objNode, mem); + } + + private static boolean isPoint(IntInterval v) { + return v != null && v.isConstantValue(); + } + + private static Function indexTransform(Node indexNode, AbstractMemory mem) { + IntInterval idx = getNodeResultInterval(indexNode, mem); + if (isPoint(idx)) { + long k = idx.getLower(); + if (k >= Integer.MIN_VALUE && k <= Integer.MAX_VALUE) { + int ik = (int) k; + return p -> p.appendArrayIndex(ik); + } + } + return AccessPath::appendArrayWildcard; + } + + private NewArrayNode resolveNewArray(Node n) { + return resolveNewArray(n, new HashSet<>(), 0); + } + + private NewArrayNode resolveNewArray(Node n, Set visited, int depth) { + if (n == null) return null; + if (depth > MAX_TRACE_DEPTH) { + AbstractInterpretationLogger.getInstance().log("resolveNewArray: max depth exceeded at " + n, LoggerVerbosity.DEBUG); + return null; + } + if (!visited.add(n)) { + return null; + } + + switch (n) { + case NewArrayNode na -> { + return na; + } + case PiNode piNode -> { + return resolveNewArray(piNode.object(), visited, depth + 1); + } + case ValueProxy vp -> { + return resolveNewArray(vp.getOriginalNode(), visited, depth + 1); + } + case AllocatedObjectNode aon -> { + if (aon.getVirtualObject() instanceof VirtualArrayNode) { + return null; + } + } + default -> { + } + } + + // Handle PhiNode (join of multiple array sources) + if (n instanceof PhiNode phi) { + NewArrayNode candidate = null; + for (int i = 0; i < phi.valueCount(); i++) { + Node in = phi.valueAt(i); + NewArrayNode r = resolveNewArray(in, visited, depth + 1); + if (r == null) return null; + if (candidate == null) { + candidate = r; + } else if (candidate != r) { + return null; + } + } + return candidate; + } + + if (n instanceof LoadIndexedNode) { + return null; + } + + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummary.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummary.java new file mode 100644 index 000000000000..23512beaab91 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummary.java @@ -0,0 +1,59 @@ +package com.oracle.svm.hosted.analysis.ai.aif.dataflow.inter; + +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.summary.Summary; + +/** + * Summary for the interval-based data-flow analysis over {@link AbstractMemory}. + * - Pre-condition: callee entry abstract state (as an AbstractMemory snapshot) + * - Post-condition: abstract state joined at all return points (from callee AbstractState#getReturnDomain) + */ +public final class DataFlowIntervalAnalysisSummary implements Summary { + + private final AbstractMemory pre; + private AbstractMemory post; + + public DataFlowIntervalAnalysisSummary(AbstractMemory pre) { + this.pre = pre == null ? new AbstractMemory() : pre.copyOf(); + this.post = new AbstractMemory(); + this.post.setToTop(); + } + + @Override + public AbstractMemory getPreCondition() { + return pre; + } + + @Override + public AbstractMemory getPostCondition() { + return post; + } + + @Override + public boolean subsumesSummary(Summary other) { + if (other == null) return false; + // This summary subsumes "other" iff our pre-condition is >= other.pre (i.e., contains it). + // We use lattice order: other.pre ⊑ this.pre => this covers the other. + return other.getPreCondition().leq(this.pre); + } + + @Override + public void finalizeSummary(AbstractState calleeAbstractState) { + if (calleeAbstractState == null) { + this.post = new AbstractMemory(); + this.post.setToTop(); + return; + } + AbstractMemory ret = calleeAbstractState.getReturnDomain(); + this.post = ret.copyOf(); + } + + @Override + public AbstractMemory applySummary(AbstractMemory domain) { + if (domain == null) return post.copyOf(); + AbstractMemory out = domain.copyOf(); + out.joinWith(post); + return out; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummaryFactory.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummaryFactory.java new file mode 100644 index 000000000000..fc8beb2d69e7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/aif/dataflow/inter/DataFlowIntervalAnalysisSummaryFactory.java @@ -0,0 +1,47 @@ +package com.oracle.svm.hosted.analysis.ai.aif.dataflow.inter; + +import com.oracle.svm.hosted.analysis.ai.summary.Summary; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryFactory; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AccessPath; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.Invoke; + +import java.util.List; + +/** + * Factory creating {@link DataFlowIntervalAnalysisSummary} instances from a call site context. + * Maps actual argument intervals onto formal parameter slots (param0, param1, ...). + */ +public final class DataFlowIntervalAnalysisSummaryFactory implements SummaryFactory { + + private static final String NODE_PREFIX = "n"; + + private static String nodeId(Node n) { + return NODE_PREFIX + Integer.toHexString(System.identityHashCode(n)); + } + + @Override + public Summary createSummary(Invoke invoke, + AbstractMemory callerPreCondition, + List argumentMemories) { + AbstractMemory pre = new AbstractMemory(); + if (invoke.callTarget() == null) { + return new DataFlowIntervalAnalysisSummary(pre); + } + var argNodes = invoke.callTarget().arguments(); + int count = Math.min(argNodes.size(), argumentMemories.size()); + for (int i = 0; i < count; i++) { + Node argNode = argNodes.get(i); + AbstractMemory argMem = argumentMemories.get(i); + String tempId = nodeId(argNode); + IntInterval iv = argMem.readStore(AccessPath.forLocal(tempId)); + String paramName = "param" + i; + AccessPath paramPath = AccessPath.forLocal(paramName); + pre.bindParamByName(paramName, paramPath); + pre.writeStoreStrong(paramPath, iv); + } + return new DataFlowIntervalAnalysisSummary(pre); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AbstractInterpretationServices.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AbstractInterpretationServices.java new file mode 100644 index 000000000000..733125b1982a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AbstractInterpretationServices.java @@ -0,0 +1,88 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.analysis.ai.exception.AbstractInterpretationException; +import com.oracle.svm.hosted.analysis.ai.stats.AbstractInterpretationStatistics; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.vm.ci.meta.ResolvedJavaType; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Represents centralized information about utilities needed by the abstract interpretation analysis + */ +public final class AbstractInterpretationServices { + + private static AbstractInterpretationServices instance; + private final Inflation inflation; + private final DebugContext debug; + private final AbstractInterpretationStatistics stats = new AbstractInterpretationStatistics(); + private final Set touchedMethods = new HashSet<>(); + + private AbstractInterpretationServices(Inflation inflation, DebugContext debug) { + this.inflation = inflation; + this.debug = debug; + } + + public static AbstractInterpretationServices getInstance(Inflation inflation, DebugContext debug) { + if (instance == null) { + instance = new AbstractInterpretationServices(inflation, debug); + } + return instance; + } + + public static AbstractInterpretationServices getInstance() { + if (instance == null) { + throw new IllegalStateException("AnalysisServices not initialized. Call getInstance(Inflation) first."); + } + return instance; + } + + public AbstractInterpretationStatistics getStats() { + return stats; + } + + public Inflation getInflation() { + return inflation; + } + + public DebugContext getDebug() { + return debug; + } + + public void markMethodTouched(AnalysisMethod method) { + if (method != null) { + touchedMethods.add(method); + } + } + + public Set getTouchedMethods() { + return Collections.unmodifiableSet(touchedMethods); + } + + public ResolvedJavaType lookUpType(Class clazz) { + return inflation.getMetaAccess().lookupJavaType(clazz); + } + + public StructuredGraph getGraph(AnalysisMethod method) { + DebugContext debug = inflation.getDebug(); + StructuredGraph graph = method.decodeAnalyzedGraph(debug, null); + if (graph == null) { + AbstractInterpretationException.analysisMethodGraphNotFound(method); + } + return graph; + } + + public List getInvokedMethods() { + return inflation.getUniverse() + .getMethods() + .stream() + .filter(AnalysisMethod::isSimplyImplementationInvoked).toList(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalysisResult.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalysisResult.java new file mode 100644 index 000000000000..14a370f7e72f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalysisResult.java @@ -0,0 +1,26 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +/** + * Represents the result of an analysis. + */ +public enum AnalysisResult { + + OK("Analysis successfully finished"), + ANALYSIS_FAILED("Some internal error occurred"), + IN_SKIP_LIST("The method should be skipped according to the provided method filters"), + MUTUAL_RECURSION_CYCLE("There is a sequence of calls that cannot be resolved due to mutual recursion"), + UNKNOWN_METHOD("The method of an Invoke was not found in the current DebugContext"), + RECURSION_LIMIT_OVERFLOW("The recursion limit was reached"), + CALL_STACK_DEPTH_OVERFLOW("The call stack depth limit was reached"); + + private final String description; + + AnalysisResult(String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/Analyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/Analyzer.java new file mode 100644 index 000000000000..ac78d3eb6fb3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/Analyzer.java @@ -0,0 +1,107 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.AnalysisMethodFilter; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.AnalysisMethodFilterManager; +import com.oracle.svm.hosted.analysis.ai.checker.core.Checker; +import com.oracle.svm.hosted.analysis.ai.checker.core.CheckerManager; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.IteratorPolicy; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.summary.Summary; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryFactory; + +/** + * Represents an abstract analyzer for performing analyses driven by a specific abstract {@link Domain}. + * The analyzer uses transfer functions, interpreters, and iterator policies to compute analysis results. + * It encapsulates the logic for creating analysis payloads and facilitates method-specific analysis. + * To create an intra-procedural analyzer, it is enough to provide the initial domain and the node interpreter. + * To create an inter-procedural analyzer, we need to also add an implementation of {@link Summary}, as well as logic + * for creating the summary from an abstract context, which is handled by {@link SummaryFactory}. + * We can also add additional parameters, like a list of checkers to be used during the analysis, or method filters, + * to restrict the analysis of specific methods, extrapolation limits such as join/widen limits, etc. + * + * @param the type of the abstract domain used for the analysis. + */ +public abstract class Analyzer> { + + protected final Domain initialDomain; + protected final AbstractInterpreter abstractInterpreter; + protected final IteratorPolicy iteratorPolicy; + protected final CheckerManager checkerManager; + protected final AnalysisMethodFilterManager methodFilterManager; + + protected Analyzer(Builder builder) { + this.initialDomain = builder.initialDomain; + this.abstractInterpreter = builder.abstractInterpreter; + this.iteratorPolicy = builder.iteratorPolicy; + this.checkerManager = builder.checkerManager; + this.methodFilterManager = builder.methodFilterManager; + } + + /** + * Execute analysis starting from the given analysis method. Concrete analyzers are free to + * traverse more methods (e.g., via invokes) as part of their strategy. + */ + public abstract void runAnalysis(AnalysisMethod method); + + public Domain getInitialDomain() { + return initialDomain; + } + + public AbstractInterpreter getAbstractInterpreter() { + return abstractInterpreter; + } + + public IteratorPolicy getIteratorPolicy() { + return iteratorPolicy; + } + + public CheckerManager getCheckerManager() { + return checkerManager; + } + + public AnalysisMethodFilterManager getMethodFilterManager() { + return methodFilterManager; + } + + public static abstract class Builder, Domain extends AbstractDomain> { + protected final Domain initialDomain; + protected final AbstractInterpreter abstractInterpreter; + protected IteratorPolicy iteratorPolicy = IteratorPolicy.DEFAULT_FORWARD_WTO; + protected CheckerManager checkerManager = new CheckerManager(); + protected AnalysisMethodFilterManager methodFilterManager = new AnalysisMethodFilterManager(); + + protected Builder(Domain initialDomain, AbstractInterpreter abstractInterpreter) { + this.initialDomain = initialDomain; + this.abstractInterpreter = abstractInterpreter; + } + + public T iteratorPolicy(IteratorPolicy iteratorPolicy) { + this.iteratorPolicy = iteratorPolicy; + return self(); + } + + public T registerChecker(Checker checker) { + checkerManager.registerChecker(checker); + return self(); + } + + public T checkerManager(CheckerManager checkerManager) { + this.checkerManager = checkerManager; + return self(); + } + + public T methodFilterManager(AnalysisMethodFilterManager methodFilterManager) { + this.methodFilterManager = methodFilterManager; + return self(); + } + + public T addMethodFilter(AnalysisMethodFilter filter) { + methodFilterManager.addMethodFilter(filter); + return self(); + } + + protected abstract T self(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalyzerManager.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalyzerManager.java new file mode 100644 index 000000000000..1663b148da72 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/AnalyzerManager.java @@ -0,0 +1,17 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import java.util.ArrayList; +import java.util.List; + +public final class AnalyzerManager { + + private final List> analyzers = new ArrayList<>(); + + public void registerAnalyzer(Analyzer analyzer) { + analyzers.add(analyzer); + } + + public List> getAnalyzers() { + return analyzers; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InterProceduralAnalyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InterProceduralAnalyzer.java new file mode 100644 index 000000000000..9ca6075a5315 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InterProceduralAnalyzer.java @@ -0,0 +1,100 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.InterAbsintInvokeHandler; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.analysis.context.CallStack; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.InterAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryFactory; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryManager; + +/** + * An inter-procedural analyzer that performs an inter-procedural analysis on the given method. + * + * @param the type of the abstract domain used for the analysis. + */ +public final class InterProceduralAnalyzer> extends Analyzer { + + private final InterAnalyzerMode analyzerMode; + private final AnalysisContext analysisContext; + private final SummaryManager summaryManager; + private final int maxRecursionDepth; + private final int maxCallStackDepth; + + private InterProceduralAnalyzer(Builder builder) { + super(builder); + this.analyzerMode = builder.analyzerMode; + this.summaryManager = new SummaryManager<>(builder.summaryFactory); + this.maxRecursionDepth = builder.maxRecursionDepth; + this.maxCallStackDepth = builder.maxCallStackDepth; + this.analysisContext = new AnalysisContext<>(builder.iteratorPolicy, builder.checkerManager, builder.methodFilterManager, builder.summaryFactory); + } + + public InterAnalyzerMode getAnalyzerMode() { + return analyzerMode; + } + + public AnalysisContext getAnalysisContext() { + return analysisContext; + } + + public SummaryManager getSummaryManager() { + return summaryManager; + } + + @Override + public void runAnalysis(AnalysisMethod method) { + if (method == null) { + return; + } + InterAbsintInvokeHandler callHandler = new InterAbsintInvokeHandler<>(initialDomain, abstractInterpreter, analysisContext, new CallStack(maxRecursionDepth, maxCallStackDepth), summaryManager); + callHandler.handleRootInvoke(method); + } + + public int getMaxRecursionDepth() { + return maxRecursionDepth; + } + + public int getMaxCallStackDepth() { + return maxCallStackDepth; + } + + public static class Builder> extends Analyzer.Builder, Domain> { + private static final int DEFAULT_MAX_RECURSION_DEPTH = 16; + private static final int DEFAULT_MAX_CALLSTACK_DEPTH = 128; + private int maxRecursionDepth = DEFAULT_MAX_RECURSION_DEPTH; + private int maxCallStackDepth = DEFAULT_MAX_CALLSTACK_DEPTH; + private final SummaryFactory summaryFactory; + private final InterAnalyzerMode analyzerMode; + + public Builder(Domain initialDomain, + AbstractInterpreter abstractInterpreter, + SummaryFactory summaryFactory, + InterAnalyzerMode analyzerMode) { + super(initialDomain, abstractInterpreter); + this.summaryFactory = summaryFactory; + this.analyzerMode = analyzerMode; + } + + public Builder maxRecursionDepth(int maxRecursionDepth) { + this.maxRecursionDepth = maxRecursionDepth; + return self(); + } + + public Builder maxCallStackDepth(int maxCallStackDepth) { + this.maxCallStackDepth = maxCallStackDepth; + return self(); + } + + public InterProceduralAnalyzer build() { + return new InterProceduralAnalyzer<>(this); + } + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/IntraProceduralAnalyzer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/IntraProceduralAnalyzer.java new file mode 100644 index 000000000000..1800d7c35914 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/IntraProceduralAnalyzer.java @@ -0,0 +1,57 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.IntraAbsintInvokeHandler; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.analysis.mode.IntraAnalyzerMode; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; + +/** + * An intra-procedural analyzer that performs an intra-procedural analysis on the given method. + * + * @param the type of the abstract domain used for the analysis. + */ +public final class IntraProceduralAnalyzer> extends Analyzer { + + private final IntraAnalyzerMode analyzerMode; + private final AnalysisContext analysisContext; + + private IntraProceduralAnalyzer(Builder builder) { + super(builder); + this.analyzerMode = builder.analyzerMode; + this.analysisContext = new AnalysisContext<>(builder.iteratorPolicy, builder.checkerManager, builder.methodFilterManager); + } + + public IntraAnalyzerMode getAnalyzerMode() { + return analyzerMode; + } + + @Override + public void runAnalysis(AnalysisMethod method) { + if (method == null) { + return; + } + IntraAbsintInvokeHandler callHandler = new IntraAbsintInvokeHandler<>(initialDomain, abstractInterpreter, analysisContext); + callHandler.handleRootInvoke(method); + } + + public static class Builder> extends Analyzer.Builder, Domain> { + + private final IntraAnalyzerMode analyzerMode; + + public Builder(Domain initialDomain, AbstractInterpreter abstractInterpreter, IntraAnalyzerMode analyzerMode) { + super(initialDomain, abstractInterpreter); + this.analyzerMode = analyzerMode; + } + + public IntraProceduralAnalyzer build() { + return new IntraProceduralAnalyzer<>(this); + } + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InvokeAnalysisOutcome.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InvokeAnalysisOutcome.java new file mode 100644 index 000000000000..788a059ea69d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/InvokeAnalysisOutcome.java @@ -0,0 +1,35 @@ +package com.oracle.svm.hosted.analysis.ai.analysis; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.summary.Summary; + +/** + * Represents the outcome of an analysis, consisting of a result and an optional summary. + * + * @param result the result of the analysis, represented as an instance of {@link AnalysisResult} + * @param summary the summary produced by the analysis, represented as an instance of {@link Summary} + * @param the type of the derived {@link AbstractDomain} used in the analysis + */ +public record InvokeAnalysisOutcome>(AnalysisResult result, + Summary summary) { + + public static > InvokeAnalysisOutcome ok(Summary summary) { + return new InvokeAnalysisOutcome<>(AnalysisResult.OK, summary); + } + + public static > InvokeAnalysisOutcome error(AnalysisResult result) { + return new InvokeAnalysisOutcome<>(result, null); + } + + public boolean isOk() { + return result == AnalysisResult.OK; + } + + @Override + public String toString() { + if (!isOk()) { + return result.toString(); + } + return ""; // We can in theory print the summary here, but the logs are scuffed and we can extract the after the analysis is finished + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/AnalysisContext.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/AnalysisContext.java new file mode 100644 index 000000000000..61e7f01c702e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/AnalysisContext.java @@ -0,0 +1,57 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.context; + +import com.oracle.svm.hosted.analysis.ai.checker.core.CheckerManager; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.IteratorPolicy; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.AnalysisMethodFilterManager; +import com.oracle.svm.hosted.analysis.ai.summary.SummaryFactory; + +/** + * Central analysis-scoped configuration and services. + * Single source of truth for iteration policy and managers that are + * shared across methods and call graphs. + */ +public final class AnalysisContext> { + + private final IteratorPolicy iteratorPolicy; + private final CheckerManager checkerManager; + private final AnalysisMethodFilterManager methodFilterManager; + private final SummaryFactory summaryFactory; /* may be null for intra */ + private final MethodGraphCache methodGraphCache = new MethodGraphCache(); + + public AnalysisContext(IteratorPolicy iteratorPolicy, + CheckerManager checkerManager, + AnalysisMethodFilterManager methodFilterManager) { + this(iteratorPolicy, checkerManager, methodFilterManager, null); + } + + public AnalysisContext(IteratorPolicy iteratorPolicy, + CheckerManager checkerManager, + AnalysisMethodFilterManager methodFilterManager, + SummaryFactory summaryFactory) { + this.iteratorPolicy = iteratorPolicy; + this.checkerManager = checkerManager; + this.methodFilterManager = methodFilterManager; + this.summaryFactory = summaryFactory; + } + + public IteratorPolicy getIteratorPolicy() { + return iteratorPolicy; + } + + public CheckerManager getCheckerManager() { + return checkerManager; + } + + public AnalysisMethodFilterManager getMethodFilterManager() { + return methodFilterManager; + } + + public SummaryFactory getSummaryFactory() { + return summaryFactory; + } + + public MethodGraphCache getMethodGraphCache() { + return methodGraphCache; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContext.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContext.java new file mode 100644 index 000000000000..6ccb931b810d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContext.java @@ -0,0 +1,48 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Objects; + +/** + * Per-invocation context passed along call edges. + * Contains a bounded call stack and/or a derived signature, and optionally actual argument abstract values. + * + * @param callStack representation by analysis methods + * @param contextSignature optional precomputed signature (e.g., k-CFA) + * @param actualArgs optional + */ +public record CallContext>(Deque callStack, + String contextSignature, List actualArgs) { + public CallContext(Deque callStack, String contextSignature, List actualArgs) { + this.callStack = new ArrayDeque<>(Objects.requireNonNull(callStack)); + this.contextSignature = contextSignature; + this.actualArgs = (actualArgs == null) ? List.of() : new ArrayList<>(actualArgs); + } + + @Override + public Deque callStack() { + return new ArrayDeque<>(callStack); + } + + /** + * Build a compact k-CFA signature from the tail of the call stack (k last methods). + */ + public static String buildKCFASignature(Deque stack, int k) { + if (stack == null || stack.isEmpty() || k <= 0) return ""; + List list = new ArrayList<>(stack); + StringBuilder sb = new StringBuilder(); + int start = Math.max(0, list.size() - k); + for (int i = start; i < list.size(); i++) { + AnalysisMethod m = list.get(i); + sb.append(m.getQualifiedName()); + if (i < list.size() - 1) sb.append(" -> "); + } + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContextHolder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContextHolder.java new file mode 100644 index 000000000000..706fe48923a1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallContextHolder.java @@ -0,0 +1,39 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Thread-local holder for the current CallContext so components that do not + * receive it explicitly (e.g., interpreters) can query a compact context signature + * to implement context-sensitive behaviors (like allocation-site sensitivity). + * This is a bridge until IteratorContext is extended to carry CallContext. + */ +public final class CallContextHolder { + private static final ThreadLocal>> context = new ThreadLocal<>(); + + private CallContextHolder() { + } + + public static void set(CallContext> ctx) { + context.set(ctx); + } + + public static void clear() { + context.remove(); + } + + public static CallContext> get() { + return context.get(); + } + + public static String getSignatureOrEmpty() { + CallContext> ctx = context.get(); + return ctx == null ? "" : (ctx.contextSignature() == null ? "" : ctx.contextSignature()); + } + + public static String buildKCFASignature(java.util.Deque stack, int k) { + return CallContext.buildKCFASignature(stack, k); + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallStack.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallStack.java new file mode 100644 index 000000000000..82db862d957f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/CallStack.java @@ -0,0 +1,142 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +/** + * Represents a call stack used to keep track and manage invoked methods during analysis. + */ +public final class CallStack { + + private final Deque callStack = new LinkedList<>(); + private final int maxRecursionDepth; + private final int maxCallStackDepth; + + public CallStack(int maxRecursionDepth, int maxCallStackDepth) { + this.maxRecursionDepth = maxRecursionDepth; + this.maxCallStackDepth = maxCallStackDepth; + } + + public void push(AnalysisMethod analysisMethod) { + callStack.push(analysisMethod); + } + + public void pop() { + callStack.pop(); + } + + public AnalysisMethod getCurrentAnalysisMethod() { + return callStack.peek(); + } + + public int getMaxRecursionDepth() { + return maxRecursionDepth; + } + + public int getMaxCallStackDepth() { + return maxCallStackDepth; + } + + public int getCurrentDepth() { + return callStack.size(); + } + + public List getMethods() { + return new ArrayList<>(callStack); + } + + + /** + * Counts the number of times an analysis method has been consecutively called in the current call stack. + * + * @param method the analysisMethod to count recursive calls for + * @return the number of consecutive the specified analysisMethod + */ + public int countConsecutiveCalls(AnalysisMethod method) { + int count = 0; + String qualifiedName = method.getQualifiedName(); + for (AnalysisMethod callStackMethod : callStack) { + if (callStackMethod.getQualifiedName().equals(qualifiedName)) { + count++; + } else { + break; + } + } + return count; + } + + public boolean hasMethodCallCycle(AnalysisMethod method) { + List compactedCallStack = new LinkedList<>(); + for (AnalysisMethod callStackMethod : callStack) { + if (compactedCallStack.isEmpty() || !compactedCallStack.getLast().equals(callStackMethod)) { + compactedCallStack.add(callStackMethod); + } + } + + /* + * We don't want a trivial cycles like foo() -> foo(), which means simple recursion. + * We are interested in cycles like foo() -> bar() -> foo() or foo() -> bar() -> baz() -> foo() + */ + List methodList = compactedCallStack.reversed(); + for (int i = 0; i < methodList.size() - 1; i++) { + if (methodList.get(i).equals(method)) { + return true; + } + } + + return false; + } + + public String formatCycleWithMethod(AnalysisMethod method) { + StringBuilder sb = new StringBuilder(); + int index = 0; + AnalysisMethod firstOccurrence = null; + + for (AnalysisMethod callStackMethod : callStack) { + if (callStackMethod.equals(method)) { + firstOccurrence = callStackMethod; + break; + } + index++; + } + + if (firstOccurrence != null) { + // Format the cycle path + int count = 0; + for (AnalysisMethod callStackMethod : callStack) { + if (count >= index) { + sb.append(callStackMethod.getQualifiedName()); + sb.append(" → "); + } + count++; + } + sb.append(method.getQualifiedName()); + } else { + sb.append("Unknown cycle with ").append(method.getQualifiedName()); + } + + return sb.toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (AnalysisMethod method : callStack) { + if (!first) { + sb.append(" ← "); + } + first = false; + sb.append(method.getQualifiedName()); + } + return sb.toString(); + } + + public Deque getCallStack() { + return callStack; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/MethodGraphCache.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/MethodGraphCache.java new file mode 100644 index 000000000000..d3c16323166d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/context/MethodGraphCache.java @@ -0,0 +1,57 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.fixpoint.wto.WeakTopologicalOrdering; +import jdk.graal.compiler.nodes.StructuredGraph; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Central cache for per-method graph artifacts (CFG, WTO), so they are built once per method + * and shared across fixpoint iterators. + */ +public final class MethodGraphCache { + + private final Map methodGraphMap = new ConcurrentHashMap<>(); + private final Map methodWtoMap = new ConcurrentHashMap<>(); + + public Map getMethodGraphMap() { + return methodGraphMap; + } + + public boolean containsMethodGraph(AnalysisMethod method) { + return methodGraphMap.containsKey(method); + } + + public void addToMethodGraphMap(AnalysisMethod method, StructuredGraph graph) { + methodGraphMap.put(method, graph); + } + + public Map getMethodWtoMap() { + return methodWtoMap; + } + + public boolean containsMethodWto(AnalysisMethod method) { + return methodWtoMap.containsKey(method); + } + + public void setMethodWtoMap(Map map) { + methodWtoMap.clear(); + methodWtoMap.putAll(map); + } + + public void addToMethodWtoMap(AnalysisMethod method, WeakTopologicalOrdering wto) { + methodWtoMap.put(method, wto); + } + + + public void joinWith(MethodGraphCache other) { + for (var method : other.methodGraphMap.keySet()) { + methodGraphMap.put(method, other.methodGraphMap.get(method)); + if (other.methodWtoMap.containsKey(method)) { + methodWtoMap.put(method, other.methodWtoMap.get(method)); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/AbsintInvokeHandler.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/AbsintInvokeHandler.java new file mode 100644 index 000000000000..87306e1364e6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/AbsintInvokeHandler.java @@ -0,0 +1,33 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.analysis.methodfilter.AnalysisMethodFilterManager; +import com.oracle.svm.hosted.analysis.ai.checker.core.CheckerManager; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractTransformer; + +/** + * Base class for invoke handler callbacks. + * + * @param type of the derived {@link AbstractDomain} used in the analysis. + */ +public abstract class AbsintInvokeHandler> implements InvokeHandler { + + protected final Domain initialDomain; + protected final AbstractTransformer abstractTransformer; + protected final CheckerManager checkerManager; + protected final AnalysisMethodFilterManager methodFilterManager; + protected final AnalysisContext analysisContext; + + @SuppressWarnings("this-escape") + public AbsintInvokeHandler(Domain initialDomain, + AbstractInterpreter abstractInterpreter, + AnalysisContext analysisContext) { + this.initialDomain = initialDomain; + this.abstractTransformer = new AbstractTransformer<>(abstractInterpreter, this::handleInvoke); + this.analysisContext = analysisContext; + this.checkerManager = analysisContext.getCheckerManager(); + this.methodFilterManager = analysisContext.getMethodFilterManager(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InterAbsintInvokeHandler.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InterAbsintInvokeHandler.java new file mode 100644 index 000000000000..5beed7f12eaa --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InterAbsintInvokeHandler.java @@ -0,0 +1,153 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.InvokeInfo; +import com.oracle.svm.hosted.analysis.ai.analysis.InvokeAnalysisOutcome; +import com.oracle.svm.hosted.analysis.ai.analysis.AnalysisResult; +import com.oracle.svm.hosted.analysis.ai.analysis.context.CallContextHolder; +import com.oracle.svm.hosted.analysis.ai.analysis.context.CallStack; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.FixpointIterator; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.FixpointIteratorFactory; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.summary.*; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Inter-procedural invoke handler which uses per-call method context and summary caching. + */ +public final class InterAbsintInvokeHandler> extends AbsintInvokeHandler { + + private final CallStack callStack; + private final SummaryManager summaryManager; + + public InterAbsintInvokeHandler( + Domain initialDomain, + AbstractInterpreter abstractInterpreter, + AnalysisContext analysisContext, + CallStack callStack, + SummaryManager summaryManager) { + super(initialDomain, abstractInterpreter, analysisContext); + this.callStack = callStack; + this.summaryManager = summaryManager; + } + + @Override + public InvokeAnalysisOutcome handleInvoke(InvokeInput invokeInput) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + Invoke invoke = invokeInput.invoke(); + + AnalysisMethod current = invokeInput.callerMethod() != null + ? invokeInput.callerMethod() + : callStack.getCurrentAnalysisMethod(); + + AnalysisMethod targetAnalysisMethod; + try { + targetAnalysisMethod = getInvokeTargetAnalysisMethod(current, invoke); + assert targetAnalysisMethod != null; + StructuredGraph graph = AbstractInterpretationServices.getInstance().getGraph(targetAnalysisMethod); + assert graph != null; + } catch (Exception e) { + InvokeAnalysisOutcome outcome = InvokeAnalysisOutcome.error(AnalysisResult.UNKNOWN_METHOD); + logger.log(outcome.toString(), LoggerVerbosity.INFO); + return outcome; + } + + /* Handle errors */ + if (methodFilterManager.shouldSkipMethod(targetAnalysisMethod)) { + return InvokeAnalysisOutcome.error(AnalysisResult.IN_SKIP_LIST); + } + + if (callStack.getMaxCallStackDepth() <= callStack.getCurrentDepth()) { + return InvokeAnalysisOutcome.error(AnalysisResult.CALL_STACK_DEPTH_OVERFLOW); + } + + if (callStack.getMaxRecursionDepth() <= callStack.countConsecutiveCalls(targetAnalysisMethod)) { + return InvokeAnalysisOutcome.error(AnalysisResult.RECURSION_LIMIT_OVERFLOW); + } + + if (callStack.hasMethodCallCycle(targetAnalysisMethod)) { + return InvokeAnalysisOutcome.error(AnalysisResult.MUTUAL_RECURSION_CYCLE); + } + + + AbstractState callerState = invokeInput.callerState(); + Domain callerPreAtInvoke = callerState.getPreCondition(invoke.asNode()); + /* Build a pre-condition summary for this call. */ + Summary calleeSummary = summaryManager.createSummary(invoke, callerPreAtInvoke, invokeInput.actualArgDomains()); + + /* Try to reuse an existing summary that subsumes this one. */ + Summary cached = summaryManager.getSummary(targetAnalysisMethod, calleeSummary); + if (cached != null) { + logger.log("Summary cache contains targetMethod: " + targetAnalysisMethod, LoggerVerbosity.SUMMARY); + return InvokeAnalysisOutcome.ok(cached); + } + + /* Analyze callee under given context. */ + callStack.push(targetAnalysisMethod); + try { + FixpointIterator fixpointIterator = FixpointIteratorFactory.createIterator(targetAnalysisMethod, initialDomain, abstractTransformer, analysisContext); + String ctxSig = invokeInput.contextSignature().orElseGet( + () -> CallContextHolder.buildKCFASignature(callStack.getCallStack(), 2)); + fixpointIterator.getIteratorContext().setCallContextSignature(ctxSig); + + AbstractState calleeAbstractState = fixpointIterator.runFixpointIteration(calleeSummary.getPreCondition()); + calleeSummary.finalizeSummary(calleeAbstractState); + + ContextKey ctxKey = invokeInput.contextKey() + .orElse(new ContextKey(targetAnalysisMethod, ctxSig.hashCode(), callStack.getCurrentDepth())); + + /* Put the new context summary for the callee method */ + summaryManager.putSummary(targetAnalysisMethod, ctxKey, calleeSummary); + /* Update the context-insensitive summary */ + summaryManager.getSummaryRepository().get(targetAnalysisMethod).joinWithContextState(calleeAbstractState); + } finally { + callStack.pop(); + } + + return InvokeAnalysisOutcome.ok(calleeSummary); + } + + @Override + public void handleRootInvoke(AnalysisMethod root) { + if (methodFilterManager.shouldSkipMethod(root)) { + return; + } + + String ctxSig = CallContextHolder.buildKCFASignature(callStack.getCallStack(), 2); + FixpointIterator fixpointIterator = FixpointIteratorFactory.createIterator(root, initialDomain, abstractTransformer, analysisContext); + fixpointIterator.getIteratorContext().setCallContextSignature(ctxSig); + callStack.push(root); + try { + AbstractState abstractState = fixpointIterator.runFixpointIteration(); + // TODO: we should even create a summary for this root method, then we can run checkers only in the engine + checkerManager.runCheckersOnSingleMethod(root, abstractState, analysisContext.getMethodGraphCache().getMethodGraphMap().get(root)); + } finally { + callStack.pop(); + } + } + + private AnalysisMethod getInvokeTargetAnalysisMethod(AnalysisMethod root, Invoke invoke) { + ResolvedJavaMethod invokeTarget = invoke.getTargetMethod(); + for (InvokeInfo invokeInfo : root.getInvokes()) { + ResolvedJavaMethod candidate = invokeInfo.getTargetMethod().wrapped; + if (sameMethod(invokeTarget, candidate)) { + return invokeInfo.getTargetMethod(); + } + } + return null; + } + + private boolean sameMethod(ResolvedJavaMethod a, ResolvedJavaMethod b) { + return a.getName().equals(b.getName()) && + a.getSignature().toMethodDescriptor().equals(b.getSignature().toMethodDescriptor()) && + a.getDeclaringClass().toJavaName().equals(b.getDeclaringClass().toJavaName()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/IntraAbsintInvokeHandler.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/IntraAbsintInvokeHandler.java new file mode 100644 index 000000000000..dc8c3ab88dab --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/IntraAbsintInvokeHandler.java @@ -0,0 +1,47 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.InvokeAnalysisOutcome; +import com.oracle.svm.hosted.analysis.ai.analysis.AnalysisResult; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.FixpointIterator; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.FixpointIteratorFactory; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import jdk.graal.compiler.nodes.StructuredGraph; + +/** + * Represents an intra-procedural invoke handler. + * It ignores methods in the sense that every {@link AnalysisMethod} is a black box, + * and therefore produces the {@link AnalysisResult#ANALYSIS_FAILED}. + * + * @param type of the derived {@link AbstractDomain} used in the analysis + */ +public final class IntraAbsintInvokeHandler> + extends AbsintInvokeHandler { + + public IntraAbsintInvokeHandler(Domain initialDomain, + AbstractInterpreter abstractInterpreter, + AnalysisContext analysisContext) { + super(initialDomain, abstractInterpreter, analysisContext); + } + + @Override + public InvokeAnalysisOutcome handleInvoke(InvokeInput invokeInput) { + return InvokeAnalysisOutcome.error(AnalysisResult.ANALYSIS_FAILED); + } + + @Override + public void handleRootInvoke(AnalysisMethod root) { + FixpointIterator fixpointIterator = FixpointIteratorFactory.createIterator(root, initialDomain, abstractTransformer, analysisContext); + AbstractState abstractState = fixpointIterator.runFixpointIteration(); + StructuredGraph graph = analysisContext.getMethodGraphCache().getMethodGraphMap().get(root); + + if (graph == null) { + return; + } + checkerManager.runCheckersOnSingleMethod(root, abstractState, graph); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeCallBack.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeCallBack.java new file mode 100644 index 000000000000..69e104c956da --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeCallBack.java @@ -0,0 +1,21 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.svm.hosted.analysis.ai.analysis.InvokeAnalysisOutcome; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Callback interface for handling method invocations during abstract interpretation. + * Developers can use this callback when they want to analyze the effects of method calls, + * + * @param the type of the derived {@link AbstractDomain} used in the analysis + */ +@FunctionalInterface +public interface InvokeCallBack> { + /** + * Handles the invocation of a method during abstract interpretation. + * + * @param invokeInput the relevant information needed to perform abstract interpretation of a given invocation + * @return the analysis outcome + */ + InvokeAnalysisOutcome handleInvoke(InvokeInput invokeInput); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeHandler.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeHandler.java new file mode 100644 index 000000000000..b5268f81f9be --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeHandler.java @@ -0,0 +1,29 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.InvokeAnalysisOutcome; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Interface for handling method invocations during abstract interpretation. + * + * @param type of the derived {@link AbstractDomain} used in the analysis + */ +public interface InvokeHandler> { + + /** + * Handles the invocation of a method during abstract interpretation. + * + * @param invokeInput the relevant information needed to perform abstract interpretation of a given invocation + * @return the analysis outcome + */ + InvokeAnalysisOutcome handleInvoke(InvokeInput invokeInput); + + /** + * The starting point of the analysis + * We receive an {@link AnalysisMethod} and we start our abstract interpretation from this analysisMethod as the starting point. + * + * @param root the {@link AnalysisMethod} that the abstract interpretation analysis starts from + */ + void handleRootInvoke(AnalysisMethod root); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeInput.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeInput.java new file mode 100644 index 000000000000..d5f1e35c48e9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeInput.java @@ -0,0 +1,49 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.summary.ContextKey; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.Invoke; + +import java.util.List; +import java.util.Optional; + +/** + * Immutable input bundle provided to an interprocedural invoke handler. + * Carries the caller abstract state at the call site, the invoke node, actual + * argument nodes, and the current context key/signature if available. + */ +public record InvokeInput>( + AnalysisMethod callerMethod, + AbstractState callerState, + Invoke invoke, + List actualArgsNodes, + List actualArgDomains, + Optional contextKey, + Optional contextSignature) { + + /** This method should be used if interpreters want to create their own custom signatures of methods **/ + public static > InvokeInput of( + AnalysisMethod callerMethod, + AbstractState callerState, + Invoke invoke, + List actualArgsNodes, + List actualArgDomains, + ContextKey contextKey, + String contextSignature) { + return new InvokeInput<>(callerMethod, callerState, invoke, actualArgsNodes, actualArgDomains, + Optional.ofNullable(contextKey), Optional.ofNullable(contextSignature)); + } + + /** In this case, if the contextKey is empty, the framework will use the default context signature builder **/ + public static > InvokeInput of( + AnalysisMethod callerMethod, + AbstractState callerState, + Invoke invoke, + List actualArgsNodes, + List actualArgDomains) { + return new InvokeInput<>(callerMethod, callerState, invoke, actualArgsNodes, actualArgDomains, Optional.empty(), Optional.empty()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeOutcome.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeOutcome.java new file mode 100644 index 000000000000..0e6ff5dbb97d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/invokehandle/InvokeOutcome.java @@ -0,0 +1,49 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.invokehandle; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Map; + +/** + * A normalized result produced by the invoke handler for a call from the interpreter. + */ +public final class InvokeOutcome> { + public enum Kind { ERROR, SUMMARY } + + private final Kind kind; + private final Domain returnValue; /* may be null for void */ + private final Domain heapDelta; /* may be null */ + private final Map placeholderMapping; // mapping from placeholders to caller roots (access paths), typed loosely + + private InvokeOutcome(Kind kind, Domain returnValue, Domain heapDelta, Map placeholderMapping) { + this.kind = kind; + this.returnValue = returnValue; + this.heapDelta = heapDelta; + this.placeholderMapping = placeholderMapping; + } + + public static > InvokeOutcome error() { + return new InvokeOutcome<>(Kind.ERROR, null, null, null); + } + + public static > InvokeOutcome summary(D returnValue, D heapDelta, Map placeholderMapping) { + return new InvokeOutcome<>(Kind.SUMMARY, returnValue, heapDelta, placeholderMapping); + } + + public Kind getKind() { + return kind; + } + + public Domain getReturnValue() { + return returnValue; + } + + public Domain getHeapDelta() { + return heapDelta; + } + + public Map getPlaceholderMapping() { + return placeholderMapping; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilter.java new file mode 100644 index 000000000000..3df1af79fdc9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilter.java @@ -0,0 +1,25 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +/** + * Represents an {@link AnalysisMethod} filter that can be used to filter methods during analysis. + * Used in some cases to skip methods that are not relevant for the analysis. + * For example, when analyzing a specific package, the filter can be used to skip methods that are not in the package. + */ +public interface AnalysisMethodFilter { + + /** + * Description of the method filter + * @return String description of the method filter + */ + String getDescription(); + + /** + * Checks if the given analysisMethod be skipped during the analysis. + * + * @param method the method to check + * @return true if the method should be analyzed, false otherwise + */ + boolean shouldSkipMethod(AnalysisMethod method); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilterManager.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilterManager.java new file mode 100644 index 000000000000..fb03acd79afa --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/AnalysisMethodFilterManager.java @@ -0,0 +1,42 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +import java.util.ArrayList; +import java.util.List; + +/** + * A wrapper around a list of {@link AnalysisMethodFilter}s. + */ +public final class AnalysisMethodFilterManager { + + private final List filters; + + public AnalysisMethodFilterManager() { + this.filters = new ArrayList<>(); + } + + /** + * Adds a new filter to the manager. + * + * @param filter the filter to add + */ + public void addMethodFilter(AnalysisMethodFilter filter) { + filters.add(filter); + } + + /** + * Checks if the given analysisMethod should be skipped based on the registered filters. + * + * @param method the analysisMethod to check + * @return true if the analysisMethod should be skipped, false otherwise + */ + public boolean shouldSkipMethod(AnalysisMethod method) { + for (AnalysisMethodFilter filter : filters) { + if (filter.shouldSkipMethod(method)) { + return true; + } + } + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipJavaLangAnalysisMethodFilter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipJavaLangAnalysisMethodFilter.java new file mode 100644 index 000000000000..031b4671f364 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipJavaLangAnalysisMethodFilter.java @@ -0,0 +1,22 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +/** + * Skip methods that are part of the java.lang package. + */ +public final class SkipJavaLangAnalysisMethodFilter implements AnalysisMethodFilter { + + @Override + public String getDescription() { + return "Skip jdk/java methods"; + } + + @Override + public boolean shouldSkipMethod(AnalysisMethod method) { + return method.getQualifiedName().startsWith("java") || + method.getQualifiedName().startsWith("jdk") || + method.getQualifiedName().startsWith("sun") || + method.getQualifiedName().startsWith("com.sun"); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipMicronautMethodFilter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipMicronautMethodFilter.java new file mode 100644 index 000000000000..0b5d4ac0b1cc --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipMicronautMethodFilter.java @@ -0,0 +1,17 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import jdk.graal.compiler.debug.MethodFilter; + +public class SkipMicronautMethodFilter implements AnalysisMethodFilter{ + + @Override + public String getDescription() { + return "Skip Micronaut Method Filter"; + } + + @Override + public boolean shouldSkipMethod(AnalysisMethod method) { + return method.getQualifiedName().startsWith("io.micronaut"); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSpringMethodFilter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSpringMethodFilter.java new file mode 100644 index 000000000000..a2f02907dafb --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSpringMethodFilter.java @@ -0,0 +1,15 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +public class SkipSpringMethodFilter implements AnalysisMethodFilter { + @Override + public String getDescription() { + return "Skip Spring Methods"; + } + + @Override + public boolean shouldSkipMethod(AnalysisMethod method) { + return method.getQualifiedName().startsWith("org.springframework"); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSvmMethodFilter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSvmMethodFilter.java new file mode 100644 index 000000000000..471c95cabbb4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/methodfilter/SkipSvmMethodFilter.java @@ -0,0 +1,16 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.methodfilter; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +public class SkipSvmMethodFilter implements AnalysisMethodFilter { + + @Override + public String getDescription() { + return "Skip substratevm methods"; + } + + @Override + public boolean shouldSkipMethod(AnalysisMethod method) { + return method.getQualifiedName().startsWith("com.oracle.svm"); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/InterAnalyzerMode.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/InterAnalyzerMode.java new file mode 100644 index 000000000000..e8559a97d845 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/InterAnalyzerMode.java @@ -0,0 +1,14 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.mode; + +import com.oracle.svm.hosted.analysis.ai.AbstractInterpretationEngine; +import com.oracle.svm.hosted.analysis.ai.analysis.InterProceduralAnalyzer; + +/** + * Represents the possible modes of {@link InterProceduralAnalyzer}. + * NOTE: if users set the mode to {@link #ANALYZE_FROM_MAIN_ENTRYPOINT} and the application does not have a single main entry point + * the {@link AbstractInterpretationEngine} will default to using {@link #ANALYZE_FROM_ALL_ROOTS} instead + */ +public enum InterAnalyzerMode { + ANALYZE_FROM_MAIN_ENTRYPOINT, /* If there is a single main entry point to the application, analyze from this method only */ + ANALYZE_FROM_ALL_ROOTS /* Analyze all call graph roots from the points-to analysis */ +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/IntraAnalyzerMode.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/IntraAnalyzerMode.java new file mode 100644 index 000000000000..2b40260d96c9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/analysis/mode/IntraAnalyzerMode.java @@ -0,0 +1,14 @@ +package com.oracle.svm.hosted.analysis.ai.analysis.mode; + +import com.oracle.svm.hosted.analysis.ai.analysis.IntraProceduralAnalyzer; +import com.oracle.svm.hosted.analysis.ai.AbstractInterpretationEngine; + +/** + * Represents the possible modes of {@link IntraProceduralAnalyzer}. + * NOTE: if users set the mode to {@link #ANALYZE_MAIN_ENTRYPOINT_ONLY} and the application does not have a single main entry point + * the {@link AbstractInterpretationEngine} will default to using {@link #ANALYZE_ALL_INVOKED_METHODS} instead + */ +public enum IntraAnalyzerMode { + ANALYZE_MAIN_ENTRYPOINT_ONLY, /* If there is a single main entry point to the application, it will only analyze it */ + ANALYZE_ALL_INVOKED_METHODS, /* We can analyze every simplyImplementationInvoked from points-to analysis */ +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/BaseApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/BaseApplier.java new file mode 100644 index 000000000000..6e7f7131fa55 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/BaseApplier.java @@ -0,0 +1,14 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +public abstract class BaseApplier implements FactApplier { + + @Override + public String getDescription() { + return "Base applier"; + } + + @Override + public boolean shouldApply() { + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/CleanupApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/CleanupApplier.java new file mode 100644 index 000000000000..fd8f2cb0c369 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/CleanupApplier.java @@ -0,0 +1,40 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.ApplierResult; +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.phases.common.CanonicalizerPhase; +import jdk.graal.compiler.phases.common.DeadCodeEliminationPhase; + +import java.util.Set; + +/** + * Performs a final sweep and verification after applying fact-driven rewrites. + */ +public final class CleanupApplier implements FactApplier { + + @Override + public Set getApplicableFactKinds() { + return Set.of(); + } + + @Override + public String getDescription() { + return "Cleanup"; + } + + @Override + public boolean shouldApply() { + return true; + } + + @Override + public ApplierResult apply(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator) { + CanonicalizerPhase.create().apply(graph, AbstractInterpretationServices.getInstance().getInflation().getProviders(method), false); + new DeadCodeEliminationPhase().apply(graph, false); + return ApplierResult.empty(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConditionTruthnessApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConditionTruthnessApplier.java new file mode 100644 index 000000000000..85e77cc1d191 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConditionTruthnessApplier.java @@ -0,0 +1,78 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.ApplierResult; +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.ConditionTruthnessFact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.StructuredGraph; + +import java.util.List; +import java.util.Set; + +/** + * Applies ConditionTruthFact by folding always-true/false branches. + */ +public final class ConditionTruthnessApplier extends BaseApplier { + + @Override + public Set getApplicableFactKinds() { + return Set.of(FactKind.CONDITION_TRUTH); + } + + @Override + public String getDescription() { + return "ConditionTruthFolding"; + } + + @Override + public boolean shouldApply() { + return true; + } + + @Override + public ApplierResult apply(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator) { + List facts = aggregator.factsOfKind(FactKind.CONDITION_TRUTH); + if (facts.isEmpty()) { + return ApplierResult.empty(); + } + + int trueFolded = 0; + int falseFolded = 0; + + for (Fact f : facts) { + if (!(f instanceof ConditionTruthnessFact tf)) { + continue; + } + IfNode ifn = tf.ifNode(); + if (ifn == null || !ifn.isAlive()) { + continue; + } + switch (tf.truth()) { + case ALWAYS_TRUE -> { + trueFolded++; + if (shouldApply()) { + GraphRewrite.foldIfTrue(graph, ifn); + } + } + case ALWAYS_FALSE -> { + falseFolded++; + if (shouldApply()) { + GraphRewrite.foldIfFalse(graph, ifn); + } + } + default -> { + // uncertain -> no action + } + } + } + + return ApplierResult.builder() + .appliedFacts(trueFolded + falseFolded) + .branchesFoldedTrue(trueFolded) + .branchesFoldedFalse(falseFolded) + .build(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConstantStampApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConstantStampApplier.java new file mode 100644 index 000000000000..341b9f99d4f5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/ConstantStampApplier.java @@ -0,0 +1,73 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.ApplierResult; +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.ConstantFact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import jdk.graal.compiler.core.common.type.Stamp; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.NodeView; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.ValueNode; + +import java.util.List; +import java.util.Set; + +/** + * Applies ConstantFact by tightening the stamp of the corresponding ValueNode + * to an exact integer interval [v, v]. + */ +public final class ConstantStampApplier extends BaseApplier { + + @Override + public Set getApplicableFactKinds() { + return Set.of(FactKind.CONSTANT); + } + + @Override + public String getDescription() { + return "ConstantStamp"; + } + + @Override + public boolean shouldApply() { + return true; + } + + @Override + public ApplierResult apply(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator) { + List facts = aggregator.factsOfKind(FactKind.CONSTANT); + if (facts.isEmpty()) { + return ApplierResult.empty(); + } + + int tightened = 0; + for (Fact f : facts) { + if (!(f instanceof ConstantFact cf)) { + continue; + } + + Node node = cf.node(); + if (!(node instanceof ValueNode vn) || !vn.isAlive()) { + continue; + } + + Stamp exact = cf.exactIntegerStampOrNull(); + if (exact == null) { + continue; + } + Stamp current = vn.stamp(NodeView.DEFAULT); + Stamp improved = current.tryImproveWith(exact); + if (improved != null && !improved.equals(current)) { + tightened++; + if (shouldApply()) { + vn.setStamp(improved); + } + } + } + + return ApplierResult.constantsStamped(tightened); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplier.java new file mode 100644 index 000000000000..028f3a126199 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplier.java @@ -0,0 +1,41 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.ApplierResult; +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import jdk.graal.compiler.nodes.StructuredGraph; + +import java.util.Set; + +/** + * Applies a specific kind of fact to a StructuredGraph. FactAppliers are run + * after all checkers have produced facts and those facts have been aggregated. + * They must preserve graph invariants and avoid unsafe partial deletions. + */ +public interface FactApplier { + + /** + * Each FactApplier can only react to a certain subset of the fact produced by the provided checkers. + * @return the kinds of facts this applier can handle. + */ + Set getApplicableFactKinds(); + + /** + * @return a description of this applier. + */ + String getDescription(); + + /** + * @return false if this applier can be skipped in the abstract interpretation analysis + */ + boolean shouldApply(); + + /** + * Apply graph rewrites driven by facts in the aggregator. Appliers should be idempotent + * and resilient to partially optimized graphs. + * + * @return per-applier counters for statistics aggregation. + */ + ApplierResult apply(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplierRegistry.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplierRegistry.java new file mode 100644 index 000000000000..642670656d87 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/FactApplierRegistry.java @@ -0,0 +1,86 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +/** + * Global registry for {@link FactApplier} instances grouped by {@link FactKind}. + *

+ * Appliers are registered once and reused across methods. A suite can pick the + * relevant appliers based on the facts present for a given method. + */ +public final class FactApplierRegistry { + + private static final Map> REGISTRY = new EnumMap<>(FactKind.class); + + static { + register(FactKind.CONSTANT, new InvokeConstantFoldingApplier()); + register(FactKind.CONSTANT, new ConstantStampApplier()); + register(FactKind.CONDITION_TRUTH, new ConditionTruthnessApplier()); + } + + private FactApplierRegistry() { + } + + /** + * Registers an applier for the given fact kind. Appends preserve registration order. + */ + public static synchronized void register(FactKind kind, FactApplier applier) { + REGISTRY.computeIfAbsent(kind, k -> new ArrayList<>()).add(applier); + } + + /** + * Registers multiple appliers for a given fact kind. + */ + public static synchronized void registerAll(FactKind kind, Collection appliers) { + if (appliers == null || appliers.isEmpty()) { + return; + } + REGISTRY.computeIfAbsent(kind, k -> new ArrayList<>()).addAll(appliers); + } + + /** + * Returns an unmodifiable view of all appliers in registration order across all kinds. + */ + public static List getAllAppliers() { + List all = new ArrayList<>(); + for (List list : REGISTRY.values()) { + all.addAll(list); + } + return Collections.unmodifiableList(all); + } + + /** + * Returns an unmodifiable list of appliers associated with a specific kind. + */ + public static List getAppliersFor(FactKind kind) { + List list = REGISTRY.get(kind); + return list == null ? List.of() : Collections.unmodifiableList(list); + + } + + /** + * Computes a de-duplicated, ordered list of appliers that are relevant to the facts + * available in the provided aggregator. Preserves registration order per kind. + */ + public static List getRelevantAppliers(FactAggregator aggregator) { + if (aggregator == null || aggregator.isEmpty()) { + return List.of(); + } + LinkedHashSet result = new LinkedHashSet<>(); + for (FactKind kind : FactKind.values()) { + if (!aggregator.factsOfKind(kind).isEmpty()) { + result.addAll(getAppliersFor(kind)); + } + } + return List.copyOf(result); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/GraphRewrite.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/GraphRewrite.java new file mode 100644 index 000000000000..85123c5e7483 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/GraphRewrite.java @@ -0,0 +1,25 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import jdk.graal.compiler.nodes.AbstractBeginNode; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.StructuredGraph; + +/** + * Simple graph rewriter helpers driven by facts from checkers. + */ +public final class GraphRewrite { + + public static void foldIfTrue(StructuredGraph graph, IfNode ifNode) { + AbstractBeginNode falseSuccessor = ifNode.falseSuccessor(); + var logger = AbstractInterpretationLogger.getInstance(); + graph.removeSplitPropagate(ifNode, ifNode.trueSuccessor()); + } + + public static void foldIfFalse(StructuredGraph graph, IfNode ifNode) { + AbstractBeginNode trueSuccessor = ifNode.trueSuccessor(); + AbstractInterpretationLogger.getInstance().log("[GraphRewrite] Folding IfNode to false branch: " + ifNode, LoggerVerbosity.CHECKER); + graph.removeSplitPropagate(ifNode, ifNode.falseSuccessor()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/InvokeConstantFoldingApplier.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/InvokeConstantFoldingApplier.java new file mode 100644 index 000000000000..62a7ec5c1c7c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/appliers/InvokeConstantFoldingApplier.java @@ -0,0 +1,118 @@ +package com.oracle.svm.hosted.analysis.ai.checker.appliers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.ApplierResult; +import com.oracle.svm.hosted.analysis.ai.checker.core.FactAggregator; +import com.oracle.svm.hosted.analysis.ai.checker.facts.ConstantFact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.FixedNode; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.InvokeNode; +import jdk.graal.compiler.nodes.InvokeWithExceptionNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.vm.ci.meta.JavaKind; + +import java.util.List; +import java.util.Set; + +/** + * This is an applier that folds invokes to the constant values that they return + */ +public final class InvokeConstantFoldingApplier extends BaseApplier { + + @Override + public Set getApplicableFactKinds() { + return Set.of(FactKind.CONSTANT); + } + + @Override + public String getDescription() { + return "InvokeConstantFolding"; + } + + @Override + public ApplierResult apply(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator) { + List facts = aggregator.factsOfKind(FactKind.CONSTANT); + if (facts.isEmpty()) { + return ApplierResult.empty(); + } + + int folded = 0; + for (Fact f : facts) { + if (!(f instanceof ConstantFact(Node node, long value))) { + continue; + } + + if (!(node instanceof Invoke invoke) || !invoke.isAlive()) { + continue; + } + + Node invokeAsNode = invoke.asNode(); + if (invokeAsNode == null) { + continue; + } + + JavaKind retKind = JavaKind.Int; + if (invoke.callTarget() != null && invoke.callTarget().targetMethod() != null) { + var sig = invoke.callTarget().targetMethod().getSignature(); + if (sig != null) { + retKind = sig.getReturnKind(); + } + } + + if (!retKind.isNumericInteger()) { + continue; + } + + if (!fitsIntoKind(retKind, value)) { + continue; + } + + folded++; + if (shouldApply()) { + ConstantNode constantNode = createIntegerConstant(graph, retKind, value); + invokeAsNode.replaceAtUsages(constantNode); + if (invokeAsNode instanceof InvokeNode invokeNode) { + FixedNode next = invokeNode.next(); + invokeNode.replaceAtPredecessor(next); + graph.removeFixed(invokeNode); + } else if (invokeAsNode instanceof InvokeWithExceptionNode invokeWithException) { + graph.removeSplitPropagate(invokeWithException, invokeWithException.getPrimarySuccessor()); + } + } + } + + return ApplierResult.builder() + .appliedFacts(folded) + .invokesReplacedWithConstants(folded) + .build(); + } + + private static ConstantNode createIntegerConstant(StructuredGraph graph, JavaKind kind, long value) { + ConstantNode cn = switch (kind) { + case Byte -> ConstantNode.forInt((byte) value, graph); + case Short -> ConstantNode.forInt((short) value, graph); + case Char -> ConstantNode.forInt((char) value, graph); + case Long -> ConstantNode.forLong(value, graph); + default -> ConstantNode.forInt((int) value, graph); + }; + if (cn.graph() == null) { + graph.add(cn); + } + return cn; + } + + private static boolean fitsIntoKind(JavaKind kind, long v) { + return switch (kind) { + case Byte -> v >= Byte.MIN_VALUE && v <= Byte.MAX_VALUE; + case Short -> v >= Short.MIN_VALUE && v <= Short.MAX_VALUE; + case Char -> v >= Character.MIN_VALUE && v <= Character.MAX_VALUE; + case Int -> v >= Integer.MIN_VALUE && v <= Integer.MAX_VALUE; + case Long -> true; + default -> false; + }; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/ConstantValueChecker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/ConstantValueChecker.java new file mode 100644 index 000000000000..c91e1a25f192 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/ConstantValueChecker.java @@ -0,0 +1,66 @@ +package com.oracle.svm.hosted.analysis.ai.checker.checkers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.Checker; +import com.oracle.svm.hosted.analysis.ai.checker.facts.ConstantFact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AccessPath; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.ConstantNode; +import jdk.graal.compiler.nodes.calc.FloatingNode; +import jdk.graal.compiler.nodes.java.NewArrayNode; +import jdk.graal.compiler.nodes.java.StoreFieldNode; +import jdk.vm.ci.meta.ResolvedJavaField; + +import java.util.ArrayList; +import java.util.List; + +public class ConstantValueChecker implements Checker { + private static final String NODE_PREFIX = "n"; + + private static String nodeId(Node n) { + return NODE_PREFIX + Integer.toHexString(System.identityHashCode(n)); + } + + public ConstantValueChecker() { + } + + @Override + public String getDescription() { + return "Constant propagation checker"; + } + + @Override + public List produceFacts(AnalysisMethod method, AbstractState abstractState) { + List facts = new ArrayList<>(); + for (Node node : abstractState.getStateMap().keySet()) { + AbstractMemory post = abstractState.getPostCondition(node); + if (post == null) continue; + + if (node instanceof ConstantNode cn || !(node instanceof FloatingNode)) { + continue; + } + + /* 1) temps bound to this node */ + String nid = nodeId(node); + var p = post.lookupTempByName(nid); + if (p != null) { + var iv = post.readStore(p); + if (iv != null && iv.isConstantValue()) { + long c = iv.getLower(); + facts.add(new ConstantFact(node, c)); + } + } + } + return facts; + } + + @Override + public boolean isCompatibleWith(AbstractState abstractState) { + return abstractState.getInitialDomain() instanceof AbstractMemory; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/DivisionByZeroChecker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/DivisionByZeroChecker.java new file mode 100644 index 000000000000..723296189efa --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/DivisionByZeroChecker.java @@ -0,0 +1,46 @@ +package com.oracle.svm.hosted.analysis.ai.checker.checkers; + +import com.oracle.svm.hosted.analysis.ai.checker.core.Checker; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +/** + * Represents a simple example of how a checker can be implemented. + * This DivisionByZeroChecker works on IntInterval domain. + */ +public final class DivisionByZeroChecker implements Checker { + + @Override + public String getDescription() { + return "Division By Zero Checker"; + } + + // TODO: implement check here +// @Override +// public List check(AnalysisMethod method, AbstractState abstractState) { +// List checkerResults = new ArrayList<>(); +// +// var stateMap = abstractState.getStateMap(); +// for (Node node : stateMap.keySet()) { +// if (!(stateMap.get(node).getPreCondition() instanceof IntInterval)) { +// checkerResults.add(new CheckerResult(CheckerStatus.ERROR, "DivisionByZeroChecker works only on IntInterval domain")); +// } +// +// if (node instanceof FloatDivNode || node instanceof RemNode) { +// var divisorNode = ((BinaryArithmeticNode) node).getY(); +// IntInterval divisorInterval = abstractState.getPostCondition(divisorNode); +// if (divisorInterval.containsValue(0)) { +// // NOTE: getNodeSourcePosition is very vague, we probably should print more information like where the 0 was assigned last etc. +// checkerResults.add(new CheckerResult(CheckerStatus.ERROR, "Division by zero on line: " + node.getNodeSourcePosition().toString())); +// } +// } +// } +// +// return checkerResults; +// } + + @Override + public boolean isCompatibleWith(AbstractState abstractState) { + return abstractState.getInitialDomain() instanceof IntInterval; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/IfConditionChecker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/IfConditionChecker.java new file mode 100644 index 000000000000..62b4d6c94843 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/checkers/IfConditionChecker.java @@ -0,0 +1,52 @@ +package com.oracle.svm.hosted.analysis.ai.checker.checkers; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.core.Checker; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.ConditionTruthnessFact; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AbstractMemory; +import com.oracle.svm.hosted.analysis.ai.domain.memory.AccessPath; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.graph.Node; + +import java.util.ArrayList; +import java.util.List; + +public final class IfConditionChecker implements Checker { + + @Override + public String getDescription() { + return "IfNode condition checker"; + } + + @Override + public List produceFacts(AnalysisMethod method, AbstractState abstractState) { + List facts = new ArrayList<>(); + for (Node n : abstractState.getStateMap().keySet()) { + if (n instanceof IfNode ifn) { + Node cond = ifn.condition(); + AbstractMemory mem = abstractState.getPostCondition(n); + if (mem == null) mem = abstractState.getPreCondition(n); + if (mem == null) continue; + IntInterval iv = mem.readStore(AccessPath.forLocal("n" + Integer.toHexString(System.identityHashCode(cond)))); + if (iv != null && !iv.isTop() && !iv.isBot() && !iv.isLowerInfinite() && !iv.isUpperInfinite()) { + if (iv.getLower() == 1 && iv.getUpper() == 1) { + facts.add(new ConditionTruthnessFact(ifn, cond, ConditionTruthnessFact.Truth.ALWAYS_TRUE)); + } else if (iv.getLower() == 0 && iv.getUpper() == 0) { + facts.add(new ConditionTruthnessFact(ifn, cond, ConditionTruthnessFact.Truth.ALWAYS_FALSE)); + } + } + } + } + return facts; + } + + @Override + public boolean isCompatibleWith(AbstractState st) { + return st.getInitialDomain() instanceof AbstractMemory; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/ApplierResult.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/ApplierResult.java new file mode 100644 index 000000000000..1c5ec2861d50 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/ApplierResult.java @@ -0,0 +1,156 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +/** + * Aggregates per-suite results from running all FactAppliers on a method's graph. + */ +public final class ApplierResult { + public final int appliedFacts; + private final int boundsEliminated; + private final int branchesFoldedTrue; + private final int branchesFoldedFalse; + private final int constantsStamped; + private final int constantsPropagated; + private final int invokesReplacedWithConstants; + + private ApplierResult(int appliedFacts, + int boundsEliminated, + int branchesFoldedTrue, + int branchesFoldedFalse, + int constantsStamped, + int constantsPropagated, + int invokesReplacedWithConstants) { + this.appliedFacts = appliedFacts; + this.boundsEliminated = boundsEliminated; + this.branchesFoldedTrue = branchesFoldedTrue; + this.branchesFoldedFalse = branchesFoldedFalse; + this.constantsStamped = constantsStamped; + this.constantsPropagated = constantsPropagated; + this.invokesReplacedWithConstants = invokesReplacedWithConstants; + } + + public static ApplierResult empty() { + return new ApplierResult(0, 0, 0, 0, 0, 0, 0); + } + + public ApplierResult plus(ApplierResult other) { + return new ApplierResult( + this.appliedFacts + other.appliedFacts, + this.boundsEliminated + other.boundsEliminated, + this.branchesFoldedTrue + other.branchesFoldedTrue, + this.branchesFoldedFalse + other.branchesFoldedFalse, + this.constantsStamped + other.constantsStamped, + this.constantsPropagated + other.constantsPropagated, + this.invokesReplacedWithConstants + other.invokesReplacedWithConstants + ); + } + + public int boundsChecksEliminated() { + return boundsEliminated; + } + + // TODO: merge branchesFoldedTrue together with branchesFoldedFalse + public int branchesFoldedTrue() { + return branchesFoldedTrue; + } + + public int branchesFoldedFalse() { + return branchesFoldedFalse; + } + + public int constantsStamped() { + return constantsStamped; + } + + public int constantsPropagated() { + return constantsPropagated; + } + + public int invokesReplacedWithConstants() { + return invokesReplacedWithConstants; + } + + public boolean anyOptimizations() { + return boundsEliminated + branchesFoldedTrue + branchesFoldedFalse + constantsPropagated + invokesReplacedWithConstants > 0; + } + + public static final class Builder { + private int appliedFacts; + private int boundsEliminated; + private int branchesTrue; + private int branchesFalse; + private int constantsStamped; + private int constantsPropagated; + private int invokesReplacedWithConstants; + + public Builder appliedFacts(int v) { + this.appliedFacts += v; + return this; + } + + public Builder boundsEliminated(int v) { + this.boundsEliminated += v; + return this; + } + + public Builder branchesFoldedTrue(int v) { + this.branchesTrue += v; + return this; + } + + public Builder branchesFoldedFalse(int v) { + this.branchesFalse += v; + return this; + } + + public Builder constantsStamped(int v) { + this.constantsStamped += v; + return this; + } + + public Builder constantsPropagated(int v) { + this.constantsPropagated += v; + return this; + } + + public Builder invokesReplacedWithConstants(int v) { + this.invokesReplacedWithConstants += v; + return this; + } + + public ApplierResult build() { + return new ApplierResult(appliedFacts, boundsEliminated, branchesTrue, branchesFalse, constantsStamped, constantsPropagated, invokesReplacedWithConstants); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static ApplierResult boundsEliminated(int count) { + return builder() + .appliedFacts(count) + .boundsEliminated(count) + .build(); + } + + public static ApplierResult constantsStamped(int count) { + return builder() + .appliedFacts(count) + .constantsStamped(count) + .build(); + } + + public static ApplierResult constantsPropagated(int count) { + return builder() + .appliedFacts(count) + .constantsPropagated(count) + .build(); + } + + public static ApplierResult invokesReplaced(int count) { + return builder() + .appliedFacts(count) + .invokesReplacedWithConstants(count) + .build(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/Checker.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/Checker.java new file mode 100644 index 000000000000..194eb3a9d1b0 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/Checker.java @@ -0,0 +1,48 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +import java.util.List; + +/** + * Checker interface focused on diagnostics and fact production. + * + * @param type of the derived {@link AbstractDomain} + */ + +public interface Checker> { + + /** + * Get a simple description of the checker. + * This description will then be displayed in logs. + * E.g. "Check for null pointer dereference" + * + * @return a description of the checker + */ + String getDescription(); + + /** + * Compatibility guard to avoid domain mismatches. + * The domain of the {@param abstractState} should be the same + * (or convertable) to the {@link Domain} used in the checker. + * + * @param abstractState the abstract state map to check compatibility with + * @return true if the checker is compatible, false otherwise + */ + boolean isCompatibleWith(AbstractState abstractState); + + /** + * Fact production pass: emit optimization / transformation facts derived + * from the abstract state. These facts drive the FactApplier pipeline. + * + * @param method the analysis method + * @param abstractState the abstract state computed by the analysis + * @return a list of CheckerFact objects describing derived facts + */ + default List produceFacts(AnalysisMethod method, AbstractState abstractState) { + return List.of(); + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/CheckerManager.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/CheckerManager.java new file mode 100644 index 000000000000..0c1513d5c3c1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/CheckerManager.java @@ -0,0 +1,79 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.summary.MethodSummary; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import jdk.graal.compiler.nodes.EncodedGraph; +import jdk.graal.compiler.nodes.GraphEncoder; +import jdk.graal.compiler.nodes.StructuredGraph; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class CheckerManager { + + private final List> checkers = new ArrayList<>(); + + public void registerChecker(Checker checker) { + checkers.add(checker); + } + + @SuppressWarnings("unchecked") + public > void runCheckersOnSingleMethod(AnalysisMethod method, AbstractState abstractState, StructuredGraph graph) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log("Running checkers on method: " + method.getQualifiedName(), LoggerVerbosity.INFO); + var stats = AbstractInterpretationServices.getInstance().getStats(); + List allFacts = new ArrayList<>(); + + for (var checker : checkers) { + if (!checker.isCompatibleWith(abstractState)) { + continue; + } + var typedChecker = (Checker) checker; + List facts = typedChecker.produceFacts(method, abstractState); + if (facts != null && !facts.isEmpty()) { + allFacts.addAll(facts); + } + } + + logger.logFacts(allFacts); + + /* Apply the facts to the analysisMethod */ + FactAggregator aggregator = FactAggregator.aggregate(allFacts); + FactApplierSuite applierSuite = FactApplierSuite.fromRegistry(aggregator, true); + ApplierResult result = applierSuite.runAppliers(method, graph, aggregator); + + /* Update stats */ + stats.addMethodBoundsEliminated(method, result.boundsChecksEliminated()); + stats.addMethodBranchesFoldedTrue(method, result.branchesFoldedTrue()); + stats.addMethodBranchesFoldedFalse(method, result.branchesFoldedFalse()); + stats.addMethodConstantsStamped(method, result.constantsStamped()); + stats.addMethodConstantsPropagated(method, result.constantsPropagated()); + stats.addMethodInvokesReplaced(method, result.invokesReplacedWithConstants()); + + applyAbstractInterpretationResults(method, graph); + } + + private void applyAbstractInterpretationResults(AnalysisMethod method, StructuredGraph graph) { + EncodedGraph encoded = GraphEncoder.encodeSingleGraph(graph, AnalysisParsedGraph.HOST_ARCHITECTURE); + method.setAnalyzedGraph(encoded); + } + + public > void runCheckersOnMethodSummaries(Map> methodSummaryMap, + Map methodGraphMap) { + for (var entry : methodSummaryMap.entrySet()) { + AnalysisMethod method = entry.getKey(); + MethodSummary methodSummary = entry.getValue(); + AbstractState abstractState = methodSummary.getStateAcrossAllContexts(); + var logger = AbstractInterpretationLogger.getInstance(); + runCheckersOnSingleMethod(method, abstractState, methodGraphMap.get(method)); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactAggregator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactAggregator.java new file mode 100644 index 000000000000..b70d59709492 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactAggregator.java @@ -0,0 +1,71 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.checker.facts.FactKind; +import jdk.graal.compiler.graph.Node; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Aggregates facts from all checkers and offers indexed queries by node and by kind. + */ +public final class FactAggregator { + + private final List all; + private Map> byNode; + private Map> byKind; + private final Map, List> byType = new HashMap<>(); + + public FactAggregator() { + this.all = new ArrayList<>(); + this.byNode = new IdentityHashMap<>(); + this.byKind = new HashMap<>(); + } + + FactAggregator(List facts) { + this.all = new ArrayList<>(facts == null ? List.of() : facts); + rebuildIndexes(); + } + + private void rebuildIndexes() { + Map> tmpByNode = new IdentityHashMap<>(); + Map> tmpByKind = new HashMap<>(); + for (Fact f : all) { + tmpByNode.computeIfAbsent(f.node(), k -> new ArrayList<>()).add(f); + tmpByKind.computeIfAbsent(f.kind(), k -> new ArrayList<>()).add(f); + } + this.byNode = tmpByNode.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Collections.unmodifiableList(e.getValue()), (a, b) -> a, IdentityHashMap::new)); + this.byKind = tmpByKind.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Collections.unmodifiableList(e.getValue()))); + } + + public static FactAggregator aggregate(List facts) { + return new FactAggregator(facts); + } + + public List allFacts() { + return Collections.unmodifiableList(all); + } + + public List factsFor(Node n) { + return byNode.getOrDefault(n, List.of()); + } + + public List factsOfKind(FactKind kind) { + return byKind.getOrDefault(kind, List.of()); + } + + public void addAll(List facts) { + if (facts == null || facts.isEmpty()) { + return; + } + this.all.addAll(facts); + rebuildIndexes(); + } + + public boolean isEmpty() { + return all.isEmpty(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactApplierSuite.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactApplierSuite.java new file mode 100644 index 000000000000..9e98d7afbe18 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/FactApplierSuite.java @@ -0,0 +1,114 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.core.option.HostedOptionValues; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import com.oracle.svm.hosted.analysis.ai.checker.appliers.*; +import com.oracle.svm.hosted.analysis.ai.exception.AbstractInterpretationException; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.debug.DebugOptions; +import jdk.graal.compiler.debug.MethodFilter; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.options.OptionValues; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a suite of {@link FactApplier} instances to be run in sequence. + */ +public final class FactApplierSuite { + + private final List appliers = new ArrayList<>(); + + public FactApplierSuite(List appliers) { + if (appliers != null) { + this.appliers.addAll(appliers); + } + } + + public FactApplierSuite register(FactApplier applier) { + if (applier != null) { + appliers.add(applier); + } + return this; + } + + /** + * Builds a suite by querying the global {@link FactApplierRegistry} for appliers relevant to + * the facts present in the aggregator. Optionally appends a cleanup applier. + */ + public static FactApplierSuite fromRegistry(FactAggregator aggregator, boolean appendCleanup) { + List relevant = FactApplierRegistry.getRelevantAppliers(aggregator); + FactApplierSuite suite = new FactApplierSuite(relevant); + if (appendCleanup) { + suite.register(new CleanupApplier()); + } + return suite; + } + + private static boolean shouldDumpToIGV(AnalysisMethod method, StructuredGraph graph) { + if (!AbstractInterpretationLogger.getInstance().isGraphIgvDumpEnabled()) { + return false; + } + if (graph == null || graph.getDebug() == null) { + return false; + } + + OptionValues currentOptions = HostedOptionValues.singleton(); + String filterString = DebugOptions.MethodFilter.getValue(currentOptions); + + if (filterString == null || filterString.isEmpty()) { + return true; + } + + MethodFilter filter = MethodFilter.parse(filterString); + return filter.matches(method.wrapped); + } + + /** + * Executes the appliers in registration order and returns aggregate counters for stats. + */ + public ApplierResult runAppliers(AnalysisMethod method, StructuredGraph graph, FactAggregator aggregator) { + if (graph == null) { + return ApplierResult.empty(); + } + + boolean shouldDump = shouldDumpToIGV(method, graph); + String methodName = method.format("%H.%n"); + + ApplierResult total = ApplierResult.empty(); + for (FactApplier applier : appliers) { + if (!applier.shouldApply()) { + continue; + } + + ApplierResult r = applier.apply(method, graph, aggregator); + total = total.plus(r); + + if (r.anyOptimizations() && !graph.verify()) { + AbstractInterpretationException.graphVerifyFailed(applier.getDescription(), method); + } + + if (shouldDump && r.anyOptimizations()) { + dumpGraph(graph, applier.getDescription()); + } + } + + return total; + } + + private void dumpGraph(StructuredGraph graph, String description) { + DebugContext debug = AbstractInterpretationServices.getInstance().getDebug(); + + try (DebugContext.Scope _ = debug.scope("GraalAF", graph)) { + debug.dump(DebugContext.BASIC_LEVEL, graph, "After Abstract Interpretation applier - %s", description); + } catch (Throwable e) { + AbstractInterpretationLogger.getInstance().log( + "Failed to dump graph: " + e.getMessage(), + com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity.CHECKER_WARN + ); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/NodeUtil.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/NodeUtil.java new file mode 100644 index 000000000000..921cb7024521 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/core/NodeUtil.java @@ -0,0 +1,42 @@ +package com.oracle.svm.hosted.analysis.ai.checker.core; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.extended.BytecodeExceptionNode; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Set; + +public final class NodeUtil { + /** + * Used to find the nearest enclosing {@link IfNode} of a given node + */ + public static IfNode findGuardingIf(Node node) { + Set seen = new HashSet<>(); + ArrayDeque work = new ArrayDeque<>(); + work.add(node); + while (!work.isEmpty()) { + Node cur = work.poll(); + if (!seen.add(cur)) continue; + for (var pred : cur.cfgPredecessors()) { + if (pred instanceof IfNode ifn) { + return ifn; + } + work.add(pred); + } + } + return null; + } + + public static boolean leadsToByteCodeException(IfNode ifNode) { + if (ifNode == null) return false; + return isDirectPredecessorOfBCE(ifNode.trueSuccessor()) || isDirectPredecessorOfBCE(ifNode.falseSuccessor()); + } + + public static boolean isDirectPredecessorOfBCE(Node node) { + if (node == null) return false; + if (node.successors().count() != 1) return false; + return node.successors().first() instanceof BytecodeExceptionNode; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConditionTruthnessFact.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConditionTruthnessFact.java new file mode 100644 index 000000000000..a59d213bee3d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConditionTruthnessFact.java @@ -0,0 +1,24 @@ +package com.oracle.svm.hosted.analysis.ai.checker.facts; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.IfNode; + +public record ConditionTruthnessFact(IfNode ifNode, Node condition, Truth truth) implements Fact { + public enum Truth {ALWAYS_TRUE, ALWAYS_FALSE} + + @Override + public FactKind kind() { + return FactKind.CONDITION_TRUTH; + } + + @Override + public String describe() { + return "IfNode=" + ifNode + ", cond=" + condition + ", truth=" + truth; + } + + @Override + public Node node() { + return ifNode; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConstantFact.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConstantFact.java new file mode 100644 index 000000000000..9c9dc3231286 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/ConstantFact.java @@ -0,0 +1,50 @@ +package com.oracle.svm.hosted.analysis.ai.checker.facts; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.ValueNode; +import jdk.graal.compiler.core.common.type.Stamp; +import jdk.graal.compiler.core.common.type.StampFactory; +import jdk.graal.compiler.core.common.type.IntegerStamp; +import jdk.graal.compiler.nodes.NodeView; +import jdk.vm.ci.meta.JavaKind; + +/** + * A fact that records a constant value for a particular node. + */ +public record ConstantFact(Node node, long value) implements Fact { + + @Override + public FactKind kind() { + return FactKind.CONSTANT; + } + + @Override + public String describe() { + return node + " has constant value: " + value; + } + + /** + * @return JavaKind of the node if it is a {@link ValueNode}, otherwise null. + */ + public JavaKind kindOfValueNode() { + if (node instanceof ValueNode vn) { + return vn.getStackKind(); + } + return null; + } + + /** + * Builds an exact integer {@link Stamp} [value, value] compatible with the node, or null if not integer. + */ + public Stamp exactIntegerStampOrNull() { + if (!(node instanceof ValueNode vn)) { + return null; + } + var stamp = vn.stamp(NodeView.DEFAULT); + if (stamp instanceof IntegerStamp is) { + JavaKind kind = vn.getStackKind(); + return StampFactory.forInteger(kind, value, value); + } + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/Fact.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/Fact.java new file mode 100644 index 000000000000..8dae4a5257f4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/Fact.java @@ -0,0 +1,16 @@ +package com.oracle.svm.hosted.analysis.ai.checker.facts; + +import jdk.graal.compiler.graph.Node; + +/** + * Represents a "Fact" that a checker was able to infer during abstract interpretation. + */ +public interface Fact { + + FactKind kind(); + + String describe(); + + Node node(); +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/FactKind.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/FactKind.java new file mode 100644 index 000000000000..9209bf0bc063 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/FactKind.java @@ -0,0 +1,7 @@ +package com.oracle.svm.hosted.analysis.ai.checker.facts; + +public enum FactKind { + BOUNDS_SAFETY, + CONDITION_TRUTH, + CONSTANT +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/SafeBoundsAccessFact.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/SafeBoundsAccessFact.java new file mode 100644 index 000000000000..0396cd01b335 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/checker/facts/SafeBoundsAccessFact.java @@ -0,0 +1,52 @@ +package com.oracle.svm.hosted.analysis.ai.checker.facts; + +import jdk.graal.compiler.graph.Node; + +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; + +@Deprecated +public final class SafeBoundsAccessFact implements Fact { + private final Node access; + private final boolean inBounds; + private final IntInterval indexRange; + private final int arrayLength; + + public SafeBoundsAccessFact(Node arrayAccess, boolean inBounds, IntInterval indexRange, int arrayLength) { + this.access = arrayAccess; + this.inBounds = inBounds; + this.indexRange = indexRange == null ? null : indexRange.copyOf(); + this.arrayLength = arrayLength; + } + + public boolean isInBounds() { + return inBounds; + } + + public IntInterval getIndexRange() { + return indexRange; + } + + public int getArrayLength() { + return arrayLength; + } + + public Node getArrayAccess() { + return access; + } + + @Override + public FactKind kind() { + return FactKind.BOUNDS_SAFETY; + } + + @Override + public String describe() { + return access + " indexRange=" + indexRange + " length=" + arrayLength + " safe=" + inBounds; + } + + @Override + public Node node() { + return access; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/AbstractDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/AbstractDomain.java new file mode 100644 index 000000000000..c324a1e1d473 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/AbstractDomain.java @@ -0,0 +1,148 @@ +package com.oracle.svm.hosted.analysis.ai.domain; + +/** + * Interface for abstract domain in GraalAF. + * Abstract domain serves as the abstraction of program state. + *

+ * New abstract domains can be implemented by extending the existing domains, + * or by creating new ones. A new domain can be created as: + * + * public MyDomain implements AbstractDomain + * ... + *

+ * + * @param type of the derived {@link AbstractDomain} + */ +public interface AbstractDomain> { + + /** + * Checks if the domain is the bottom element + * + * @return true if the domain is the bottom element + */ + boolean isBot(); + + /** + * Checks if the domain is the top element + * + * @return true if the domain is the top element + */ + boolean isTop(); + + /** + * Checks if the domain is less or equal to the other domain + * + * @param other domain to compare with + * @return true if the domain is less or equal to the other domain + */ + boolean leq(Derived other); + + /** + * Sets the domain to the bottom element + */ + void setToBot(); + + /** + * Sets the domain to the top element + */ + void setToTop(); + + /** + * Joins the domain with the other domain, modifying the domain + * + * @param other domain to join with + */ + void joinWith(Derived other); + + /** + * Widens the domain with the other domain, modifying the domain + * + * @param other domain to widen with + */ + void widenWith(Derived other); + + /** + * Meets the domain with the other domain, modifying the domain + * + * @param other domain to meet with + */ + void meetWith(Derived other); + + /** + * String representation of the domain + * + * @return string representation of the domain + */ + String toString(); + + /** + * Creates a copy of the domain + * + * @return copy of the domain + */ + Derived copyOf(); + + /** + * Joins the domain with the other domain, returning a new domain + * If the domain is a lattice, this is the least upper bound operation + * + * @param other domain to join with + * @return new domain after joining + */ + default Derived join(Derived other) { + Derived copy = copyOf(); + copy.joinWith(other); + return copy; + } + + /** + * Widens the domain with the other domain, returning a new domain + * Used for acceleration of the fixpoint computation + * + * @param other domain to widen with + * @return new domain after widening + */ + default Derived widen(Derived other) { + Derived copy = copyOf(); + copy.widenWith(other); + return copy; + } + + /** + * Meets the domain with the other domain, returning a new domain + * If the domain is a lattice, this is the greatest lower bound operation + * + * @param other domain to meet with + * @return new domain after meeting + */ + default Derived meet(Derived other) { + Derived copy = copyOf(); + copy.meetWith(other); + return copy; + } + + /** + * Creates a top value of the domain + * + * @param domain of which we want to get a top value + * @return a new instance of the domain set to top + */ + + static > Domain createTop(Domain domain) { + Domain copy = domain.copyOf(); + copy.setToTop(); + return copy; + } + + /** + * Creates a bot value of the domain + * + * @param domain of which we want to get a bot value + * @return a new instance of the domain set to bot + */ + static > Domain createBot(Domain domain) { + Domain copy = domain.copyOf(); + copy.setToBot(); + return copy; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/PairDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/PairDomain.java new file mode 100644 index 000000000000..9e4387355232 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/PairDomain.java @@ -0,0 +1,71 @@ +package com.oracle.svm.hosted.analysis.ai.domain.composite; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Represents a pair of two abstract domains. + * Implemented using the cartesian product of the two abstract domains. + * + * @param the type of the first abstract domain + * @param the type of the second abstract domain + */ +public record PairDomain< + First extends AbstractDomain, + Second extends AbstractDomain>(First first, Second second) + implements AbstractDomain> { + + @Override + public boolean isBot() { + return first.isBot() && second.isBot(); + } + + @Override + public boolean isTop() { + return first.isTop() && second.isTop(); + } + + @Override + public boolean leq(PairDomain other) { + return first.leq(other.first) && second.leq(other.second); + } + + @Override + public void setToBot() { + first.setToBot(); + second.setToBot(); + } + + @Override + public void setToTop() { + first.setToTop(); + second.setToTop(); + } + + @Override + public void joinWith(PairDomain other) { + first.joinWith(other.first); + second.joinWith(other.second); + } + + @Override + public void widenWith(PairDomain other) { + first.widenWith(other.first); + second.widenWith(other.second); + } + + @Override + public void meetWith(PairDomain other) { + first.meetWith(other.first); + second.meetWith(other.second); + } + + @Override + public String toString() { + return "PairDomain{" + first + ", " + second + '}'; + } + + @Override + public PairDomain copyOf() { + return new PairDomain<>(first.copyOf(), second.copyOf()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/ProductDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/ProductDomain.java new file mode 100644 index 000000000000..def265b0d59e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/ProductDomain.java @@ -0,0 +1,155 @@ +package com.oracle.svm.hosted.analysis.ai.domain.composite; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/* + This abstract domain represents a cartesian product of other abstract domains. + Future improvement could be implementing reduced product domain. + */ +public final class ProductDomain implements AbstractDomain { + + private final List> domains; + + public ProductDomain() { + this.domains = new ArrayList<>(); + } + + public ProductDomain(List> domains) { + this.domains = new ArrayList<>(domains); + } + + public ProductDomain(ProductDomain other) { + this.domains = new ArrayList<>(); + for (AbstractDomain domain : other.domains) { + this.domains.add(domain.copyOf()); + } + } + + public List> getDomains() { + return domains; + } + + @Override + public boolean isBot() { + return domains.stream().allMatch(AbstractDomain::isBot); + } + + @Override + public boolean isTop() { + return domains.stream().allMatch(AbstractDomain::isTop); + } + + @Override + public boolean leq(ProductDomain other) { + if (domains.size() != other.domains.size()) { + return false; + } + for (int i = 0; i < domains.size(); i++) { + AbstractDomain thisDomain = domains.get(i); + AbstractDomain otherDomain = other.domains.get(i); + if (thisDomain.getClass() != otherDomain.getClass()) { + return false; + } + + if (!leqDomains(thisDomain, otherDomain)) + return false; + } + return true; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ProductDomain that = (ProductDomain) o; + return Objects.equals(domains, that.domains); + } + + @Override + public int hashCode() { + return Objects.hashCode(domains); + } + + @Override + public void setToBot() { + domains.forEach(AbstractDomain::setToBot); + } + + @Override + public void setToTop() { + domains.forEach(AbstractDomain::setToTop); + } + + @Override + public void joinWith(ProductDomain other) { + for (int i = 0; i < domains.size(); i++) { + AbstractDomain thisDomain = domains.get(i); + AbstractDomain otherDomain = other.domains.get(i); + if (thisDomain.getClass() != otherDomain.getClass()) { + throw new RuntimeException("Cannot join domains of different types"); + } + + joinDomains(thisDomain, otherDomain); + } + } + + @Override + public void widenWith(ProductDomain other) { + for (int i = 0; i < domains.size(); i++) { + AbstractDomain thisDomain = domains.get(i); + AbstractDomain otherDomain = other.domains.get(i); + if (thisDomain.getClass() != otherDomain.getClass()) { + throw new RuntimeException("Cannot widen domains of different types"); + } + widenDomains(thisDomain, otherDomain); + } + } + + @Override + public void meetWith(ProductDomain other) { + for (int i = 0; i < domains.size(); i++) { + AbstractDomain thisDomain = domains.get(i); + AbstractDomain otherDomain = other.domains.get(i); + if (thisDomain.getClass() != otherDomain.getClass()) { + throw new RuntimeException("Cannot meet domains of different types"); + } + + meetDomains(thisDomain, otherDomain); + } + } + + @Override + public String toString() { + return "ProductDomain{" + + "domains=" + domains + + '}'; + } + + @Override + public ProductDomain copyOf() { + return new ProductDomain(this); + } + + @SuppressWarnings("unchecked") + private > void joinDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).joinWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > void widenDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).widenWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > void meetDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).meetWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > boolean leqDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + return ((T) thisDomain).leq((T) otherDomain); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/IntervalSignReducedProductDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/IntervalSignReducedProductDomain.java new file mode 100644 index 000000000000..2d43aa4f3702 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/IntervalSignReducedProductDomain.java @@ -0,0 +1,88 @@ +package com.oracle.svm.hosted.analysis.ai.domain.composite.reducedproduct; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.SignDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.Sign; + +import java.util.List; + + +public final class IntervalSignReducedProductDomain extends ReducedProductDomain { + + private IntervalSignReducedProductDomain(List> domains, List reducers) { + super(domains, reducers); + } + + private IntervalSignReducedProductDomain(IntervalSignReducedProductDomain other) { + super(other); + } + + @Override + public IntervalSignReducedProductDomain copyOf() { + return new IntervalSignReducedProductDomain(this); + } + + public IntInterval getInterval() { + return getDomain(0); + } + + public SignDomain getSign() { + return (SignDomain) getDomain(1); + } + + /** + * Creates an IntervalSignReducedProductDomain with the given interval and sign domains. + */ + public static IntervalSignReducedProductDomain create(IntInterval interval, SignDomain sign) { + return new Builder<>(IntervalSignReducedProductDomain::new) + .withDomains(interval, sign) + .withReducer(domains -> { + IntInterval intervalDomain = (IntInterval) domains.get(0); + SignDomain signDomain = (SignDomain) domains.get(1); + + // Refine interval based on sign + long lowerBound = intervalDomain.getLower(); + long upperBound = intervalDomain.getUpper(); + boolean changed = false; + + if (signDomain.getState() == Sign.POS) { + if (lowerBound < 1) { + lowerBound = 1; + changed = true; + } + } else if (signDomain.getState() == Sign.NEG) { + if (upperBound > -1) { + upperBound = -1; + changed = true; + } + } else if (signDomain.getState() == Sign.ZERO) { + if (lowerBound != 0 || upperBound != 0) { + lowerBound = 0; + upperBound = 0; + changed = true; + } + } + + // Create new interval if bounds changed + if (changed) { + domains.set(0, new IntInterval(lowerBound, upperBound)); + intervalDomain = (IntInterval) domains.getFirst(); + } + + // Refine sign based on interval + if (intervalDomain.getLower() > 0) { + signDomain.setState(Sign.POS); + } else if (intervalDomain.getUpper() < 0) { + signDomain.setState(Sign.NEG); + } else { + if (intervalDomain.getLower() == 0) { + if (intervalDomain.getUpper() == 0) { + signDomain.setState(Sign.ZERO); + } + } + } + }) + .build(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/ReducedProductDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/ReducedProductDomain.java new file mode 100644 index 000000000000..fb1eb880162f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/composite/reducedproduct/ReducedProductDomain.java @@ -0,0 +1,275 @@ +package com.oracle.svm.hosted.analysis.ai.domain.composite.reducedproduct; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * A reduced product domain that combines multiple domains and allows information to flow between them + * through reduction operations. + * + * @param type of the derived ReducedProductDomain + */ +public class ReducedProductDomain> implements AbstractDomain { + + private final List> domains; + private final List reducers; + + /** + * Interface for defining reduction operations between domains. + */ + @FunctionalInterface + public interface Reducer { + void reduce(List> domains); + } + + /** + * Constructor for a reduced product domain with the given domains and reducers. + * + * @param domains the abstract domains forming the product + * @param reducers the reduction operations between domains + */ + @SuppressWarnings("this-escape") + protected ReducedProductDomain(List> domains, List reducers) { + this.domains = new ArrayList<>(domains.size()); + for (AbstractDomain domain : domains) { + this.domains.add(domain.copyOf()); + } + this.reducers = new ArrayList<>(reducers); + applyReduction(); + } + + /** + * Copy constructor. + * + * @param other the domain to copy + */ + protected ReducedProductDomain(ReducedProductDomain other) { + this.domains = new ArrayList<>(other.domains.size()); + for (AbstractDomain domain : other.domains) { + this.domains.add(domain.copyOf()); + } + this.reducers = new ArrayList<>(other.reducers); + } + + /** + * Gets the domain at the specified index. + * + * @param index the index of the domain + * @return the domain at the given index + */ + @SuppressWarnings("unchecked") + public > T getDomain(int index) { + return (T) domains.get(index); + } + + /** + * Applies all reduction operations. + */ + protected void applyReduction() { + for (Reducer reducer : reducers) { + reducer.reduce(domains); + } + } + + @Override + public boolean isBot() { + return domains.stream().anyMatch(AbstractDomain::isBot); + } + + @Override + public boolean isTop() { + return domains.stream().allMatch(AbstractDomain::isTop); + } + + @Override + @SuppressWarnings("unchecked") + public boolean leq(Domain other) { + ReducedProductDomain otherDomain = other; + if (domains.size() != otherDomain.domains.size()) { + return false; + } + + for (int i = 0; i < domains.size(); i++) { + if (!leqDomains(domains.get(i), otherDomain.domains.get(i))) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReducedProductDomain that = (ReducedProductDomain) o; + return Objects.equals(domains, that.domains); + } + + @Override + public int hashCode() { + return Objects.hash(domains); + } + + @Override + public void setToBot() { + if (!domains.isEmpty()) { + domains.get(0).setToBot(); + } + } + + @Override + public void setToTop() { + domains.forEach(AbstractDomain::setToTop); + } + + @Override + @SuppressWarnings("unchecked") + public void joinWith(Domain other) { + ReducedProductDomain otherDomain = other; + for (int i = 0; i < domains.size(); i++) { + joinDomains(domains.get(i), otherDomain.domains.get(i)); + } + applyReduction(); + } + + @Override + @SuppressWarnings("unchecked") + public void widenWith(Domain other) { + ReducedProductDomain otherDomain = other; + for (int i = 0; i < domains.size(); i++) { + widenDomains(domains.get(i), otherDomain.domains.get(i)); + } + applyReduction(); + } + + @Override + @SuppressWarnings("unchecked") + public void meetWith(Domain other) { + ReducedProductDomain otherDomain = other; + for (int i = 0; i < domains.size(); i++) { + meetDomains(domains.get(i), otherDomain.domains.get(i)); + } + applyReduction(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ReducedProductDomain{"); + for (int i = 0; i < domains.size(); i++) { + builder.append("\n ").append(i).append(": ").append(domains.get(i)); + } + builder.append("\n}"); + return builder.toString(); + } + + @Override + public Domain copyOf() { + /* Should be implemented in the subclasses */ + return null; + } + + @SuppressWarnings("unchecked") + private > void joinDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).joinWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > void widenDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).widenWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > void meetDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + ((T) thisDomain).meetWith((T) otherDomain); + } + + @SuppressWarnings("unchecked") + private > boolean leqDomains(AbstractDomain thisDomain, AbstractDomain otherDomain) { + return ((T) thisDomain).leq((T) otherDomain); + } + + /** + * Builder for creating reduced product domains. + * + * @param the type of the reduced product domain + */ + public static class Builder> { + private final List> domains = new ArrayList<>(); + private final List reducers = new ArrayList<>(); + private final Factory factory; + + /** + * Factory interface for creating instances of the reduced product domain. + * + * @param the type of the reduced product domain + */ + @FunctionalInterface + public interface Factory> { + D create(List> domains, List reducers); + } + + /** + * Creates a new builder with the given factory. + * + * @param factory the factory for creating the reduced product domain + */ + public Builder(Factory factory) { + this.factory = factory; + } + + /** + * Adds domains to the reduced product. + * + * @param newDomains the domains to add + * @return this builder + */ + public Builder withDomains(AbstractDomain... newDomains) { + domains.addAll(Arrays.asList(newDomains)); + return this; + } + + /** + * Adds a reducer between domains. + * + * @param reducer the reducer to add + * @return this builder + */ + public Builder withReducer(Reducer reducer) { + reducers.add(reducer); + return this; + } + + /** + * Adds a reducer that operates on specific domains. + * + * @param indices the indices of the domains to reduce + * @param reducer the reducer operation + * @return this builder + */ + public final Builder withReducer(int[] indices, Consumer[]> reducer) { + reducers.add(domains -> { + AbstractDomain[] selectedDomains = new AbstractDomain[indices.length]; + for (int i = 0; i < indices.length; i++) { + selectedDomains[i] = domains.get(indices[i]); + } + reducer.accept(selectedDomains); + }); + return this; + } + + /** + * Builds the reduced product domain. + * + * @return the reduced product domain + */ + public D build() { + return factory.create(domains, reducers); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AbstractMemory.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AbstractMemory.java new file mode 100644 index 000000000000..9ea0d23486d8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AbstractMemory.java @@ -0,0 +1,506 @@ +package com.oracle.svm.hosted.analysis.ai.domain.memory; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.numerical.IntInterval; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * AbsMemory represents the product domain (env, store) used by access-path analyses. + * - env: mapping from AbsVar -> AccessPath (which root a variable refers to) + * - store: mapping from AccessPath -> IntInterval (heap/field values) + *

+ * Lattice semantics: + * - join: pointwise join; env keeps a mapping only if both sides map the same AccessPath for a variable (otherwise mapping is lost) + * - store: union of keys; intervals are joined via IntInterval.joinWith + * - widen: same shape as join but uses widenWith on intervals + * - meet: intersection of env/store keys; env keeps mapping only when equal; store meets intervals + */ +public class AbstractMemory implements AbstractDomain { + /* FIXME: we need to take a look at isBot and isTop and find a better way) */ + private boolean isBot; + private boolean isTop; + private final Map env; + private final Map store; + private final Map envMulti = new HashMap<>(); + + public AbstractMemory() { + this.isBot = true; + this.isTop = false; + this.env = new HashMap<>(); + this.store = new HashMap<>(); + } + + public AbstractMemory(Map env, Map store) { + this.isBot = true; + this.isTop = false; + this.env = new HashMap<>(env); + this.store = new HashMap<>(store); + } + + private void ensureNotBotTop() { + if (isBot || isTop) { + isBot = false; + isTop = false; + } + } + + public void bindVar(Var v, AccessPath p) { + Objects.requireNonNull(v); + Objects.requireNonNull(p); + ensureNotBotTop(); + env.put(v, p); + } + + public AccessPath lookupVar(Var v) { + return env.get(v); + } + + public void removeVar(Var v) { + env.remove(v); + } + + public void removeStore(AccessPath path) { + store.remove(path); + } + + public void writeStore(AccessPath p, IntInterval val) { + Objects.requireNonNull(p); + Objects.requireNonNull(val); + ensureNotBotTop(); + IntInterval cur = store.get(p); + if (cur == null) store.put(p, val.copyOf()); + else cur.joinWith(val); + } + + /** + * Strong update: overwrite the exact path and conservatively join into deeper paths that start with it. + */ + public void writeStoreStrong(AccessPath p, IntInterval val) { + Objects.requireNonNull(p); + Objects.requireNonNull(val); + ensureNotBotTop(); + store.put(p, val.copyOf()); + // join into any deeper keys starting with p + Set keys = new HashSet<>(store.keySet()); + for (AccessPath k : keys) { + if (k.startsWith(p) && !k.equals(p)) { + IntInterval cur = store.get(k); + if (cur != null) { + cur.joinWith(val); + store.put(k, cur); + } + } + } + } + + public Set getStoreKeys() { + return new HashSet<>(store.keySet()); + } + + public Map getEnvSnapshot() { + return new HashMap<>(env); + } + + public void bindLocalByName(String localName, AccessPath p) { + bindVar(Var.local(localName), p); + } + + public void bindParamByName(String paramName, AccessPath p) { + bindVar(Var.param(paramName), p); + } + + public void bindTempByName(String tempName, AccessPath p) { + bindVar(Var.temp(tempName), p); + } + + public AccessPath lookupLocalByName(String localName) { + return lookupVar(Var.local(localName)); + } + + public AccessPath lookupParamByName(String paramName) { + return lookupVar(Var.param(paramName)); + } + + public AccessPath lookupTempByName(String tempName) { + return lookupVar(Var.temp(tempName)); + } + + /** + * Return a map of store entries whose access paths start with the given prefix. + */ + public Map getPathsWithPrefix(AccessPath prefix) { + Objects.requireNonNull(prefix); + Map res = new HashMap<>(); + for (Map.Entry e : store.entrySet()) { + if (e.getKey().startsWith(prefix)) res.put(e.getKey(), e.getValue().copyOf()); + } + return res; + } + + /** + * Apply a callee summary into this caller state. + * placeholderToActualRoot maps placeholder root names (strings) to caller AccessPath roots. + * We translate each summary store entry by replacing the placeholder root with the actual root and weakly joining the value. + */ + public void applySummary(AbstractMemory summary, Map placeholderToActualRoot) { + Objects.requireNonNull(summary); + Objects.requireNonNull(placeholderToActualRoot); + /* Apply store entries */ + for (Map.Entry e : summary.store.entrySet()) { + AccessPath phPath = e.getKey(); + if (phPath.getRootKind() != AccessPath.RootKind.PLACEHOLDER) { + /* if the summary contains non-placeholder roots, either merge or skip; here we skip */ + continue; + } + String phRoot = phPath.getRootName(); + AccessPath actualRoot = placeholderToActualRoot.get(phRoot); + if (actualRoot == null) continue; + AccessPath actual = actualRoot; + for (String f : phPath.getFields()) actual = actual.appendField(f); + /* weak join into caller store */ + writeStore(actual, e.getValue()); + } + + /* Apply env mappings: if callee bound a placeholder local, map it to caller actual if provided */ + for (Map.Entry e : summary.env.entrySet()) { + AccessPath vpath = e.getValue(); + if (vpath.getRootKind() == AccessPath.RootKind.PLACEHOLDER) { + AccessPath mapped = placeholderToActualRoot.get(vpath.getRootName()); + if (mapped != null) bindVar(e.getKey(), mapped); + } + } + } + + @Override + public boolean isBot() { + return isBot; + } + + @Override + public boolean isTop() { + return isTop; + } + + @Override + public boolean leq(AbstractMemory other) { + if (other == null) return false; + if (this.isBot()) return true; + if (other.isTop()) return true; + if (this.isTop()) return other.isTop(); + /* env: for each binding in this.env, other.env must have identical mapping */ + for (Map.Entry e : this.env.entrySet()) { + AccessPath otherP = other.env.get(e.getKey()); + if (otherP == null || !otherP.equals(e.getValue())) return false; + } + /* store: for each entry in this.store, other.store must have an interval >= (i.e., otherInterval contains this interval) */ + for (Map.Entry e : this.store.entrySet()) { + IntInterval otherI = other.store.get(e.getKey()); + if (otherI == null) return false; + if (!e.getValue().leq(otherI)) return false; + } + return true; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof AbstractMemory)) return false; + AbstractMemory o = (AbstractMemory) other; + if (this.isBot != o.isBot) return false; + if (this.isTop != o.isTop) return false; + return this.env.equals(o.env) && this.store.equals(o.store); + } + + @Override + public int hashCode() { + int result = Boolean.hashCode(isBot); + result = 31 * result + Boolean.hashCode(isTop); + result = 31 * result + env.hashCode(); + result = 31 * result + store.hashCode(); + return result; + } + + @Override + public void setToBot() { + isBot = true; + isTop = false; + env.clear(); + store.clear(); + } + + @Override + public void setToTop() { + isTop = true; + isBot = false; + env.clear(); + store.clear(); + } + + @Override + public void joinWith(AbstractMemory other) { + if (other == null) return; + if (this.isTop || other.isTop) { + setToTop(); + return; + } + if (this.isBot) { + // adopt other's contents + this.isBot = other.isBot; + this.isTop = other.isTop; + this.env.clear(); + this.env.putAll(other.env); + this.store.clear(); + for (Map.Entry e : other.store.entrySet()) + this.store.put(e.getKey(), e.getValue().copyOf()); + return; + } + /* generic join: env keep mapping only when equal in both sides */ + Set keys = new HashSet<>(); + keys.addAll(this.env.keySet()); + keys.addAll(other.env.keySet()); + Map newEnv = new HashMap<>(); + for (Var v : keys) { + AccessPath a = this.env.get(v); + AccessPath b = other.env.get(v); + if (a != null && a.equals(b)) newEnv.put(v, a); + } + this.env.clear(); + this.env.putAll(newEnv); + + // store: union of keys with joined intervals + Set skeys = new HashSet<>(); + skeys.addAll(this.store.keySet()); + skeys.addAll(other.store.keySet()); + Map newStore = new HashMap<>(); + for (AccessPath p : skeys) { + IntInterval a = this.store.get(p); + IntInterval b = other.store.get(p); + if (a != null && b != null) { + IntInterval c = a.copyOf(); + c.joinWith(b); + newStore.put(p, c); + } else if (a != null) newStore.put(p, a.copyOf()); + else if (b != null) newStore.put(p, b.copyOf()); + } + this.store.clear(); + this.store.putAll(newStore); + } + + @Override + public void widenWith(AbstractMemory other) { + if (other == null) return; + if (this.isTop || other.isTop) { + setToTop(); + return; + } + if (this.isBot) { + this.isBot = other.isBot; + this.env.clear(); + this.env.putAll(other.env); + this.store.clear(); + for (Map.Entry e : other.store.entrySet()) + this.store.put(e.getKey(), e.getValue().copyOf()); + return; + } + + /* env: keep only equal mappings */ + Set keys = new HashSet<>(); + keys.addAll(this.env.keySet()); + keys.addAll(other.env.keySet()); + Map newEnv = new HashMap<>(); + for (Var v : keys) { + AccessPath a = this.env.get(v); + AccessPath b = other.env.get(v); + if (a != null && b != null && a.equals(b)) newEnv.put(v, a); + } + this.env.clear(); + this.env.putAll(newEnv); + + /* store: union of keys with widened intervals */ + Set skeys = new HashSet<>(); + skeys.addAll(this.store.keySet()); + skeys.addAll(other.store.keySet()); + Map newStore = new HashMap<>(); + for (AccessPath p : skeys) { + IntInterval a = this.store.get(p); + IntInterval b = other.store.get(p); + if (a != null && b != null) { + IntInterval c = a.copyOf(); + c.widenWith(b); + newStore.put(p, c); + } else if (a != null) newStore.put(p, a.copyOf()); + else if (b != null) newStore.put(p, b.copyOf()); + } + this.store.clear(); + this.store.putAll(newStore); + } + + @Override + public void meetWith(AbstractMemory other) { + if (other == null) return; + if (this.isBot || other.isBot) { + setToBot(); + return; + } + if (this.isTop) { + this.isTop = false; + this.env.clear(); + this.env.putAll(other.env); + this.store.clear(); + for (Map.Entry e : other.store.entrySet()) + this.store.put(e.getKey(), e.getValue().copyOf()); + return; + } + if (other.isTop) { + return; + } + + /* env: intersection where equal */ + Map newEnv = new HashMap<>(); + for (Map.Entry e : this.env.entrySet()) { + AccessPath b = other.env.get(e.getKey()); + if (b != null && b.equals(e.getValue())) newEnv.put(e.getKey(), e.getValue()); + } + this.env.clear(); + this.env.putAll(newEnv); + + // store: intersection keys with meet of intervals + Map newStore = new HashMap<>(); + for (Map.Entry e : this.store.entrySet()) { + IntInterval b = other.store.get(e.getKey()); + if (b != null) { + IntInterval c = e.getValue().copyOf(); + c.meetWith(b); + newStore.put(e.getKey(), c); + } + } + this.store.clear(); + this.store.putAll(newStore); + } + + @Override + public String toString() { + if (isBot) return "AbsMemory(⊥)"; + if (isTop) return "AbsMemory(⊤)"; + return "{env=" + env + ", store=" + store + '}'; + } + + @Override + public AbstractMemory copyOf() { + AbstractMemory c = new AbstractMemory(); + c.isBot = this.isBot; + c.isTop = this.isTop; + c.env.clear(); + c.env.putAll(this.env); + c.store.clear(); + for (Map.Entry e : this.store.entrySet()) + c.store.put(e.getKey(), e.getValue().copyOf()); + return c; + } + + public IntInterval readStore(AccessPath p) { + IntInterval v = store.get(p); + if (v == null) { + IntInterval top = new IntInterval(); + top.setToTop(); + return top; + } + return v.copyOf(); + } + + public String[] getAllTempNames() { + Set tempNames = new HashSet<>(); + for (Var v : env.keySet()) { + if (v.kind() == Var.Kind.TEMP) { + tempNames.add(v.name()); + } + } + return tempNames.toArray(new String[0]); + } + + public void bindVarToMany(Var v, Set paths) { + Objects.requireNonNull(v); + Objects.requireNonNull(paths); + ensureNotBotTop(); + // clear singleton mapping and record multi + env.remove(v); + envMulti.put(v, AliasSet.ofSet(paths)); + } + + public AliasSet lookupVarSet(Var v) { + AliasSet multi = envMulti.get(v); + if (multi != null && !multi.isEmpty()) return multi; + AccessPath single = env.get(v); + if (single != null) return AliasSet.of(single); + return AliasSet.ofSet(new HashSet<>()); + } + + public void removeVarFromMulti(Var v) { + envMulti.remove(v); + } + + /** Read from a set of access paths after applying a transform (e.g., append field or array wildcard) */ + public IntInterval readFrom(AliasSet aliasSet, java.util.function.Function pathTransform) { + IntInterval acc = new IntInterval(); + acc.setToBot(); + for (AccessPath p : aliasSet.paths()) { + AccessPath tp = pathTransform.apply(p); + IntInterval val = readStore(tp); + acc.joinWith(val); + } + return acc; + } + + /** Write to a set of access paths; strong update if singleton, else weak update to each element. */ + public void writeTo(AliasSet aliasSet, java.util.function.Function pathTransform, IntInterval val) { + if (aliasSet.isSingleton()) { + AccessPath p = aliasSet.paths().iterator().next(); + writeStoreStrong(pathTransform.apply(p), val); + } else { + for (AccessPath p : aliasSet.paths()) { + writeStore(pathTransform.apply(p), val); + } + } + } + + public AliasSet lookupLocalSetByName(String localName) { + return lookupVarSet(Var.local(localName)); + } + + public AliasSet lookupParamSetByName(String paramName) { + return lookupVarSet(Var.param(paramName)); + } + + public AliasSet lookupTempSetByName(String tempName) { + return lookupVarSet(Var.temp(tempName)); + } + + public boolean hasStoreEntry(AccessPath p) { + return store.containsKey(p); + } + + public void bindTempSetByName(String nestedArrayId, AliasSet nestedArrayAliases) { + Objects.requireNonNull(nestedArrayId); + Objects.requireNonNull(nestedArrayAliases); + Var tempVar = Var.temp(nestedArrayId); + + if (nestedArrayAliases.isEmpty()) { + env.remove(tempVar); + envMulti.remove(tempVar); + return; + } + + if (nestedArrayAliases.isSingleton()) { + AccessPath singlePath = nestedArrayAliases.paths().iterator().next(); + bindTempByName(nestedArrayId, singlePath); + envMulti.remove(tempVar); + } else { + bindVarToMany(tempVar, nestedArrayAliases.paths()); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AccessPath.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AccessPath.java new file mode 100644 index 000000000000..d0b72a47d660 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AccessPath.java @@ -0,0 +1,137 @@ +package com.oracle.svm.hosted.analysis.ai.domain.memory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Immutable access-path representation used by AbsMemory. + * Root is a typed root (local, static class, alloc-site, placeholder). + * Fields is a sequence of field names or array markers. + */ +public final class AccessPath { + + public enum RootKind { LOCAL, STATIC_CLASS, ALLOC_SITE, PLACEHOLDER } + + private final RootKind rootKind; + private final String rootName; // e.g., local name, class name for static, alloc id, or placeholder id + private final List fields; // immutable list of field names, array indices may be represented as "[i]" or "[*]" + + private AccessPath(RootKind rootKind, String rootName, List fields) { + this.rootKind = Objects.requireNonNull(rootKind); + this.rootName = Objects.requireNonNull(rootName); + this.fields = List.copyOf(fields); + } + + public static AccessPath forLocal(String localName) { + return new AccessPath(RootKind.LOCAL, Objects.requireNonNull(localName), Collections.emptyList()); + } + + public static AccessPath forStaticClass(String className) { + return new AccessPath(RootKind.STATIC_CLASS, Objects.requireNonNull(className), Collections.emptyList()); + } + + public static AccessPath forAllocSite(String allocId) { + return new AccessPath(RootKind.ALLOC_SITE, Objects.requireNonNull(allocId), Collections.emptyList()); + } + + public static AccessPath forAllocSiteWithContext(String allocSiteId, String contextSignature) { + String name = Objects.requireNonNull(allocSiteId) + "@" + Objects.requireNonNull(contextSignature); + return new AccessPath(RootKind.ALLOC_SITE, name, Collections.emptyList()); + } + + public static AccessPath forPlaceholder(String placeholder) { + return new AccessPath(RootKind.PLACEHOLDER, Objects.requireNonNull(placeholder), Collections.emptyList()); + } + + public RootKind getRootKind() { + return rootKind; + } + + public String getRootName() { + return rootName; + } + + public List getFields() { + return fields; + } + + public boolean isRootLocal() { + return rootKind == RootKind.LOCAL; + } + + public boolean isStaticRoot() { + return rootKind == RootKind.STATIC_CLASS; + } + + public AccessPath appendField(String field) { + Objects.requireNonNull(field); + List newFields = new ArrayList<>(fields.size() + 1); + newFields.addAll(fields); + newFields.add(field); + return new AccessPath(rootKind, rootName, newFields); + } + + public AccessPath appendArrayIndex(int index) { + return appendField("[" + index + "]"); + } + + public AccessPath appendArrayWildcard() { + return appendField("[*]"); + } + + public int depth() { + return fields.size(); + } + + /** + * Truncate path to maximum of k fields. If original fields.size() > k, + * returned path contains first k fields followed by "*" marker. + */ + public AccessPath truncate(int k) { + if (k < 0) throw new IllegalArgumentException("k must be >= 0"); + if (fields.size() <= k) return this; + List newFields = new ArrayList<>(k + 1); + for (int i = 0; i < k; i++) newFields.add(fields.get(i)); + newFields.add("*"); + return new AccessPath(rootKind, rootName, newFields); + } + + /** + * True if this path starts with given prefix (root kind and name must match, and fields prefix matched). + */ + public boolean startsWith(AccessPath prefix) { + if (prefix == null) return false; + if (this.rootKind != prefix.rootKind) return false; + if (!this.rootName.equals(prefix.rootName)) return false; + List pfields = prefix.fields; + if (pfields.size() > this.fields.size()) return false; + for (int i = 0; i < pfields.size(); i++) { + if (!Objects.equals(this.fields.get(i), pfields.get(i))) return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{").append(rootKind).append(":").append(rootName).append("}"); + if (fields.isEmpty()) return sb.toString(); + for (String f : fields) sb.append('.').append(f); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AccessPath)) return false; + AccessPath that = (AccessPath) o; + return this.rootKind == that.rootKind && this.rootName.equals(that.rootName) && this.fields.equals(that.fields); + } + + @Override + public int hashCode() { + return Objects.hash(rootKind, rootName, fields); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AliasSet.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AliasSet.java new file mode 100644 index 000000000000..0965476644e8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/AliasSet.java @@ -0,0 +1,97 @@ +package com.oracle.svm.hosted.analysis.ai.domain.memory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * A small utility to represent a may-alias set of access paths. + */ +public final class AliasSet { + + private final Set paths; + + private AliasSet(Set paths) { + this.paths = Collections.unmodifiableSet(paths); + } + + public static AliasSet of(AccessPath... ps) { + Set s = new HashSet<>(); + if (ps != null) { + for (AccessPath p : ps) { + if (p != null) s.add(p); + } + } + return new AliasSet(s); + } + + public static AliasSet ofSet(Set set) { + if (set == null || set.isEmpty()) return new AliasSet(Collections.emptySet()); + return new AliasSet(new HashSet<>(set)); + } + + public boolean isEmpty() { + return paths.isEmpty(); + } + + public boolean isSingleton() { + return paths.size() == 1; + } + + public Set paths() { + return paths; + } + + public AliasSet union(AliasSet other) { + if (other == null || other.paths.isEmpty()) return this; + if (this.paths.isEmpty()) return other; + Set s = new HashSet<>(this.paths); + s.addAll(other.paths); + return new AliasSet(s); + } + + public AliasSet intersect(AliasSet other) { + if (other == null) return ofSet(Collections.emptySet()); + Set s = new HashSet<>(this.paths); + s.retainAll(other.paths); + return new AliasSet(s); + } + + public AliasSet map(Function f) { + Objects.requireNonNull(f); + Set s = new HashSet<>(paths.size()); + for (AccessPath p : paths) { + AccessPath np = f.apply(p); + if (np != null) s.add(np); + } + return new AliasSet(s); + } + + public AliasSet mapField(String field) { + return map(p -> p.appendField(field)); + } + + public AliasSet mapArrayWildcard() { + return map(AccessPath::appendArrayWildcard); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AliasSet aliasSet)) return false; + return paths.equals(aliasSet.paths); + } + + @Override + public int hashCode() { + return paths.hashCode(); + } + + @Override + public String toString() { + return "AliasSet" + paths; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/Var.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/Var.java new file mode 100644 index 000000000000..23866b3162c4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/memory/Var.java @@ -0,0 +1,41 @@ +package com.oracle.svm.hosted.analysis.ai.domain.memory; + +import java.util.Objects; + +/** + * Lightweight abstraction of a program variable used as an environment key. + */ +public record Var(Var.Kind kind, String name) { + + public enum Kind {LOCAL, PARAM, TEMP} + + public Var(Kind kind, String name) { + this.kind = Objects.requireNonNull(kind); + this.name = Objects.requireNonNull(name); + } + + public static Var local(String name) { + return new Var(Kind.LOCAL, name); + } + + public static Var param(String name) { + return new Var(Kind.PARAM, name); + } + + public static Var temp(String name) { + return new Var(Kind.TEMP, name); + } + + @Override + public String toString() { + return kind + ":" + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Var(Kind kind1, String name1))) return false; + return kind == kind1 && name.equals(name1); + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/IntInterval.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/IntInterval.java new file mode 100644 index 000000000000..7bc08a97c065 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/IntInterval.java @@ -0,0 +1,381 @@ +package com.oracle.svm.hosted.analysis.ai.domain.numerical; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * A simple integer interval domain. + * Representation: + * - top: represents all integers + * - bottom: represents empty set + * - otherwise: [lo, hi], where lo <= hi + * For +, -, * we are conservative and produce sound intervals. + * Widening is implemented in a simple standard way: when a bound grows + * beyond the previous, we set it to infinite (Long.MIN_VALUE / Long.MAX_VALUE). + */ +public final class IntInterval implements AbstractDomain { + + public static final long NEG_INF = Long.MIN_VALUE; + public static final long POS_INF = Long.MAX_VALUE; + + private long lowerBound; + private long upperBound; + + public IntInterval() { + setToBot(); + } + + public IntInterval(long value) { + this.lowerBound = value; + this.upperBound = value; + } + + public IntInterval(long lowerBound, long upperBound) { + this.lowerBound = lowerBound; + this.upperBound = upperBound; + } + + public IntInterval(IntInterval other) { + this.lowerBound = other.lowerBound; + this.upperBound = other.upperBound; + } + + /* This is a utility function to get the interval + that represents all integers lower than the given interval + FIXME: remove once we have DataFlowIntervalAnalysis ready + */ + public static IntInterval getLowerInterval(IntInterval interval) { + return new IntInterval(NEG_INF, interval.getLower() - 1); + } + + public static IntInterval getHigherInterval(IntInterval interval) { + return new IntInterval(interval.getUpper() + 1, POS_INF); + } + + public long getUpper() { + return upperBound; + } + + public long getLower() { + return lowerBound; + } + + public void setLower(long lowerBound) { + this.lowerBound = lowerBound; + } + + public void setUpper(long upperBound) { + this.upperBound = upperBound; + } + + public boolean isBot() { + return lowerBound > upperBound; + } + + public boolean isTop() { + return lowerBound == NEG_INF && upperBound == POS_INF; + } + + /* Helper: returns true if the lower bound is -infinity sentinel. */ + public boolean isLowerInfinite() { + return lowerBound == NEG_INF; + } + + /* Helper: returns true if the upper bound is +infinity sentinel. */ + public boolean isUpperInfinite() { + return upperBound == POS_INF; + } + + @Override + public boolean leq(IntInterval other) { + if (isBot()) { + return true; + } + if (other.isTop()) { + return true; + } + if (isTop()) { + return false; + } + return other.lowerBound <= lowerBound && upperBound <= other.upperBound; + } + + public void setToBot() { + lowerBound = POS_INF; + upperBound = NEG_INF; + } + + public void setToTop() { + lowerBound = NEG_INF; + upperBound = POS_INF; + } + + public boolean containsValue(long value) { + if (isBot()) return false; + return lowerBound <= value && value <= upperBound; + } + + public void joinWith(IntInterval other) { + if (other.isBot()) return; + if (isBot()) { + lowerBound = other.lowerBound; + upperBound = other.upperBound; + return; + } + if (isTop() || other.isTop()) { + setToTop(); + return; + } + lowerBound = Math.min(lowerBound, other.lowerBound); + upperBound = Math.max(upperBound, other.upperBound); + } + + public void widenWith(IntInterval other) { + if (isBot()) { + lowerBound = other.lowerBound; + upperBound = other.upperBound; + return; + } + if (other.isBot()) return; + + long newLower = (other.lowerBound < lowerBound) ? NEG_INF : lowerBound; + long newUpper = (other.upperBound > upperBound) ? POS_INF : upperBound; + lowerBound = newLower; + upperBound = newUpper; + } + + public void meetWith(IntInterval other) { + if (isBot() || other.isBot()) { + setToBot(); + return; + } + if (isTop()) { + lowerBound = other.lowerBound; + upperBound = other.upperBound; + return; + } + if (other.isTop()) { + return; + } + lowerBound = Math.max(lowerBound, other.lowerBound); + upperBound = Math.min(upperBound, other.upperBound); + if (isBot()) setToBot(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof IntInterval)) return false; + IntInterval other = (IntInterval) o; + if (isBot() && other.isBot()) return true; + if (isTop() && other.isTop()) return true; + return lowerBound == other.lowerBound && upperBound == other.upperBound; + } + + @Override + public int hashCode() { + return Objects.hash(lowerBound, upperBound); + } + + @Override + public IntInterval copyOf() { + return new IntInterval(this); + } + + @Override + public String toString() { + if (isBot()) return "⊥"; + if (isTop()) return "⊤"; + String lo = (lowerBound == NEG_INF) ? "-∞" : String.valueOf(lowerBound); + String hi = (upperBound == POS_INF) ? "∞" : String.valueOf(upperBound); + return "[" + lo + ", " + hi + "]"; + } + + + private static long safeAdd(long a, long b) { + if (a == POS_INF || b == POS_INF) return POS_INF; + if (a == NEG_INF || b == NEG_INF) return NEG_INF; + return a + b; + } + + private static long safeSub(long a, long b) { + if (a == POS_INF || b == NEG_INF) return POS_INF; + if (a == NEG_INF || b == POS_INF) return NEG_INF; + return a - b; + } + + private static long safeMul(long a, long b) { + if ((a == 0 || b == 0) && (a == POS_INF || a == NEG_INF || b == POS_INF || b == NEG_INF)) + return 0; // 0 * ∞ = 0 (conservatively safe) + if ((a == POS_INF && b > 0) || (b == POS_INF && a > 0)) return POS_INF; + if ((a == NEG_INF && b > 0) || (b == NEG_INF && a > 0)) return NEG_INF; + if ((a == POS_INF && b < 0) || (b == POS_INF && a < 0)) return NEG_INF; + if ((a == NEG_INF && b < 0) || (b == NEG_INF && a < 0)) return POS_INF; + if (a == POS_INF || a == NEG_INF || b == POS_INF || b == NEG_INF) + return (a > 0) == (b > 0) ? POS_INF : NEG_INF; + return a * b; + } + + private static long safeDiv(long a, long b) { + if (b == 0) return 0; // undefined, handled outside + if (a == POS_INF || a == NEG_INF || b == POS_INF || b == NEG_INF) { + if (b == POS_INF || b == NEG_INF) return 0; + if (a == POS_INF && b > 0) return POS_INF; + if (a == POS_INF && b < 0) return NEG_INF; + if (a == NEG_INF && b > 0) return NEG_INF; + if (a == NEG_INF && b < 0) return POS_INF; + } + return a / b; + } + + public void addWith(IntInterval other) { + if (isBot() || other.isBot()) { + setToBot(); + return; + } + if (isTop() || other.isTop()) { + setToTop(); + return; + } + lowerBound = safeAdd(lowerBound, other.lowerBound); + upperBound = safeAdd(upperBound, other.upperBound); + } + + public IntInterval add(IntInterval other) { + IntInterval res = copyOf(); + res.addWith(other); + return res; + } + + public void subWith(IntInterval other) { + if (isBot() || other.isBot()) { + setToBot(); + return; + } + if (isTop() || other.isTop()) { + setToTop(); + return; + } + long lo = safeSub(lowerBound, other.upperBound); + long hi = safeSub(upperBound, other.lowerBound); + lowerBound = lo; + upperBound = hi; + } + + public IntInterval sub(IntInterval other) { + IntInterval res = copyOf(); + res.subWith(other); + return res; + } + + public void mulWith(IntInterval other) { + if (isBot() || other.isBot()) { + setToBot(); + return; + } + if (isTop() || other.isTop()) { + setToTop(); + return; + } + + long a = safeMul(lowerBound, other.lowerBound); + long b = safeMul(lowerBound, other.upperBound); + long c = safeMul(upperBound, other.lowerBound); + long d = safeMul(upperBound, other.upperBound); + + lowerBound = Math.min(Math.min(a, b), Math.min(c, d)); + upperBound = Math.max(Math.max(a, b), Math.max(c, d)); + } + + public IntInterval mul(IntInterval other) { + IntInterval res = copyOf(); + res.mulWith(other); + return res; + } + + public void divWith(IntInterval other) { + if (isBot() || other.isBot()) { + setToBot(); + return; + } + if (isTop() || other.isTop()) { + setToTop(); + return; + } + + if (other.lowerBound <= 0 && other.upperBound >= 0) { + setToTop(); + return; + } + + long a = safeDiv(lowerBound, other.lowerBound); + long b = safeDiv(lowerBound, other.upperBound); + long c = safeDiv(upperBound, other.lowerBound); + long d = safeDiv(upperBound, other.upperBound); + + lowerBound = Math.min(Math.min(a, b), Math.min(c, d)); + upperBound = Math.max(Math.max(a, b), Math.max(c, d)); + } + + public IntInterval div(IntInterval other) { + IntInterval res = copyOf(); + res.divWith(other); + return res; + } + + public IntInterval rem(IntInterval other) { + if (isBot() || other.isBot()) { + IntInterval res = new IntInterval(); + res.setToBot(); + return res; + } + if (isTop() || other.isTop()) { + IntInterval res = new IntInterval(); + res.setToTop(); + return res; + } + + if (other.lowerBound <= 0 && other.upperBound >= 0) { + IntInterval res = new IntInterval(); + res.setToTop(); + return res; + } + + long maxAbsDivisor = Math.max(Math.abs(other.lowerBound), Math.abs(other.upperBound)) - 1; + if (maxAbsDivisor == 0) { + IntInterval res = new IntInterval(); + res.setToBot(); + return res; + } + + long lo, hi; + if (lowerBound >= 0) { + lo = 0; + hi = Math.min(upperBound, maxAbsDivisor); + } else if (upperBound <= 0) { + lo = Math.max(lowerBound, -maxAbsDivisor); + hi = 0; + } else { + lo = -maxAbsDivisor; + hi = maxAbsDivisor; + } + return new IntInterval(lo, hi); + } + + public boolean isUpperBoundStrictlyLessThan(IntInterval iy) { + return upperBound < iy.lowerBound; + } + + public boolean isLowerBoundGreaterOrEqual(IntInterval iy) { + return lowerBound >= iy.upperBound; + } + + public boolean isConstantValue() { + return !isBot() && !isTop() && !isLowerInfinite() && !isUpperInfinite() && lowerBound == upperBound; + } + + public boolean upperLessThan(long lower) { + return upperBound < lower; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/PentagonDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/PentagonDomain.java new file mode 100644 index 000000000000..38d15c358f25 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/PentagonDomain.java @@ -0,0 +1,335 @@ +package com.oracle.svm.hosted.analysis.ai.domain.numerical; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Pentagon abstract domain that combines intervals and variable relationships. + * Based on the paper: + * + * @param the type used for identifying different variables in the pentagon domain (String, Access Path, ...) + */ +public class PentagonDomain implements AbstractDomain> { + + /* We could use something like MapDomain here, but this is just for demonstration and easier work */ + private final Map intervals; + private final Map> lessThan; // x < y: x -> {y} + + public PentagonDomain() { + this.intervals = new HashMap<>(); + this.lessThan = new HashMap<>(); + } + + public PentagonDomain(PentagonDomain other) { + this.intervals = new HashMap<>(); + for (Map.Entry entry : other.intervals.entrySet()) { + this.intervals.put(entry.getKey(), entry.getValue().copyOf()); + } + + this.lessThan = new HashMap<>(); + for (Map.Entry> entry : other.lessThan.entrySet()) { + this.lessThan.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + } + + public IntInterval getInterval(Variable var) { + return intervals.getOrDefault(var, new IntInterval()); + } + + public void setInterval(Variable var, IntInterval interval) { + intervals.put(var, interval); + applyReduction(); + } + + public void addLessThanRelation(Variable x, Variable y) { + lessThan.computeIfAbsent(x, k -> new HashSet<>()).add(y); + applyReduction(); + } + + public Set getVariableNames() { + return intervals.keySet(); + } + + /** + * Checks if there exists a less-than relationship between variables (either direct or transitive). + * For example, if x < y and y < z are in the domain, then x < z is also true. + * + * @param x the first variable + * @param z the second variable + * @return true if x < z, false otherwise + */ + public boolean lessThan(Variable x, Variable z) { + // Check direct relationship + if (lessThan.getOrDefault(x, Set.of()).contains(z)) { + return true; + } + + // Check transitive relationship using DFS + Set visited = new HashSet<>(); + return findTransitiveRelation(x, z, visited); + } + + private boolean findTransitiveRelation(Variable x, Variable z, Set visited) { + // Avoid cycles + if (visited.contains(x)) { + return false; + } + + visited.add(x); + + // Check all variables y where x < y + Set directRelations = lessThan.getOrDefault(x, Set.of()); + for (Variable y : directRelations) { + if (y.equals(z) || findTransitiveRelation(y, z, visited)) { + return true; + } + } + + return false; + } + + private void applyReduction() { + boolean changed; + do { + changed = false; + + // Update intervals based on inequalities + for (Map.Entry> entry : lessThan.entrySet()) { + Variable x = entry.getKey(); + IntInterval xInterval = getInterval(x); + if (xInterval.isBot()) continue; + + for (Variable y : entry.getValue()) { + IntInterval yInterval = getInterval(y); + if (yInterval.isBot()) continue; + + // If x < y then x's upper bound must be less than y's lower bound + if (xInterval.getUpper() >= yInterval.getLower()) { + long newXUpper = Math.min(xInterval.getUpper(), yInterval.getLower() - 1); + long newYLower = Math.max(yInterval.getLower(), xInterval.getUpper() + 1); + + // Only create new interval if it would be valid + if (xInterval.getLower() <= newXUpper) { + IntInterval newXInterval = new IntInterval(xInterval.getLower(), newXUpper); + intervals.put(x, newXInterval); + changed = true; + } else { + // Cannot satisfy constraint - set to bottom + intervals.put(x, new IntInterval()); + changed = true; + break; // No need to continue with this variable + } + + if (newYLower <= yInterval.getUpper()) { + IntInterval newYInterval = new IntInterval(newYLower, yInterval.getUpper()); + intervals.put(y, newYInterval); + } else { + // Cannot satisfy constraint - set to bottom + intervals.put(y, new IntInterval()); + } + } + } + } + + } while (changed); + } + + @Override + public boolean isBot() { + return intervals.values().stream().anyMatch(IntInterval::isBot); + } + + @Override + public boolean isTop() { + return intervals.isEmpty() && lessThan.isEmpty(); + } + + @Override + public boolean leq(PentagonDomain other) { + for (Map.Entry entry : intervals.entrySet()) { + IntInterval thisInterval = entry.getValue(); + IntInterval otherInterval = other.getInterval(entry.getKey()); + if (!thisInterval.leq(otherInterval)) { + return false; + } + } + + for (Map.Entry> entry : lessThan.entrySet()) { + Variable x = entry.getKey(); + Set thisYVars = entry.getValue(); + Set otherYVars = other.lessThan.getOrDefault(x, Set.of()); + + for (Variable y : thisYVars) { + if (!otherYVars.contains(y)) { + return false; + } + } + } + + return true; + } + + @SuppressWarnings("unchecked") + @Override + public void setToBot() { + intervals.clear(); + intervals.put((Variable) new Object(), new IntInterval()); + lessThan.clear(); + } + + @Override + public void setToTop() { + intervals.clear(); + lessThan.clear(); + } + + @Override + public void joinWith(PentagonDomain other) { + // Join intervals + Set allVars = new HashSet<>(intervals.keySet()); + allVars.addAll(other.intervals.keySet()); + + for (Variable var : allVars) { + IntInterval thisInterval = getInterval(var); + IntInterval otherInterval = other.getInterval(var); + IntInterval joinedInterval = thisInterval.join(otherInterval); + intervals.put(var, joinedInterval); + } + + // Join inequalities (intersection) + Map> newLessThan = new HashMap<>(); + + for (Map.Entry> entry : lessThan.entrySet()) { + Variable x = entry.getKey(); + Set otherYVars = other.lessThan.get(x); + + if (otherYVars != null) { + Set newYVars = new HashSet<>(entry.getValue()); + newYVars.retainAll(otherYVars); + + if (!newYVars.isEmpty()) { + newLessThan.put(x, newYVars); + } + } + } + + lessThan.clear(); + lessThan.putAll(newLessThan); + applyReduction(); + filterContradictoryRelations(); + } + + @Override + public void widenWith(PentagonDomain other) { + // Similar to join but with widening for intervals + Set allVars = new HashSet<>(intervals.keySet()); + allVars.addAll(other.intervals.keySet()); + + for (Variable var : allVars) { + IntInterval thisInterval = getInterval(var); + IntInterval otherInterval = other.getInterval(var); + IntInterval widenedInterval = thisInterval.widen(otherInterval); + intervals.put(var, widenedInterval); + } + + // Same as join for inequalities + Map> newLessThan = new HashMap<>(); + + for (Map.Entry> entry : lessThan.entrySet()) { + Variable x = entry.getKey(); + Set otherYVars = other.lessThan.get(x); + + if (otherYVars != null) { + Set newYVars = new HashSet<>(entry.getValue()); + newYVars.retainAll(otherYVars); + + if (!newYVars.isEmpty()) { + newLessThan.put(x, newYVars); + } + } + } + + lessThan.clear(); + lessThan.putAll(newLessThan); + applyReduction(); + filterContradictoryRelations(); + } + + // Language: java + @Override + public void meetWith(PentagonDomain other) { + // Compute the meet of intervals + Set allVars = new HashSet<>(); + allVars.addAll(this.intervals.keySet()); + allVars.addAll(other.intervals.keySet()); + Map newIntervals = new HashMap<>(); + for (Variable var : allVars) { + IntInterval interval1 = getInterval(var).copyOf(); + IntInterval interval2 = other.getInterval(var); + interval1.meetWith(interval2); + newIntervals.put(var, interval1); + } + this.intervals.clear(); + this.intervals.putAll(newIntervals); + + // Intersect less-than relations + Map> newLessThan = new HashMap<>(); + for (Variable var : this.lessThan.keySet()) { + if (other.lessThan.containsKey(var)) { + Set set1 = new HashSet<>(this.lessThan.get(var)); + Set set2 = other.lessThan.get(var); + set1.retainAll(set2); + if (!set1.isEmpty()) { + newLessThan.put(var, set1); + } + } + } + + this.lessThan.clear(); + this.lessThan.putAll(newLessThan); + applyReduction(); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PentagonDomain that = (PentagonDomain) o; + return Objects.equals(intervals, that.intervals) && + Objects.equals(lessThan, that.lessThan); + } + + @Override + public int hashCode() { + return Objects.hash(intervals, lessThan); + } + + @Override + public String toString() { + return "PentagonDomain{intervals=" + intervals + ", lessThan=" + lessThan + '}'; + } + + @Override + public PentagonDomain copyOf() { + return new PentagonDomain<>(this); + } + + private void filterContradictoryRelations() { + lessThan.entrySet().removeIf(entry -> { + Variable x = entry.getKey(); + IntInterval xInterval = getInterval(x); + + Set ys = entry.getValue(); + ys.removeIf(y -> { + IntInterval yInterval = getInterval(y); + return xInterval.getUpper() >= yInterval.getLower(); // contradiction + }); + + return ys.isEmpty(); + }); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/SignDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/SignDomain.java new file mode 100644 index 000000000000..23dbbf969f58 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/numerical/SignDomain.java @@ -0,0 +1,33 @@ +package com.oracle.svm.hosted.analysis.ai.domain.numerical; + +import com.oracle.svm.hosted.analysis.ai.domain.util.FiniteDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; +import com.oracle.svm.hosted.analysis.ai.domain.value.Sign; + +public final class SignDomain extends FiniteDomain { + + public SignDomain() { + super(Sign.BOT, AbstractValueKind.BOT); + } + + public SignDomain(Sign sign) { + super(sign, sign == Sign.BOT ? AbstractValueKind.BOT : sign == Sign.TOP ? AbstractValueKind.TOP : AbstractValueKind.VAL); + } + + public SignDomain(SignDomain signDomain) { + super(signDomain.getState(), signDomain.getKind()); + } + + @Override + public SignDomain copyOf() { + return new SignDomain(this); + } + + public SignDomain plus(SignDomain other) { + return new SignDomain(this.getState().plus(other.getState())); + } + + public SignDomain minus(SignDomain other) { + return new SignDomain(this.getState().minus(other.getState())); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanAndDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanAndDomain.java new file mode 100644 index 000000000000..0887edffe518 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanAndDomain.java @@ -0,0 +1,103 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * Represents a boolean domain ordered by a || ¬b. + * This domain can be used when we want to have a boolean value + * that is true only when it is true in all paths. + */ +public final class BooleanAndDomain implements AbstractDomain { + + private boolean value; + + public BooleanAndDomain() { + this.value = true; + } + + public BooleanAndDomain(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public boolean isBot() { + return !value; + } + + @Override + public boolean isTop() { + return value; + } + + @Override + public boolean leq(BooleanAndDomain other) { + return this.value || !other.value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BooleanAndDomain that = (BooleanAndDomain) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public void setToBot() { + this.value = false; + } + + @Override + public void setToTop() { + this.value = true; + } + + @Override + public void joinWith(BooleanAndDomain other) { + this.value = this.value && other.value; + } + + @Override + public void widenWith(BooleanAndDomain other) { + joinWith(other); + } + + @Override + public void meetWith(BooleanAndDomain other) { + this.value = this.value || other.value; + } + + @Override + public String toString() { + return "BooleanAndDomain{ " + value + " }"; + } + + @Override + public BooleanAndDomain copyOf() { + return new BooleanAndDomain(this.value); + } + + public void negate() { + this.value = !this.value; + } + + public BooleanAndDomain getNegated() { + BooleanAndDomain copy = this.copyOf(); + copy.negate(); + return copy; + } + + public static BooleanAndDomain TRUE = new BooleanAndDomain(true); + + public static BooleanAndDomain FALSE = new BooleanAndDomain(false); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanOrDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanOrDomain.java new file mode 100644 index 000000000000..ee24cd9b176c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/BooleanOrDomain.java @@ -0,0 +1,103 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * Represents a boolean domain ordered by ¬a || b. + * This domain can be used when we want to have a boolean value + * that is true only when it is true in all paths. + */ +public final class BooleanOrDomain implements AbstractDomain { + + private boolean value; + + public BooleanOrDomain() { + this.value = false; + } + + public BooleanOrDomain(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public boolean isBot() { + return !value; + } + + @Override + public boolean isTop() { + return value; + } + + @Override + public boolean leq(BooleanOrDomain other) { + return !this.value || other.value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BooleanOrDomain that = (BooleanOrDomain) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public void setToBot() { + this.value = false; + } + + @Override + public void setToTop() { + this.value = true; + } + + @Override + public void joinWith(BooleanOrDomain other) { + this.value = this.value || other.value; + } + + @Override + public void widenWith(BooleanOrDomain other) { + joinWith(other); + } + + @Override + public void meetWith(BooleanOrDomain other) { + this.value = this.value && other.value; + } + + @Override + public String toString() { + return "BooleanOrDomain{ " + value + " }"; + } + + @Override + public BooleanOrDomain copyOf() { + return new BooleanOrDomain(this.value); + } + + public void negate() { + this.value = !this.value; + } + + public BooleanOrDomain getNegated() { + BooleanOrDomain copy = this.copyOf(); + copy.negate(); + return copy; + } + + public static BooleanOrDomain TRUE = new BooleanOrDomain(true); + + public static BooleanOrDomain FALSE = new BooleanOrDomain(false); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ConstantDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ConstantDomain.java new file mode 100644 index 000000000000..4a28c0bfa38e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ConstantDomain.java @@ -0,0 +1,185 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; + +import java.util.Objects; + +/** + * Abstract domain for flat lattice, also known as a three-level lattice. + * For domains that can be represented as a constant value and have infinite ascending and descending chains. + * ⊤ + * / | \ + * -1 0 1 + * \ | / + * ⊥ + * + * @param the type of the constant value (e.g., Integer, Long, Float, Double) + */ +public final class ConstantDomain implements AbstractDomain> { + + private AbstractValueKind kind; + private Value value; + + public ConstantDomain() { + this.kind = AbstractValueKind.BOT; + } + + public ConstantDomain(Value value) { + this.kind = AbstractValueKind.VAL; + this.value = value; + } + + public ConstantDomain(AbstractValueKind kind) { + this.kind = kind; + if (kind == AbstractValueKind.VAL) { + throw new IllegalArgumentException("Invalid kind for this constructor"); + } + } + + public Value getValue() { + if (kind == AbstractValueKind.VAL) { + return value; + } + return null; + } + + public static ConstantDomain bottom() { + return new ConstantDomain<>(AbstractValueKind.BOT); + } + + public static ConstantDomain top() { + return new ConstantDomain<>(AbstractValueKind.TOP); + } + + @Override + public boolean isBot() { + return kind == AbstractValueKind.BOT; + } + + @Override + public boolean isTop() { + return kind == AbstractValueKind.TOP; + } + + public boolean isValue() { + return kind == AbstractValueKind.VAL; + } + + @Override + public void setToBot() { + kind = AbstractValueKind.BOT; + value = null; + } + + @Override + public void setToTop() { + kind = AbstractValueKind.TOP; + value = null; + } + + @Override + public boolean leq(ConstantDomain other) { + if (isBot()) { + return true; + } + if (other.isBot()) { + return false; + } + if (other.isTop()) { + return true; + } + if (isTop()) { + return false; + } + return value.equals(other.value); + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConstantDomain that = (ConstantDomain) o; + return equals((ConstantDomain) that); + } + + @Override + public int hashCode() { + return Objects.hash(kind, value); + } + + @Override + public void joinWith(ConstantDomain other) { + if (isTop() || other.isBot()) { + return; + } + if (other.isTop()) { + setToTop(); + return; + } + if (isBot()) { + kind = other.kind; + value = other.value; + return; + } + if (!value.equals(other.value)) { + setToTop(); + } + } + + @Override + public void widenWith(ConstantDomain other) { + joinWith(other); + } + + @Override + public void meetWith(ConstantDomain other) { + if (isBot() || other.isTop()) { + return; + } + if (other.isBot()) { + setToBot(); + return; + } + if (isTop()) { + kind = other.kind; + value = other.value; + return; + } + if (!value.equals(other.value)) { + setToBot(); + } + } + + @Override + public ConstantDomain copyOf() { + ConstantDomain copy = new ConstantDomain<>(); + copy.kind = this.kind; + copy.value = this.value; + return copy; + } + + @Override + public String toString() { + return switch (kind) { + case BOT -> "⊥"; + case TOP -> "⊤"; + case VAL -> value.toString(); + default -> throw new IllegalStateException("Unexpected value: " + kind); + }; + } + + private boolean equals(ConstantDomain other) { + if (isBot() && other.isBot()) { + return true; + } + if (isTop() && other.isTop()) { + return true; + } + if (isValue() && other.isValue()) { + return value.equals(other.value); + } + return false; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/CountDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/CountDomain.java new file mode 100644 index 000000000000..adcd6eebb2f1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/CountDomain.java @@ -0,0 +1,121 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * Represents a basic counting domain with a bounded maximum value. + * The value can be incremented and decremented. + */ +public final class CountDomain implements AbstractDomain { + + private int value; + private final int maxValue; + + public CountDomain(int maxValue) { + this.value = 0; + this.maxValue = maxValue; + } + + public CountDomain(int value, int maxValue) { + this.value = Math.min(value, maxValue); + this.maxValue = maxValue; + } + + public int getValue() { + return value; + } + + public int getMaxValue() { + return maxValue; + } + + public void increment() { + if (value < maxValue) { + value++; + } + } + + public void decrement() { + if (value > 0) { + value--; + } + } + + public CountDomain getIncremented() { + if (value < maxValue) { + return new CountDomain(value + 1, maxValue); + } + return new CountDomain(value, maxValue); + } + + public CountDomain getDecremented() { + if (value > 0) { + return new CountDomain(value - 1, maxValue); + } + return new CountDomain(value, maxValue); + } + + @Override + public boolean isBot() { + return value == 0; + } + + @Override + public boolean isTop() { + return value == maxValue; + } + + @Override + public boolean leq(CountDomain other) { + return this.value <= other.value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CountDomain that = (CountDomain) o; + return value == that.value && maxValue == that.maxValue; + } + + @Override + public int hashCode() { + return Objects.hash(value, maxValue); + } + + @Override + public void setToBot() { + this.value = 0; + } + + @Override + public void setToTop() { + this.value = maxValue; + } + + @Override + public void joinWith(CountDomain other) { + this.value = Math.max(this.value, other.value); + } + + @Override + public void widenWith(CountDomain other) { + joinWith(other); + } + + @Override + public void meetWith(CountDomain other) { + this.value = Math.min(this.value, other.value); + } + + @Override + public String toString() { + return "CountDomain{" + "value = " + value + '}'; + } + + @Override + public CountDomain copyOf() { + return new CountDomain(this.value, this.maxValue); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/DownwardCountDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/DownwardCountDomain.java new file mode 100644 index 000000000000..58a0fa7566c5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/DownwardCountDomain.java @@ -0,0 +1,103 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * Represents a bounded counting domain with a non-negative count and a bounded maximum value. + * The difference between CountDomain is that join is implemented as a minimum and the top value is zero. + */ +public final class DownwardCountDomain implements AbstractDomain { + + private int value; + private final int maxCount; + + public DownwardCountDomain(int maxCount) { + this.value = maxCount; + this.maxCount = maxCount; + } + + public DownwardCountDomain(int value, int maxCount) { + this.value = Math.min(value, maxCount); + this.maxCount = maxCount; + } + + public int getValue() { + return value; + } + + public void increment() { + if (value < maxCount) { + value++; + } + } + + public void decrement() { + if (value > 0) { + value--; + } + } + + @Override + public boolean isBot() { + return value == maxCount; + } + + @Override + public boolean isTop() { + return value == 0; + } + + @Override + public boolean leq(DownwardCountDomain other) { + return this.value >= other.value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + DownwardCountDomain that = (DownwardCountDomain) o; + return value == that.value && maxCount == that.maxCount; + } + + @Override + public int hashCode() { + return Objects.hash(value, maxCount); + } + + @Override + public void setToBot() { + this.value = maxCount; + } + + @Override + public void setToTop() { + this.value = 0; + } + + @Override + public void joinWith(DownwardCountDomain other) { + this.value = Math.min(this.value, other.value); + } + + @Override + public void widenWith(DownwardCountDomain other) { + joinWith(other); + } + + @Override + public void meetWith(DownwardCountDomain other) { + this.value = Math.max(this.value, other.value); + } + + @Override + public String toString() { + return "DownwardCountDomain{" + "value=" + value + ", maxCount=" + maxCount + '}'; + } + + @Override + public DownwardCountDomain copyOf() { + return new DownwardCountDomain(this.value, this.maxCount); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/EmptyDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/EmptyDomain.java new file mode 100644 index 000000000000..240f11592658 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/EmptyDomain.java @@ -0,0 +1,74 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Represents an empty {@link AbstractDomain}. + * Mostly used for testing purposes. + * This class is used as a placeholder for an abstract domain that does not contain any elements. + * It is useful in scenarios where the framework requires an abstract domain, but no actual + * domain-specific logic is needed. + */ +public final class EmptyDomain implements AbstractDomain { + + public EmptyDomain() { + } + + public EmptyDomain(EmptyDomain other) { + } + + @Override + public EmptyDomain copyOf() { + return new EmptyDomain(); + } + + @Override + public boolean isBot() { + return true; + } + + @Override + public boolean isTop() { + return true; + } + + @Override + public boolean leq(EmptyDomain other) { + return true; + } + + @Override + public boolean equals(Object other) { + return false; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public void setToBot() { + } + + @Override + public void setToTop() { + } + + @Override + public void joinWith(EmptyDomain other) { + } + + @Override + public void widenWith(EmptyDomain other) { + } + + @Override + public void meetWith(EmptyDomain other) { + } + + @Override + public String toString() { + return "Empty"; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/FiniteDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/FiniteDomain.java new file mode 100644 index 000000000000..466a0d75ac46 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/FiniteDomain.java @@ -0,0 +1,125 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; + +import java.util.Objects; + +/** + * A generic wrapper class for handling domains with finitely many states. + * For example, SignDomain, ParityDomain, etc. + * + * @param the type of the finite state + */ +public abstract class FiniteDomain implements AbstractDomain> { + private State state; + private AbstractValueKind kind; + + public FiniteDomain(State initialState, AbstractValueKind initialKind) { + this.state = initialState; + this.kind = initialKind; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public AbstractValueKind getKind() { + return kind; + } + + public void setKind(AbstractValueKind kind) { + this.kind = kind; + } + + @Override + public boolean isBot() { + return kind == AbstractValueKind.BOT; + } + + @Override + public boolean isTop() { + return kind == AbstractValueKind.TOP; + } + + @Override + public boolean leq(FiniteDomain other) { + if (isBot()) return true; + if (other.isBot()) return false; + if (other.isTop()) return true; + if (isTop()) return false; + return state.equals(other.state); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + FiniteDomain that = (FiniteDomain) o; + return Objects.equals(state, that.state) && kind == that.kind; + } + + @Override + public int hashCode() { + return Objects.hash(state, kind); + } + + @Override + public void setToBot() { + kind = AbstractValueKind.BOT; + } + + @Override + public void setToTop() { + kind = AbstractValueKind.TOP; + } + + @Override + public void joinWith(FiniteDomain other) { + if (isTop() || other.isBot()) return; + if (other.isTop()) { + setToTop(); + return; + } + if (isBot()) { + state = other.state; + kind = other.kind; + return; + } + kind = state.equals(other.state) ? AbstractValueKind.VAL : AbstractValueKind.TOP; + } + + @Override + public void widenWith(FiniteDomain other) { + joinWith(other); + } + + @Override + public void meetWith(FiniteDomain other) { + if (isBot() || other.isTop()) return; + if (other.isBot()) { + setToBot(); + return; + } + if (isTop()) { + state = other.state; + kind = other.kind; + return; + } + kind = state.equals(other.state) ? AbstractValueKind.VAL : AbstractValueKind.BOT; + } + + @Override + public String toString() { + return "FiniteDomain{" + + "state=" + state + + ", kind=" + kind + + '}'; + } + + @Override + public abstract FiniteDomain copyOf(); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/InvertedDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/InvertedDomain.java new file mode 100644 index 000000000000..e40236754a84 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/InvertedDomain.java @@ -0,0 +1,77 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.Objects; + +/** + * Reverse Adaptor of an {@link AbstractDomain} + * Reverses the top and bottom elements of an abstract domain + * and also reverses meet and join operation + * NOTE: Our framework doesn't use narrowing ( yet ) so we don't have a counterpart for widening. + * We can overcome this obstacle by implementing widening as a meet operation. But this is not ideal, + * since programs that do not terminate will not be able to use this domain + * + the fixpoint computation may be much slower on programs that use loops. + */ +public record InvertedDomain>(Domain domain) + implements AbstractDomain> { + + @Override + public boolean isBot() { + return domain.isTop(); + } + + @Override + public boolean isTop() { + return domain.isBot(); + } + + @Override + public boolean leq(InvertedDomain other) { + return domain.equals(other.domain) || !domain.leq(other.domain); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + InvertedDomain that = (InvertedDomain) o; + return Objects.equals(domain, that.domain); + } + + @Override + public void setToBot() { + domain.setToTop(); + } + + @Override + public void setToTop() { + domain.setToBot(); + } + + @Override + public void joinWith(InvertedDomain other) { + domain.meetWith(other.domain()); + } + + @Override + public void widenWith(InvertedDomain other) { + domain.meetWith(other.domain()); + } + + @Override + public void meetWith(InvertedDomain other) { + domain.joinWith(other.domain()); + } + + @Override + public String toString() { + return "InvertedDomain{" + + "domain=" + domain + + '}'; + } + + @Override + public InvertedDomain copyOf() { + return new InvertedDomain<>(domain.copyOf()); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/LatticeDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/LatticeDomain.java new file mode 100644 index 000000000000..edffa7e6d72e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/LatticeDomain.java @@ -0,0 +1,195 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValue; +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * LatticeDomain provides basic logic for handling operations + * on abstract domains that are lattices by definition + * Sample usage: + *

+ * public final class CustomAbstractValue extends AbstractValue {} + *

+ * public final class CustomAbstractDomain extends LatticeDomain {} + *

+ * This way, we only have to implement specific methods for a more complicated abstract domain + * without writing boilerplate code for methods enforced by {@link AbstractDomain} + * + * @param the type of derived {@link AbstractValue} + * @param the type of derived {@link AbstractDomain} + */ +public class LatticeDomain< + Value extends AbstractValue, + Domain extends LatticeDomain> + implements AbstractDomain { + + private AbstractValueKind kind; + private Value value; + + /** + * Supply the value directly, and deduce the kind from the value + * + * @param valueSupplier the supplier of the value + */ + public LatticeDomain(Supplier valueSupplier) { + this.value = valueSupplier.get(); + this.kind = value.getKind(); + } + + /** + * Supply the kind and the value + * + * @param kind the kind of the domain + * @param valueSupplier the supplier of the value + */ + public LatticeDomain(AbstractValueKind kind, Supplier valueSupplier) { + this.kind = kind; + this.value = valueSupplier.get(); + } + + public Value getValue() { + return value; + } + + public AbstractValueKind getKind() { + return kind; + } + + public boolean isBot() { + return kind == AbstractValueKind.BOT; + } + + public boolean isTop() { + return kind == AbstractValueKind.TOP; + } + + public boolean isVal() { + return kind == AbstractValueKind.VAL; + } + + public void setToBot() { + kind = AbstractValueKind.BOT; + value.clear(); + } + + public void setToTop() { + kind = AbstractValueKind.TOP; + value.clear(); + } + + public boolean leq(Domain other) { + if (isBot()) return true; + if (other.isBot()) return false; + if (other.isTop()) return true; + if (isTop()) return false; + checkKind(); + return value.leq(other.getValue()); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + LatticeDomain that = (LatticeDomain) o; + return kind == that.kind && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(kind, value); + } + + public void joinWith(Domain other) { + performJoinLikeOperation(other, () -> kind = value.joinWith(other.getValue())); + } + + public void widenWith(Domain other) { + performJoinLikeOperation(other, () -> kind = value.widenWith(other.getValue())); + } + + public void meetWith(Domain other) { + performMeetOperation(other, () -> kind = value.meetWith(other.getValue())); + } + + /* Wrapper method for providing logic to handle join or widen operations */ + protected void performJoinLikeOperation(Domain other, Runnable operation) { + if (isTop() || other.isBot()) { + return; + } + + if (other.isTop()) { + setToTop(); + return; + } + if (isBot()) { + kind = other.getKind(); + value = other.getValue(); + return; + } + operation.run(); + updateKind(); + } + + protected void performMeetOperation(Domain other, Runnable operation) { + if (isBot() || other.isTop()) { + return; + } + + if (other.isBot()) { + setToBot(); + return; + } + if (isTop()) { + kind = other.getKind(); + value = other.getValue(); + return; + } + operation.run(); + updateKind(); + } + + protected void setValue(Value value) { + this.kind = value.getKind(); + this.value = value.copyOf(); + updateKind(); + } + + /** + * NOTE: + * This analysisMethod is used for keeping the kind in a consistent state after performing operations + * Use this in the derived domain in every analysisMethod that somehow modifies the internal state + */ + protected void updateKind() { + kind = value.getKind(); + + if (kind == AbstractValueKind.BOT) { + return; + } + if (kind == AbstractValueKind.TOP) { + value.clear(); + } + } + + private void checkKind() { + if (kind != AbstractValueKind.VAL) { + throw new IllegalStateException("Invalid kind for operation"); + } + } + + @Override + public String toString() { + return "LatticeDomain{" + + "value=" + value + + ", kind=" + kind + + '}'; + } + + /* Should be implemented by the derived classes */ + @Override + public Domain copyOf() { + return null; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/MapDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/MapDomain.java new file mode 100644 index 000000000000..6b7c617b0846 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/MapDomain.java @@ -0,0 +1,111 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.domain.value.MapValue; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * This abstract domain maps elements (variables, memory locations, etc.) to a common + * abstract domain. + * One example could be mapping variables to intervals, signs, etc. + * to minimize the size of the used map, + * if a Key is not present, we return TOP value of the {@link AbstractDomain} + */ +public abstract class MapDomain< + Key, + Domain extends AbstractDomain, + Self extends MapDomain> + extends LatticeDomain, Self> { + + private final Domain initialDomain; + + public MapDomain(Domain initialDomain) { + super(() -> new MapValue<>(initialDomain)); + this.initialDomain = initialDomain; + } + + @SuppressWarnings("this-escape") + public MapDomain(Map map, Domain initialDomain) { + super(() -> new MapValue<>(initialDomain)); + this.initialDomain = initialDomain.copyOf(); + map.forEach(this::put); + } + + public MapDomain(MapDomain other) { + super(other.getKind(), () -> new MapValue<>(other.getValue())); + this.initialDomain = other.initialDomain.copyOf(); + } + + public Domain get(Key key) { + return getValue().getDomainAtKey(key); + } + + public void removeIf(Predicate> predicate) { + getValue().removeIf(predicate); + updateKind(); + } + + public void transform(Function function) { + getValue().transform(function); + updateKind(); + } + + public void update(Function function, Key key) { + getValue().update(function, key); + updateKind(); + } + + public void put(Key key, Domain value) { + getValue().insertOrAssign(key, value); + updateKind(); + } + + public void remove(Key key) { + getValue().remove(key); + updateKind(); + } + + public void removeAllKeys() { + getValue().clear(); + updateKind(); + } + + public void eraseAllMatching(Key key) { + getValue().eraseAllMatching(key); + updateKind(); + } + + public void unionWith(MapDomain other) { + getValue().unionWith(Domain::join, other.getValue()); + updateKind(); + } + + public void intersectionWith(MapDomain other) { + getValue().intersectionWith(Domain::meet, other.getValue()); + updateKind(); + } + + public void differenceWith(BiFunction combine, MapValue other) { + getValue().differenceWith(combine, other); + updateKind(); + } + + public int getSize() { + return getValue().getSize(); + } + + @Override + public String toString() { + return "map: " + getValue().toString() + + System.lineSeparator() + + "kind: " + getKind(); + } + + /* NOTE: implement this in derived classes */ + @Override + public abstract Self copyOf(); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ParityDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ParityDomain.java new file mode 100644 index 000000000000..bae6bc8aec19 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/ParityDomain.java @@ -0,0 +1,27 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.value.AbstractValueKind; +import com.oracle.svm.hosted.analysis.ai.domain.value.Parity; + +/** + * Abstract domain representing the parity of a value. + */ +public final class ParityDomain extends FiniteDomain { + + public ParityDomain() { + super(Parity.BOT, AbstractValueKind.BOT); + } + + public ParityDomain(Parity parity) { + super(parity, parity == Parity.BOT ? AbstractValueKind.BOT : parity == Parity.TOP ? AbstractValueKind.TOP : AbstractValueKind.VAL); + } + + public ParityDomain(ParityDomain ParityDomain) { + super(ParityDomain.getState(), ParityDomain.getKind()); + } + + @Override + public ParityDomain copyOf() { + return new ParityDomain(this); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/SetDomain.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/SetDomain.java new file mode 100644 index 000000000000..7bce6c562340 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/util/SetDomain.java @@ -0,0 +1,83 @@ +package com.oracle.svm.hosted.analysis.ai.domain.util; + +import com.oracle.svm.hosted.analysis.ai.domain.value.SetValue; + +import java.util.Set; +import java.util.function.Predicate; + +/* + This abstract domain represents a set of elements. + For example the domain could represent set of live variables + */ +public final class SetDomain extends LatticeDomain, SetDomain> { + + public SetDomain() { + super(SetValue::new); + } + + public SetDomain(SetDomain other) { + super(() -> new SetValue<>(other.getValue())); + } + + public void add(Element element) { + getValue().add(element); + updateKind(); + } + + public void remove(Element element) { + getValue().remove(element); + updateKind(); + } + + public void removeIf(Predicate predicate) { + getValue().getSet().removeIf(predicate); + updateKind(); + } + + public void clear() { + getValue().clear(); + updateKind(); + } + + public boolean empty() { + return getValue().empty(); + } + + public int getSize() { + return getValue().getSize(); + } + + public Set getSet() { + return getValue().getSet(); + } + + public void filter(Predicate predicate) { + getValue().removeIf(predicate); + updateKind(); + } + + public void unionWith(SetDomain other) { + getValue().unionWith(other.getValue()); + updateKind(); + } + + public void intersectionWith(SetDomain other) { + getValue().intersectionWith(other.getValue()); + updateKind(); + } + + public void differenceWith(SetDomain other) { + getValue().differenceWith(other.getValue()); + updateKind(); + } + + @Override + public String toString() { + return getValue().toString(); + } + + @Override + public SetDomain copyOf() { + return new SetDomain<>(this); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValue.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValue.java new file mode 100644 index 000000000000..fe92e6a207d1 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValue.java @@ -0,0 +1,77 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +/** + * Represents the structure of abstract contexts inside from an abstract domain. + * This can be used for easier implementation of complex abstract domains. + * + * @param the type of the derived {@link AbstractValue} + */ +public interface AbstractValue> { + + /** + * Returns the kind of this abstract value. + * + * @return the kind of the value (TOP, BOT, or VAL) + */ + AbstractValueKind getKind(); + + /** + * Checks if this value is less than or equal to another value. + * + * @param other the other value to compare with + * @return true if this value is less than or equal to the other value, false otherwise + */ + boolean leq(Derived other); + + /** + * Checks if this value is equal to another value. + * + * @param other the other value to compare with + * @return true if this value is equal to the other value, false otherwise + */ + boolean equals(Object other); + + /** + * Joins this value with another value. + * + * @param other the other value to join with + * @return the kind of AbstractValue resulted from this operation + */ + AbstractValueKind joinWith(Derived other); + + /** + * Widens this value with another value. + * + * @param other the other value to widen with + * @return the kind of AbstractValue resulted from this operation + */ + AbstractValueKind widenWith(Derived other); + + /** + * Meets this value with another value. + * + * @param other the other value to meet with + * @return the kind of AbstractValue resulted from this operation + */ + AbstractValueKind meetWith(Derived other); + + /** + * Returns a string representation of this value. + * + * @return a string representation of this value + */ + String toString(); + + /** + * Some abstract values require a lot of memory to store their state. + * This analysisMethod can be used to clear the memory and reset the value to a default state. + */ + void clear(); + + /** + * Creates a copy of this abstract value. + * + * @return a copy of this abstract value + */ + Derived copyOf(); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValueKind.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValueKind.java new file mode 100644 index 000000000000..e07521fa375a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/AbstractValueKind.java @@ -0,0 +1,21 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +/** + * Encoding of abstract value in an abstract domain. + * This is done for easier implementation of operations on abstract values. + */ +public enum AbstractValueKind { + + TOP, /* The top of the 'lattice' or more generally the maximal element */ + VAL, /* Every element that is not the top or the bottom */ + BOT; /* The bottom of the 'lattice' or more generally the minimal element */ + + @Override + public String toString() { + return switch (this) { + case TOP -> "⊤"; + case VAL -> "VAL"; + case BOT -> "⊥"; + }; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/MapValue.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/MapValue.java new file mode 100644 index 000000000000..8ace22c745c7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/MapValue.java @@ -0,0 +1,180 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Represents an AbstractValue that maps keys to a common abstract domain + * + * @param type of the Key + * @param type of the derived {@link AbstractDomain} + */ +public final class MapValue< + Key, + Domain extends AbstractDomain> + implements AbstractValue> { + + private final HashMap map; + private final Domain initialDomain; + + public MapValue(Domain initialDomain) { + this.map = new HashMap<>(); + this.initialDomain = initialDomain; + } + + public MapValue(MapValue other) { + this.map = new HashMap<>(other.map); + this.initialDomain = other.initialDomain; + } + + public MapValue(Map other, Domain initialDomain) { + this.map = new HashMap<>(other); + this.initialDomain = initialDomain; + } + + public Map getMap() { + return map; + } + + @Override + public AbstractValueKind getKind() { + return map.isEmpty() ? AbstractValueKind.BOT : AbstractValueKind.VAL; + } + + @Override + public boolean leq(MapValue other) { + for (Map.Entry entry : map.entrySet()) { + if (!entry.getValue().leq(other.map.getOrDefault(entry.getKey(), AbstractDomain.createTop(initialDomain)))) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + MapValue mapValue = (MapValue) o; + return Objects.equals(map, mapValue.map) && Objects.equals(initialDomain, mapValue.initialDomain); + } + + @Override + public int hashCode() { + return Objects.hash(map, initialDomain); + } + + @Override + public AbstractValueKind joinWith(MapValue other) { + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), Domain::join); + } + return getKind(); + } + + @Override + public AbstractValueKind widenWith(MapValue other) { + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), Domain::widen); + } + return getKind(); + } + + @Override + public AbstractValueKind meetWith(MapValue other) { + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), Domain::meet); + } + return getKind(); + } + + @Override + public String toString() { + if (map.isEmpty()) { + return "{}"; + } + + return map.entrySet() + .stream() + .map(entry -> entry.getKey() + " : " + entry.getValue()) + .collect(Collectors.joining("\n", "{\n", "\n}")); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public MapValue copyOf() { + return new MapValue<>(map, initialDomain); + } + + public boolean empty() { + return map.isEmpty(); + } + + public int getSize() { + return map.size(); + } + + /* Return TOP value of the {@link AbstractDomain}, when the key is not found */ + public Domain getDomainAtKey(Key key) { + return map.getOrDefault(key, AbstractDomain.createTop(initialDomain)); + } + + public void insertOrAssign(Key key, Domain value) { + map.put(key, value); + } + + public void remove(Key key) { + map.remove(key); + } + + public void visit(Function, Void> visitor) { + for (Map.Entry entry : map.entrySet()) { + visitor.apply(entry); + } + } + + public void removeIf(Predicate> predicate) { + map.entrySet().removeIf(entry -> !predicate.test(entry)); + } + + public void eraseAllMatching(Key key) { + map.keySet().removeIf(k -> (k.hashCode() & key.hashCode()) != 0); + } + + public void update(Function operation, Key key) { + map.put(key, operation.apply(map.get(key))); + } + + public void transform(Function function) { + map.replaceAll((k, v) -> function.apply(v)); + } + + public void unionWith(BiFunction combine, MapValue other) { + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), combine); + } + } + + public void intersectionWith(BiFunction combine, MapValue other) { + map.entrySet().removeIf(entry -> !other.map.containsKey(entry.getKey())); + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), combine); + } + } + + public void differenceWith(BiFunction combine, MapValue other) { + for (Map.Entry entry : other.map.entrySet()) { + map.merge(entry.getKey(), entry.getValue(), combine); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Parity.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Parity.java new file mode 100644 index 000000000000..16dc6934b0a4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Parity.java @@ -0,0 +1,12 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +/** + * Represents the parity of a value in the abstract domain. + */ +public enum Parity { + + TOP, + ODD, + EVEN, + BOT +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/SetValue.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/SetValue.java new file mode 100644 index 000000000000..fc81f368cc68 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/SetValue.java @@ -0,0 +1,118 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Represents an AbstractValue that contains a set of elements + * + * @param type of the elements in the set + */ +public final class SetValue implements AbstractValue> { + + private final HashSet set; + + public SetValue() { + this.set = new HashSet<>(); + } + + public SetValue(Set set) { + this.set = new HashSet<>(set); + } + + public SetValue(SetValue other) { + this.set = new HashSet<>(other.set); + } + + public Set getSet() { + return set; + } + + @Override + public AbstractValueKind getKind() { + return set.isEmpty() ? AbstractValueKind.BOT : AbstractValueKind.VAL; + } + + @Override + public boolean leq(SetValue other) { + return other.set.containsAll(set); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SetValue setValue = (SetValue) o; + return Objects.equals(set, setValue.set); + } + + @Override + public int hashCode() { + return Objects.hashCode(set); + } + + @Override + public AbstractValueKind joinWith(SetValue other) { + set.addAll(other.set); + return getKind(); + } + + @Override + public AbstractValueKind widenWith(SetValue other) { + return joinWith(other); + } + + @Override + public AbstractValueKind meetWith(SetValue other) { + set.retainAll(other.set); + return getKind(); + } + + @Override + public String toString() { + return set.toString(); + } + + @Override + public void clear() { + set.clear(); + } + + @Override + public SetValue copyOf() { + return new SetValue<>(this); + } + + public boolean empty() { + return set.isEmpty(); + } + + public int getSize() { + return set.size(); + } + + public void add(Element element) { + set.add(element); + } + + public void remove(Element element) { + set.remove(element); + } + + public void removeIf(Predicate predicate) { + set.removeIf(predicate.negate()); + } + + public void unionWith(SetValue other) { + set.addAll(other.set); + } + + public void intersectionWith(SetValue other) { + set.retainAll(other.set); + } + + public void differenceWith(SetValue other) { + set.removeAll(other.set); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Sign.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Sign.java new file mode 100644 index 000000000000..0f7b7b0600c4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/domain/value/Sign.java @@ -0,0 +1,42 @@ +package com.oracle.svm.hosted.analysis.ai.domain.value; + +/** + * Represents the sign of a value in the abstract domain. + */ +public enum Sign { + + TOP, + POS, + ZERO, + NEG, + BOT; + + public Sign plus(Sign other) { + if (this == BOT || other == BOT) return BOT; + if (this == TOP || other == TOP) return TOP; + if (this == ZERO) return other; + if (other == ZERO) return this; + if (this == POS && other == POS) return POS; + if (this == NEG && other == NEG) return NEG; + return TOP; + } + + public Sign minus(Sign other) { + if (this == BOT || other == BOT) return BOT; + if (this == TOP || other == TOP) return TOP; + if (this == ZERO) return other.negate(); + if (other == ZERO) return this; + if (this == POS && other == POS) return TOP; + if (this == NEG && other == POS) return NEG; + if (this == POS && other == NEG) return POS; + return TOP; + } + + public Sign negate() { + if (this == BOT) return BOT; + if (this == TOP) return TOP; + if (this == ZERO) return ZERO; + if (this == POS) return NEG; + return POS; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/exception/AbstractInterpretationException.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/exception/AbstractInterpretationException.java new file mode 100644 index 000000000000..1f565fa5dd32 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/exception/AbstractInterpretationException.java @@ -0,0 +1,88 @@ +package com.oracle.svm.hosted.analysis.ai.exception; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import jdk.graal.compiler.graph.Node; + +import java.io.Serial; + +public class AbstractInterpretationException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + + AbstractInterpretationException(String message) { + super(message); + } + + AbstractInterpretationException(String message, Throwable cause) { + super(message, cause); + } + + AbstractInterpretationException(Throwable ex) { + super(ex); + } + + public static void exceededWideningThreshold(Node node, AnalysisMethod method) { + throw new WideningThresholdExceededException(("Widen iteration threshold exceeded for node: " + node + ", in method: " + method.getName() + " please check the provided widening operator/abstract interpreter")); + } + + public static void analysisMethodGraphNotFound(AnalysisMethod method) { + throw new AnalysisMethodGraphUnavailableException(("The graph of analysis method: " + method.getQualifiedName() + " could not be found during abstract interpretation")); + } + + public static void graphVerifyFailed(String description, AnalysisMethod method) { + throw new GraphVerifyFailedException("[analysisMethod: " + method.getQualifiedName() + "] graph.verify() failed after performing fact applier: " + description); + } + + public static class WideningThresholdExceededException extends AbstractInterpretationException { + @Serial + private static final long serialVersionUID = 1L; + + WideningThresholdExceededException(String message) { + super(message); + } + + WideningThresholdExceededException(String message, Throwable cause) { + super(message, cause); + } + + WideningThresholdExceededException(Throwable ex) { + super(ex); + } + } + + public static class AnalysisMethodGraphUnavailableException extends AbstractInterpretationException { + @Serial + private static final long serialVersionUID = 1L; + + AnalysisMethodGraphUnavailableException(String message) { + super(message); + } + + AnalysisMethodGraphUnavailableException(String message, Throwable cause) { + super(message, cause); + } + + AnalysisMethodGraphUnavailableException(Throwable ex) { + super(ex); + } + } + + public static class GraphVerifyFailedException extends AbstractInterpretationException { + @Serial + private static final long serialVersionUID = 1L; + + GraphVerifyFailedException(String message) { + super(message); + } + + GraphVerifyFailedException(String message, Throwable cause) { + super(message, cause); + } + + GraphVerifyFailedException(Throwable ex) { + super(ex); + } + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/BasicIteratorContext.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/BasicIteratorContext.java new file mode 100644 index 000000000000..4aa3dd504998 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/BasicIteratorContext.java @@ -0,0 +1,314 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.GraphTraversalHelper; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.LoopBeginNode; +import jdk.graal.compiler.nodes.LoopEndNode; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraph; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Basic implementation of {@link IteratorContext} that tracks fixpoint iteration state + * and provides context information to abstract interpreters. + *

+ * This class is mutable and should be updated by the fixpoint iterator as it progresses. + */ +public final class BasicIteratorContext implements IteratorContext { + + private final GraphTraversalHelper graphTraversalHelper; + private final Map loopIterationCounts; + private final Map nodeVisitCounts; + AnalysisMethod currentAnalysisMethod; + private AbstractState abstractState; + private HIRBlock previousBlock; + private HIRBlock currentBlock; + private IteratorPhase currentPhase; + private boolean hasConverged; + private int globalIterationCount; + private String callContextSignature; + + public BasicIteratorContext(GraphTraversalHelper graphTraversalHelper, AnalysisMethod currentAnalysisMethod) { + this.graphTraversalHelper = graphTraversalHelper; + this.loopIterationCounts = new HashMap<>(); + this.nodeVisitCounts = new HashMap<>(); + this.currentAnalysisMethod = currentAnalysisMethod; + this.globalIterationCount = 0; + this.currentPhase = IteratorPhase.ASCENDING; + this.hasConverged = false; + this.previousBlock = null; + this.abstractState = null; + this.callContextSignature = ""; + } + + @Override + public Node getPredecessor(Node node, int index) { + if (graphTraversalHelper == null) { + return null; + } + HIRBlock block = getBlockForNode(node); + if (block == null || index >= graphTraversalHelper.getPredecessorCount(block)) { + return null; + } + HIRBlock predBlock = graphTraversalHelper.getPredecessorAt(block, index); + return graphTraversalHelper.getEndNode(predBlock); + } + + @Override + public List getPredecessors(Node node) { + if (graphTraversalHelper == null) { + return List.of(); + } + HIRBlock block = getBlockForNode(node); + if (block == null) { + return List.of(); + } + List predecessors = new ArrayList<>(); + for (int i = 0; i < graphTraversalHelper.getPredecessorCount(block); i++) { + HIRBlock predBlock = graphTraversalHelper.getPredecessorAt(block, i); + predecessors.add(graphTraversalHelper.getEndNode(predBlock)); + } + return predecessors; + } + + @Override + public int getPredecessorCount(Node node) { + HIRBlock block = getBlockForNode(node); + return block != null ? graphTraversalHelper.getPredecessorCount(block) : 0; + } + + @Override + public boolean isLoopHeader(Node node) { + return node instanceof LoopBeginNode; + } + + @Override + public boolean isBackEdge(Node source, Node target) { + if (!(target instanceof LoopBeginNode)) { + return false; + } + // A back-edge is from a LoopEndNode to a LoopBeginNode + return source instanceof LoopEndNode loopEnd && + loopEnd.loopBegin() == target; + } + + @Override + public int getGlobalIterationCount() { + return globalIterationCount; + } + + @Override + public int getLoopIterationCount(Node loopHeader) { + if (!isLoopHeader(loopHeader)) { + return 0; + } + return loopIterationCounts.getOrDefault(loopHeader, 0); + } + + @Override + public boolean hasConverged() { + return hasConverged; + } + + @Override + public boolean isWideningPhase() { + return currentPhase == IteratorPhase.WIDENING; + } + + @Override + public boolean isNarrowingPhase() { + return currentPhase == IteratorPhase.NARROWING; + } + + @Override + public IteratorPhase getCurrentPhase() { + return currentPhase; + } + + @Override + public HIRBlock getCurrentBlock() { + return currentBlock; + } + + @Override + public HIRBlock getBlockForNode(Node node) { + if (graphTraversalHelper == null) { + return null; + } + // Search through all blocks to find the one containing this node + ControlFlowGraph cfg = graphTraversalHelper.getCfgGraph(); + if (cfg == null) { + return null; + } + for (HIRBlock block : cfg.getBlocks()) { + for (Node blockNode : block.getNodes()) { + if (blockNode == node) { + return block; + } + } + } + return null; + } + + @Override + public boolean isFirstVisit(Node node) { + return nodeVisitCounts.getOrDefault(node, 0) == 0; + } + + // === Mutation methods for the fixpoint iterator === + + /** + * Increment the global iteration counter. + * Should be called by the fixpoint iterator at the start of each global iteration. + */ + public void incrementGlobalIteration() { + globalIterationCount++; + } + + /** + * Increment the loop iteration counter for a specific loop header. + * Should be called when re-analyzing a loop. + * + * @param loopHeader The loop header node + */ + public void incrementLoopIteration(Node loopHeader) { + if (isLoopHeader(loopHeader)) { + loopIterationCounts.merge(loopHeader, 1, Integer::sum); + } + } + + public int getNodeVisitCount(Node node) { + return nodeVisitCounts.getOrDefault(node, 0); + } + + public void incrementNodeVisitCount(Node node) { + nodeVisitCounts.merge(node, 1, Integer::sum); + } + + public void resetNodeVisitCount(Node node) { + nodeVisitCounts.put(node, 0); + } + + public void setCurrentBlock(HIRBlock block) { + this.previousBlock = this.currentBlock; + this.currentBlock = block; + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log(String.format("Context: previous block set to %s, current block set to %s", + previousBlock, currentBlock), LoggerVerbosity.DEBUG); + } + + /** + * Update context when traversing an edge from source block to target block. + * This ensures previousBlock is correctly set to the source when analyzing the target. + * + * @param sourceBlock The block we're coming from + * @param targetBlock The block we're going to + */ + public void setEdgeTraversal(HIRBlock sourceBlock, HIRBlock targetBlock) { + this.previousBlock = sourceBlock; + this.currentBlock = targetBlock; + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log(String.format("Edge traversal: %s -> %s", sourceBlock, targetBlock), LoggerVerbosity.DEBUG); + } + + public void setPhase(IteratorPhase phase) { + this.currentPhase = phase; + } + + public void setConverged(boolean converged) { + this.hasConverged = converged; + } + + public void reset() { + globalIterationCount = 0; + loopIterationCounts.clear(); + nodeVisitCounts.clear(); + currentBlock = null; + previousBlock = null; + currentPhase = IteratorPhase.ASCENDING; + hasConverged = false; + callContextSignature = ""; + } + + @Override + public HIRBlock getPreviousBlock() { + return previousBlock; + } + + @Override + public int getPreviousBlockIndex() { + if (previousBlock == null || currentBlock == null) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log(String.format("getPreviousBlockIndex: previousBlock=%s, currentBlock=%s -> returning -1", + previousBlock, currentBlock), LoggerVerbosity.DEBUG); + return -1; + } + + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + int predCount = graphTraversalHelper.getPredecessorCount(currentBlock); + logger.log(String.format("getPreviousBlockIndex: looking for %s among %d predecessors of %s", + previousBlock, predCount, currentBlock), LoggerVerbosity.DEBUG); + + for (int i = 0; i < predCount; i++) { + HIRBlock pred = graphTraversalHelper.getPredecessorAt(currentBlock, i); + logger.log(String.format(" Checking predecessor[%d]: %s (matches: %s)", + i, pred, pred == previousBlock), LoggerVerbosity.DEBUG); + if (pred == previousBlock) { + logger.log(String.format(" Found previousBlock at index %d", i), LoggerVerbosity.DEBUG); + return i; + } + } + + logger.log(String.format("getPreviousBlockIndex: previousBlock %s not found among predecessors -> returning -1", + previousBlock), LoggerVerbosity.DEBUG); + return -1; + } + + @Override + public GraphTraversalHelper getGraphTraversalHelper() { + return graphTraversalHelper; + } + + @Override + public String getCallContextSignature() { + return callContextSignature == null ? "" : callContextSignature; + } + + @Override + public void setCallContextSignature(String signature) { + this.callContextSignature = signature; + } + + @Override + public AnalysisMethod getCurrentAnalysisMethod() { + return currentAnalysisMethod; + } + + /** + * Set the abstract state reference. Called by the fixpoint iterator. + * + * @param abstractState The abstract state + */ + public void setAbstractState(AbstractState abstractState) { + this.abstractState = abstractState; + } + + /** + * Update the current block and remember the previous one. + * This should be called by the fixpoint iterator when moving to a new block. + * + * @param newBlock The new block being analyzed + */ + public void updateBlock(HIRBlock newBlock) { + this.previousBlock = this.currentBlock; + this.currentBlock = newBlock; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorContext.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorContext.java new file mode 100644 index 000000000000..dbb9ff4d0d5c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorContext.java @@ -0,0 +1,186 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.context; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.GraphTraversalHelper; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.util.List; +import java.util.Optional; + +/** + * Provides context information from the fixpoint iterator to the abstract interpreter. + * This allows interpreters to make more precise decisions based on the state of the + * fixpoint computation (e.g., which iteration, which predecessor, whether to widen, etc.). + *

+ * The context is optional - interpreters should handle null context gracefully + * and fall back to conservative behavior. + */ +public interface IteratorContext { + + /** + * Get the CFG predecessor at the given index for a merge/loop node. + * This is used for flow-sensitive phi node evaluation. + * + * @param node The merge or loop node (PhiNode owner) + * @param index The index of the predecessor + * @return The predecessor node at the given index, or null if not available + */ + Node getPredecessor(Node node, int index); + + /** + * Get all CFG predecessors of a node in order. + * The order corresponds to the order of phi inputs. + * + * @param node The merge or loop node + * @return List of predecessor nodes, possibly empty + */ + List getPredecessors(Node node); + + /** + * Get the number of CFG predecessors for a merge/loop node. + * + * @param node The merge or loop node + * @return The number of predecessors + */ + int getPredecessorCount(Node node); + + /** + * Check if the given node is a loop header (loop begin node). + * + * @param node The node to check + * @return true if the node is a loop header + */ + boolean isLoopHeader(Node node); + + /** + * Check if the edge from source to target is a loop back-edge. + * Back-edges are where widening is typically applied. + * + * @param source The source node of the edge + * @param target The target node of the edge + * @return true if this is a back-edge + */ + boolean isBackEdge(Node source, Node target); + + /** + * Get the current global iteration count of the fixpoint computation. + * Iteration 0 is the initial pass. + * + * @return The current iteration number + */ + int getGlobalIterationCount(); + + /** + * Get the iteration count for a specific loop header. + * This counts how many times the loop has been re-analyzed. + * + * @param loopHeader The loop header node + * @return The number of iterations for this loop, or 0 if not a loop header + */ + int getLoopIterationCount(Node loopHeader); + + /** + * Check if the analysis has stabilized (reached fixpoint). + * + * @return true if no changes occurred in the last iteration + */ + boolean hasConverged(); + + /** + * Check if we're in the widening phase. + * During widening, growing intervals are extrapolated to infinity. + * + * @return true if in widening phase + */ + boolean isWideningPhase(); + + /** + * Check if we're in the narrowing phase. + * During narrowing, intervals are refined downward using meet. + * + * @return true if in narrowing phase + */ + boolean isNarrowingPhase(); + + /** + * Get the current analysis phase name (for logging/debugging). + * + * @return A string describing the current phase (e.g., "ascending", "widening", "narrowing") + */ + IteratorPhase getCurrentPhase(); + + /** + * Get the current basic block being analyzed. + * + * @return The current HIRBlock, or null if not available + */ + HIRBlock getCurrentBlock(); + + /** + * Get the block containing the given node. + * + * @param node The node to look up + * @return The HIRBlock containing the node, or null if not available + */ + HIRBlock getBlockForNode(Node node); + + /** + * Check if this is the first visit to the given node. + * + * @param node The node to check + * @return true if this is the first visit + */ + boolean isFirstVisit(Node node); + + + /** + * Get the basic block analyzed prior to the current one. + * + * @return The previous HIRBlock, or null if not available + */ + HIRBlock getPreviousBlock(); + + int getPreviousBlockIndex(); + + /** + * Get the GraphTraversalHelper associated with this iterator context. + */ + GraphTraversalHelper getGraphTraversalHelper(); + + int getNodeVisitCount(Node node); + + void setEdgeTraversal(HIRBlock sourceBlock, HIRBlock targetBlock); + + /** + * Get a compact call-context signature (e.g., k-CFA call string) attached to this iterator + * instance by the invoke handler. + */ + String getCallContextSignature(); + + void setCallContextSignature(String signature); + + /** + * Decide whether the iterator wants the transformer to collect/merge CFG predecessors + * into the given node's pre-condition. This allows the iterator to centralize + * policy about seeding/extrapolation/widening (e.g. skip merging when a loop header + * already has an extrapolated pre-condition). + * + * @param node the node for which to decide a predecessor collection + * @return true if predecessors should be collected and merged, false to skip + */ + default boolean shouldCollectPredecessors(Node node) { + if (node == null) { + return true; + } + if (isLoopHeader(node)) { + int visitCount = getNodeVisitCount(node); + return visitCount == 0; + } + return true; + } + + AnalysisMethod getCurrentAnalysisMethod(); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorPhase.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorPhase.java new file mode 100644 index 000000000000..6e8547b254b9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/context/IteratorPhase.java @@ -0,0 +1,7 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.context; + +public enum IteratorPhase { + ASCENDING, /* Initial forward pass */ + WIDENING, /* Extrapolating growing values */ + NARROWING /* Refining values downward */ +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIterator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIterator.java new file mode 100644 index 000000000000..5486f6135bc4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIterator.java @@ -0,0 +1,54 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; + +/** + * API for a fixpoint iterator in abstract interpretation. + * A fixpoint iterator is responsible for computing abstract states (pre-condition and post-condition pairs) + * for each node in the Graal IR by iteratively applying transfer functions until a stable solution + * (fixpoint) is reached. + * + * @param the abstract domain used in the analysis + */ +public interface FixpointIterator> { + + /** + * Performs the fixpoint computation and returns the resulting {@link AbstractState} + * after the fixpoint is reached. + * + * @return the abstract state after the fixpoint is reached + */ + AbstractState doRunFixpointIteration(); + + default AbstractState runFixpointIteration() { + AbstractInterpretationServices.getInstance().markMethodTouched(getIteratorContext().getCurrentAnalysisMethod()); + return doRunFixpointIteration(); + } + + /** + * Seed the entry pre-condition and run the fixpoint. + * Equivalent to getAbstractState().setStartNodeState(initialEntryPre) followed by iterateUntilFixpoint(). + */ + default AbstractState runFixpointIteration(Domain initialEntryPre) { + getAbstractState().setStartNodeState(initialEntryPre); + return runFixpointIteration(); + } + + /** + * Returns the mutable abstract state managed by this iterator. + */ + AbstractState getAbstractState(); + + /** + * Clears the internal state of the iterator so it can be reused. + */ + void clear(); + + /** + * Returns the iterator context that provides CFG and analysis-phase information to interpreters. + */ + IteratorContext getIteratorContext(); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorBase.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorBase.java new file mode 100644 index 000000000000..66a4b5eb8591 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorBase.java @@ -0,0 +1,114 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.BasicIteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.IteratorPolicy; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.WideningOverflowPolicy; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractTransformer; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import com.oracle.svm.hosted.analysis.ai.exception.AbstractInterpretationException; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.StructuredGraph; + +/** + * Common functionality for fixpoint iterators. + */ +public abstract class FixpointIteratorBase> implements FixpointIterator { + + protected final Domain initialDomain; + protected final AbstractTransformer abstractTransformer; + protected final AnalysisContext analysisContext; + protected final StructuredGraph graph; + protected final AbstractState abstractState; + protected final AbstractInterpretationLogger logger; + protected final AnalysisMethod analysisMethod; + protected final GraphTraversalHelper graphTraversalHelper; + protected final BasicIteratorContext iteratorContext; + + protected FixpointIteratorBase(AnalysisMethod method, + Domain initialDomain, + AbstractTransformer abstractTransformer, + AnalysisContext analysisContext) { + + this.logger = AbstractInterpretationLogger.getInstance(); + this.analysisMethod = method; + this.initialDomain = initialDomain; + this.abstractTransformer = abstractTransformer; + this.analysisContext = analysisContext; + var methodGraphCache = analysisContext.getMethodGraphCache(); + if (methodGraphCache.containsMethodGraph(method)) { + this.graph = methodGraphCache.getMethodGraphMap().get(method); + } else { + var services = AbstractInterpretationServices.getInstance(); + this.graph = services.getGraph(method); + methodGraphCache.addToMethodGraphMap(method, graph); + } + + logger.log("Fixpoint iteration of method: " + analysisMethod.wrapped.format("%H.%n(%p)") ,LoggerVerbosity.CHECKER); + logger.exportGraphToJson(graph, analysisMethod, analysisMethod.getName() + "_before_absint"); + this.abstractState = new AbstractState<>(initialDomain, graph); + this.graphTraversalHelper = new GraphTraversalHelper(graph, analysisContext.getIteratorPolicy().direction()); + this.iteratorContext = new BasicIteratorContext(graphTraversalHelper, analysisMethod); + } + + @Override + public AbstractState getAbstractState() { + return abstractState; + } + + public IteratorContext getIteratorContext() { + return iteratorContext; + } + + @Override + public void clear() { + logger.log("Clearing the abstract state", LoggerVerbosity.DEBUG); + if (abstractState != null) abstractState.clear(); + if (iteratorContext != null) iteratorContext.reset(); + } + + protected void extrapolate(Node node) { + var state = abstractState.getNodeState(node); + int visitedAmount = iteratorContext.getNodeVisitCount(node); + + var newPre = abstractState.getPreCondition(node).copyOf(); + IteratorPolicy policy = analysisContext.getIteratorPolicy(); + + if (abstractState.getPostCondition(node).leq(abstractState.getPreCondition(node))) { + logger.log("Extrapolation skipped (post ⊑ pre) for node: " + node, LoggerVerbosity.DEBUG); + iteratorContext.incrementNodeVisitCount(node); + return; + } + + if (visitedAmount < policy.maxJoinIterations()) { + logger.log("Extrapolating (join) at visit " + visitedAmount + " for node: " + node, LoggerVerbosity.DEBUG); + newPre.joinWith(abstractState.getPostCondition(node)); + } else if (visitedAmount < policy.maxWidenIterations() + policy.maxJoinIterations()) { + logger.log("Extrapolating (widen) at visit " + visitedAmount + " for node: " + node, LoggerVerbosity.DEBUG); + newPre.widenWith(abstractState.getPostCondition(node)); + } else { + if (!newPre.isTop()) { + AbstractInterpretationException.exceededWideningThreshold(node, analysisMethod); + } + iteratorContext.setConverged(true); + } + + logger.log("After extrapolation: newPre = " + newPre, LoggerVerbosity.DEBUG); + abstractState.setPreCondition(node, newPre); + iteratorContext.incrementNodeVisitCount(node); + + int limit = policy.maxWidenIterations() + policy.maxJoinIterations(); + if (iteratorContext.getNodeVisitCount(node) > limit) { + logger.log("Extrapolation hard limit exceeded for node: " + node + ", leaving state as is", LoggerVerbosity.INFO); + if (policy.wideningOverflowPolicy() == WideningOverflowPolicy.SET_TO_TOP) { + state.getPreCondition().setToTop(); + } + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorFactory.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorFactory.java new file mode 100644 index 000000000000..36df0e6756f7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/FixpointIteratorFactory.java @@ -0,0 +1,23 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.IteratorStrategy; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractTransformer; + +/** + * Factory class for creating different types of fixpoint iterators. + */ +public final class FixpointIteratorFactory { + + public static > FixpointIterator createIterator(AnalysisMethod method, + Domain initialDomain, + AbstractTransformer abstractTransformer, + AnalysisContext analysisContext) { + assert method.getAnalyzedGraph() != null; + return switch (analysisContext.getIteratorPolicy().strategy()) { + case IteratorStrategy.WTO -> new WtoFixpointIterator<>(method, initialDomain, abstractTransformer, analysisContext); + }; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/GraphTraversalHelper.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/GraphTraversalHelper.java new file mode 100644 index 000000000000..6ecc380b76b6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/GraphTraversalHelper.java @@ -0,0 +1,139 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator; + +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy.IteratorDirection; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraphBuilder; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper class for direction-aware traversal of control flow graphs. + */ +public final class GraphTraversalHelper { + + private final ControlFlowGraph cfgGraph; + private final IteratorDirection direction; + + public GraphTraversalHelper(ControlFlowGraph cfgGraph, IteratorDirection direction) { + this.cfgGraph = cfgGraph; + this.direction = direction; + } + + public GraphTraversalHelper(StructuredGraph graph, IteratorDirection direction) { + ControlFlowGraph cfg = graph.getLastCFG(); + if (cfg == null) { + cfg = new ControlFlowGraphBuilder(graph).build(); + } + this.cfgGraph = cfg; + this.direction = direction; + } + + public HIRBlock getEntryBlock() { + return direction == IteratorDirection.FORWARD + ? cfgGraph.getStartBlock() + : cfgGraph.getBlocks()[cfgGraph.getBlocks().length - 1]; + } + + public HIRBlock getSuccessorAt(HIRBlock block, int idx) { + return direction == IteratorDirection.FORWARD + ? block.getSuccessorAt(idx) + : block.getPredecessorAt(idx); + } + + public int getSuccessorCount(HIRBlock block) { + return direction == IteratorDirection.FORWARD + ? block.getSuccessorCount() + : block.getPredecessorCount(); + } + + public HIRBlock getPredecessorAt(HIRBlock block, int idx) { + return direction == IteratorDirection.FORWARD + ? block.getPredecessorAt(idx) + : block.getSuccessorAt(idx); + } + + public Iterable getBlocks() { + if (direction == IteratorDirection.FORWARD) { + return List.of(cfgGraph.getBlocks()); + } + + List blockList = new ArrayList<>(); + Collections.addAll(blockList, cfgGraph.getBlocks()); + Collections.reverse(blockList); + return blockList; + } + + public Iterable getPredecessors(HIRBlock block) { + int count = getPredecessorCount(block); + List predecessors = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + predecessors.add(getPredecessorAt(block, i)); + } + return predecessors; + } + + public Iterable getSuccessors(HIRBlock block) { + int count = getSuccessorCount(block); + List successors = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + successors.add(getSuccessorAt(block, i)); + } + return successors; + } + + public int getPredecessorCount(HIRBlock block) { + return direction == IteratorDirection.FORWARD + ? block.getPredecessorCount() + : block.getSuccessorCount(); + } + + public Iterable getNodes() { + if (direction == IteratorDirection.FORWARD) { + return cfgGraph.graph.getNodes(); + } + + List nodeList = new ArrayList<>(); + cfgGraph.graph.getNodes().forEach(nodeList::add); + Collections.reverse(nodeList); + return nodeList; + } + + public Node getBeginNode(HIRBlock block) { + return direction == IteratorDirection.FORWARD + ? block.getBeginNode() + : block.getEndNode(); + } + + public Node getEndNode(HIRBlock block) { + return direction == IteratorDirection.FORWARD + ? block.getEndNode() + : block.getBeginNode(); + } + + public Node getGraphStart() { + return direction == IteratorDirection.FORWARD + ? cfgGraph.graph.start() + : cfgGraph.getBlocks()[cfgGraph.getBlocks().length - 1].getEndNode(); + } + + public Iterable getNodeCfgPredecessors(Node node) { + return direction == IteratorDirection.FORWARD + ? node.cfgPredecessors() + : node.cfgSuccessors(); + } + + public Iterable getNodeCfgSuccessors(Node current) { + return direction == IteratorDirection.FORWARD + ? current.cfgSuccessors() + : current.cfgPredecessors(); + } + + public ControlFlowGraph getCfgGraph() { + return cfgGraph; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/WtoFixpointIterator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/WtoFixpointIterator.java new file mode 100644 index 000000000000..496c215e2b4c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/WtoFixpointIterator.java @@ -0,0 +1,144 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.context.AnalysisContext; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorPhase; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.fixpoint.wto.WeakTopologicalOrdering; +import com.oracle.svm.hosted.analysis.ai.fixpoint.wto.WtoComponent; +import com.oracle.svm.hosted.analysis.ai.fixpoint.wto.WtoCycle; +import com.oracle.svm.hosted.analysis.ai.fixpoint.wto.WtoVertex; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractTransformer; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import jdk.graal.compiler.graph.Node; + +/** + * Represents a fixpoint iterator based on the Weak Topological Ordering (WTO) algorithm. + * F. Bourdoncle. Efficient chaotic iteration strategies with widenings. + * In Formal Methods in Programming and Their Applications, pp 128-141. + * + * + * @param type of the derived AbstractDomain + */ +public final class WtoFixpointIterator> extends FixpointIteratorBase { + + private final WeakTopologicalOrdering weakTopologicalOrdering; + + public WtoFixpointIterator(AnalysisMethod method, + Domain initialDomain, + AbstractTransformer abstractTransformer, + AnalysisContext analysisContext) { + + super(method, initialDomain, abstractTransformer, analysisContext); + var cache = analysisContext.getMethodGraphCache(); + if (cache.containsMethodWto(method)) { + this.weakTopologicalOrdering = cache.getMethodWtoMap().get(method); + } else { + logger.log("Computing Weak Topological Ordering for " + method.getQualifiedName(), LoggerVerbosity.DEBUG); + logger.log("Using AnalysisDirection:" + analysisContext.getIteratorPolicy().direction(), LoggerVerbosity.INFO); + + this.weakTopologicalOrdering = new WeakTopologicalOrdering(graphTraversalHelper); + cache.addToMethodWtoMap(method, weakTopologicalOrdering); + } + logger.log("Weak Topological Ordering for " + method.getQualifiedName() + ": ", LoggerVerbosity.DEBUG); + logger.log(weakTopologicalOrdering.toString(), LoggerVerbosity.DEBUG); + } + + @Override + public AbstractState doRunFixpointIteration() { + logger.log("Starting WTO fixpoint iteration of method: " + analysisMethod.getName(), LoggerVerbosity.INFO); + iteratorContext.reset(); + assert graph != null; + + for (WtoComponent component : weakTopologicalOrdering.getComponents()) { + iteratorContext.incrementGlobalIteration(); + analyzeComponent(component); + } + + iteratorContext.setConverged(true); + logger.log("Finished WTO fixpoint iteration of method: " + analysisMethod.getName(), LoggerVerbosity.INFO); + return abstractState; + } + + private void analyzeComponent(WtoComponent component) { + logger.log("Analyzing component: " + component, LoggerVerbosity.DEBUG); + if (component instanceof WtoVertex vertex) { + analyzeVertex(vertex); + } else if (component instanceof WtoCycle cycle) { + analyzeCycle(cycle); + } + } + + private void analyzeVertex(WtoVertex vertex) { + logger.log("Analyzing vertex: " + vertex, LoggerVerbosity.DEBUG); + Node node = graphTraversalHelper.getBeginNode(vertex.block()); + + /* Track node visits (used for isFirstVisit and widening decisions) */ + iteratorContext.incrementNodeVisitCount(node); + if (node == graphTraversalHelper.getGraphStart() && !abstractState.hasNode(node)) { + abstractState.setPreCondition(node, initialDomain); + } + iteratorContext.setCurrentBlock(vertex.block()); + abstractTransformer.analyzeBlock(vertex.block(), abstractState, iteratorContext); + } + + private void analyzeCycle(WtoCycle cycle) { + logger.log("Analyzing cycle: " + cycle, LoggerVerbosity.DEBUG); + boolean iterate = true; + Node headBegin = cycle.head().getBeginNode(); + + if (iteratorContext.isLoopHeader(headBegin)) { + iteratorContext.incrementLoopIteration(headBegin); + } + + iteratorContext.setPhase(IteratorPhase.WIDENING); + + while (iterate) { + int visitCount = iteratorContext.getNodeVisitCount(headBegin); + logger.log("Loop iteration (visit count: " + visitCount + ") for cycle: " + cycle, LoggerVerbosity.DEBUG); + + // Ensure a pre-condition exists + Domain currentPre = abstractState.getPreCondition(headBegin); + if (currentPre == null) { + abstractState.setPreCondition(headBegin, initialDomain.copyOf()); + currentPre = abstractState.getPreCondition(headBegin); + } + Domain oldPreCondition = currentPre.copyOf(); + + iteratorContext.setCurrentBlock(cycle.head()); + abstractTransformer.analyzeBlock(cycle.head(), abstractState, iteratorContext); + + // Analyze nested components inside the cycle body (excluding head already handled) + for (WtoComponent component : cycle.components()) { + analyzeComponent(component); + } + + Domain postCondition = abstractState.getPostCondition(headBegin); + if (postCondition == null) { + // Defensive: if transformer failed to set a post condition, treat as TOP and converge + logger.log("Post-condition missing for loop head " + headBegin + ", forcing convergence", LoggerVerbosity.INFO); + abstractState.setPostCondition(headBegin, oldPreCondition.copyOf()); + break; + } + + logger.log("Visit " + visitCount + ": old pre = " + oldPreCondition + ", post = " + postCondition, LoggerVerbosity.DEBUG); + + // Convergence criteria: + // 2. postCondition <= oldPreCondition (domain order) after widening: stable over-approximation + boolean dominated = postCondition.leq(oldPreCondition); + if (dominated) { + logger.log("Loop converged after: " + (visitCount + 1) + " visits", LoggerVerbosity.DEBUG); + iterate = false; + } else { + logger.log("No convergence yet (post !<= oldPre). Extrapolating.", LoggerVerbosity.DEBUG); + extrapolate(headBegin); + if (iteratorContext.isLoopHeader(headBegin)) { + iteratorContext.incrementLoopIteration(headBegin); + } + } + } + + iteratorContext.setPhase(IteratorPhase.ASCENDING); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorDirection.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorDirection.java new file mode 100644 index 000000000000..779cb354990a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorDirection.java @@ -0,0 +1,6 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy; + +public enum IteratorDirection { + FORWARD, + BACKWARD +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorPolicy.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorPolicy.java new file mode 100644 index 000000000000..4af072826618 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorPolicy.java @@ -0,0 +1,13 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy; + +/** + * Represents the attributes of a fixpoint iterator. + */ +public record IteratorPolicy(int maxJoinIterations, + int maxWidenIterations, + WideningOverflowPolicy wideningOverflowPolicy, + IteratorStrategy strategy, + IteratorDirection direction) { + public static final IteratorPolicy DEFAULT_FORWARD_WTO = new IteratorPolicy(5, 5, WideningOverflowPolicy.ERROR, IteratorStrategy.WTO, IteratorDirection.FORWARD); + public static final IteratorPolicy DEFAULT_BACKWARD_WTO = new IteratorPolicy(5, 5, WideningOverflowPolicy.ERROR, IteratorStrategy.WTO, IteratorDirection.BACKWARD); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorStrategy.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorStrategy.java new file mode 100644 index 000000000000..8dcb238040f3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/IteratorStrategy.java @@ -0,0 +1,5 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy; + +public enum IteratorStrategy { + WTO, +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/WideningOverflowPolicy.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/WideningOverflowPolicy.java new file mode 100644 index 000000000000..fe6e8eb05c72 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/iterator/policy/WideningOverflowPolicy.java @@ -0,0 +1,10 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.policy; + +/** + * Enum representing the policy to handle a situation when we exceed the + * widening limit on during fixpoint computation. + */ +public enum WideningOverflowPolicy { + ERROR, + SET_TO_TOP +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/AbstractState.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/AbstractState.java new file mode 100644 index 000000000000..4b85a1154c2a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/AbstractState.java @@ -0,0 +1,168 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.state; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.ReturnNode; +import jdk.graal.compiler.nodes.StartNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraph; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class maps Nodes of StructuredGraph to a common abstract domain. + * It maintains pre-conditions and post-conditions for each node during fixpoint iteration. + * + * @param type of the derived {@link AbstractDomain} + */ +public final class AbstractState> { + + private final Domain initialDomain; + private final Map> stateMap; + private final StructuredGraph graph; + + public AbstractState(Domain initialDomain, StructuredGraph graph) { + this(initialDomain, graph, new HashMap<>()); + } + + public AbstractState(Domain initialDomain, StructuredGraph graph, Map> stateMap) { + this.initialDomain = initialDomain; + this.graph = graph; + this.stateMap = stateMap; + } + + + public Domain getInitialDomain() { + return initialDomain; + } + + public Map> getStateMap() { + return stateMap; + } + + public boolean hasNode(Node node) { + return stateMap.containsKey(node); + } + + public NodeState getNodeState(Node node) { + return stateMap.computeIfAbsent(node, n -> new NodeState<>(initialDomain)); + } + + public Domain getPreCondition(Node node) { + return getNodeState(node).getPreCondition(); + } + + public Domain getPostCondition(Node node) { + return getNodeState(node).getPostCondition(); + } + + public void setPostCondition(Node node, Domain postCondition) { + NodeState state = getNodeState(node); + state.setPostCondition(postCondition); + } + + public void setPreCondition(Node node, Domain preCondition) { + NodeState state = getNodeState(node); + state.setPreCondition(preCondition); + } + + public void joinWith(AbstractState other) { + for (Node node : other.stateMap.keySet()) { + NodeState state = other.stateMap.get(node); + NodeState thisState = getNodeState(node); + thisState.getPreCondition().joinWith(state.getPreCondition()); + thisState.getPostCondition().joinWith(state.getPostCondition()); + } + } + + public String toString() { + List lines = new ArrayList<>(); + for (Node node : stateMap.keySet()) { + lines.add(node + " -> Pre: " + getPreCondition(node) + + ", Post: " + getPostCondition(node)); + } + return String.join(System.lineSeparator(), lines); + } + + public void clear() { + stateMap.clear(); + } + + public NodeState getStartNodeState() { + Node start = getStartNode(); + if (start != null) { + return stateMap.get(start); + } + throw new IllegalStateException("No start node found in the control flow graph"); + } + + /** + * Set the {@link NodeState} of the {@link jdk.graal.compiler.nodes.StartNode} of a graph to a give domain. + * We will always have a single start node in a {@link ControlFlowGraph}. + * This is done to allow more flexibility when handling inter-procedural calls. + * Instead of setting the {@code initialDomain} of the {@link AbstractState} to a summary pre-condition, + * we keep the initial domain provided by the developers, and set the start node to the summary pre-condition. + * + * @param domain the domain to set the start node to + */ + public void setStartNodeState(Domain domain) { + Node start = getStartNode(); + stateMap.putIfAbsent(start, new NodeState<>(domain, initialDomain)); + } + + /** + * Merge abstract contexts from different {@link ReturnNode}s into a single abstract context. + * This context represents the over-approximation of all different return contexts, + * which is used to compute the final post-condition at the of the method. + * + * @return the joined abstract context over every {@link ReturnNode} in the method + */ + // FIXME: some weird shit is probably happening here, investigate + public Domain getReturnDomain() { + Domain returnDomain = initialDomain.copyOf(); + if (!returnDomain.isBot()) { + returnDomain.setToBot(); + } + for (Node node : stateMap.keySet()) { + if (node instanceof ReturnNode) { + Domain pre = getPreCondition(node); + if (pre != null && pre.isBot()) { + continue; + } + returnDomain.joinWith(getPostCondition(node)); + } + } + // If no reachable returns joined, fall back to initialDomain (sound over-approximation) + if (returnDomain.isBot()) { + returnDomain.joinWith(initialDomain); + } + return returnDomain; + } + + /** + * Get the start node of the graph. + * This is done to allow more flexibility when handling inter-procedural calls. + * + * @return the start node of the control flow graph + */ + private Node getStartNode() { + for (Node node : graph.getNodes()) { + if (node instanceof StartNode) { + return node; + } + } + return null; + } + + public AbstractState copyOf() { + var initDomain = initialDomain.copyOf(); + var stateMap = new HashMap>(); + for (Node node : this.stateMap.keySet()) { + stateMap.put(node, new NodeState<>(this.stateMap.get(node).getPreCondition(), this.stateMap.get(node).getPostCondition())); + } + return new AbstractState<>(initDomain, graph, stateMap); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/NodeState.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/NodeState.java new file mode 100644 index 000000000000..5f8577803816 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/state/NodeState.java @@ -0,0 +1,64 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.state; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Represents a state of a node in the fixpoint iteration. + * Contains the pre- and post-conditions for the abstract domain. + * + * @param type of the derived AbstractDomain + */ +public final class NodeState> { + + private Domain preCondition; + private Domain postCondition; + private NodeMark mark = NodeMark.NORMAL; + + public enum NodeMark { + NORMAL, + UNREACHABLE + } + + public NodeState(Domain initialDomain) { + this.preCondition = initialDomain.copyOf(); + this.postCondition = initialDomain.copyOf(); + } + + public NodeState(Domain preCondition, Domain postCondition) { + this.preCondition = preCondition.copyOf(); + this.postCondition = postCondition.copyOf(); + } + + public Domain getPreCondition() { + return preCondition; + } + + public void setPreCondition(Domain preCondition) { + this.preCondition = preCondition.copyOf(); + } + + public Domain getPostCondition() { + return postCondition; + } + + public void setPostCondition(Domain postCondition) { + this.postCondition = postCondition.copyOf(); + } + + public boolean isUnreachable() { + return mark == NodeMark.UNREACHABLE; + } + + public void setMark(NodeMark mark) { + this.mark = mark; + } + + public NodeMark getMark() { + return mark; + } + + @Override + public String toString() { + return "Pre-Condition: " + preCondition + System.lineSeparator() + "Post-Condition: " + postCondition; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WeakTopologicalOrdering.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WeakTopologicalOrdering.java new file mode 100644 index 000000000000..fab9da1616fb --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WeakTopologicalOrdering.java @@ -0,0 +1,99 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.wto; + +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.GraphTraversalHelper; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +public final class WeakTopologicalOrdering { + + private final List components; + private final Map dfnTable; + private final Stack stack; + private final GraphTraversalHelper graphTraversalHelper; + private int num; + + public WeakTopologicalOrdering(GraphTraversalHelper graphTraversalHelper) { + this.graphTraversalHelper = graphTraversalHelper; + this.components = new ArrayList<>(); + this.dfnTable = new HashMap<>(); + this.stack = new Stack<>(); + this.num = 0; + build(graphTraversalHelper.getEntryBlock()); + Collections.reverse(components); + } + + private void build(HIRBlock root) { + visit(root); + } + + private int visit(HIRBlock vertex) { + stack.push(vertex); + int headDfn = setDfn(vertex, ++num); + boolean isLoop = false; + + int successorCount = graphTraversalHelper.getSuccessorCount(vertex); + for (int i = 0; i < successorCount; i++) { + var successor = graphTraversalHelper.getSuccessorAt(vertex, i); + int successorDfn = getDfn(successor); + int min; + if (successorDfn == 0) { + min = visit(successor); + } else { + min = successorDfn; + } + if (min <= headDfn) { + headDfn = min; + isLoop = true; + } + } + + if (headDfn == getDfn(vertex)) { + setDfn(vertex, Integer.MAX_VALUE); + HIRBlock element = stack.pop(); + if (isLoop) { + List cycleComponents = new ArrayList<>(); + while (!element.equals(vertex)) { + setDfn(element, 0); + cycleComponents.add(new WtoVertex(element)); + element = stack.pop(); + } + components.add(new WtoCycle(vertex, cycleComponents)); + } else { + components.add(new WtoVertex(vertex)); + } + } + return headDfn; + } + + private int getDfn(HIRBlock block) { + return dfnTable.getOrDefault(block, 0); + } + + private int setDfn(HIRBlock block, int number) { + if (number == 0) { + dfnTable.remove(block); + } else { + dfnTable.put(block, number); + } + return number; + } + + public List getComponents() { + return components; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (WtoComponent component : components) { + sb.append(component.toString()).append(" "); + } + return sb.toString().trim(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoComponent.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoComponent.java new file mode 100644 index 000000000000..b8529abf88ac --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoComponent.java @@ -0,0 +1,10 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.wto; + +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +public interface WtoComponent { + + String toString(); + + HIRBlock getBlock(); +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoCycle.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoCycle.java new file mode 100644 index 000000000000..5fb50b308b54 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoCycle.java @@ -0,0 +1,24 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.wto; + +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.util.List; + +public record WtoCycle(HIRBlock head, List components) implements WtoComponent { + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("(").append(head); + for (WtoComponent component : components) { + sb.append(" ").append(component); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public HIRBlock getBlock() { + return head; + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoVertex.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoVertex.java new file mode 100644 index 000000000000..3362846b8c6e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/fixpoint/wto/WtoVertex.java @@ -0,0 +1,16 @@ +package com.oracle.svm.hosted.analysis.ai.fixpoint.wto; + +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +public record WtoVertex(HIRBlock block) implements WtoComponent { + + @Override + public String toString() { + return block.toString(); + } + + @Override + public HIRBlock getBlock() { + return block; + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractInterpreter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractInterpreter.java new file mode 100644 index 000000000000..24ef4107b21c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractInterpreter.java @@ -0,0 +1,38 @@ +package com.oracle.svm.hosted.analysis.ai.interpreter; + +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.InvokeCallBack; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import jdk.graal.compiler.graph.Node; + +/** + * This interface provides an API for interpreting semantical operations + * on nodes and edges within the GraalIR. + * + * @param type of the derived {@link AbstractDomain} used in the analysis + */ +public interface AbstractInterpreter> { + + /** + * Interpret the effect of executing an edge between two nodes {@link AbstractState}. + * For efficiency, this method should modify the pre-condition of {@param target} directly. + * + * @param source the node from which the edge originates + * @param target the node to which the edge goes + * @param abstractState of the analyzed method + * @param iteratorContext context information from the fixpoint iterator + */ + void execEdge(Node source, Node target, AbstractState abstractState, IteratorContext iteratorContext); + + /** + * Interpret the effect of executing a Graal IR node within given {@link AbstractState}. + * For efficiency, this method should modify the post-condition of {@param target} directly. + * + * @param node to interpret + * @param abstractState of the analyzed method + * @param invokeCallBack callback that can be used to analyze invokes + * @param iteratorContext context information from the fixpoint iterator + */ + void execNode(Node node, AbstractState abstractState, InvokeCallBack invokeCallBack, IteratorContext iteratorContext); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractTransformer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractTransformer.java new file mode 100644 index 000000000000..84eacbab027f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/interpreter/AbstractTransformer.java @@ -0,0 +1,133 @@ +package com.oracle.svm.hosted.analysis.ai.interpreter; + +import com.oracle.svm.hosted.analysis.ai.analysis.invokehandle.InvokeCallBack; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.context.IteratorContext; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.GraphTraversalHelper; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.NodeState; +import com.oracle.svm.hosted.analysis.ai.log.AbstractInterpretationLogger; +import com.oracle.svm.hosted.analysis.ai.log.LoggerVerbosity; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +/** + * Represents the transfer functions used in abstract interpretation. + * This class is responsible for applying abstract operations corresponding to the semantics + * of Graal IR nodes and edges and transforming the abstract state during an analysis. + * + * @param type of the derived {@link AbstractDomain} used in the analysis. + */ +public record AbstractTransformer>( + AbstractInterpreter abstractInterpreter, InvokeCallBack invokeCallBack) { + + /** + * Performs semantic transformation of the given {@link Node} with iterator context. + * For efficiency, it modifies the post-condition of {@param node}. + * + * @param node to analyze + * @param abstractState current abstract state during fixpoint iteration + * @param context context information from the fixpoint iterator + */ + public void analyzeNode(Node node, AbstractState abstractState, IteratorContext context) { + abstractInterpreter.execNode(node, abstractState, invokeCallBack, context); + } + + /** + * Performs semantic transformation of an edge between two {@link Node}s with iterator context. + * For efficiency, it modifies the pre-condition of the {@param target} Node. + * + * @param source the node from which the edge originates + * @param target the node to which the edge goes + * @param abstractState abstract state during fixpoint iteration + * @param context context information from the fixpoint iterator + */ + public void analyzeEdge(Node source, Node target, AbstractState abstractState, IteratorContext context) { + abstractInterpreter.execEdge(source, target, abstractState, context); + } + + /** + * Collects invariants from all CFG predecessors of the given {@link Node}. + * + * @param node the node whose predecessors to analyze + * @param abstractState abstract state during fixpoint iteration + * @param context context information from the fixpoint iterator + */ + public void collectInvariantsFromCfgPredecessors(Node node, AbstractState abstractState, IteratorContext context) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log("Collecting invariants predecessors for node: " + node, LoggerVerbosity.DEBUG); + GraphTraversalHelper graphTraversalHelper = context.getGraphTraversalHelper(); + HIRBlock currentBlock = context.getBlockForNode(node); + assert currentBlock != null; + + /* Node is considered initially unreachable unless it is a graph entry with no predecessors. */ + int predecessorCount = graphTraversalHelper.getPredecessorCount(currentBlock); + NodeState.NodeMark aggregateMark = predecessorCount == 0 ? NodeState.NodeMark.NORMAL : NodeState.NodeMark.UNREACHABLE; + NodeState nodeState = abstractState.getNodeState(node); + + for (int i = 0; i < predecessorCount; i++) { + HIRBlock predBlock = graphTraversalHelper.getPredecessorAt(currentBlock, i); + Node predecessor = graphTraversalHelper.getEndNode(predBlock); + + context.setEdgeTraversal(predBlock, currentBlock); + logger.log(" Processing edge: " + predBlock + " -> " + currentBlock, LoggerVerbosity.DEBUG); + + /* Skip edges coming from unreachable predecessors. */ + if (predecessor == null || abstractState.getNodeState(predecessor).isUnreachable()) { + continue; + } + + /* Reset mark for this edge traversal and let execEdge potentially mark it unreachable. */ + nodeState.setMark(NodeState.NodeMark.NORMAL); + analyzeEdge(predecessor, node, abstractState, context); + + if (nodeState.getMark() == NodeState.NodeMark.NORMAL) { + aggregateMark = NodeState.NodeMark.NORMAL; + } + } + + nodeState.setMark(aggregateMark); + } + + /** + * Performs semantic transformation of given {@link HIRBlock} with given iterator context. + * @param block the block to analyze + * @param abstractState the current abstract state before analyzing this block + * @param context the current iterator context before analyzing this block + */ + public void analyzeBlock(HIRBlock block, AbstractState abstractState, IteratorContext context) { + AbstractInterpretationLogger logger = AbstractInterpretationLogger.getInstance(); + logger.log("Analyzing block: " + block, LoggerVerbosity.DEBUG); + + Node blockBeginNode = block.getBeginNode(); + if (context.shouldCollectPredecessors(blockBeginNode)) { + collectInvariantsFromCfgPredecessors(blockBeginNode, abstractState, context); + } + + boolean blockUnreachable = abstractState.getNodeState(blockBeginNode).isUnreachable(); + logger.log(" Block head " + blockBeginNode + " unreachable=" + blockUnreachable, LoggerVerbosity.DEBUG); + + Node previousNode = null; + for (Node node : block.getNodes()) { + if (previousNode != null) { + Domain prevPost = abstractState.getPostCondition(previousNode); + // Inside a block the abstract state flows trivially + abstractState.setPreCondition(node, prevPost); + } + + Domain pre = abstractState.getPreCondition(node); + + if (blockUnreachable) { + // Propagate pre to post unchanged and mark all nodes in this block unreachable. + abstractState.setPostCondition(node, pre); + logger.log(" Skipping unreachable node: " + node, LoggerVerbosity.DEBUG); + abstractState.getNodeState(node).setMark(NodeState.NodeMark.UNREACHABLE); + previousNode = node; + continue; + } + + analyzeNode(node, abstractState, context); + previousNode = node; + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/AbstractInterpretationLogger.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/AbstractInterpretationLogger.java new file mode 100644 index 000000000000..f5e709b8b12c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/AbstractInterpretationLogger.java @@ -0,0 +1,343 @@ +package com.oracle.svm.hosted.analysis.ai.log; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.InvokeInfo; +import com.oracle.svm.hosted.analysis.ai.checker.facts.Fact; +import com.oracle.svm.hosted.analysis.ai.fixpoint.iterator.GraphTraversalHelper; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.NodeState; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraphBuilder; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * Represents a logger for abstract interpretation analysis. + */ +public final class AbstractInterpretationLogger { + private static AbstractInterpretationLogger instance; + private PrintWriter fileWriter; + private final String logFilePath; + + private LoggerVerbosity fileThreshold; + private LoggerVerbosity consoleThreshold; + private boolean colorEnabled = true; + private boolean consoleEnabled = true; + private boolean fileEnabled = true; + private boolean isGraphJsonExportEnabled = false; + private boolean isGraphIgvDumpEnabled = false; + + private AbstractInterpretationLogger(String customFileName, + LoggerVerbosity loggerVerbosity) { + String fileName; + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + if (customFileName != null && !customFileName.isEmpty()) { + fileName = customFileName + "_" + timeStamp + ".log"; + } else { + fileName = "absint_" + timeStamp + ".log"; + } + this.logFilePath = new File(fileName).getAbsolutePath(); + this.fileThreshold = loggerVerbosity == null ? LoggerVerbosity.INFO : loggerVerbosity; + this.consoleThreshold = this.fileThreshold; + instance = this; + } + + public static AbstractInterpretationLogger getInstance(String customFileName, LoggerVerbosity loggerVerbosity) { + if (instance == null) { + instance = new AbstractInterpretationLogger(customFileName, loggerVerbosity); + } + return instance; + } + + public static AbstractInterpretationLogger getInstance(String customFileName) { + return getInstance(customFileName, LoggerVerbosity.INFO); + } + + public static AbstractInterpretationLogger getInstance(LoggerVerbosity loggerVerbosity) { + return getInstance(null, loggerVerbosity); + } + + public static AbstractInterpretationLogger getInstance() { + if (instance == null) { + instance = new AbstractInterpretationLogger("GraalAF", LoggerVerbosity.INFO); + instance.setConsoleEnabled(false).setFileEnabled(false); + } + return instance; + } + + public AbstractInterpretationLogger setFileThreshold(LoggerVerbosity threshold) { + if (threshold != null) this.fileThreshold = threshold; + return this; + } + + public AbstractInterpretationLogger setConsoleThreshold(LoggerVerbosity threshold) { + if (threshold != null) this.consoleThreshold = threshold; + return this; + } + + public AbstractInterpretationLogger setColorEnabled(boolean enabled) { + this.colorEnabled = enabled; + return this; + } + + public AbstractInterpretationLogger setConsoleEnabled(boolean enabled) { + this.consoleEnabled = enabled; + return this; + } + + public AbstractInterpretationLogger setFileEnabled(boolean enabled) { + this.fileEnabled = enabled; + return this; + } + + public AbstractInterpretationLogger setGraphJsonExportEnabled(boolean enabled) { + this.isGraphJsonExportEnabled = enabled; + return this; + } + + public AbstractInterpretationLogger setGraphIgvDumpEnabled(boolean enabled) { + this.isGraphIgvDumpEnabled = enabled; + return this; + } + + public boolean isGraphIgvDumpEnabled() { + return isGraphIgvDumpEnabled; + } + + private static class ANSI { + public static final String RESET = "\033[0m"; + public static final String RED = "\033[0;31m"; + public static final String GREEN = "\033[0;32m"; + public static final String YELLOW = "\033[0;33m"; + public static final String BLUE = "\033[0;34m"; + public static final String PURPLE = "\033[0;35m"; + public static final String CYAN = "\033[0;36m"; + public static final String BOLD = "\033[1m"; + } + + private static String prefixFor(LoggerVerbosity v) { + return switch (v) { + case CHECKER -> "[CHECKER] "; + case CHECKER_ERR -> "[CHECKER_ERR] "; + case CHECKER_WARN -> "[CHECKER_WARN] "; + case FACT -> "[FACT] "; + case SUMMARY -> "[SUMMARY] "; + case INFO -> "[INFO] "; + case DEBUG -> "[DEBUG] "; + case WARN -> "[WARN] "; + case SILENT -> ""; + case ERROR -> "[ERROR] "; + }; + } + + private static String colorFor(LoggerVerbosity v) { + return switch (v) { + case CHECKER -> ANSI.GREEN; + case CHECKER_WARN, WARN -> ANSI.YELLOW; + case CHECKER_ERR, ERROR -> ANSI.RED; + case FACT -> ANSI.PURPLE; + case SUMMARY, DEBUG -> ANSI.BLUE; + case INFO -> ANSI.CYAN; + case SILENT -> ANSI.RESET; + }; + } + + private synchronized void ensureFileWriter() { + if (!fileEnabled || fileThreshold == LoggerVerbosity.SILENT) { + return; + } + if (fileWriter == null) { + try { + fileWriter = new PrintWriter(new FileWriter(logFilePath, true)); + } catch (IOException ioe) { + fileEnabled = false; + System.err.println("[AI-LOGGER] Failed to (re)open log file '" + logFilePath + "': " + ioe.getMessage()); + } + } + } + + public synchronized void log(String message, LoggerVerbosity verbosity) { + String prefix = prefixFor(verbosity); + String ts = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()); + String fileLine = ts + " " + prefix + message; + + // File output with threshold (re-open lazily if closed) + if (fileEnabled && fileThreshold != LoggerVerbosity.SILENT && verbosity.compareTo(fileThreshold) <= 0) { + ensureFileWriter(); + if (fileWriter != null) { + fileWriter.println(fileLine); + fileWriter.flush(); + } + } + + // Console output with threshold + if (consoleEnabled && consoleThreshold != LoggerVerbosity.SILENT && verbosity.compareTo(consoleThreshold) <= 0) { + String colored = colorEnabled ? (colorFor(verbosity) + prefix + message + ANSI.RESET) : (prefix + message); + if (verbosity == LoggerVerbosity.CHECKER_ERR || verbosity == LoggerVerbosity.ERROR) { + System.err.println(colored); + } else { + System.out.println(colored); + } + } + } + + public void printGraph(AnalysisMethod root, ControlFlowGraph graph, GraphTraversalHelper graphTraversalHelper) { + if (graph == null) { + throw new IllegalArgumentException("ControlFlowGraph is null"); + } + + log("Graph of AnalysisMethod: " + root, LoggerVerbosity.DEBUG); + for (HIRBlock block : graph.getBlocks()) { + log(block.toString(), LoggerVerbosity.DEBUG); + log("Block predecessors: ", LoggerVerbosity.DEBUG); + for (HIRBlock predecessor : graphTraversalHelper.getPredecessors(block)) { + log("\t" + predecessor.toString(), LoggerVerbosity.DEBUG); + } + + log("Block successors: ", LoggerVerbosity.DEBUG); + for (HIRBlock successor : graphTraversalHelper.getSuccessors(block)) { + log("\t" + successor.toString(), LoggerVerbosity.DEBUG); + } + + for (Node node : block.getNodes()) { + log(node.toString(), LoggerVerbosity.DEBUG); + log("\tSuccessors: ", LoggerVerbosity.DEBUG); + for (Node successor : node.successors()) { + log("\t\t" + successor.toString(), LoggerVerbosity.DEBUG); + } + log("\tInputs: ", LoggerVerbosity.DEBUG); + for (Node input : node.inputs()) { + log("\t\t" + input.toString(), LoggerVerbosity.DEBUG); + } + } + } + + log("The Invokes of the AnalysisMethod: " + root, LoggerVerbosity.DEBUG); + for (InvokeInfo invoke : root.getInvokes()) { + log("\tInvoke: " + invoke, LoggerVerbosity.DEBUG); + for (AnalysisMethod callee : invoke.getOriginalCallees()) { + log("\t\tCallee: " + callee, LoggerVerbosity.DEBUG); + } + } + } + + public void printLabelledGraph(StructuredGraph graph, AnalysisMethod analysisMethod, AbstractState abstractState) { + assert graph != null; + log("Computed post-conditions of method: " + analysisMethod, LoggerVerbosity.INFO); + for (Node node : graph.getNodes()) { + NodeState nodeState = abstractState.getNodeState(node); + log(node + " -> " + nodeState.getPostCondition(), LoggerVerbosity.INFO); + } + } + + private void logFact(Fact fact) { + log(fact.describe(), LoggerVerbosity.FACT); + } + + /** + * Export a graph to JSON format for sharing and analysis. + * This is useful for getting help with abstract interpretation analysis. + * + * @param cfg The structured graph to export + * @param method The analysis method + * @param outputPath Path to write the JSON file + */ + public void exportGraphToJson(ControlFlowGraph cfg, AnalysisMethod method, String outputPath) { + try { + GraphExporter.exportToJson(cfg, method, outputPath); + log("Graph exported to JSON: " + outputPath, LoggerVerbosity.INFO); + } catch (Exception e) { + log("Failed to export graph to JSON: " + e.getMessage(), LoggerVerbosity.CHECKER_WARN); + } + } + + public void exportGraphToJson(StructuredGraph graph, AnalysisMethod method, String outputPath) { + if (isGraphJsonExportEnabled) { + exportGraphToJson(new ControlFlowGraphBuilder(graph).build(), method, outputPath); + } + } + + public void logFacts(List facts) { + if (facts.isEmpty()) { + log("No facts", LoggerVerbosity.INFO); + return; + } + + log("Aggregated facts produced by checkers:", LoggerVerbosity.FACT); + for (Fact fact : facts) { + logFact(fact); + } + } + + + public static final class IGVDumpSession implements AutoCloseable { + private final DebugContext debug; + private final StructuredGraph graph; + private final String scopeName; + private boolean dumpedBefore; + + public IGVDumpSession(DebugContext debug, StructuredGraph graph, String scopeName) throws Throwable { + this.debug = debug; + this.graph = graph; + this.scopeName = scopeName == null ? "GraalAF" : scopeName; + this.dumpedBefore = false; + } + + public void dumpBeforeSuite(String title) { + debug.dump(DebugContext.VERBOSE_LEVEL, graph, "%s - Before %s", scopeName, title); + if (debug.isDumpEnabled(DebugContext.BASIC_LEVEL)) { + debug.dump(DebugContext.BASIC_LEVEL, graph, "%s - Before %s", scopeName, title); + dumpedBefore = true; + } else if (debug.isDumpEnabled(DebugContext.INFO_LEVEL)) { + debug.dump(DebugContext.INFO_LEVEL, graph, "%s - %s", scopeName, title); + } + } + + public void dumpApplierSubphase(String applierDescription) { + if (debug.isDumpEnabled(DebugContext.BASIC_LEVEL)) { + debug.dump(DebugContext.BASIC_LEVEL, graph, "%s - %s", scopeName, applierDescription); + } else if (debug.isDumpEnabled(DebugContext.INFO_LEVEL + 1)) { + debug.dump(DebugContext.INFO_LEVEL + 1, graph, "%s - After applier %s", scopeName, applierDescription); + } else if (debug.isDumpEnabled(DebugContext.ENABLED_LEVEL) && dumpedBefore) { + debug.dump(DebugContext.ENABLED_LEVEL, graph, "%s - After applier %s", scopeName, applierDescription); + } + } + + public void dumpAfterSuite(String title) { + if (debug.isDumpEnabled(DebugContext.BASIC_LEVEL)) { + debug.dump(DebugContext.BASIC_LEVEL, graph, "%s - After %s", scopeName, title); + } else if (debug.isDumpEnabled(DebugContext.INFO_LEVEL)) { + debug.dump(DebugContext.INFO_LEVEL, graph, "%s - After %s", scopeName, title); + } else if (!dumpedBefore && debug.isDumpEnabled(DebugContext.ENABLED_LEVEL)) { + debug.dump(DebugContext.ENABLED_LEVEL, graph, "%s - After %s", scopeName, title); + } + } + + @Override + public void close() { + // No scope to close + } + } + + public synchronized void close() { + if (fileWriter != null) { + try { + fileWriter.flush(); + } finally { + fileWriter.close(); + fileWriter = null; + } + } + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/GraphExporter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/GraphExporter.java new file mode 100644 index 000000000000..31cce45e46fe --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/GraphExporter.java @@ -0,0 +1,402 @@ +package com.oracle.svm.hosted.analysis.ai.log; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.LoopBeginNode; +import jdk.graal.compiler.nodes.LoopEndNode; +import jdk.graal.compiler.nodes.PhiNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.cfg.ControlFlowGraph; +import jdk.graal.compiler.nodes.cfg.HIRBlock; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +public class GraphExporter { + + public static void exportToJson(ControlFlowGraph cfg, AnalysisMethod method, String outputPath) throws IOException { + StructuredGraph graph = cfg.graph; + try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath))) { + writer.println("{"); + writer.println(" \"method\": \"" + method.format("%H.%n(%p)") + "\","); + writer.println(" \"nodes\": ["); + + Map nodeIds = new HashMap<>(); + int id = 0; + for (Node node : graph.getNodes()) { + nodeIds.put(node, id++); + } + + boolean first = true; + for (Node node : graph.getNodes()) { + if (!first) writer.println(","); + first = false; + + writer.println(" {"); + writer.println(" \"node\": \"" + node.toString().replace("\"", "\\\"") + "\","); + writer.println(" \"type\": \"" + node.getNodeClass().shortName() + "\","); + writer.println(" \"class\": \"" + node.getClass().getSimpleName() + "\","); + + writer.print(" \"successors\": ["); + boolean firstSucc = true; + for (Node succ : node.successors()) { + if (!firstSucc) writer.print(", "); + writer.print(nodeIds.get(succ)); + firstSucc = false; + } + writer.println("],"); + + writer.print(" \"inputs\": ["); + boolean firstInput = true; + for (Node input : node.inputs()) { + if (!firstInput) writer.print(", "); + writer.print(nodeIds.get(input)); + firstInput = false; + } + writer.println("],"); + + writer.print(" \"usages\": ["); + boolean firstUsage = true; + for (Node usage : node.usages()) { + if (!firstUsage) writer.print(", "); + writer.print(nodeIds.get(usage)); + firstUsage = false; + } + writer.println("],"); + + writer.print(" \"properties\": {"); + switch (node) { + case PhiNode phi -> writer.print("\"valueCount\": " + phi.valueCount()); + case LoopBeginNode loopBeginNode -> writer.print("\"loopHeader\": true"); + case LoopEndNode loopEnd -> + writer.print("\"loopEnd\": true, \"loopBegin\": " + nodeIds.get(loopEnd.loopBegin())); + default -> { + } + } + writer.println("}"); + + writer.print(" }"); + } + writer.println(); + writer.println(" ],"); + + writer.println(" \"cfg\": ["); + first = true; + for (HIRBlock block : cfg.getBlocks()) { + if (!first) writer.println(","); + first = false; + + writer.println(" {"); + writer.println(" \"id\": " + block.getId() + ","); + writer.print(" \"predecessors\": ["); + for (int i = 0; i < block.getPredecessorCount(); i++) { + if (i > 0) writer.print(", "); + writer.print(block.getPredecessorAt(i).getId()); + } + writer.println("],"); + + writer.print(" \"successors\": ["); + for (int i = 0; i < block.getSuccessorCount(); i++) { + if (i > 0) writer.print(", "); + writer.print(block.getSuccessorAt(i).getId()); + } + writer.println("],"); + + writer.print(" \"nodes\": ["); + boolean firstNode = true; + for (Node n : block.getNodes()) { + if (!firstNode) writer.print(", "); + writer.print(nodeIds.get(n)); + firstNode = false; + } + writer.println("]"); + + writer.print(" }"); + } + writer.println(); + writer.println(" ]"); + writer.println("}"); + } + } + + public void exportToText(ControlFlowGraph cfg, AnalysisMethod method, String outputPath) throws IOException { + StructuredGraph graph = cfg.graph; + try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath))) { + writer.println("=".repeat(80)); + writer.println("GRAPH EXPORT FOR ABSTRACT INTERPRETATION ANALYSIS"); + writer.println("=".repeat(80)); + writer.println(); + writer.println("Method: " + method.format("%H.%n(%p)")); + writer.println("Node Count: " + graph.getNodeCount()); + writer.println("Block Count: " + cfg.getBlocks().length); + writer.println(); + + writer.println("=".repeat(80)); + writer.println("NODES (Data Flow Graph)"); + writer.println("=".repeat(80)); + writer.println(); + + for (Node node : graph.getNodes()) { + writer.println("Node: " + node.toString()); + writer.println(" Class: " + node.getClass().getSimpleName()); + writer.println(" String: " + node); + + // Control flow successors + if (node.successors().iterator().hasNext()) { + writer.print(" Successors (control): "); + boolean first = true; + for (Node succ : node.successors()) { + if (!first) writer.print(", "); + writer.print(succ.toString()); + first = false; + } + writer.println(); + } + + // Data flow inputs + if (node.inputs().iterator().hasNext()) { + writer.print(" Inputs (data): "); + boolean first = true; + for (Node input : node.inputs()) { + if (!first) writer.print(", "); + writer.print(input.toString() + " (" + input.getNodeClass().shortName() + ")"); + first = false; + } + writer.println(); + } + + // Usages + if (node.usages().iterator().hasNext()) { + writer.print(" Usages: "); + boolean first = true; + for (Node usage : node.usages()) { + if (!first) writer.print(", "); + writer.print(usage.toString()); + first = false; + } + writer.println(); + } + + // Special handling for important node types + switch (node) { + case PhiNode phi -> { + writer.println(" >>> PHI NODE <<<"); + writer.println(" Value count: " + phi.valueCount()); + for (int i = 0; i < phi.valueCount(); i++) { + Node value = phi.valueAt(i); + writer.println(" [" + i + "]: " + value.toString() + " (" + value.getNodeClass().shortName() + ")"); + } + } + case LoopBeginNode loopBeginNode -> writer.println(" >>> LOOP HEADER <<<"); + case LoopEndNode loopEnd -> { + writer.println(" >>> LOOP END <<<"); + writer.println(" Loop Begin: " + loopEnd.loopBegin().toString()); + } + default -> { + } + } + + writer.println(); + } + + writer.println("=".repeat(80)); + writer.println("CONTROL FLOW GRAPH (CFG Blocks)"); + writer.println("=".repeat(80)); + writer.println(); + + for (HIRBlock block : cfg.getBlocks()) { + writer.println("Block B" + block.getId() + ":"); + writer.println(" Loop Depth: " + block.getLoopDepth()); + + writer.print(" Predecessors: "); + if (block.getPredecessorCount() == 0) { + writer.print("(entry)"); + } else { + for (int i = 0; i < block.getPredecessorCount(); i++) { + if (i > 0) writer.print(", "); + writer.print("B" + block.getPredecessorAt(i).getId()); + } + } + writer.println(); + + writer.print(" Successors: "); + if (block.getSuccessorCount() == 0) { + writer.print("(exit)"); + } else { + for (int i = 0; i < block.getSuccessorCount(); i++) { + if (i > 0) writer.print(", "); + writer.print("B" + block.getSuccessorAt(i).getId()); + } + } + writer.println(); + + writer.println(" Nodes:"); + for (Node node : block.getNodes()) { + writer.println(" " + node.toString() + " - " + node.getNodeClass().shortName()); + } + writer.println(); + } + + writer.println("=".repeat(80)); + writer.println("LOOP ANALYSIS"); + writer.println("=".repeat(80)); + writer.println(); + + boolean foundLoop = false; + for (Node node : graph.getNodes()) { + if (node instanceof LoopBeginNode loopBegin) { + foundLoop = true; + writer.println("Loop at node: " + loopBegin.toString()); + + // Find loop end + for (Node n : graph.getNodes()) { + if (n instanceof LoopEndNode loopEnd && loopEnd.loopBegin() == loopBegin) { + writer.println(" Back-edge: " + loopEnd.toString() + " -> " + loopBegin.toString()); + } + } + + // Find phis + writer.println(" Phi nodes:"); + for (Node usage : loopBegin.usages()) { + if (usage instanceof PhiNode phi) { + writer.println(" " + phi.toString()); + } + } + writer.println(); + } + } + + if (!foundLoop) { + writer.println("No loops detected."); + } + + writer.println("=".repeat(80)); + writer.println("END OF GRAPH EXPORT"); + writer.println("=".repeat(80)); + } + } + + public void exportToCompact(StructuredGraph graph, ControlFlowGraph cfg, AnalysisMethod method, String outputPath) throws IOException { + try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath))) { + writer.println("Method: " + method.format("%H.%n(%p)")); + writer.println(); + + // Create node ID mapping + Map nodeIds = new HashMap<>(); + int id = 0; + for (Node node : graph.getNodes()) { + nodeIds.put(node, id++); + } + + writer.println("NODES:"); + for (Node node : graph.getNodes()) { + writer.print(" n" + nodeIds.get(node) + ": " + node.getNodeClass().shortName()); + + if (node instanceof PhiNode phi) { + writer.print(" [phi, values=" + phi.valueCount() + "]"); + } else if (node instanceof LoopBeginNode) { + writer.print(" [LOOP-HEADER]"); + } else if (node instanceof LoopEndNode loopEnd) { + writer.print(" [LOOP-END -> n" + nodeIds.get(loopEnd.loopBegin()) + "]"); + } + + writer.println(); + } + + writer.println(); + writer.println("DATA EDGES:"); + for (Node node : graph.getNodes()) { + for (Node input : node.inputs()) { + writer.println(" n" + nodeIds.get(input) + " -> n" + nodeIds.get(node) + " (data)"); + } + } + + writer.println(); + writer.println("CONTROL EDGES:"); + for (Node node : graph.getNodes()) { + for (Node succ : node.successors()) { + writer.println(" n" + nodeIds.get(node) + " -> n" + nodeIds.get(succ) + " (control)"); + } + } + + writer.println(); + writer.println("CFG BLOCKS:"); + for (HIRBlock block : cfg.getBlocks()) { + writer.print(" B" + block.getId() + ": ["); + boolean first = true; + for (Node n : block.getNodes()) { + if (!first) writer.print(", "); + writer.print("n" + nodeIds.get(n)); + first = false; + } + writer.print("] -> "); + + if (block.getSuccessorCount() == 0) { + writer.print("(exit)"); + } else { + for (int i = 0; i < block.getSuccessorCount(); i++) { + if (i > 0) writer.print(", "); + writer.print("B" + block.getSuccessorAt(i).getId()); + } + } + writer.println(); + } + } + } + + public void exportToDot(StructuredGraph graph, AnalysisMethod method, String outputPath) throws IOException { + try (PrintWriter writer = new PrintWriter(new FileWriter(outputPath))) { + writer.println("digraph G {"); + writer.println(" rankdir=TB;"); + writer.println(" node [shape=box];"); + writer.println(" label=\"" + method.format("%H.%n(%p)") + "\";"); + writer.println(); + + Map nodeIds = new HashMap<>(); + int id = 0; + for (Node node : graph.getNodes()) { + nodeIds.put(node, id++); + } + + for (Node node : graph.getNodes()) { + String label = node.getNodeClass().shortName(); + String color = "black"; + String style = "solid"; + + if (node instanceof PhiNode) { + color = "blue"; + style = "filled"; + } else if (node instanceof LoopBeginNode) { + color = "red"; + style = "filled"; + } else if (node instanceof LoopEndNode) { + color = "orange"; + style = "filled"; + } + + writer.println(" n" + nodeIds.get(node) + " [label=\"" + label + "\\nn" + nodeIds.get(node) + + "\", color=" + color + ", style=" + style + "];"); + } + + writer.println(); + + for (Node node : graph.getNodes()) { + for (Node succ : node.successors()) { + writer.println(" n" + nodeIds.get(node) + " -> n" + nodeIds.get(succ) + " [style=solid, color=black];"); + } + } + + for (Node node : graph.getNodes()) { + for (Node input : node.inputs()) { + writer.println(" n" + nodeIds.get(input) + " -> n" + nodeIds.get(node) + " [style=dashed, color=blue];"); + } + } + + writer.println("}"); + } + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/LoggerVerbosity.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/LoggerVerbosity.java new file mode 100644 index 000000000000..466f3340f8b8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/log/LoggerVerbosity.java @@ -0,0 +1,17 @@ +package com.oracle.svm.hosted.analysis.ai.log; + +/** + * Logger verbosity levels ordered from least verbose to most verbose. + * When a verbosity level is set, all messages at that level AND lower levels are logged. + * For example, DEBUG logs everything, while CHECKER logs only checker messages. + */ +public enum LoggerVerbosity { + CHECKER, /* Log basic checker info -> amount of warnings and errors */ + CHECKER_ERR, /* Log detailed checker errors */ + CHECKER_WARN, /* Log detailed checker warnings */ + FACT, /* Log produced facts */ + SUMMARY, /* Log function summaries */ + INFO, /* Log analysis information */ + DEBUG, + WARN, SILENT, ERROR, /* Log debug information - most verbose, logs everything */ +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/clean.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/clean.sh new file mode 100755 index 000000000000..1f07db25c848 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/clean.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rm -rf graal_dumps/ +shopt -s extglob +rm !(*.sh) \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compare.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compare.sh new file mode 100755 index 000000000000..ad9127ba65d4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compare.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Compare Analyses - Run both intra and inter procedural +# Compares results to show the benefit of context sensitivity + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo "Example: $0 LoopSummation" + exit 1 +fi + +MAIN_CLASS=$1 +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +COMPARE_DIR="compare_${MAIN_CLASS}_${TIMESTAMP}" + +mkdir -p "$COMPARE_DIR" + +echo "========================================" +echo "Running Comparison Analysis" +echo "Main Class: $MAIN_CLASS" +echo "Output: $COMPARE_DIR/" +echo "========================================" + +# Run intraprocedural +echo "" +echo "[1/2] Running intraprocedural analysis..." +native-image \ + -H:+RunAbstractInterpretation \ + -H:+IntraproceduralAnalysis \ + -H:-InterproceduralAnalysis \ + -H:AILogFilePath="$COMPARE_DIR/intra.log" \ + -H:+PrintAIStatistics \ + $MAIN_CLASS > "$COMPARE_DIR/intra_build.log" 2>&1 && echo " ✓ Intra complete" || echo " ✗ Intra failed" + +# Run interprocedural +echo "[2/2] Running interprocedural analysis..." +native-image \ + -H:+RunAbstractInterpretation \ + -H:+InterproceduralAnalysis \ + -H:-IntraproceduralAnalysis \ + -H:AILogFilePath="$COMPARE_DIR/inter.log" \ + -H:+PrintAIStatistics \ + $MAIN_CLASS > "$COMPARE_DIR/inter_build.log" 2>&1 && echo " ✓ Inter complete" || echo " ✗ Inter failed" + +echo "" +echo "========================================" +echo "Comparison Complete!" +echo "Intra: $COMPARE_DIR/intra.log" +echo "Inter: $COMPARE_DIR/inter.log" +echo "" +echo "Compare with:" +echo " diff $COMPARE_DIR/intra.log $COMPARE_DIR/inter.log" +echo " grep 'optimized' $COMPARE_DIR/*.log" +echo "========================================" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compile-test-suite.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compile-test-suite.sh new file mode 100755 index 000000000000..6eed2c6fa7f4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/compile-test-suite.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +SUITE=$1 +javac -d ~/graal/absint-tests/out ~/graal/absint-tests/src/$SUITE/intra/*.java +javac -d ~/graal/absint-tests/out ~/graal/absint-tests/src/$SUITE/inter/*.java diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/prepare.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/prepare.sh new file mode 100755 index 000000000000..3223271998f6 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/prepare.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +mx build +mx igv \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-batch.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-batch.sh new file mode 100755 index 000000000000..4a00cb439b3c --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-batch.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Analyzes all Java files in the directory + +set -e + +TEST_DIR=${1:-.} +OUTPUT_DIR="batch_results_$(date +%Y%m%d_%H%M%S)" + +echo "========================================" +echo "Running AI Batch Analysis" +echo "Test Directory: $TEST_DIR" +echo "Output: $OUTPUT_DIR/" +echo "========================================" + +mkdir -p "$OUTPUT_DIR" + +JAVA_FILES=$(find "$TEST_DIR" -maxdepth 1 -name "*.java" -type f) + +if [ -z "$JAVA_FILES" ]; then + echo "No Java files found in $TEST_DIR" + exit 1 +fi + +COUNT=0 +SUCCESS=0 +FAILED=0 + +for file in $JAVA_FILES; do + CLASS_NAME=$(basename "$file" .java) + COUNT=$((COUNT + 1)) + echo "" + echo "[$COUNT] Analyzing $CLASS_NAME..." + + if mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:PrintGraph=Network \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:+RunAbstractInterpretation \ + -H:+InterproceduralAnalysis \ + -H:AILogLevel=INFO \ + -H:+AILogToFile \ + -H:AILogFilePath="$OUTPUT_DIR/${CLASS_NAME}.log" \ + -H:+PrintOptimizationSummary \ + $CLASS_NAME > "$OUTPUT_DIR/${CLASS_NAME}_build.log" 2>&1; then + SUCCESS=$((SUCCESS + 1)) + echo " ✓ Success" + else + FAILED=$((FAILED + 1)) + echo " ✗ Failed (see $OUTPUT_DIR/${CLASS_NAME}_build.log)" + fi +done + +echo "" +echo "========================================" +echo "Batch Analysis Complete!" +echo "Total: $COUNT | Success: $SUCCESS | Failed: $FAILED" +echo "Results: $OUTPUT_DIR/" +echo "========================================" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-bounds.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-bounds.sh new file mode 100755 index 000000000000..16d5ac7d9945 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-bounds.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Bounds Check Elimination Focus +# Optimized for array bounds check removal + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-args...]" + echo "Example: $0 BoundsAllContextSafe" + exit 1 +fi + +MAIN_CLASS=$1 +shift + +echo "========================================" +echo "Running AI: Bounds Check Elimination" +echo "Main Class: $MAIN_CLASS" +echo "Optimizations: Bounds checks only" +echo "========================================" + +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:PrintGraph=Network \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:+RunAbstractInterpretation \ + -H:+InterproceduralAnalysis \ + -H:+EnableBoundsCheckElimination \ + -H:-EnableConstantPropagation \ + -H:-EnableDeadBranchElimination \ + -H:-EnableConstantMethodInlining \ + -H:+TrackArrayLengths \ + -H:MaxRecursionDepth=10 \ + -H:AILogLevel=INFO \ + -H:+AILogToFile \ + -H:AILogFilePath=bounds_${MAIN_CLASS}.log \ + -H:+PrintOptimizationSummary \ + "$@" \ + $MAIN_CLASS + +echo "" +echo "Bounds check analysis complete!" +echo "Check bounds_${MAIN_CLASS}.log for eliminated checks" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-constants.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-constants.sh new file mode 100755 index 000000000000..28f36688dde5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-constants.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Constant Propagation Focus +# Optimized for constant folding and method inlining + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-args...]" + echo "Example: $0 Factorial" + exit 1 +fi + +MAIN_CLASS=$1 +shift + +echo "========================================" +echo "Running AI: Constant Propagation" +echo "Main Class: $MAIN_CLASS" +echo "Optimizations: Constants & inlining" +echo "========================================" + +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:PrintGraph=Network \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:+RunAbstractInterpretation \ + -H:+InterproceduralAnalysis \ + -H:+EnableConstantPropagation \ + -H:+EnableConstantMethodInlining \ + -H:+EnableDeadBranchElimination \ + -H:-EnableBoundsCheckElimination \ + -H:MaxRecursionDepth=10 \ + -H:AILogLevel=INFO \ + -H:+AILogToFile \ + -H:AILogFilePath=const_${MAIN_CLASS}.log \ + -H:+PrintOptimizationSummary \ + "$@" \ + $MAIN_CLASS + +echo "" +echo "Constant propagation analysis complete!" +echo "Check const_${MAIN_CLASS}.log for folded constants" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-debug.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-debug.sh new file mode 100755 index 000000000000..825e03383657 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-debug.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Debug Mode AI Analysis +# Runs AI with detailed logging and IGV dumps + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-args...]" + echo "Example: $0 LoopSummation" + exit 1 +fi + +MAIN_CLASS=$1 +shift + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOG_FILE="debug_${MAIN_CLASS}_${TIMESTAMP}.log" +JSON_DIR="json_graphs_${MAIN_CLASS}_${TIMESTAMP}" + +echo "========================================" +echo "Running DEBUG AI Analysis" +echo "Main Class: $MAIN_CLASS" +echo "Log File: $LOG_FILE" +echo "JSON Graphs: $JSON_DIR" +echo "========================================" + +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:PrintGraph=Network \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:+RunAbstractInterpretation \ + -H:AILogLevel=DEBUG \ + -H:+AILogToConsole \ + -H:+AILogToFile \ + -H:AILogFilePath="$LOG_FILE" \ + -H:+AIEnableIGVDump \ + -H:+AIExportGraphToJSON \ + -H:AIJSONExportPath="$JSON_DIR" \ + -H:+PrintAIStatistics \ + -H:+PrintOptimizationSummary \ + -H:+PrintTopOptimizedMethods \ + "$@" \ + $MAIN_CLASS + +echo "" +echo "========================================" +echo "Debug Analysis Complete!" +echo "Log file: $LOG_FILE" +echo "JSON graphs: $JSON_DIR/" +echo "IGV: Connect to localhost:4445" +echo "========================================" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-inter.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-inter.sh new file mode 100755 index 000000000000..e910247d27e3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-inter.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [recursion-depth] [additional-args...]" + echo "Example: $0 Factorial 10" + echo "Default recursion depth: 10" + exit 1 +fi + +MAIN_CLASS=$1 +RECURSION_DEPTH=${2:-10} +shift +shift 2>/dev/null || shift + +echo "========================================" +echo "Running Interprocedural AI Analysis" +echo "Main Class: $MAIN_CLASS" +echo "Recursion Depth: $RECURSION_DEPTH" +echo "========================================" + +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:PrintGraph=Network \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:+RunAbstractInterpretation \ + -H:+InterproceduralAnalysis \ + -H:-IntraproceduralAnalysis \ + -H:MaxRecursionDepth=$RECURSION_DEPTH \ + -H:MaxCallStackDepth=15 \ + -H:KCFADepth=3 \ + -H:AILogLevel=INFO \ + -H:+AILogToFile \ + -H:AILogFilePath=inter_${MAIN_CLASS}.log \ + -H:+PrintAIStatistics \ + -H:+PrintTopOptimizedMethods \ + "$@" \ + $MAIN_CLASS + +echo "" +echo "Interprocedural analysis complete!" +echo "Check inter_${MAIN_CLASS}.log for details" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-intra.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-intra.sh new file mode 100755 index 000000000000..50e47f5639bd --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint-intra.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-args...]" + echo "Example: $0 RecursiveFibonacci" + exit 1 +fi + +MAIN_CLASS=$1 +shift + +echo "========================================" +echo "Running Intraprocedural AI Analysis" +echo "Main Class: $MAIN_CLASS" +echo "========================================" + +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS \ + -H:+ReportExceptionStackTraces \ + -H:Log=AbstractInterpretation \ + -H:Dump=:2 \ + -H:MethodFilter=$MAIN_CLASS.* \ + -H:PrintGraph=Network \ + -H:+RunAbstractInterpretation \ + -H:+IntraproceduralAnalysis \ + -H:-InterproceduralAnalysis \ + -H:+AIEnableIGVDump \ + -H:AILogLevel=DEBUG \ + -H:+AILogToFile \ + -H:AILogFilePath=intra_${MAIN_CLASS}.log \ + -H:+PrintOptimizationSummary \ + "$@" \ + $MAIN_CLASS + +echo "" +echo "Intraprocedural analysis complete!" +echo "Check intra_${MAIN_CLASS}.log for details" + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint.sh new file mode 100755 index 000000000000..5b6580165618 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-absint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +MAIN_CLASS=$1 +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS -H:+ReportExceptionStackTraces -H:Log=AbstractInterpretation -H:Dump=:2 -H:PrintGraph=Network -H:MethodFilter=$MAIN_CLASS.* -H:+RunAbsint diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-native-image.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-native-image.sh new file mode 100755 index 000000000000..73a5d52440df --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/run-native-image.sh @@ -0,0 +1,3 @@ +#!/bin/sh +MAIN_CLASS=$1 +mx native-image -cp ~/graal/absint-tests/out $MAIN_CLASS -H:+ReportExceptionStackTraces -H:Log=AbstractInterpretation -H:Dump=:2 -H:PrintGraph=Network -H:MethodFilter=$MAIN_CLASS.* diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/unittest.sh b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/unittest.sh new file mode 100755 index 000000000000..e751e85042a3 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/scripts/unittest.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +mx build +mx unittest test.ai diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/AbstractInterpretationStatistics.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/AbstractInterpretationStatistics.java new file mode 100644 index 000000000000..8cf690c7a48f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/AbstractInterpretationStatistics.java @@ -0,0 +1,170 @@ +package com.oracle.svm.hosted.analysis.ai.stats; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.analysis.AbstractInterpretationServices; + +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Aggregates high-level statistics for an abstract interpretation run and any checker/applier + * optimizations that followed. + */ +public class AbstractInterpretationStatistics { + // TODO: maybe we should think about how to print the method filter statistics perhaps + private final EnumMap globalOptCounters = new EnumMap<>(OptimizationKind.class); + private final ConcurrentHashMap methodStats = new ConcurrentHashMap<>(); + + public static final class MethodStats { + public final EnumMap optCounters = new EnumMap<>(OptimizationKind.class); + + public boolean anyOptimization() { + return optCounters.values().stream().anyMatch(v -> v != null && v > 0); + } + + public int get(OptimizationKind kind) { + return optCounters.getOrDefault(kind, 0); + } + + public void add(OptimizationKind kind, int delta) { + if (delta <= 0) return; + optCounters.merge(kind, delta, Integer::sum); + } + + @Override + public String toString() { + StringJoiner sj = new StringJoiner(", ", "{", "}"); + for (OptimizationKind k : OptimizationKind.values()) { + int v = get(k); + if (v > 0) { + sj.add(k.label() + "=" + v); + } + } + if (sj.length() == 2) { + sj.add("no-optimizations"); + } + return sj.toString(); + } + } + + public AbstractInterpretationStatistics() { + for (OptimizationKind k : OptimizationKind.values()) { + globalOptCounters.put(k, 0); + } + } + + private MethodStats statsFor(AnalysisMethod method) { + Objects.requireNonNull(method, "method"); + return methodStats.computeIfAbsent(method, m -> new MethodStats()); + } + + private void addMethodOpt(AnalysisMethod method, OptimizationKind kind, int count) { + if (count <= 0) return; + MethodStats ms = statsFor(method); + ms.add(kind, count); + globalOptCounters.merge(kind, count, Integer::sum); + } + + public void addMethodBoundsEliminated(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.BOUNDS_CHECK_ELIMINATED, count); + } + + public void addMethodConstantsStamped(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.CONSTANT_STAMP_TIGHTENED, count); + } + + public void addMethodConstantsPropagated(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.CONSTANT_PROPAGATED, count); + } + + public void addMethodBranchesFoldedTrue(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.BRANCH_FOLDED_TRUE, count); + } + + public void addMethodBranchesFoldedFalse(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.BRANCH_FOLDED_FALSE, count); + } + + public void addMethodInvokesReplaced(AnalysisMethod method, int count) { + addMethodOpt(method, OptimizationKind.INVOKE_REPLACED_WITH_CONSTANT, count); + } + + public void merge(AbstractInterpretationStatistics other) { + if (other == null) return; + for (var e : other.globalOptCounters.entrySet()) { + globalOptCounters.merge(e.getKey(), e.getValue(), Integer::sum); + } + for (Map.Entry e : other.methodStats.entrySet()) { + MethodStats dst = statsFor(e.getKey()); + MethodStats src = e.getValue(); + for (OptimizationKind k : OptimizationKind.values()) { + int v = src.get(k); + if (v > 0) { + dst.add(k, v); + } + } + } + } + + private int getNumOfOptimizedMethods() { + return Math.toIntExact(methodStats.entrySet().stream().filter(entry -> entry.getValue().anyOptimization()).count()); + } + + @Override + public String toString() { + StringJoiner sj = new StringJoiner(", ", "[AI Stats] ", ""); + sj.add("methodsAnalyzed=" + methodStats.size()) + .add("methodsOptimized=" + getNumOfOptimizedMethods()); + for (OptimizationKind k : OptimizationKind.values()) { + int v = globalOptCounters.getOrDefault(k, 0); + if (v > 0) { + sj.add(k.label() + "=" + v); + } + } + return sj.toString(); + } + + public String toMultilineReport() { + StringBuilder sb = new StringBuilder(512); + sb.append("Methods analyzed: ").append(AbstractInterpretationServices.getInstance().getTouchedMethods().size()).append('\n'); + sb.append("Methods optimized: ").append(getNumOfOptimizedMethods()).append('\n'); + + sb.append("Optimizations performed: "); + boolean first = true; + for (OptimizationKind k : OptimizationKind.values()) { + int v = globalOptCounters.getOrDefault(k, 0); + if (v <= 0) continue; + if (!first) sb.append(", "); + sb.append(k.label()).append("=").append(v); + first = false; + } + if (first) { + sb.append("none"); + } + sb.append('\n'); + + sb.append("\nMost-optimized methods:\n"); + methodStats.entrySet().stream() + .filter(entry -> entry.getValue().anyOptimization()) + .sorted((o1, o2) -> Integer.compare(totalOptimizations(o2.getValue()), totalOptimizations(o1.getValue()))) + .limit(10) + .forEach(entry -> sb.append(" ") + .append(entry.getKey().wrapped.format("%H.%n(%p)")) + .append(" ") + .append(entry.getValue()) + .append('\n')); + return sb.toString(); + } + + private static int totalOptimizations(MethodStats ms) { + int sum = 0; + for (OptimizationKind k : OptimizationKind.values()) { + sum += ms.get(k); + } + return sum; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/OptimizationKind.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/OptimizationKind.java new file mode 100644 index 000000000000..d14fa9901bda --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/stats/OptimizationKind.java @@ -0,0 +1,25 @@ +package com.oracle.svm.hosted.analysis.ai.stats; + +/** + * Lists high-level optimization kinds performed by abstract interpretation-based + * checkers/appliers. + */ +public enum OptimizationKind { + BOUNDS_CHECK_ELIMINATED("boundsEliminated"), + CONSTANT_STAMP_TIGHTENED("constantsStamped"), + CONSTANT_PROPAGATED("constantsPropagated"), + BRANCH_FOLDED_TRUE("branchesTrue"), + BRANCH_FOLDED_FALSE("branchesFalse"), + INVOKE_REPLACED_WITH_CONSTANT("invokesReplaced"); + + private final String label; + + OptimizationKind(String label) { + this.label = label; + } + + public String label() { + return label; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextKey.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextKey.java new file mode 100644 index 000000000000..9895475c43ef --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextKey.java @@ -0,0 +1,23 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; + +import java.util.Objects; + +public record ContextKey(AnalysisMethod method, int callStringHash, int depth) { + + @Override + public boolean equals(Object o) { + if (!(o instanceof ContextKey(AnalysisMethod method1, int stringHash, int depth1))) return false; + return callStringHash == stringHash && depth == depth1 && Objects.equals(method, method1); + } + + @Override + public String toString() { + return "ContextKey{" + + "method=" + method + + ", callStringHash=" + callStringHash + + ", depth=" + depth + + '}'; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextSummary.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextSummary.java new file mode 100644 index 000000000000..535410f351a5 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/ContextSummary.java @@ -0,0 +1,33 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +/** + * Per-context summary for an {@link AnalysisMethod}. + *

+ * A context is identified by a {@link ContextKey} and typically represents one + * abstract invocation context (e.g., a call string, receiver type, etc.). + */ +public final class ContextSummary> { + + private final ContextKey contextKey; + private Summary summary; + + public ContextSummary(ContextKey contextKey, Summary summary) { + this.contextKey = contextKey; + this.summary = summary; + } + + public ContextKey contextKey() { + return contextKey; + } + + public Summary summary() { + return summary; + } + + public void setSummary(Summary otherSummary) { + this.summary = otherSummary; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/MethodSummary.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/MethodSummary.java new file mode 100644 index 000000000000..d4b35220f92e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/MethodSummary.java @@ -0,0 +1,61 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +import java.util.HashMap; +import java.util.Map; + +/** + * Holds all context-sensitive summaries for a single method. + */ +public final class MethodSummary> { + + private final AnalysisMethod method; + private final Map> contexts = new HashMap<>(); + private AbstractState stateAcrossAllContexts = null; + + public MethodSummary(AnalysisMethod method) { + this.method = method; + } + + public AnalysisMethod getMethod() { + return method; + } + + public ContextSummary getOrCreate(ContextKey key, Summary preConditionSummary) { + return contexts.computeIfAbsent(key, k -> new ContextSummary<>(k, preConditionSummary)); + } + + public ContextSummary get(ContextKey key) { + return contexts.get(key); + } + + public Map> getAllContexts() { + return contexts; + } + + public AbstractState getStateAcrossAllContexts() { + return stateAcrossAllContexts; + } + + public void joinWithContextState(AbstractState other) { + if (stateAcrossAllContexts == null) { + stateAcrossAllContexts = other.copyOf(); + return; + } + stateAcrossAllContexts.joinWith(other); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MethodSummary{" + method + "}\n"); + contexts.forEach((k, v) -> sb.append(k).append(" -> ").append(v).append('\n')); + return sb.toString(); + } + + public Map> getContexts() { + return contexts; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/Summary.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/Summary.java new file mode 100644 index 000000000000..feff858e1226 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/Summary.java @@ -0,0 +1,56 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.fixpoint.state.AbstractState; + +/** + * Represents a summary of a method. It is used to avoid reanalyzing the method's body at every call site. + * For instance when the analysis has already analyzed method foo(), we already know the effects of it, and it is not necessary to reanalyze it. + * New summaries are created in the provided {@link SummaryFactory} and then checked for subsumption in {@link SummaryRepository}. + * When we are creating a summary, we only know the abstract context at the invocation site, actual + formal parameters + * Therefore, {@link SummaryFactory} can only create incomplete summaries (summaries only with their pre-condition known). + * After the framework does the necessary fixpoint computation of the method body, we know everything to create a complete summary in {@code finalizeSummary}. + * + * @param type of the derived {@link AbstractDomain} used in abstract interpretation + */ +public interface Summary> { + + /** + * Returns the pre-condition of the summary, which is created by a corresponding {@link SummaryFactory}. + * Pre-condition of a method summary is the relevant abstract context at the entry point of the method . + * {@link SummaryFactory} Implementations can choose to keep only the relevant information from the + * caller abstract context and create a compact pre-condition for the callee. + */ + Domain getPreCondition(); + + /** + * Returns the post-condition of the summary. + * This is the abstract context at method exit (join of all normal returns). + * Implementations may omit storing information not needed by callers when applying the summary. + */ + Domain getPostCondition(); + + /** + * Checks if this summary covers (subsumes) the other summary's pre-condition. + * + * @param other the other {@link Summary} to compare with + * @return true if this summary subsumes other summary + */ + boolean subsumesSummary(Summary other); + + /** + * This method is supposed to modify the post-condition of a summary + * Called after the callee fixpoint finishes; implementations should populate post-condition accordingly. + * Must handle BOT/TOP callee states conservatively. + */ + void finalizeSummary(AbstractState calleeAbstractState); + + /** + * Apply this summary back to the caller abstract context, + * this is necessary to propagate analysis results back to the caller. + * + * @param domain caller domain + * @return resulting domain + */ + Domain applySummary(Domain domain); +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryFactory.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryFactory.java new file mode 100644 index 000000000000..eabf9e02999b --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryFactory.java @@ -0,0 +1,37 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import com.oracle.svm.hosted.analysis.ai.interpreter.AbstractInterpreter; +import jdk.graal.compiler.nodes.Invoke; + +import java.util.List; + +/** + * Represents a factory for creating {@link Summary} instances. + * This is used to create summaries for analysisMethod calls during analysis. + * + * @param type of the derived {@link AbstractDomain} used in the analysis + */ +public interface SummaryFactory> { + + /** + * Creates a {@link Summary} from the abstract context at a given call site. + * The created summary (alongside the context key) will be then used for lookup in {@link SummaryRepository}, more specifically, + * the framework will check if this summary is subsumed by any of the summaries in the cache. + * When calling {@code createSummary} we don't know what the post-condition of the summary will be yet, + * so implementation can either leave the post-condition empty or set it to some default value (TOP value of the domain most of the time). + * The summaryFactory implementations need to be able to deal with BOT/TOP values in {@code callerPreCondition}. + * NOTE: It should only be necessary to have the abstract context at the call site and arguments to create a summary. + * Creation of a summary can include: + * Taking only a part of the abstract context, that is relevant for the invoke. + * Renaming the formal arguments to actual arguments, etc. + * + * @param invoke contains information about the invocation + * @param callerPreCondition the abstract context precondition at the call site + * @param arguments converted to the used abstract domain using the provided {@link AbstractInterpreter} + * @return a {@link Summary} containing only the pre-condition of the summary + */ + Summary createSummary(Invoke invoke, + Domain callerPreCondition, + List arguments); +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryManager.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryManager.java new file mode 100644 index 000000000000..9f3c799e0371 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryManager.java @@ -0,0 +1,190 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; +import jdk.graal.compiler.nodes.Invoke; + +import java.util.List; +import java.util.Objects; + +/** + * Manages summaries for methods. + * + *

This is a thin facade over {@link SummaryFactory} and {@link SummaryRepository}. + * It is responsible for + *

    + *
  • creating pre-condition summaries for calls,
  • + *
  • looking up an existing context-sensitive summary for a callee,
  • + *
  • and registering finalized summaries once a callee has been analyzed.
  • + *
+ * + * @param the type of derived {@link AbstractDomain} used in the analysis + */ +public final class SummaryManager> { + + private final SummaryFactory summaryFactory; + private final SummaryRepository summaryRepository; + + public SummaryManager(SummaryFactory summaryFactory) { + this(summaryFactory, new SummaryRepository<>()); + } + + public SummaryManager(SummaryFactory summaryFactory, SummaryRepository summaryRepository) { + this.summaryFactory = summaryFactory; + this.summaryRepository = summaryRepository; + } + + public SummaryRepository getSummaryRepository() { + return summaryRepository; + } + + /** + * Create a new pre-condition-only summary for a given invoke. + */ + public Summary createSummary(Invoke invoke, + Domain callerPreCondition, + List domainArguments) { + return summaryFactory.createSummary(invoke, callerPreCondition, domainArguments); + } + + /** + * Lookup the most general summary for {@code calleeMethod} that subsumes the given + * {@code summaryPrecondition}, or {@code null} if none exists. + */ + public Summary getSummary(AnalysisMethod calleeMethod, Summary summaryPrecondition) { + MethodSummary methodSummary = summaryRepository.get(calleeMethod); + if (methodSummary == null) { + return null; + } + + Summary mostGeneral = null; + for (ContextSummary ctx : methodSummary.getAllContexts().values()) { + Summary existing = ctx.summary(); + if (existing.subsumesSummary(summaryPrecondition)) { + if (mostGeneral == null || existing.subsumesSummary(mostGeneral)) { + mostGeneral = existing; + } + } + } + return mostGeneral; + } + + /** + * Register a finalized summary for a given context. + */ + public void putSummary(AnalysisMethod calleeMethod, ContextKey contextKey, Summary summary) { + MethodSummary methodSummary = summaryRepository.getOrCreate(calleeMethod); + methodSummary.getOrCreate(contextKey, summary); + } + + public void mergeFrom(SummaryManager other) { + if (other == null || other == this) { + return; + } + + SummaryRepository otherRepo = other.getSummaryRepository(); + if (otherRepo == null) { + return; + } + + for (var entry : otherRepo.getMethodSummaryMap().entrySet()) { + AnalysisMethod method = entry.getKey(); + MethodSummary otherMethodSummary = entry.getValue(); + if (otherMethodSummary == null) { + continue; + } + + MethodSummary thisMethodSummary = summaryRepository.getOrCreate(method); + + // Merge aggregate state across contexts + if (otherMethodSummary.getStateAcrossAllContexts() != null) { + thisMethodSummary.joinWithContextState(otherMethodSummary.getStateAcrossAllContexts()); + } + + // Merge individual contexts with subsumption-aware reconciliation + for (var ctxEntry : otherMethodSummary.getAllContexts().entrySet()) { + ContextKey ctxKey = ctxEntry.getKey(); + ContextSummary otherCtx = ctxEntry.getValue(); + if (otherCtx == null) { + continue; + } + + Summary otherSummary = otherCtx.summary(); + ContextSummary thisCtx = thisMethodSummary.getContexts().get(ctxKey); + + if (thisCtx == null) { + // No existing entry under this key: insert as-is + thisMethodSummary.getOrCreate(ctxKey, otherSummary); + continue; + } + + Summary thisSummary = thisCtx.summary(); + if (thisSummary == null && otherSummary != null) { + // Prefer non-null + thisCtx.setSummary(otherSummary); + continue; + } + if (otherSummary == null) { + // Nothing to merge + continue; + } + + // If identical by reference or equals, skip duplicate + if (thisSummary == otherSummary || Objects.equals(thisSummary, otherSummary)) { + continue; + } + + // Subsumption checks: keep the more general one under the same key + if (thisSummary.subsumesSummary(otherSummary)) { + // Existing is as general or more general: keep it + continue; + } + if (otherSummary.subsumesSummary(thisSummary)) { + // Replace with more general summary + thisCtx.setSummary(otherSummary); + continue; + } + + // Incomparable: keep both by inserting other under an alternate deterministic key + ContextKey altKey = deriveAlternateKey(method, ctxKey, otherSummary); + if (!thisMethodSummary.getContexts().containsKey(altKey)) { + thisMethodSummary.getOrCreate(altKey, otherSummary); + } + } + } + } + + public void mergeAggregateOnly(SummaryManager other) { + if (other == null || other == this) { + return; + } + SummaryRepository otherRepo = other.getSummaryRepository(); + if (otherRepo == null) { + return; + } + + for (var entry : otherRepo.getMethodSummaryMap().entrySet()) { + AnalysisMethod method = entry.getKey(); + MethodSummary otherMethodSummary = entry.getValue(); + if (otherMethodSummary == null) { + continue; + } + MethodSummary thisMethodSummary = summaryRepository.getOrCreate(method); + if (otherMethodSummary.getStateAcrossAllContexts() != null) { + thisMethodSummary.joinWithContextState(otherMethodSummary.getStateAcrossAllContexts()); + } + // Intentionally skip merging per-context entries. + } + } + + private ContextKey deriveAlternateKey(AnalysisMethod method, ContextKey baseKey, Summary otherSummary) { + // Use base depth and a stable composite hash of baseKey and other pre-condition + int depth = baseKey.depth(); + int preHash = 0; + if (otherSummary != null && otherSummary.getPreCondition() != null) { + preHash = Objects.hash(otherSummary.getPreCondition()); + } + int composite = Objects.hash(baseKey, preHash); + return new ContextKey(method, composite, depth); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryRepository.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryRepository.java new file mode 100644 index 000000000000..f89f054de894 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/ai/summary/SummaryRepository.java @@ -0,0 +1,35 @@ +package com.oracle.svm.hosted.analysis.ai.summary; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.svm.hosted.analysis.ai.domain.AbstractDomain; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Repository of method summaries per analysis method. + */ +public final class SummaryRepository> { + + private final Map> methods = new HashMap<>(); + + public MethodSummary getOrCreate(AnalysisMethod method) { + return methods.computeIfAbsent(method, MethodSummary::new); + } + + public MethodSummary get(AnalysisMethod method) { + return methods.get(method); + } + + public List getMethods() { + return new ArrayList<>(methods.keySet()); + } + + public Map> getMethodSummaryMap() { + return methods; + } +} + diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageHeap.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageHeap.java index 76f6d93136ad..3d4c6551b949 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageHeap.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/NativeImageHeap.java @@ -1186,4 +1186,4 @@ public boolean isWritable() { public long getSize() { throw VMError.shouldNotReachHereAtRuntime(); // ExcludeFromJacocoGeneratedReport } -} +} \ No newline at end of file