Skip to content

Commit c579002

Browse files
authored
Add an integer range analysis (#17)
1 parent 2577ed1 commit c579002

7 files changed

Lines changed: 265 additions & 0 deletions

File tree

asl_xdsl/analysis/__init__.py

Whitespace-only changes.

asl_xdsl/analysis/integer_range.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from dataclasses import dataclass, field
5+
6+
from xdsl.ir import Operation, Region, SSAValue
7+
from xdsl.traits import OpTrait
8+
9+
10+
@dataclass(frozen=True)
11+
class IntegerRange:
12+
"""
13+
The range of an integer value, defined by its lower and upper bounds (inclusive).
14+
If either bound is None, it means that the bound is not known.
15+
"""
16+
17+
lower_bound: int | None
18+
upper_bound: int | None
19+
20+
def is_empty(self) -> bool:
21+
"""Check if the range is empty."""
22+
if self.lower_bound is None or self.upper_bound is None:
23+
return False
24+
return self.lower_bound > self.upper_bound
25+
26+
def get_as_constant(self) -> int | None:
27+
"""
28+
Get the range as a constant integer value if it is a single value range.
29+
If the range is not a single value, returns None.
30+
"""
31+
if self.lower_bound is not None and self.upper_bound is not None:
32+
if self.lower_bound == self.upper_bound:
33+
return self.lower_bound
34+
return None
35+
36+
@staticmethod
37+
def top() -> IntegerRange:
38+
"""
39+
Get the top range, which is the range that covers all possible integer values.
40+
This is used when no specific range is known for a value.
41+
"""
42+
return IntegerRange(None, None)
43+
44+
@staticmethod
45+
def bottom() -> IntegerRange:
46+
"""
47+
Get the bottom range, which is the empty range.
48+
This is used when a value is known to be outside of any possible integer range.
49+
"""
50+
return IntegerRange(1, -1)
51+
52+
def __contains__(self, value: int) -> bool:
53+
"""Check if a value is within the range."""
54+
if self.lower_bound is not None:
55+
if value < self.lower_bound:
56+
return False
57+
if self.upper_bound is not None:
58+
if value > self.upper_bound:
59+
return False
60+
return True
61+
62+
def __or__(self, other: IntegerRange) -> IntegerRange:
63+
"""Combine two integer ranges into one that covers both ranges."""
64+
if self.lower_bound is None:
65+
lower_bound = other.lower_bound
66+
elif other.lower_bound is None:
67+
lower_bound = self.lower_bound
68+
else:
69+
lower_bound = min(self.lower_bound, other.lower_bound)
70+
71+
if self.upper_bound is None:
72+
upper_bound = other.upper_bound
73+
elif other.upper_bound is None:
74+
upper_bound = self.upper_bound
75+
else:
76+
upper_bound = max(self.upper_bound, other.upper_bound)
77+
78+
return IntegerRange(lower_bound, upper_bound)
79+
80+
def __and__(self, other: IntegerRange) -> IntegerRange:
81+
"""Intersect two integer ranges into one that covers the intersection."""
82+
if self.lower_bound is None:
83+
lower_bound = other.lower_bound
84+
elif other.lower_bound is None:
85+
lower_bound = self.lower_bound
86+
else:
87+
lower_bound = max(self.lower_bound, other.lower_bound)
88+
89+
if self.upper_bound is None:
90+
upper_bound = other.upper_bound
91+
elif other.upper_bound is None:
92+
upper_bound = self.upper_bound
93+
else:
94+
upper_bound = min(self.upper_bound, other.upper_bound)
95+
96+
return IntegerRange(lower_bound, upper_bound)
97+
98+
99+
@dataclass
100+
class IntegerRangeAnalysis:
101+
"""An analysis that contains the integer ranges of integer values."""
102+
103+
ranges: dict[SSAValue, IntegerRange] = field(
104+
default_factory=dict[SSAValue, IntegerRange]
105+
)
106+
107+
def get_range(self, value: SSAValue) -> IntegerRange:
108+
"""
109+
Get the integer range of a value.
110+
If the value is not an integer type, returns None.
111+
"""
112+
return self.ranges.get(value, IntegerRange.top())
113+
114+
def set_range(self, value: SSAValue, integer_range: IntegerRange) -> None:
115+
"""
116+
Set the integer range of a value.
117+
If the value is not an integer type, this will raise an error.
118+
"""
119+
# No need to set the top range, as it is the default
120+
if integer_range == IntegerRange.top():
121+
return
122+
self.ranges[value] = integer_range
123+
124+
def compute_operation_analysis(self, op: Operation) -> None:
125+
"""
126+
Compute the integer ranges of the operation's results.
127+
This function will call the IntegerRangeTrait.compute_analysis method
128+
if the operation has the IntegerRangeTrait trait.
129+
"""
130+
trait = op.get_trait(IntegerRangeTrait)
131+
if trait is None:
132+
return
133+
134+
trait.compute_analysis(op, self)
135+
136+
def compute_single_block_region_analysis(self, region: Region) -> None:
137+
"""
138+
Compute the integer ranges of all the SSA values in an SSACFG region
139+
with a single block.
140+
"""
141+
assert len(region.blocks) == 1, "Region must have a single block"
142+
block = region.block
143+
144+
for op in block.ops:
145+
self.compute_operation_analysis(op)
146+
147+
148+
class IntegerRangeTrait(OpTrait, ABC):
149+
"""
150+
A trait that indicates that an operation can be used for an integer range analysis.
151+
In practice, this means that we can compute the lower and upper bounds of the
152+
operation's integer results based on its operands and attributes ranges.
153+
"""
154+
155+
@staticmethod
156+
@abstractmethod
157+
def compute_analysis(op: Operation, analysis: IntegerRangeAnalysis):
158+
"""
159+
Compute the integer ranges of the operation's results based on the
160+
integer ranges of its operands and attributes.
161+
"""
162+
...

asl_xdsl/dialects/asl.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
SymbolUserOpInterface,
4646
)
4747

48+
from asl_xdsl.analysis.integer_range import (
49+
IntegerRange,
50+
IntegerRangeAnalysis,
51+
IntegerRangeTrait,
52+
)
53+
4854

4955
@irdl_attr_definition
5056
class ConstraintExactAttr(ParametrizedAttribute):
@@ -308,6 +314,13 @@ def print_parameters(self, printer: Printer) -> None:
308314
printer.print(">")
309315

310316

317+
class ConstantIntIntegerRangeTrait(IntegerRangeTrait):
318+
@staticmethod
319+
def compute_analysis(op: Operation, analysis: IntegerRangeAnalysis) -> None:
320+
assert isinstance(op, ConstantIntOp), "Expected ConstantIntOp"
321+
analysis.set_range(op.res, IntegerRange(op.value.data, op.value.data))
322+
323+
311324
@irdl_op_definition
312325
class ConstantIntOp(IRDLOperation):
313326
"""A constant arbitrary-sized integer operation."""
@@ -317,6 +330,8 @@ class ConstantIntOp(IRDLOperation):
317330
value = prop_def(builtin.IntAttr)
318331
res = result_def(IntegerType)
319332

333+
traits = traits_def(ConstantIntIntegerRangeTrait())
334+
320335
def __init__(
321336
self, value: int | builtin.IntAttr, attr_dict: Mapping[str, Attribute] = {}
322337
):
@@ -573,6 +588,21 @@ class AlignIntOp(BinaryIntOp):
573588
name = "asl.align_int"
574589

575590

591+
class MoxPow2IntIntegerRangeTrait(IntegerRangeTrait):
592+
@staticmethod
593+
def compute_analysis(op: Operation, analysis: IntegerRangeAnalysis) -> None:
594+
assert isinstance(op, ModPow2IntOp), "Expected ModPow2IntOp"
595+
rhs_upper = analysis.get_range(op.rhs).upper_bound
596+
lhs_bounds = analysis.get_range(op.lhs)
597+
if rhs_upper is None:
598+
# If the rhs is unbounded, the result is not more bounded than the lhs.
599+
analysis.set_range(op.res, lhs_bounds)
600+
return
601+
602+
bound_from_rhs = IntegerRange(0, 2**rhs_upper - 1)
603+
analysis.set_range(op.res, lhs_bounds & bound_from_rhs)
604+
605+
576606
@irdl_op_definition
577607
class ModPow2IntOp(BinaryIntOp):
578608
"""
@@ -582,6 +612,8 @@ class ModPow2IntOp(BinaryIntOp):
582612

583613
name = "asl.mod_pow2_int"
584614

615+
traits = traits_def(MoxPow2IntIntegerRangeTrait())
616+
585617

586618
@irdl_op_definition
587619
class IsPow2IntOp(IRDLOperation):

asl_xdsl/tools/asl_opt.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ def register_all_dialects(self):
1717
self.ctx.load_dialect(ASLDepDialect)
1818

1919
def register_all_passes(self):
20+
def get_integer_range_analysis_pass():
21+
from asl_xdsl.transforms.test_integer_range_analysis import (
22+
TestIntegerRangeAnalysis,
23+
)
24+
25+
return TestIntegerRangeAnalysis
26+
27+
self.register_pass(
28+
"test-integer-range-analysis", get_integer_range_analysis_pass
29+
)
2030
return super().register_all_passes()
2131

2232
def register_all_targets(self):

asl_xdsl/transforms/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from dataclasses import dataclass
2+
3+
from xdsl.context import Context
4+
from xdsl.dialects.builtin import ArrayAttr, IntAttr, ModuleOp, NoneAttr
5+
from xdsl.ir import Attribute
6+
from xdsl.passes import ModulePass
7+
8+
from asl_xdsl.analysis.integer_range import IntegerRange, IntegerRangeAnalysis
9+
10+
11+
def bound_to_attr(bound: int | None) -> Attribute:
12+
"""Convert a bound (int or None) to an attribute."""
13+
if bound is None:
14+
return NoneAttr()
15+
return IntAttr(bound)
16+
17+
18+
def range_to_attr(integer_range: IntegerRange) -> Attribute:
19+
"""Convert an IntegerRange to an attribute."""
20+
return ArrayAttr(
21+
[
22+
bound_to_attr(integer_range.lower_bound),
23+
bound_to_attr(integer_range.upper_bound),
24+
]
25+
)
26+
27+
28+
@dataclass(frozen=True)
29+
class TestIntegerRangeAnalysis(ModulePass):
30+
"""
31+
Test the integer range analysis pass by adding integer range analysis information
32+
to the module using attributes.
33+
"""
34+
35+
name = "test-integer-range-analysis"
36+
37+
def apply(self, ctx: Context, op: ModuleOp) -> None:
38+
analysis = IntegerRangeAnalysis()
39+
analysis.compute_single_block_region_analysis(op.body)
40+
41+
for sub_op in op.walk():
42+
if not sub_op.results:
43+
continue
44+
result_ranges = [analysis.get_range(result) for result in sub_op.results]
45+
result_ranges_attrs = ArrayAttr(
46+
[range_to_attr(result) for result in result_ranges]
47+
)
48+
sub_op.attributes["__integer_ranges"] = result_ranges_attrs
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// RUN: asl-opt %s -p=test-integer-range-analysis | filecheck %s
2+
3+
builtin.module {
4+
5+
// CHECK: %unknown = "test.op"() {__integer_ranges = {{[}}[#none, #none]]} : () -> !asl.int
6+
%unknown = "test.op"() : () -> !asl.int
7+
8+
// CHECK-NEXT: %cst16 = asl.constant_int 16 {__integer_ranges = {{[}}[#builtin.int<16>, #builtin.int<16>]]}
9+
%cst16 = asl.constant_int 16
10+
11+
// CHECK-NEXT: %res = asl.mod_pow2_int %unknown, %cst16 : (!asl.int, !asl.int) -> !asl.int {__integer_ranges = {{[}}[#builtin.int<0>, #builtin.int<65535>]]}
12+
%res = asl.mod_pow2_int %unknown, %cst16 : (!asl.int, !asl.int) -> !asl.int
13+
}

0 commit comments

Comments
 (0)