Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • yassine.ettayeb/anastat-frama-c-24-25
  • nicolas.descamps/anastat
  • rayan.lachguel/anastat-frama-c-24-25
  • merlin.baviere/ana-stat-frama-c-24-25-merlin-baviere
  • thomas.norodom/norodom-ana-stat-frama-c-24-25
  • quentin.vicens/ana-stat-frama-c-24-25
  • stanislas.plowiecki/anastat-frama-c-24-25
  • virgile.prevosto/anastat-frama-c-24-25
8 results
Show changes
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) })
from syntax import *
from cfg import Cfg
from iteration import *
from opsem import eval_bexp, eval_aexp
one = AECst(1)
zero = AECst(0)
x = AEVar("x")
dead_if=ICond(BEPlain(one),ISet("x",one),ISet("y",zero))
infinite_loop=ILoop(BEPlain(one),ISet("x",AEBop(Bop.ADD,x,one)))
prog_with_dead_code=ISeq(dead_if,infinite_loop)
class NoInfo(Transfer[frozenset[bool]]):
"""Almost trivial static analysis
We only have two elements in our lattice, the empty set and the singleton {True}.
The latter is propagated from initial state to any potentially reachable state.
In the end, the only nodes still at bottom are the one which are guarded by
always-false tests.
"""
type S = frozenset[bool]
def bottom(self) -> S: return frozenset()
def init_state(self) -> S: return frozenset([True])
def join(self, s1:S, s2:S) -> S: return s1 | s2
def included(self, s1:S, s2:S) -> bool: return s1.issubset(s2)
def tr_skip(self,s:S): return s
def tr_set(self, s:S, v:str, e: ArithExpr) -> S: return s
def tr_test(self,s:S, c:BoolExpr) -> S:
if eval_bexp({},c) is False:
return frozenset()
else:
return s
def tr_err(self, s:S, e:Expr) -> S:
if isinstance(e,ArithExpr):
if eval_aexp({},e) is not None:
return frozenset()
else:
return s
else:
if isinstance(e,BoolExpr):
if eval_bexp({},e) is not None:
return frozenset()
else:
return s
else: assert False
cfg = Cfg(prog_with_dead_code)
print(f"Cfg is {cfg}")
res = fixpoint_iteration(NoInfo(),cfg)
unreachable = [node for node, state in res.items() if state == frozenset()]
for node in unreachable:
print(f"{node} is unreachable")
from syntax import *
from opsem import eval_instr
x = AEVar("x")
y = AEVar("y")
zero = AECst(0)
one = AECst(1)
xneg = BELeq(x, zero)
bound_check = ICond(xneg,ISet("x",one),ISkip())
init_y = ISet("y",one)
body = ISeq(ISet("y",AEBop(Bop.MUL,y,x)),ISet("x",AEBop(Bop.ADD,x,AEUop(Uop.OPP,one))))
loop = ILoop(BENeg(BELeq(x,zero)),body)
factorial = ISeq(ISeq(bound_check,init_y),loop)
print(f"prog is\n{factorial}")
env = { "x" : 4 }
envf = eval_instr(env,factorial)
print(f"init is \n{env}")
print(f"result is\n{envf}")
from sign_analysis import analyze
from example_opsem import factorial
analyze(factorial)
from syntax import *
from sign_analysis import eval_aexp, eval_bexp, abstract_env, sign
x: ArithExpr = AEVar("x")
y: ArithExpr = AEVar("y")
# y * (x - x / y)
test_aexp: ArithExpr = AEBop(Bop.MUL,y,AEBop(Bop.ADD,x, AEUop(Uop.OPP,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(sign) for (var,sign) in env.items()} }')
print(f"{test_aexp} -> {res_a}")
print(f"{test_bexp} -> {res_b}")
test({ 'x': sign.SPOS, 'y': sign.SPOS })
test({ 'x': sign.NEG, 'y': sign.NEG })
test({ 'x': sign.INT, 'y': sign.Z })
test({ 'x': sign.POS, 'y': sign.NZ })
test({})
from syntax import *
from sign_analysis import state, abstract_env, sign, 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 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 { {var: str(sign) for (var,sign) in env.items()} }')
print(f"{test_eq} -> {res_eq}")
print(f"{test_neq} -> {res_neq}")
print(f"{test_leq} -> {res_leq}")
print(f"{test_gt} -> {res_gt}")
test({ 'x': sign.POS, 'y': sign.NEG })
test({ 'x': sign.NZ, 'y': sign.NEG })
test({ 'x': sign.POS, 'y': sign.SNEG })
test({ 'x': sign.Z, 'y': sign.POS })
from iteration import Transfer, fixpoint_iteration
from syntax import *
from cfg import Cfg
from example_opsem import factorial
@dataclass
class Number_step:
bound: int | None
def __str__(self):
if self.bound is None: return "+∞"
else: return str(self.bound)
type S = Number_step | None
class Analysis(Transfer[S]):
def bottom(self) -> S:
return None
def init_state(self) -> S:
return Number_step(0)
def join(self,s1:S,s2:S) -> S:
if s1 is None: return s2
if s2 is None: return s1
if s1.bound is None: return s1
if s2.bound is None: return s2
return Number_step(max(s1.bound, s2.bound))
def widen(self, s1:S, s2:S) -> S:
if s1 is None: return s2
if s2 is None: return s1
if s1.bound is None: return s1
if s2.bound is None: return s2
if s1.bound < s2.bound: return Number_step(None)
return s1
def included(self,s1: S,s2: S) -> bool:
if s1 is None: return True
if s2 is None: return False
if s2.bound is None: return True
if s1.bound is None: return False
return s1.bound <= s2.bound
def tr_skip(self,s: S) -> S:
if s is None: return None
if s.bound is None: return s
return Number_step(s.bound + 1)
def tr_set(self,s: S,v: str,e: ArithExpr) -> S:
return self.tr_skip(s)
def tr_test(self,s: S,c: BoolExpr) -> S:
return self.tr_skip(s)
def tr_err(self,s: S,e: Expr) -> S:
return self.tr_skip(s)
res = fixpoint_iteration(Analysis(),Cfg(factorial))
for (node, nb) in res.items():
print(f'{node} -> {nb}')
# 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:
raise NotImplementedError
def interval_mul_pos(i1: Interval, i2: Interval) -> Interval:
assert (i1.lower_bound is not None and i1.lower_bound >= 0)
assert (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)
def interval_opt_join(i1: Interval | None, i2: Interval) -> Interval:
if i1 is None: return i2
return interval_join(i1,i2)
def interval_mul(i1: Interval, i2: Interval) -> Interval:
i1_neg = interval_meet(i1,Interval(None,0))
i2_neg = interval_meet(i2,Interval(None,0))
i1_pos = interval_meet(i1,Interval(0,None))
i2_pos = interval_meet(i2,Interval(0,None))
if i1_neg is None or i2_pos is None:
res = None
else:
res = interval_opp(interval_mul_pos(interval_opp(i1_neg),i2_pos))
if i1_pos is not None and i2_neg is not None:
res1 = interval_opp(interval_mul_pos(i1_pos,interval_opp(i2_neg)))
res = interval_opt_join(res, res1)
if i1_neg is not None and i2_neg is not None:
res1 = interval_mul_pos(interval_opp(i1_neg),interval_opp(i2_neg))
res = interval_opt_join(res,res1)
if i1_pos is not None and i2_pos is not None:
res1 = interval_mul_pos(i1_pos, i2_pos)
res = interval_opt_join(res, res1)
# at least one of the 4 cases is not empty
assert res is not None
return res
def interval_div(i1: Interval, i2: Interval) -> Interval | Top | None:
raise NotImplementedError
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:
raise NotImplementedError
def reduce_neq(s:state, x: str, i: Interval) -> state:
raise NotImplementedError
def reduce_leq(s:state,x:str,upper_bound: int) -> state:
raise NotImplementedError
def reduce_geq(s: state, x: str, lower_bound: int) -> state:
raise NotImplementedError
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}")
"""Fixpoint iterator over a CfG
- Transfer is a base class that must be instantiated for
each static analysis.
- fixpoint_iteration computes a fixpoint over a cfg for the given
transfer instance
"""
from cfg import *
from typing import Protocol
class Transfer[S](Protocol):
"""lists the functions that must be implemented for a static analysis over Lattice S"""
def bottom(self) -> S:
"""the empty state, associated to unvisited nodes"""
...
def init_state(self) -> S:
"""the state associated to the initial node"""
...
def join(self,s1:S,s2:S) -> S:
"""join of two states when merging branches"""
...
def included(self,s1: S,s2: S) -> bool:
"""return True when the first state is included in the second"""
...
def tr_skip(self,s: S) -> S:
"""transfer state across a Skip transition
Almost always transfer the state unchanged
"""
return s
def tr_set(self,s: S,v: str,e: ArithExpr) -> S:
"""transfer state across an assignment"""
...
def tr_test(self,s: S,c: BoolExpr) -> S:
"""transfer state across a test transition"""
...
def tr_err(self,s: S,e: Expr) -> S:
"""transfer state across a transition to error state"""
...
def fixpoint_iteration[T](transfer: Transfer[T], cfg: Cfg) -> dict[Node,T]:
"""computes the fixpoint for the given transfer functions over the given CfG
output maps each node to the computed state
"""
raise NotImplementedError
"""
Operational semantics for the Why language
Provides evaluation functions for the three syntactic classes,
based on a environment mapping variables to integer values.
These functions return None when an error occur somewhere (Big step semantics)
"""
from syntax import *
def eval_aexp(env: dict[str,int], exp: ArithExpr) -> int | None:
"""evaluates an arithmetic expression"""
match exp:
case AECst(value):
raise NotImplementedError
case AEVar(var):
raise NotImplementedError
case AEUop(uop,expr):
raise NotImplementedError
case AEBop(bop,left_expr,right_expr):
raise NotImplementedError
case _: assert False
def eval_bexp(env: dict[str,int], exp:BoolExpr) -> bool | None:
"""evaluates a boolean expression"""
match exp:
case BEPlain(aexpr):
raise NotImplementedError
case BEEq(left_expr,right_expr):
raise NotImplementedError
case BELeq(left_expr,right_expr):
raise NotImplementedError
case BENeg(expr):
raise NotImplementedError
case _: assert False
def eval_instr(env: dict[str,int], instr: Instr) -> dict[str,int] | None:
"""evaluates an instruction"""
match instr:
case ISkip():
raise NotImplementedError
case ISet(var,expr):
raise NotImplementedError
case ISeq(first,second):
raise NotImplementedError
case ICond(cond,true_branch,false_branch):
raise NotImplementedError
case ILoop(cond,body):
raise NotImplementedError
case _: assert False
# pyright: strict
"""
implements a sign analysis
"""
from enum import Enum, auto
from dataclasses import dataclass
from cfg import Cfg
from iteration import Transfer, fixpoint_iteration
from syntax import *
class sign(Enum):
SNEG = auto()
SPOS = auto()
Z = auto()
NEG = auto()
POS = auto()
NZ = auto()
INT = auto()
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, sign]
"""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 sign_leq(v1: sign, v2:sign) -> bool:
"""order relation on the sign lattice"""
if v1 == v2: return True
match v1,v2:
case _, sign.INT: return True
case sign.INT, _: return False
case sign.SNEG,(sign.NEG | sign.NZ): return True
case sign.SPOS,(sign.POS | sign.NZ): return True
case sign.Z,(sign.POS | sign.NEG): return True
case sign.NEG,sign.NZ: return True
case sign.POS,sign.NZ: return True
case _: return False
def sign_join(v1: sign, v2:sign) -> sign:
"""computes least upper bound"""
match v1,v2:
case sign.INT, _: return sign.INT
case _, sign.INT: return sign.INT
case sign.NZ, (sign.NZ | sign.SPOS | sign.SNEG): return sign.NZ
case sign.NZ,_: return sign.INT
case sign.NEG, (sign.NEG | sign.SNEG | sign.Z): return sign.NEG
case sign.NEG, _: return sign.INT
case sign.POS, (sign.POS | sign.SPOS | sign.Z): return sign.POS
case sign.POS,_: return sign.INT
case sign.SNEG, sign.SNEG: return sign.SNEG
case sign.SNEG, (sign.Z | sign.NEG): return sign.NEG
case sign.SNEG, (sign.SPOS | sign.NZ): return sign.NZ
case sign.SNEG, _: return sign.INT
case sign.SPOS, sign.SPOS: return sign.SPOS
case sign.SPOS, (sign.POS | sign.Z): return sign.POS
case sign.SPOS, (sign.SNEG | sign.NZ): return sign.NZ
case sign.SPOS, _: return sign.INT
case sign.Z, sign.SNEG: return sign.NEG
case sign.Z, sign.SPOS: return sign.POS
case sign.Z, sign.NZ: return sign.INT
case sign.Z, _: return v2
def sign_meet(s1: sign, s2:sign) -> sign | None:
"""computes greatest lower bound. Can be None (i.e. bottom)"""
match s1,s2:
case sign.INT, _: return s2
case _, sign.INT: return s1
case sign.SNEG, (sign.NEG | sign.NZ | sign.SNEG): return s1
case (sign.NEG | sign.NZ | sign.SNEG), sign.SNEG: return s2
case sign.SNEG, _: return None
case _, sign.SNEG: return None
case sign.SPOS, (sign.POS | sign.NZ | sign.SPOS): return s1
case (sign.POS | sign.NZ | sign.SPOS), sign.SPOS: return s2
case sign.SPOS, _: return None
case _, sign.SPOS: return None
case sign.Z, (sign.NEG | sign.POS): return s1
case (sign.NEG | sign.POS), sign.Z: return s2
case sign.Z, sign.Z: return s1
case sign.Z, _: return None
case _, sign.Z: return None
case sign.NEG, sign.NEG: return s1
case sign.POS, sign.POS: return s1
case sign.NEG, sign.POS: return sign.Z
case sign.POS, sign.NEG: return sign.Z
case sign.NZ, sign.NEG: return sign.SNEG
case sign.NEG, sign.NZ: return sign.SNEG
case sign.NZ, sign.POS: return sign.SPOS
case sign.POS, sign.NZ: return sign.SPOS
case sign.NZ, sign.NZ: return sign.NZ
def sign_opp(s:sign) -> sign:
"""unary minus"""
match s:
case sign.SNEG: return sign.SPOS
case sign.SPOS: return sign.SNEG
case sign.POS: return sign.NEG
case sign.NEG: return sign.POS
case _: return s
def sign_add(s1:sign,s2:sign) -> sign:
"""addition"""
match (s1,s2):
case sign.INT,_: return sign.INT
case _,sign.INT: return sign.INT
case sign.Z,_: return s2
case _,sign.Z: return s1
case sign.SNEG, (sign.SNEG | sign.NEG): return sign.SNEG
case (sign.SNEG | sign.NEG), sign.SNEG: return sign.SNEG
case sign.NEG, sign.NEG: return sign.NEG
case sign.SPOS, (sign.SPOS | sign.POS): return sign.SPOS
case (sign.SPOS | sign.POS), sign.SPOS: return sign.SPOS
case sign.POS, sign.POS: return sign.POS
case _: return sign.INT
def sign_mul(s1: sign, s2: sign) -> sign:
"""multiplication"""
match s1,s2:
case sign.INT,_: return sign.INT
case _,sign.INT: return sign.INT
case sign.Z,_: return sign.Z
case _,sign.Z: return sign.Z
case sign.SPOS, _: return s2
case _, sign.SPOS: return s1
case sign.SNEG, _: return sign_opp(s2)
case _,sign.SNEG: return sign_opp(s1)
case sign.NZ, _: return sign.INT
case _, sign.NZ: return sign.INT
case sign.NEG, sign.NEG: return sign.POS
case sign.NEG, sign.POS: return sign.NEG
case sign.POS, sign.NEG: return sign.NEG
case sign.POS, sign.POS: return sign.POS
def sign_div(s1: sign, s2: sign) -> sign | Top | None:
"""division: can lead to errors even if operands are plain sign"""
match s1, s2:
case _,sign.Z: return None
case _,sign.INT: return Top()
case _,sign.POS: return Top()
case _,sign.NEG: return Top()
case sign.Z,_: return sign.Z
case sign.INT,_: return sign.INT
case sign.NZ,_: return sign.INT
case _,sign.NZ: return sign.INT
case (sign.POS | sign.SPOS), sign.SPOS: return sign.POS
case (sign.NEG | sign.SNEG), sign.SPOS: return sign.NEG
case (sign.POS | sign.SPOS), sign.SNEG: return sign.NEG
case (sign.NEG | sign.SNEG), sign.SNEG: return sign.POS
def eval_aexp(env: abstract_env, e: ArithExpr) -> sign | Top | None:
"""evaluate an arithmetic expression in an abstract environment
returns None in case of error
"""
match e:
case AECst(value):
raise NotImplementedError
case AEVar(var):
raise NotImplementedError
case AEUop(uop,expr):
raise NotImplementedError
case AEBop(bop,left_expr,right_expr):
raise NotImplementedError
case _: pass
def eval_bexp(env: abstract_env, e: BoolExpr) -> bool | BTop | None:
"""abstract evaluation of a boolean expression"""
match e:
case BEPlain(aexpr):
raise NotImplementedError
case BEEq(left_expr,right_expr):
raise NotImplementedError
case BELeq(left_expr,right_expr):
raise NotImplementedError
case BENeg(expr):
raise NotImplementedError
case _: pass
def reduce_eq_sign(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x == expr"""
if s is None: return None
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
res = sign_meet(s[x],v)
if res is None: return None
return s | { x: res }
def reduce_neq_sign(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x != expr"""
if s is None: return None
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
match s[x],v:
case sign.POS, sign.Z: return s | { x: sign.SPOS }
case sign.NEG, sign.Z: return s | { x: sign.SNEG }
case sign.INT, sign.Z: return s | { x: sign.NZ }
case _: return s
def reduce_upper_bound(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x <= expr"""
if s is None: return None
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
upper = sign.INT
match v:
case sign.NEG | sign.SNEG:
upper = v
case sign.Z:
upper = sign.NEG
case _: pass
res = sign_meet(s[x],upper)
if res is None: return None
return s | { x: res }
def reduce_strict_upper_bound(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x < expr"""
if s is None: return None
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
match v:
case sign.NEG | sign.SNEG | sign.Z:
upper = sign.SNEG
case _:
upper = sign.INT
res = sign_meet(s[x],upper)
if res is None: return None
return s | { x: res }
def reduce_lower_bound(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x >= expr"""
if s is None: return None
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
lower = sign.INT
match v:
case sign.POS | sign.SPOS:
lower = v
case sign.Z:
lower = sign.POS
case _: pass
res = sign_meet(s[x],lower)
if res is None: return None
return s | { x: res }
def reduce_strict_lower_bound(s: state, x: str, expr: ArithExpr) -> state:
"""reduce the value associated to x in s under the hypothesis that x > expr"""
if s is None: return s
v = eval_aexp(s,expr)
if v is None: return None
if isinstance(v,Top): return s
match v:
case sign.POS | sign.SPOS | sign.Z:
lower = sign.SPOS
case _:
lower = sign.INT
res = sign_meet(s[x],lower)
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:
# To be completed (see course's slides)
case _: return s
class Sign_interp(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, sign.INT)
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]
res[var] = sign_join(v1,v2)
return res
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 sign_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:
if s is None: return s
if isinstance(e,ArithExpr):
raise NotImplementedError
if isinstance(e,BoolExpr):
raise NotImplementedError
def analyze(i: Instr) -> None:
cfg = Cfg(i)
res = fixpoint_iteration(Sign_interp(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}")
"""Nodes of the abstract syntax tree of the program:
- Uop/Bop represent unary/binary arithmetic operator
- ArithExpr and its children describe arithmetic expressions
- BoolExpr and its children describe boolean expressions
- Instr and its children describe instruction
"""
from enum import Enum
from dataclasses import dataclass
class Uop(Enum):
"""unary operators. Currently only unary minus"""
OPP = 1
def __str__(self):
if self is Uop.OPP:
return "-"
return ""
class Bop(Enum):
"""binary operators: addition, multiplication, and division"""
ADD = 1
MUL = 2
DIV = 3
def __str__(self):
match self:
case Bop.ADD: return "+"
case Bop.MUL: return "*"
case Bop.DIV: return "/"
class Expr:
"""generic class for expressions, either arithmetic or boolean"""
pass
@dataclass
class ArithExpr(Expr):
"""generic class for arithmetic expressions"""
pass
@dataclass
class AECst(ArithExpr):
"""integer constants"""
value: int
def __str__(self):
return f"{self.value}"
@dataclass
class AEVar(ArithExpr):
"""variables"""
var: str
def __str__(self):
return self.var
@dataclass
class AEUop(ArithExpr):
"""unary operation"""
uop: Uop
expr: ArithExpr
def __str__(self):
return f"{self.uop}{self.expr}"
@dataclass
class AEBop(ArithExpr):
"""binary operation"""
bop: Bop
left_expr: ArithExpr
right_expr: ArithExpr
def __str__(self):
#TODO: only put parentheses if necessary
return f"({self.left_expr}) {self.bop} ({self.right_expr})"
@dataclass
class BoolExpr(Expr):
"""generic class for boolean expressions"""
pass
@dataclass
class BEPlain(BoolExpr):
"""arithmetic expression (true if not 0)"""
aexpr: ArithExpr
def __str__(self):
return f"{self.aexpr}"
@dataclass
class BEEq(BoolExpr):
"""equality between two arithmetic expression"""
left_expr: ArithExpr
right_expr:ArithExpr
def __str__(self):
return f"{self.left_expr} = {self.right_expr}"
@dataclass
class BELeq(BoolExpr):
"""less or equal"""
left_expr: ArithExpr
right_expr: ArithExpr
def __str__(self):
return f"{self.left_expr} <= {self.right_expr}"
@dataclass
class BENeg(BoolExpr):
"""Negation"""
expr: BoolExpr
def __str__(self):
return f"!{self.expr}"
class Instr:
"""generic class for instruction"""
pass
@dataclass
class ISkip(Instr):
"""Skip (do nothing)"""
def __str__(self):
return "skip"
@dataclass
class ISet(Instr):
"""assignment to a variable"""
var: str
expr: ArithExpr
def __str__(self):
return f"{self.var} := {self.expr}"
@dataclass
class ISeq(Instr):
"""sequence of two instructions"""
first: Instr
second: Instr
def __str__(self):
return f"{self.first};\n{self.second}"
@dataclass
class ICond(Instr):
"""if then else"""
cond: BoolExpr
true_branch: Instr
false_branch: Instr
def __str__(self):
return f"if {self.cond} then\n{self.true_branch}\nelse\n{self.false_branch}\nfi"
@dataclass
class ILoop(Instr):
"""while loop"""
cond: BoolExpr
body: Instr
def __str__(self):
return f"while {self.cond} do\n{self.body}\ndone"
def variables_of_aexpr(e: ArithExpr) -> frozenset[str]:
"""return the set of variables appearing in the expression"""
match e:
case AECst(_): return frozenset()
case AEVar(var): return frozenset(var)
case AEUop(expr=e): return variables_of_aexpr(e)
case AEBop(left_expr=le,right_expr=re):
return variables_of_aexpr(le) | variables_of_aexpr(re)
case _: assert False
def variables_of_bexpr(e: BoolExpr) -> frozenset[str]:
"""return the set of variable appearing in the expression"""
match e:
case BEPlain(expr): return variables_of_aexpr(expr)
case BEEq(left_expr=le,right_expr=re):
return variables_of_aexpr(le) | variables_of_aexpr(re)
case BELeq(left_expr=le,right_expr=re):
return variables_of_aexpr(le) | variables_of_aexpr(re)
case BENeg(expr):
return variables_of_bexpr(expr)
case _: assert False
def variables_of_instr(instr: Instr) -> frozenset[str]:
"""return the set of variables appearing in a program"""
match instr:
case ISkip(): return frozenset()
case ISet(var=v,expr=e):
return frozenset([v]) | variables_of_aexpr(e)
case ISeq(first=i1,second=i2):
return variables_of_instr(i1) | variables_of_instr(i2)
case ICond(cond=c,true_branch=tb,false_branch=fb):
vbranches = variables_of_instr(tb) | variables_of_instr(fb)
return variables_of_bexpr(c) | vbranches
case ILoop(cond=c,body=b):
return variables_of_bexpr(c) | variables_of_instr(b)
case _ : assert False
# Analyse Statique de Programmes -- TP Analyse Statique
CentraleSupélec
Enseignant: Virgile Prevosto
## Préliminaires
Ce TP utilise exclusivement Python. On pourra utiliser l'image Docker, comme
au TP précédent, ou directement sa machine si Python y est installé.
### Rappel Docker
L'image est ici: https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl et peut être utilisée soit localement, soit depuis `MyDocker`. De plus le répertoire `config` est monté automatiquement dans le container docker si vous utilisez les scripts associé ([PowerShell](https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl/blob/main/start-3asl.ps1) pour Windows ou [sh](https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl/blob/main/start-3asl.sh) pour Linux/macOS/BSD). Ces scripts devraient automatiquement ouvrir un onglet de votre navigateur web avec une session IceWM. Si ce n'est pas le cas, vous pouvez le faire manuellement: http://localhost:3000
NB: l'interpréteur Python est `python3` et non `python`.
### Typage statique
Les fichiers proposés ont des annotations de types permettant de vérifier que
les programmes sont bien typés avant de les exécuter. On pourra utiliser
l'outil [`pyright`](https://microsoft.github.io/pyright/#/) pour la vérification. Il n'est pas présent sur l'image
Docker, mais peut s'installer via les commandes suivantes:
```sh
sudo apt update
sudo apt install pipx
sudo apt install pyright
```
Il dispose également d'un plugin vscode.
## Introduction
Le fichier [syntax.py](config/syntax.py) définit les différentes classes
syntaxique du langage While vu en cours, soit les expressions arithmétiques,
les expressions booléennes et les instructions.
## Sémantique opérationnelle
Le fichier [opsem.py](config/opsem.py) donne un squelette des fonctions à
écrire pour définir un interpréteur concret du langage While. Les fonctions
`eval_aexpr`, et `eval_bexpr` permettent d'évaluer respectivement une
expression arithmétique et une expression booléenne dans un environnement
qui associe à des noms de variable une valeur entière. En cas d'erreur
dans l'évaluation d'une (sous-)expression, on renverra None. La fonction `eval_instr`, renvoie l'environnement final obtenu en évaluant dans un environnement initial une instruction donnée. Là encore, en cas d'erreur au cours de l'évaluation, on renvoie `None`.
Compléter ces définitions, et tester le résultat à l'aide du fichier
[example_opsem.py](config/example_opsem.py), qui définit le programme de calcul de la factorielle,
et l'évalue pour `x = 4`.
## Calcul de point fixe
Le fichier [cfg.py](config/cfg.py) est donné. Il contient la définition des
différents labels des arêtes du graphe de flot de contrôle (skip, affectation,
test, et erreur). On a aussi une classe pour les nœuds, pour un graphe, et pour
un CFG proprement dit, qui ajoute au graphe les informations sur le nœud
initial, le nœud final, et le nœud d'erreur. Un CFG est construit à partir d'un
programme, et cette construction est donnée également.
Le fichier [iteration.py](config/iteration.py) contient une classe abstraite
`Transfer` qui permet d'implémenter des analyses statiques en donnant l'élément
`bottom` du treillis, l'état initial, le `join` de deux états, le test
d'inclusion, et les fonctions de transfert pour chaque étiquette.
Compléter la fonction `fixpoint_iteration` de ce fichier, qui prend en argument
une instance de `Transfer` et un CFG, et calcule le point fixe associant à
chaque nœud du CFG l'état correspondant. L'algorithme est le suivant (on ne se
préoccupe pas de terminaison):
1. le mapping initial associe l'état initial au nœud initial, et `bottom` à tous
les autres. On met toutes les arêtes qui partent du nœud initial dans la
worklist.
2. tant que la worklist n'est pas vide, on prend son premier élément, une arête
qui va du nœud `src` au nœud `dst` avec le label `l`.
3. on associe au nœud `dst` le `join` de son état actuel avec le résultat
de la fonction de transfert pour `l` appliqué à l'état associé à `src`
4. si le nouvel état est inclus dans l'état associé précédemment à `dst`, on
ne fait rien. Sinon, on ajoute les arêtes qui sortent de `dst` à la worklist
5. on revient en 2
NB: en pratique, une itération efficace sélectionne l'arête d'intérêt de manière
beaucoup plus précise, mais cela sort du cadre de ce TP.
Tester le résultat avec le fichier
[example_iteration.py](config/example_iteration.py), qui définit une analyse
statique minimaliste, qui propage un état non vide dans tous les nœuds sauf ceux
qui sont gardés par une condition qui s'évalue à `False` dans un environnement
vide. Le programme exemple a un CFG qui contient de telles arêtes. À quoi
correspondent les nœuds indiqués comme inatteignables par l'analyse?
## Propagation de constantes
Le fichier [constant_propagation.py](config/constant_propagation.py) vise à
implémenter la propagation de constantes dans le cadre défini précédemment.
Pour cela, on donne un type de valeurs abstraites qui sont soit un entier (quand
on est sûr qu'une variable est associé à cet entier), soit `None`, pour indiquer
que plusieurs valeurs sont possibles. Un environnement abstrait associe à des
variables des valeurs abstraites, et notre état sera un environnement abstrait
ou `None` pour indiquer que le nœud associé n'est jamais atteint. On donne
également les fonctions d'évaluation abstraite des expressions, dans un
environnement abstrait et qui renvoient une valeur abstraite ou `None` si une
erreur se produit.
Implémenter les différentes méthodes de la classe `Constant_propagation`. On
définira un état initial où toutes les variables utilisées dans le programme
sont associées à la valeur `abstract_value(None)`, indiquant qu'elles sont
présentes dans l'environnement mais peuvent avoir n'importe quelle valeur.
On testera le résultat à l'aide du fichier
[example_constant_propagation.py](config/example_constant_propagation.py), qui
définit un programme dont les variables reçoivent des valeurs constantes. NB:
pour que l'analyseur arrive à montrer que `c` est constante en fin de programme,
il faut raffiner la fonction de transfert du `test`, en réduisant l'état propagé
quand le test est une égalité entre une variable et une expression qui s'évalue
à une constante, pour tenir compte du fait que dans la branche correspondante,
la variable est associée à une constante. Ce raffinement est facultatif.
# Analyse Statique de Programmes -- TP Analyse Statique
CentraleSupélec
Enseignant: Virgile Prevosto
## Préliminaires
Ce TP est la suite directe du [précédent](tp.md), et vise à implémenter une
analyse des signes et une analyse des intervalles en plus de la propagation de
constante vue précédemment. En particulier, il est impératif d'avoir terminé
l'implémentation du calcul de point fixe (c'est-à-dire avoir complété la
fonction `fixpoint_iteration` du fichier [`iteration.py`](config/iteration.py))
pour pouvoir exécuter les exemples proposés dans ce TP.
### Rappel Docker
L'image est ici: https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl et peut être utilisée soit localement, soit depuis `MyDocker`. De plus le répertoire `config` est monté automatiquement dans le container docker si vous utilisez les scripts associé ([PowerShell](https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl/blob/main/start-3asl.ps1) pour Windows ou [sh](https://github.com/Frederic-Boulanger-UPS/docker-webtop-3asl/blob/main/start-3asl.sh) pour Linux/macOS/BSD). Ces scripts devraient automatiquement ouvrir un onglet de votre navigateur web avec une session IceWM. Si ce n'est pas le cas, vous pouvez le faire manuellement: http://localhost:3000
NB: l'interpréteur Python est `python3` et non `python`.
### Typage statique
Les fichiers proposés ont des annotations de types permettant de vérifier que
les programmes sont bien typés avant de les exécuter. On pourra utiliser
l'outil [`pyright`](https://microsoft.github.io/pyright/#/) pour la vérification. Il n'est pas présent sur l'image
Docker, mais peut s'installer via les commandes suivantes:
```sh
sudo apt update
sudo apt install pipx
sudo apt install pyright
```
Il dispose également d'un plugin vscode.
## Treillis des signes
Dans un premier temps, on va s'intéresser à l'analyse des signes telle que vue en cours. Le but de cet exercice est de
compléter le fichier [`sign_analysis.py`](config/sign_analysis.py). Les différentes valeurs possibles sont définies dans
l'énumération `sign`, complétée par un élément `Top()` (une erreur a pu avoir lieu, mais n'est pas certaine), et `None` qui
indique comme précédemment l'absence de résultat, c'est à dire le fait qu'on a certainement une erreur. Une variante de `Top()`,
`BTop(True|False)` sera utilisée pour l'évaluation abstraite des expressions booléennes.
Comme pour la propagation de constante, les environnements abstraits sont des dictionnaires associant un `sign` à un nom de variable,
et un état est soit un environnement, soit `None` si on n'est jamais passé par le nœud correspondant.
On se donne également les opérations de treillis sur les `sign` (relation d'ordre, `join` et `meet`, ce dernier pouvant renvoyer
`None`), ainsi que les opérations arithmétiques abstraites.
### Évaluation des expressions
Compléter les fonctions `eval_aexp` et `eval_bexp`, et interpréter les résultats affichés par
[`example_sign_eval_exp.py`](config/example_sign_eval_exp.py).
NB: les opérations arithmétiques données travaillent uniquement sur des `sign`. C'est donc à `eval_aexp` de prendre en compte les
cas `None` et `Top`.
### Réduction d'un état
Compléter la fonction `reduce_state` qui étant donné un état et une expression booléenne essaie de réduire cet état sous l'hypothèse que
l'expression s'évalue à `True`. On pourra s'aider des fonctions définies au-dessus, qui prennent en argument un état, un nom de
variable `x`, et une expression arithmétique `expr`, et réduisent si possible la valeur associée à `x` en fonction de la comparaison
effectuée et de la valeur de `expr`. Le fichier [`example_sign_reduce.py`](config/example_sign_reduce.py) contient un certain nombre
de tests pour cette fonction.
### Fonctions de transfert
Compléter les fonctions de transfert de la classe `Sign_interp` et tester le résultat avec [`example_sign.py`](config/example_sign.py),
qui effectue l'analyse des signes pour le calcul de la factorielle
## Treillis des intervalles, début
On s'intéresse maintenant au treillis des intervalles. Le fichier [`interval_analysis.py`](config/interval_analysis.py) suit
la même structure que pour le treillis des signes, si ce n'est que les valeurs abstraites sont maintenant des objets de la classe
`Interval`, comportant deux champs, `lower_bound` et `upper_bound`, donc chacun peut être un entier ou `None`, ce qui indique qu'il
n'y a pas de borne.
On demande cette fois-ci de compléter les fonctions `interval_add` (addition d'intervalles) et `interval_div` (division d'intervalle).
Pour `interval_div`, on pourra notamment utiliser les fonctions `is_zero` et `contains_zero` qui renvoient `True` si leur argument est
respectivement le singleton `0` ou un intervalle contenant `0`. On pourra ensuite tester ces fonctions avec
[`example_interval_eval_exp.py`](config/example_interval_eval_exp.py)
Compléter ensuite les fonctions `reduce_eq`, `reduce_neq`, `reduce_leq` et `reduce_geq`, qui réduisent
la valeur d'une variable `x` sous l'hypothèse qu'elle est respectivement égale, différente, inférieure ou égale, ou supérieur ou égale à un entier donné. On pourra ensuite tester ces fonctions avec [`example_interval_reduce.py`](config/example_interval_reduce.py)
## Ajout de l'élargissement
Pour l'instant, notre fonction d'itération ne fait pas d'élargissement, donc l'analyse d'intervalle risque de ne pas terminer.
Modifier dans le fichier [`cfg.py`](config/cfg.py) la classe `LSkip` de manière à ce qu'elle ait un champ indiquant s'il s'agit
d'un arc de retour vers le nœud initial d'une boucle:
```python
@dataclass
class LSkip(Label):
"""label skip (does nothing)"""
is_back_edge: bool
def __str__(self):
return "skip"
```
Dans ce même fichier, mettez à jour `cfg_aux` pour créer des labels `LSkip` avec le booléen adéquat
Modifier également le fichier [`iteration.py`](config/iteration.py) de la manière suivante:
- ajouter une fonction `widen` à la class `Transfer`, qui par défaut fait un simple `self.join`
- modifier `fixpoint_iteration` selon la version mise à jour de l'algorithme telle que montrée en cours.
On pourra tester l'algorithme avec le fichier [`example_widen.py`](config/example_widen.py), dont l'analyse compte le nombre maximum
d'arêtes rencontrées depuis le début du programme.
## Treillis des intervalles, version finale
Compléter la fonction `widen` et les fonctions de transfert de la classe `Interval_analysis`, et faire l'analyse d'intervalle du calcul de la factorielle avec le fichier [`example_interval.py`](config/example_interval.py)