diff --git a/TP2/example_interval_eval_exp.py b/TP2/example_interval_eval_exp.py new file mode 100644 index 0000000000000000000000000000000000000000..1203cab645be505a71cd14888374824eca277e68 --- /dev/null +++ b/TP2/example_interval_eval_exp.py @@ -0,0 +1,32 @@ +from syntax import * +from interval_analysis import eval_aexp, eval_bexp, abstract_env, Interval + +x: ArithExpr = AEVar("x") +y: ArithExpr = AEVar("y") + +# x - y * (x / y) +test_aexp: ArithExpr = AEBop(Bop.ADD,x, AEUop(Uop.OPP,AEBop(Bop.MUL,y,AEBop(Bop.DIV,x,y)))) + +# ! (x - y <= y * (x - x/y)) +test_bexp: BoolExpr = BENeg(BELeq(AEBop(Bop.ADD,x,AEUop(Uop.OPP,y)),test_aexp)) + +def test(env: abstract_env) -> None: + res_a = eval_aexp(env,test_aexp) + res_b = eval_bexp(env,test_bexp) + print(f'In environment { {var: str(i) for (var,i) in env.items()} }') + print(f"{test_aexp} -> {res_a}") + print(f"{test_bexp} -> {res_b}") + +test({ 'x': Interval(20,20), 'y': Interval(2,2) }) + +test({ 'x': Interval(10,20), 'y': Interval(1,2) }) + +test({ 'x': Interval(-30,0), 'y': Interval(-10,0) }) + +test({ 'x': Interval(-20,20), 'y': Interval(0,0) }) + +test({ 'x': Interval(0,20), 'y': Interval(2,5) }) + +test({ 'x': Interval(-10,20), 'y': Interval(-5,-2) }) + +test({}) diff --git a/TP2/example_interval_reduce.py b/TP2/example_interval_reduce.py new file mode 100644 index 0000000000000000000000000000000000000000..2d7cdda54e9d02cfd00b41b9a65d67b63f5120b9 --- /dev/null +++ b/TP2/example_interval_reduce.py @@ -0,0 +1,39 @@ +from syntax import * +from interval_analysis import state, abstract_env, Interval, reduce_state + +x: ArithExpr = AEVar("x") +y: ArithExpr = AEVar("y") + +test_eq: BoolExpr = BEEq(x,y) + +test_neq: BoolExpr = BENeg(test_eq) + +test_leq: BoolExpr = BELeq(x,y) + +test_gt: BoolExpr = BENeg(test_leq) + +def env_str(env: abstract_env) -> str: + return '{ ' + ', '.join([ f'{var}: {i}' for (var,i) in env.items()]) + ' }' + +def state_str(env: state) -> str: + if env is None: return 'None' + return env_str(env) + +def test(env: abstract_env) -> None: + res_eq = reduce_state(env,test_eq) + res_neq = reduce_state(env,test_neq) + res_leq = reduce_state(env,test_leq) + res_gt = reduce_state(env,test_gt) + print(f'In environment { env_str(env) }') + print(f"{test_eq} -> {state_str(res_eq)}") + print(f"{test_neq} -> {state_str(res_neq)}") + print(f"{test_leq} -> {state_str(res_leq)}") + print(f"{test_gt} -> {state_str(res_gt)}") + +test({ 'x': Interval(0,5), 'y': Interval(-5,0) }) + +test({ 'x': Interval(-5,5), 'y': Interval(3,7) }) + +test({ 'x': Interval(0,5), 'y': Interval(-10,-5) }) + +test({ 'x': Interval(0,0), 'y': Interval(0,4) }) diff --git a/TP2/interval_analysis.py b/TP2/interval_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..4f7b2e37ac7e9e192b48adc6e2a5acd556df8f1c --- /dev/null +++ b/TP2/interval_analysis.py @@ -0,0 +1,483 @@ +# pyright: strict + +""" +implements a constant propagation analysis +""" + +from dataclasses import dataclass +from cfg import Cfg +from iteration import Transfer, fixpoint_iteration +from syntax import * + +@dataclass +class Interval: + """ potentially unbounded interval + + None in either bound means that we don't have info. + Hence `Interval(None,None)` denotes all integers + """ + lower_bound: int | None + upper_bound: int | None + def __str__(self): + if self.lower_bound is None: + l = "-∞" + else: + l = str(self.lower_bound) + if self.upper_bound is None: + u = "+∞" + else: + u = str(self.upper_bound) + return f"[{l}; {u}]" + +class Top: + def __str__(self): return "TOP" + +@dataclass +class BTop: + """class used for the evaluation of boolean expressions + + BTop(False) indicates that we don't know if the result is True or False, but + that the evaluation cannot lead to an error + BTop(True) indicates that we neither know the result nor whether an error can occur + """ + has_error: bool + def __str__(self): + if self.has_error: return "TOP (maybe error)" + else: return "TOP (no error)" + +type abstract_env = dict[str, Interval] +"""mapping from variables to abstract values. + +As with concrete environment, a variable not in the dictionary will +lead to an error if we try to obtain its value +""" + +type state = abstract_env | None +"""abstract state is either an abstract env or bottom +""" + +def interval_leq(i1: Interval, i2: Interval) -> bool: + if i2.lower_bound is None: + if i2.upper_bound is None: + return True + if i1.upper_bound is None: + return False + return i1.upper_bound <= i2.upper_bound + if i2.upper_bound is None: + if i1.lower_bound is None: + return False + return i1.lower_bound >= i2.lower_bound + if i1.lower_bound is None or i1.upper_bound is None: + return False + return i1.lower_bound >= i2.lower_bound and i1.upper_bound <= i2.upper_bound + +def interval_join(i1: Interval, i2: Interval) -> Interval: + if i1.lower_bound is None or i2.lower_bound is None: + l = None + else: + l = min(i1.lower_bound,i2.lower_bound) + if i1.upper_bound is None or i2.upper_bound is None: + u = None + else: + u = max(i1.upper_bound,i2.upper_bound) + return Interval(l,u) + +def interval_meet(i1: Interval, i2: Interval) -> Interval | None: + if i1.lower_bound is None: + l = i2.lower_bound + elif i2.lower_bound is None: + l = i1.lower_bound + else: + l = max(i1.lower_bound,i2.lower_bound) + if i1.upper_bound is None: + u = i2.upper_bound + elif i2.upper_bound is None: + u = i1.upper_bound + else: + u = min(i1.upper_bound,i2.upper_bound) + if l is not None and u is not None and l > u: return None + return Interval(l,u) + +def interval_widen(i1: Interval, i2: Interval) -> Interval: + if i1.lower_bound is None or i2.lower_bound is None: + l = None + elif i1.lower_bound <= i2.lower_bound: + l = i1.lower_bound + else: + l = None + if i1.upper_bound is None or i2.upper_bound is None: + u = None + elif i1.upper_bound >= i2.upper_bound: + u = i1.upper_bound + else: + u = None + return Interval(l,u) + +def has_strict_positive_val(i: Interval) -> bool: + return i.upper_bound is None or i.upper_bound > 0 + +def has_strict_negative_val(i: Interval) -> bool: + return i.lower_bound is None or i.lower_bound < 0 + +def contains_zero(i: Interval) -> bool: + if i.lower_bound is None: + return i.upper_bound is None or i.upper_bound >= 0 + if i.upper_bound is None: + return i.lower_bound <= 0 + return i.lower_bound <= 0 and i.upper_bound >= 0 + +def is_zero(i: Interval) -> bool: + return i.lower_bound == 0 and i.upper_bound == 0 + +def interval_opp(i: Interval) -> Interval: + if i.lower_bound is None: + u = None + else: + u = -i.lower_bound + if i.upper_bound is None: + l = None + else: + l = -i.upper_bound + return Interval(l,u) + +def interval_add(i1: Interval, i2: Interval) -> Interval: + """addition of two intervals""" + if i1.lower_bound is None or i2.lower_bound is None: + l = None + else: + l = i1.lower_bound + i2.lower_bound + if i1.upper_bound is None or i2.upper_bound is None: + u = None + else: + u = i1.upper_bound + i2.upper_bound + return Interval(l, u) + +def interval_mul(i1: Interval, i2: Interval) -> Interval: + """multiplication of two intervals""" + if i1.lower_bound is not None and i1.upper_bound is not None and i1.lower_bound > i1.upper_bound: + return Interval(None, None) + if i2.lower_bound is not None and i2.upper_bound is not None and i2.lower_bound > i2.upper_bound: + return Interval(None, None) + + if contains_zero(i1) or contains_zero(i2): + return Interval(None, None) + + if i1.lower_bound is not None and i1.lower_bound >= 0 and i2.lower_bound is not None and i2.lower_bound >= 0: + l = i1.lower_bound * i2.lower_bound + if i1.upper_bound is None or i2.upper_bound is None: + u = None + else: + u = i1.upper_bound * i2.upper_bound + return Interval(l, u) + + if i1.upper_bound is not None and i1.upper_bound <= 0 and i2.upper_bound is not None and i2.upper_bound <= 0: + l = i1.upper_bound * i2.upper_bound + if i1.lower_bound is None or i2.lower_bound is None: + u = None + else: + u = i1.lower_bound * i2.lower_bound + return Interval(l, u) + + if (i1.lower_bound is not None and i1.lower_bound >= 0 and i2.upper_bound is not None and i2.upper_bound <= 0) or \ + (i2.lower_bound is not None and i2.lower_bound >= 0 and i1.upper_bound is not None and i1.upper_bound <= 0): + l = None + if i1.lower_bound is not None and i2.lower_bound is not None: + l = min(i1.lower_bound * i2.upper_bound, i1.upper_bound * i2.lower_bound) + u = None + if i1.upper_bound is not None and i2.upper_bound is not None: + u = max(i1.lower_bound * i2.lower_bound, i1.upper_bound * i2.upper_bound) + return Interval(l, u) + + return Interval(None, None) + +def interval_div(i1: Interval, i2: Interval) -> Interval | Top | None: + """division of two intervals""" + if is_zero(i2): + return None + + if contains_zero(i2): + return Top() + + if i2.lower_bound is not None and i2.lower_bound > 0: + if i1.lower_bound is None: + l = None + else: + l = i1.lower_bound // i2.lower_bound + if i1.upper_bound is None or i2.upper_bound is None: + u = None + else: + u = i1.upper_bound // i2.upper_bound + return Interval(l, u) + + if i2.upper_bound is not None and i2.upper_bound < 0: + if i1.lower_bound is None: + l = None + else: + l = i1.lower_bound // i2.upper_bound + if i1.upper_bound is None or i2.lower_bound is None: + u = None + else: + u = i1.upper_bound // i2.lower_bound + return Interval(l, u) + + return Top() + +def eval_aexp(env: abstract_env, e: ArithExpr) -> Interval | Top | None: + """evaluate an arithmetic expression in an abstract environment + returns None in case of error + """ + match e: + case AECst(value): return Interval(value,value) + case AEVar(var): + if var in env: return env[var] + else: return None + case AEUop(uop,expr): + res = eval_aexp(env,expr) + if res is None or isinstance(res,Top): return res + if uop == Uop.OPP: return interval_opp(res) + return None + case AEBop(bop,left_expr,right_expr): + v1 = eval_aexp(env,left_expr) + v2 = eval_aexp(env,right_expr) + if v1 is None or v2 is None: return None + if isinstance(v1,Top) or isinstance(v2,Top): + return Top() + match bop: + case Bop.ADD: return interval_add(v1,v2) + case Bop.MUL: return interval_mul(v1,v2) + case Bop.DIV: return interval_div(v1,v2) + case _: pass + +def eval_bexp(env: abstract_env, e: BoolExpr) -> bool | BTop | None: + """abstract evaluation of a boolean expression""" + match e: + case BEPlain(aexpr): + res = eval_aexp(env, aexpr) + if res is None: return None + if isinstance(res,Top): return BTop(True) + if res.lower_bound == 0 and res.upper_bound == 0: return True + if not interval_leq(Interval(0,0), res): return False + return BTop(False) + case BEEq(left_expr,right_expr): + v1 = eval_aexp(env, left_expr) + v2 = eval_aexp(env, right_expr) + if v1 is None or v2 is None: return None + if isinstance(v1,Top) or isinstance(v2,Top): return BTop(True) + if v1.lower_bound is None or v2.lower_bound is None or v1.upper_bound is None or v2.upper_bound is None: + return BTop(False) + if v1.lower_bound > v2.upper_bound or v1.upper_bound < v2.lower_bound: + return False + if v1.lower_bound == v1.upper_bound and v2.lower_bound == v2.upper_bound and v1.lower_bound == v2.lower_bound: + return True + return BTop(False) + case BELeq(left_expr,right_expr): + v1 = eval_aexp(env, left_expr) + v2 = eval_aexp(env, right_expr) + if v1 is None or v2 is None: return None + if isinstance(v1,Top) or isinstance(v2,Top): return BTop(True) + if v1.upper_bound is None: + if v1.lower_bound is None: + return BTop(False) + if v2.upper_bound is None: + return BTop(False) + if v2.upper_bound < v1.lower_bound: + return False + return BTop(False) + if v2.lower_bound is None: + return BTop(False) + if v1.upper_bound <= v2.lower_bound: + return True + if v1.lower_bound is None: + return BTop(False) + if v2.upper_bound is None: + return BTop(False) + if v2.upper_bound < v1.lower_bound: + return False + return BTop(False) + case BENeg(expr): + v = eval_bexp(env,expr) + if v is None: return None + if isinstance(v,BTop): return v + return not v + case _: pass + +def reduce_eq(s: state, x: str, i: Interval) -> state: + """Reduce the value of x under the hypothesis that it equals i""" + if s is None: + return None + if x not in s: + return None + res = interval_meet(s[x], i) + if res is None: + return None + return s | {x: res} + +def reduce_neq(s: state, x: str, i: Interval) -> state: + """Reduce the value of x under the hypothesis that it differs from i""" + if s is None: + return None + if x not in s: + return None + if i.lower_bound == i.upper_bound and s[x].lower_bound == i.lower_bound: + if s[x].upper_bound is None or s[x].upper_bound > i.upper_bound: + return s | {x: Interval(i.upper_bound + 1, s[x].upper_bound)} + return None + if i.lower_bound == i.upper_bound and s[x].upper_bound == i.upper_bound: + if s[x].lower_bound is None or s[x].lower_bound < i.lower_bound: + return s | {x: Interval(s[x].lower_bound, i.lower_bound - 1)} + return None + return s + +def reduce_leq(s: state, x: str, upper_bound: int) -> state: + """Reduce the value of x under the hypothesis that it is less than or equal to upper_bound""" + if s is None: + return None + if x not in s: + return None + res = interval_meet(s[x], Interval(None, upper_bound)) + if res is None: + return None + return s | {x: res} + +def reduce_geq(s: state, x: str, lower_bound: int) -> state: + """Reduce the value of x under the hypothesis that it is greater than or equal to lower_bound""" + if s is None: + return None + if x not in s: + return None + res = interval_meet(s[x], Interval(lower_bound, None)) + if res is None: + return None + return s | {x: res} + +def reduce_state(s: state,c: BoolExpr) -> state: + if s is None: return s + match c: + case BEEq(AEVar(x), AEVar(y)): + vx = eval_aexp(s,AEVar(x)) + vy = eval_aexp(s,AEVar(y)) + if vx is None or vy is None: return None + if not isinstance(vx,Top): + s = reduce_eq(s,y,vx) + if not isinstance(vy,Top): + s = reduce_eq(s,x,vy) + return s + case BEEq(AEVar(x), right_expr): + v = eval_aexp(s,right_expr) + if v is None: return None + if isinstance(v,Top): return s + return reduce_eq(s,x,v) + case BEEq(left_expr, AEVar(y)): + v = eval_aexp(s,left_expr) + if v is None: return None + if isinstance(v,Top): return s + return reduce_eq(s,y,v) + case BELeq(AEVar(x), AEVar(y)): + vx = eval_aexp(s,AEVar(x)) + vy = eval_aexp(s,AEVar(y)) + if vx is None or vy is None: return None + if not isinstance(vy,Top) and vy.upper_bound is not None: + s = reduce_leq(s,x,vy.upper_bound) + if not isinstance(vx,Top) and vx.lower_bound is not None: + s = reduce_geq(s,y,vx.lower_bound) + return s + case BELeq(AEVar(x),right_expr): + v = eval_aexp(s,right_expr) + if v is None: return None + if isinstance(v,Top) or v.upper_bound is None: return s + return reduce_leq(s,x,v.upper_bound) + case BELeq(left_expr,AEVar(y)): + v = eval_aexp(s,left_expr) + if v is None: return None + if isinstance(v,Top) or v.lower_bound is None: return s + return reduce_geq(s,y,v.lower_bound) + case BENeg(BEEq(AEVar(x), AEVar(y))): + vx = eval_aexp(s,AEVar(x)) + vy = eval_aexp(s,AEVar(y)) + if vx is None or vy is None: return None + if not isinstance(vx,Top): + s = reduce_neq(s,y,vx) + if not isinstance(vy,Top): + s = reduce_neq(s,x,vy) + return s + case BENeg(BEEq(AEVar(x), right_expr)): + v = eval_aexp(s,right_expr) + if v is None: return None + if isinstance(v,Top): return s + return reduce_neq(s,x,v) + case BENeg(BEEq(left_expr, AEVar(y))): + v = eval_aexp(s,left_expr) + if v is None: return None + if isinstance(v,Top): return s + return reduce_neq(s,y,v) + case BENeg(BELeq(AEVar(x), AEVar(y))): + vx = eval_aexp(s,AEVar(x)) + vy = eval_aexp(s,AEVar(y)) + if vx is None or vy is None: return None + if not isinstance(vx,Top) and vx.upper_bound is not None: + s = reduce_leq(s,y,vx.upper_bound - 1) + if not isinstance(vy,Top) and vy.lower_bound is not None: + s = reduce_geq(s,x,vy.lower_bound + 1) + return s + case BENeg(BELeq(AEVar(x),right_expr)): + v = eval_aexp(s,right_expr) + if v is None: return None + if isinstance(v,Top) or v.lower_bound is None: return s + return reduce_geq(s,x,v.lower_bound + 1) + case BENeg(BELeq(left_expr,AEVar(y))): + v = eval_aexp(s,left_expr) + if v is None: return None + if isinstance(v,Top) or v.upper_bound is None: return s + return reduce_leq(s,y,v.upper_bound - 1) + case _: return s + +class Interval_analysis(Transfer[state]): + variables: frozenset[str] + def __init__(self,instr: Instr): + self.variables = variables_of_instr(instr) + def bottom(self) -> state: + return None + def init_state(self) -> state: + return dict.fromkeys(self.variables, Interval(None,None)) + def join(self,s1:state,s2:state) -> state: + if s1 is None: return s2 + if s2 is None: return s1 + res: abstract_env = {} + for var in self.variables: + v1 = s1[var] + v2 = s2[var] + v = interval_join(v1,v2) + res[var] = v + return res + + def widen(self, s1:state, s2:state) -> state: + raise NotImplementedError + + def included(self,s1: state,s2: state) -> bool: + if s1 is None: return True + if s2 is None: return False + for var in self.variables: + if not interval_leq(s1[var],s2[var]): return False + return True + + def tr_skip(self,s: state) -> state: + return s + + def tr_set(self,s: state,v: str,e: ArithExpr) -> state: + raise NotImplementedError + + def tr_test(self,s: state,c: BoolExpr) -> state: + raise NotImplementedError + + def tr_err(self,s: state,e: Expr) -> state: + raise NotImplementedError + +def analyze(i: Instr) -> None: + cfg = Cfg(i) + res = fixpoint_iteration(Interval_analysis(i), cfg) + for node in cfg.g.nodes: + print(f"State at {node}:") + s = res[node] + if s is not None: + for (v, s) in s.items(): + print(f" {v}: {s}")