diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f4a6e982c2bf46f9d16524dec503e8473a4e63f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,165 @@ +### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Aargh nix +Pystan/.direnv/ diff --git a/Pystan/.envrc b/Pystan/.envrc new file mode 100644 index 0000000000000000000000000000000000000000..3550a30f2de389e537ee40ca5e64a77dc185c79b --- /dev/null +++ b/Pystan/.envrc @@ -0,0 +1 @@ +use flake diff --git a/Pystan/config/.gitignore b/Pystan/config/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f35b094363ce2e9783e3c95ce2960898844d7da8 --- /dev/null +++ b/Pystan/config/.gitignore @@ -0,0 +1,164 @@ +### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + diff --git a/Pystan/config/cfg.py b/Pystan/config/cfg.py new file mode 100644 index 0000000000000000000000000000000000000000..e8d828750d6842487b77be38a0607c6ddb0c9c6d --- /dev/null +++ b/Pystan/config/cfg.py @@ -0,0 +1,144 @@ +"""Control flow graphs + +- Label and its subclasses describe the label on the edges of the CfGs +- Nodes describes nodes, with an id and its successors +- Graph describes a graph +- Cfg describes a cfg, i.e. a graph with initial, final, and error nodes +- function cfg creates a cfg for the given program +""" +from syntax import * +from dataclasses import dataclass + +@dataclass +class Label: + """generic class for edges' labels""" + pass + +@dataclass +class LSkip(Label): + """label skip (does nothing)""" + def __str__(self): + return "skip" + +@dataclass +class LSet(Label): + """assignment label""" + var: str + expr: ArithExpr + def __str__(self): + return f"{self.var} := {self.expr}" + +@dataclass +class LTest(Label): + """test label""" + cond: BoolExpr + def __str__(self): + return f"?{self.cond}" + +@dataclass +class LErr(Label): + """error label""" + err: Expr + def __str__(self): + return f"Err({self.err})" + +class Node: + """node of the graph, with its id and its successors""" + def __init__(self,nid): + self.nid = nid + self.succs = [] + def __str__(self): + return f"Node {self.nid}" + +class Graph: + nb_nid: int + nodes: list[Node] + + """a graph, i.e. a list of nodes""" + def __init__(self): + self.nb_nid = 0 + self.nodes = [] + + def new_node(self) -> Node: + """creates a new node, with a fresh id and no succs""" + node = Node(self.nb_nid) + self.nb_nid += 1 + self.nodes += [node] + return node + + def new_edge(self,n1: Node,n2: Node,l: Label) -> None: + """creates a new edge. + If the nodes are not already in the graph, they are added. + In that case, no check is made that a node with the same id + is already present or not. + """ + if n1 not in self.nodes: + self.nodes += [n1] + if n2 not in self.nodes: + self.nodes += [n2] + n1.succs += [(n2,l)] + +def cfg_aux(g:Graph,e:Node,i:Node,instr:Instr) -> Node: + """auxiliary function to build a CfG + takes as argument a graph, the error node, the node starting + the current instruction, and the instruction itself, returns + the final node of current instruction + """ + match instr: + case ISkip(): + f = g.new_node() + g.new_edge(i,f,LSkip()) + return f + case ISet(var,expr): + f = g.new_node() + g.new_edge(i,f,LSet(var,expr)) + g.new_edge(i,e,LErr(expr)) + return f + case ISeq(first,second): + m = cfg_aux(g,e,i,first) + return cfg_aux(g,e,m,second) + case ICond(cond,true_branch,false_branch): + t = g.new_node() + f = g.new_node() + g.new_edge(i,t,LTest(cond)) + g.new_edge(i,f,LTest(BENeg(cond))) + g.new_edge(i,e,LErr(cond)) + tf = cfg_aux(g,e,t,true_branch) + ff = cfg_aux(g,e,f,false_branch) + f = g.new_node() + g.new_edge(tf,f,LSkip()) + g.new_edge(ff,f,LSkip()) + return f + case ILoop(cond,body): + ib = g.new_node() + f = g.new_node() + g.new_edge(i,ib,LTest(cond)) + g.new_edge(i,f,LTest(BENeg(cond))) + g.new_edge(i,e,LErr(cond)) + fb = cfg_aux(g,e,ib,body) + g.new_edge(fb,i,LSkip()) + return f + case _: assert False + +class Cfg: + """describes a CfG""" + g: Graph + err_node: Node + init_node: Node + final_node: Node + def __init__(self,instr): + self.g = Graph() + self.err_node = self.g.new_node() + self.init_node = self.g.new_node() + self.final_node = cfg_aux(self.g,self.err_node,self.init_node,instr) + + def __str__(self): + res = "" + for node in self.g.nodes: + res+=f"{node}:\n" + for (dest,lab) in node.succs: + res+=f" {lab} --> {dest}\n" + res+=f"init node is {self.init_node}\n" + res+=f"final node is {self.final_node}\n" + res+=f"err node is {self.err_node}\n" + return res diff --git a/Pystan/config/constant_propagation.py b/Pystan/config/constant_propagation.py new file mode 100644 index 0000000000000000000000000000000000000000..81bc7a20ab8fd0995277217eff73cbd5c4094e9d --- /dev/null +++ b/Pystan/config/constant_propagation.py @@ -0,0 +1,154 @@ +""" +implements a constant propagation analysis +""" + +from dataclasses import dataclass +from cfg import Label, LSkip, LSet, LTest, LErr +from iteration import Transfer +from syntax import * + +@dataclass +class abstract_value[T]: + """None indicates that we don't know the precise value""" + value: T | None + def __str__(self): + return f"{self.value}" + +type abstract_env = dict[str, abstract_value[int]] +"""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 eval_aexp(env: abstract_env, e: ArithExpr) -> abstract_value[int] | None: + """evaluate an arithmetic expression in an abstract environment + returns None in case of error + """ + match e: + case AECst(value): return abstract_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: return None + if res.value is None: return abstract_value(None) + if uop == Uop.OPP: return abstract_value(-res.value) + 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 v1.value is None or v2.value is None: + return abstract_value(None) + match bop: + case Bop.ADD: return abstract_value(v1.value+v2.value) + case Bop.MUL: return abstract_value(v1.value*v2.value) + case Bop.DIV: + if v2.value == 0: return None + return abstract_value(v1.value//v2.value) + case _: assert False + +def eval_bexp(env: abstract_env, e: BoolExpr) -> abstract_value[bool] | None: + """abstract evaluation of a boolean expression""" + match e: + case BEPlain(aexpr): + res = eval_aexp(env, aexpr) + if res is None: return None + if res.value is None: return abstract_value(None) + return abstract_value(res.value!=0) + 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 v1.value is None or v2.value is None: return abstract_value(None) + return abstract_value(v1.value == v2.value) + 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 v1.value is None or v2.value is None: return abstract_value(None) + return abstract_value(v1.value <= v2.value) + case BENeg(expr): + v = eval_bexp(env,expr) + if v is None: return None + if v.value is None: return v + return abstract_value(not v.value) + case _: assert False + +class Constant_propagation(Transfer[state]): + variables: frozenset[str] + def __init__(self,instr): + self.variables = variables_of_instr(instr) + def bottom(self) -> state: + return None + def init_state(self) -> state: + return {v : abstract_value(None) for v in self.variables} + def join(self,s1:state,s2:state) -> state: + match (s1, s2): + case None, s2: + return s2 + case s1, None: + return s1 + case s1, s2: + s: state = s1.copy() + for var in s2: + if not(var in s1) or s1[var] == abstract_value(None): + s = s | {var:s2[var]} + elif s2[var] == abstract_value(None): + pass + elif s1[var] != s2[var]: + s = s | {var: abstract_value(None)} + return s + + def included(self,s1: state,s2: state) -> bool: + match (s1, s2): + case None, s2: + return True + case s1, None: + return False + case s1, s2: + for v in s1: + if (s1[v] != abstract_value(None) and s1[v] != s2[v]): + return False + return True + def tr_skip(self,s: state) -> state: + return s + + def tr_set(self,s: state,v: str,e: ArithExpr) -> state: + if s == None: + return None + value = eval_aexp(s,e) + if value == None: + # Do we return None or abstract_value(None) + return None + return s | {v:value} + + def tr_test(self,s: state,c: BoolExpr) -> state: + if s == None: + return None + # Are we sure of that ? + if eval_bexp(s,c) == abstract_value(False): + return None + return s + + def tr_err(self,s: state,e: Expr) -> state: + if s == None: + return None + match e: + case ArithExpr(): + if eval_aexp(s,e) == None: + return s + else: + return None + case BoolExpr(): + if eval_bexp(s,e) == None: + return s + else: + return None + case _: + return None diff --git a/Pystan/config/example_constant_propagation.py b/Pystan/config/example_constant_propagation.py new file mode 100644 index 0000000000000000000000000000000000000000..0bf11f1a7a796124d13f8a3ea0e579835985a6d2 --- /dev/null +++ b/Pystan/config/example_constant_propagation.py @@ -0,0 +1,29 @@ +from syntax import * +from cfg import Cfg +from iteration import fixpoint_iteration +from constant_propagation import Constant_propagation + +x = AEVar("x") +y = AEVar("y") +c = AEVar("c") +one = AECst(1) +zero = AECst(0) + +propagate_compute=ISeq(ISet("x",one),ISet("x", AEBop(Bop.ADD,x,one))) +same_branch_if=ICond(BELeq(c,zero), ISet("y", one), ISet("y",one)) +equal_branch=ICond(BEEq(c,one),ISkip(),ISet("c",one)) + +prog = ISeq(ISeq(propagate_compute,same_branch_if),equal_branch) + +cfg = Cfg(prog) + +cst_propagation = Constant_propagation(prog) + +res = fixpoint_iteration(cst_propagation,cfg) + +for (node, state) in res.items(): + print(f"{node}:") + if state is None: print(" None") + else: + for (var, value) in state.items(): + print(f" {var} -> {value}") diff --git a/Pystan/config/example_interval.py b/Pystan/config/example_interval.py new file mode 100644 index 0000000000000000000000000000000000000000..8aa35e01334ffe53cbe00789994a19c11f544c59 --- /dev/null +++ b/Pystan/config/example_interval.py @@ -0,0 +1,4 @@ +from interval_analysis import analyze +from example_opsem import factorial + +analyze(factorial) diff --git a/Pystan/config/example_interval_eval_exp.py b/Pystan/config/example_interval_eval_exp.py new file mode 100644 index 0000000000000000000000000000000000000000..1203cab645be505a71cd14888374824eca277e68 --- /dev/null +++ b/Pystan/config/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/Pystan/config/example_interval_reduce.py b/Pystan/config/example_interval_reduce.py new file mode 100644 index 0000000000000000000000000000000000000000..2d7cdda54e9d02cfd00b41b9a65d67b63f5120b9 --- /dev/null +++ b/Pystan/config/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/Pystan/config/example_iteration.py b/Pystan/config/example_iteration.py new file mode 100644 index 0000000000000000000000000000000000000000..3a9a71bb66f797cb0a6ac81b285ed1c2a8da2f75 --- /dev/null +++ b/Pystan/config/example_iteration.py @@ -0,0 +1,52 @@ +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") diff --git a/Pystan/config/example_opsem.py b/Pystan/config/example_opsem.py new file mode 100644 index 0000000000000000000000000000000000000000..bcc5dca4e8bbb042e3de024382c4cab6ac304b65 --- /dev/null +++ b/Pystan/config/example_opsem.py @@ -0,0 +1,21 @@ +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}") diff --git a/Pystan/config/example_sign.py b/Pystan/config/example_sign.py new file mode 100644 index 0000000000000000000000000000000000000000..6aa6de3d4adb559dddcd246d9cdb3159f2f12b67 --- /dev/null +++ b/Pystan/config/example_sign.py @@ -0,0 +1,4 @@ +from sign_analysis import analyze +from example_opsem import factorial + +analyze(factorial) diff --git a/Pystan/config/example_sign_eval_exp.py b/Pystan/config/example_sign_eval_exp.py new file mode 100644 index 0000000000000000000000000000000000000000..c8bdd995dad92d272d407c6a2c040666e1f2ad29 --- /dev/null +++ b/Pystan/config/example_sign_eval_exp.py @@ -0,0 +1,28 @@ +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({}) diff --git a/Pystan/config/example_sign_reduce.py b/Pystan/config/example_sign_reduce.py new file mode 100644 index 0000000000000000000000000000000000000000..17ed63624d1e282d9f535cccecbdf5d04e169e20 --- /dev/null +++ b/Pystan/config/example_sign_reduce.py @@ -0,0 +1,33 @@ +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 }) + diff --git a/Pystan/config/example_widen.py b/Pystan/config/example_widen.py new file mode 100644 index 0000000000000000000000000000000000000000..c9372be7b6162f6f47cd8f7ed8a5c2e240aa187d --- /dev/null +++ b/Pystan/config/example_widen.py @@ -0,0 +1,61 @@ +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}') diff --git a/Pystan/config/interval_analysis.py b/Pystan/config/interval_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..9186efbf8ffeedb0fe1e207381313552f1b34163 --- /dev/null +++ b/Pystan/config/interval_analysis.py @@ -0,0 +1,406 @@ +# 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}") diff --git a/Pystan/config/iteration.py b/Pystan/config/iteration.py new file mode 100644 index 0000000000000000000000000000000000000000..d06c222af6de8d4ed4bfa294d6ccecfff82d8584 --- /dev/null +++ b/Pystan/config/iteration.py @@ -0,0 +1,83 @@ +"""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""" + + env : dict[Node, T] = {} + # map bottom to all nodes + for node in cfg.g.nodes: + env = env | {node : transfer.bottom()} + # except map initial state to initial node + env = env | {cfg.init_node : transfer.init_state()} + # build worklist with all the nodes from the initial node + worklist: list[tuple[Node, Node, Label]]= [] + for edge in cfg.init_node.succs: + worklist.append((cfg.init_node,) + edge) + + # as long as the worklist is not empty + while worklist != []: + e = worklist.pop() + src = e[0] + dst = e[1] + l = e[2] + + s1 = env[dst] + s_src = env[src] + s2 = s1 # to handled elegantly unmatched cases + match l: + case LSet(var, expr): + s2 = transfer.tr_set(s_src, var, expr) + case LTest(cond): + s2 = transfer.tr_test(s_src, cond) + case LErr(err): + s2 = transfer.tr_err(s_src, err) + case LSkip(): + s2 = transfer.tr_skip(s_src) + case _: + # What should we do here ? Isn't this supposed to be unreachable ? + pass + + env = env | {dst : transfer.join(s1, s2)} + if not(transfer.included(env[dst], s1)): + for edge in dst.succs: + worklist.append((dst,) + edge) + + return env diff --git a/Pystan/config/opsem.py b/Pystan/config/opsem.py new file mode 100644 index 0000000000000000000000000000000000000000..ba9165b642f31bcdadbe4297b1ca773f4ca7afd5 --- /dev/null +++ b/Pystan/config/opsem.py @@ -0,0 +1,110 @@ +""" +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): + return value + case AEVar(var): + if var in env: + return env[var] + else: + return None + case AEUop(uop,expr): + v = eval_aexp(env, expr) + match uop: + case Uop.OPP: + if v != None: + return - v + else: + return None + case AEBop(bop,left_expr,right_expr): + l = eval_aexp(env, left_expr) + r = eval_aexp(env, right_expr) + if (l == None) or (r == None): + return None + match bop: + case Bop.ADD: + return l + r + case Bop.MUL: + return l * r + case Bop.DIV: + if r!= 0: + return l // r + else: + return None + case _: assert False + +def eval_bexp(env: dict[str,int], exp:BoolExpr) -> bool | None: + """evaluates a boolean expression""" + match exp: + case BEPlain(aexpr): + v = eval_aexp(env, aexpr) + if (v == None): + return None + return v != 0 + case BEEq(left_expr,right_expr): + l = eval_aexp(env, left_expr) + r = eval_aexp(env, right_expr) + if (l == None) or (r == None): + return None + return l == r + case BELeq(left_expr,right_expr): + l = eval_aexp(env, left_expr) + r = eval_aexp(env, right_expr) + if (l == None) or (r == None): + return None + return l <= r + case BENeg(expr): + v = eval_bexp(env, expr) + if v == None: + return None + return not(v) + case _: assert False + +def eval_instr(env: dict[str,int], instr: Instr) -> dict[str,int] | None: + """evaluates an instruction""" + match instr: + case ISkip(): + return env + case ISet(var,expr): + v = eval_aexp(env, expr) + if v == None: + return None + return env | {var:v} + case ISeq(first,second): + rho1 = eval_instr(env, first) + if rho1 == None: + return None + return eval_instr(rho1, second) + case ICond(cond,true_branch,false_branch): + v = eval_bexp(env, cond) + t = eval_instr(env, true_branch) + f = eval_instr(env, false_branch) + + if (v == None) or (t == None) or (f == None): + return None + if v: + return t + else: + return f + case ILoop(cond,body): + v = eval_bexp(env, cond) + if (v == None): + return None + if v: + new_env = eval_instr(env, body) + if (new_env == None): + return None + return eval_instr(new_env, ILoop(cond, body)) + else: + return eval_instr(env, ISkip()) + case _: assert False diff --git a/Pystan/config/pyrightconfig.json b/Pystan/config/pyrightconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..80d140e60839ebbee0b542d63f517d4701ec2ae9 --- /dev/null +++ b/Pystan/config/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "typeCheckingMode" : "standard" +} diff --git a/Pystan/config/sign_analysis.py b/Pystan/config/sign_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..1988c81fe417dd1d8593f0f5a66546115699d04e --- /dev/null +++ b/Pystan/config/sign_analysis.py @@ -0,0 +1,338 @@ +# 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}") diff --git a/Pystan/config/syntax.py b/Pystan/config/syntax.py new file mode 100644 index 0000000000000000000000000000000000000000..c0938bda1f9a7554cc4db3a13763d0d68732c5bc --- /dev/null +++ b/Pystan/config/syntax.py @@ -0,0 +1,185 @@ +"""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 diff --git a/Pystan/flake.lock b/Pystan/flake.lock new file mode 100644 index 0000000000000000000000000000000000000000..1d0fc862615d94a19522e37133ddb0d382e57af0 --- /dev/null +++ b/Pystan/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1739736696, + "narHash": "sha256-zON2GNBkzsIyALlOCFiEBcIjI4w38GYOb+P+R4S8Jsw=", + "rev": "d74a2335ac9c133d6bbec9fc98d91a77f1604c1f", + "revCount": 754461, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.754461%2Brev-d74a2335ac9c133d6bbec9fc98d91a77f1604c1f/01951426-5a87-7b75-8413-1a0d9ec5ff04/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/Pystan/flake.nix b/Pystan/flake.nix new file mode 100644 index 0000000000000000000000000000000000000000..e4ad2916d3f801928b7621cbc12a1a99fad9c79a --- /dev/null +++ b/Pystan/flake.nix @@ -0,0 +1,26 @@ +{ + description = "A Nix-flake-based Python development environment"; + + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + + outputs = { self, nixpkgs }: + let + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { inherit system; }; + }); + in + { + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + venvDir = ".venv"; + packages = with pkgs; [ python312 ] ++ + (with pkgs.python312Packages; [ + pip + venvShellHook + basedpyright + ]); + }; + }); + }; +} diff --git a/Pystan/tp.md b/Pystan/tp.md new file mode 100644 index 0000000000000000000000000000000000000000..44dac1c51d659714556557ac49fde9c553a76072 --- /dev/null +++ b/Pystan/tp.md @@ -0,0 +1,113 @@ +# 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.