From 3c9ad83bd4e9b2dba6e6796a9227e81144692810 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 10 Sep 2025 11:12:27 +0200 Subject: [PATCH 001/126] add build folder to gitingore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cca6cf503..7e57a365e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ syntax: glob env bin +build deps/JPype1 __pycache__ .virtualenv @@ -14,4 +15,4 @@ tmp docs/build .idea *.pyc -viper_out +viper_out \ No newline at end of file From 0204b54be54ed2534f2923db55e478d1f7d14b94 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 11 Sep 2025 09:38:35 +0200 Subject: [PATCH 002/126] Start working on bytearray implementation --- src/nagini_translation/lib/constants.py | 2 + src/nagini_translation/lib/resolver.py | 4 + src/nagini_translation/resources/all.sil | 1 + .../resources/builtins.json | 44 ++++++++++ .../resources/bytearray.sil | 82 +++++++++++++++++++ src/nagini_translation/sif/resources/all.sil | 1 + .../sif/resources/builtins.json | 44 ++++++++++ 7 files changed, 178 insertions(+) create mode 100644 src/nagini_translation/resources/bytearray.sil diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 731a72030..8b680afdc 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -303,6 +303,8 @@ BYTES_TYPE = 'bytes' +BYTEARRAY_TYPE = 'bytearray' + INT_TYPE = 'int' PERM_TYPE = 'perm' diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 63275055e..3986f02d7 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -12,6 +12,7 @@ BOOL_TYPE, BUILTINS, BYTES_TYPE, + BYTEARRAY_TYPE, DICT_TYPE, FLOAT_TYPE, INT_TYPE, @@ -434,6 +435,9 @@ def _get_call_type(node: ast.Call, module: PythonModule, return module.global_module.classes[INT_TYPE] if func_name in ('token', 'ctoken', 'MustTerminate', 'MustRelease'): return module.global_module.classes[BOOL_TYPE] + # if func_name == BYTEARRAY_TYPE: + # return _get_collection_literal_type(node, ['args'], BYTEARRAY_TYPE, module, + # containers, container) if func_name == PSEQ_TYPE: return _get_collection_literal_type(node, ['args'], PSEQ_TYPE, module, containers, container) diff --git a/src/nagini_translation/resources/all.sil b/src/nagini_translation/resources/all.sil index 2043eb14f..3df036c45 100644 --- a/src/nagini_translation/resources/all.sil +++ b/src/nagini_translation/resources/all.sil @@ -23,6 +23,7 @@ domain SIFDomain[T] { import "bool.sil" import "float.sil" import "references.sil" +import "bytearray.sil" import "bytes.sil" import "iterator.sil" import "list.sil" diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index cb3878b3a..4b61a08e6 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -613,6 +613,50 @@ }, "extends": "object" }, +"bytearray": { + "methods": { + "__init__": { + "args": [], + "type": null, + "MustTerminate": true + }, + "append": { + "args": ["bytearray", "int"], + "type": null, + "requires": ["__sil_seq__"], + "MustTerminate": true + }, + "extend": { + "args": ["bytearray", "bytearray"], + "type": null, + "MustTerminate": true + }, + "__setitem__": { + "args": ["bytearray", "__prim__int", "int"], + "type": null, + "MustTerminate": true, + "requires": ["__len__", "__getitem__"] + } + }, + "functions": { + "__len__": { + "args": ["bytearray"], + "type": "__prim__int", + "requires": ["__val__"] + }, + "__create__": { + "args": ["__prim__Seq", "__prim__int"], + "type": "bytearray", + "requires": ["__len__", "__val__"] + }, + "__getitem__": { + "args": ["bytearray", "__prim__int"], + "type": "int", + "requires": ["__len__", "__val__"] + } + }, + "extends": "object" +}, "tuple": { "functions": { "__create0__": { diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil new file mode 100644 index 000000000..7f68fd0dd --- /dev/null +++ b/src/nagini_translation/resources/bytearray.sil @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 ETH Zurich + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +field bytearray_acc : Seq[Ref] + +method bytearray___init__() returns (res: Ref) + ensures acc(res.bytearray_acc) + ensures res.bytearray_acc == Seq[Ref]() + ensures typeof(res) == bytearray() + ensures Low(res) +{ + assume false +} + +function bytearray___val__(self: Ref) : Seq[Ref] + decreases _ + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, wildcard) +{ + self.bytearray_acc +} + +function bytearray___len__(self: Ref) : Int + decreases _ + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, wildcard) + ensures result >= 0 +{ + |self.bytearray_acc| +} + +function bytearray___create__(value: Seq[Ref], ctr: Int) : Ref + decreases _ + ensures typeof(result) == bytearray() + ensures bytearray___len__(result) == |value| + ensures bytearray___val__(result) == value + +// function bytearray___getitem__(self: Ref, key: Ref): Ref +// decreases _ +// requires issubtype(typeof(self), bytearray()) +// requires issubtype(typeof(key), int()) +// requires acc(self.bytearray_acc, wildcard) +// requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) +// requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) +// ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) +// ensures issubtype(typeof(result), int()) + +// method bytearray___setitem__(self: Ref, key: Int, item: Ref) returns () +// requires issubtype(typeof(self), bytearray()) +// requires acc(self.bytearray_acc) +// requires @error("Bytearray index may be negative.")(key >= 0) +// requires @error("Bytearray index may be out of bounds.")(key < bytearray___len__(self)) +// requires issubtype(typeof(item), int()) +// ensures acc(self.bytearray_acc) +// ensures self.bytearray_acc == old(self.bytearray_acc)[key := item] +// ensures (Low(key) && Low(item)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) +// { +// assume false +// } + +// method bytearray_append(self: Ref, item: Ref) returns () +// requires issubtype(typeof(self), bytearray()) +// requires acc(self.bytearray_acc) +// requires issubtype(typeof(item), int()) +// ensures acc(self.bytearray_acc) +// ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(item) +// { +// assume false +// } + +// method list_extend(self: Ref, other: Ref) returns () +// requires issubtype(typeof(self), bytearray()) +// requires issubtype(typeof(other), bytearray()) +// requires acc(self.bytearray_acc) +// requires acc(other.bytearray_acc, 1/100) +// ensures acc(self.bytearray_acc) +// ensures acc(other.bytearray_acc, 1/100) +// ensures self.bytearray_acc == old(self.bytearray_acc) ++ other.bytearray_acc \ No newline at end of file diff --git a/src/nagini_translation/sif/resources/all.sil b/src/nagini_translation/sif/resources/all.sil index 3d9388b72..1b9fddb5c 100644 --- a/src/nagini_translation/sif/resources/all.sil +++ b/src/nagini_translation/sif/resources/all.sil @@ -17,6 +17,7 @@ domain SIFDomain[T] { import "../../resources/bool.sil" import "../../resources/float.sil" import "references.sil" +import "../../resources/bytearray.sil" import "../../resources/bytes.sil" import "../../resources/iterator.sil" import "../../resources/list.sil" diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 5815523de..317cb0eeb 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -607,6 +607,50 @@ }, "extends": "object" }, +"bytearray": { + "methods": { + "__init__": { + "args": [], + "type": null, + "MustTerminate": true + }, + "append": { + "args": ["bytearray", "int"], + "type": null, + "requires": ["__sil_seq__"], + "MustTerminate": true + }, + "extend": { + "args": ["bytearray", "bytearray"], + "type": null, + "MustTerminate": true + }, + "__setitem__": { + "args": ["bytearray", "__prim__int", "int"], + "type": null, + "MustTerminate": true, + "requires": ["__len__", "__getitem__"] + } + }, + "functions": { + "__len__": { + "args": ["bytearray"], + "type": "__prim__int", + "requires": ["__val__"] + }, + "__create__": { + "args": ["__prim__Seq", "__prim__int"], + "type": "bytearray", + "requires": ["__len__", "__val__"] + }, + "__getitem__": { + "args": ["bytearray", "__prim__int"], + "type": "int", + "requires": ["__len__", "__val__"] + } + }, + "extends": "object" +}, "tuple": { "functions": { "__create0__": { From 612d0cdf3b6471d0c8373486e4c4dd6a1c2879f2 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 15 Sep 2025 15:36:15 +0200 Subject: [PATCH 003/126] Print name of unsupported function --- src/nagini_translation/translators/call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index a3ff0f5dd..63ad0e45a 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -783,11 +783,11 @@ def translate_args(self, target: PythonMethod, arg_nodes: List, # are just set to null. if keywords: raise UnsupportedException(node, desc='Keyword arguments in call to ' - 'builtin function.') + 'builtin function: ' + target.name) diff = target.nargs - len(unpacked_args) if diff < 0: raise UnsupportedException(node, 'Unsupported version of builtin ' - 'function.') + 'function: ' + target.name) if diff > 0: null = self.viper.NullLit(self.no_position(ctx), self.no_info(ctx)) unpacked_args += [null] * diff From 9fdbca850006b59d8831e811bca051d0f442ad27 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 15 Sep 2025 15:50:26 +0200 Subject: [PATCH 004/126] Trying call extension Just some work in progress --- src/nagini_translation/lib/constants.py | 3 ++- .../resources/builtins.json | 14 +++++++----- .../resources/bytearray.sil | 5 ++--- .../sif/resources/builtins.json | 14 +++++++----- src/nagini_translation/translators/call.py | 22 +++++++++++++++++++ .../translators/expression.py | 1 + 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 8b680afdc..8d00b6272 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -25,7 +25,8 @@ 'range', 'type', 'list', - 'enumerate'] + 'enumerate', + 'bytearray'] THREADING = ['Thread'] diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 4b61a08e6..2ec76cf11 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -639,16 +639,20 @@ } }, "functions": { + "__create__": { + "args": ["__prim__Seq", "__prim__int"], + "type": "bytearray", + "requires": ["__val__"] + }, + "__val__": { + "args": ["bytearray"], + "type": "__prim__Seq" + }, "__len__": { "args": ["bytearray"], "type": "__prim__int", "requires": ["__val__"] }, - "__create__": { - "args": ["__prim__Seq", "__prim__int"], - "type": "bytearray", - "requires": ["__len__", "__val__"] - }, "__getitem__": { "args": ["bytearray", "__prim__int"], "type": "int", diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 7f68fd0dd..3aeefce2d 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -33,11 +33,10 @@ function bytearray___len__(self: Ref) : Int |self.bytearray_acc| } -function bytearray___create__(value: Seq[Ref], ctr: Int) : Ref +function bytearray___create__(values: Seq[Ref], ctr: Int) : Ref decreases _ ensures typeof(result) == bytearray() - ensures bytearray___len__(result) == |value| - ensures bytearray___val__(result) == value + ensures bytearray___val__(result) == values // function bytearray___getitem__(self: Ref, key: Ref): Ref // decreases _ diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 317cb0eeb..255c9bfbd 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -633,16 +633,20 @@ } }, "functions": { + "__create__": { + "args": ["__prim__Seq", "__prim__int"], + "type": "bytearray", + "requires": ["__val__"] + }, + "__val__": { + "args": ["bytearray"], + "type": "__prim__Seq" + }, "__len__": { "args": ["bytearray"], "type": "__prim__int", "requires": ["__val__"] }, - "__create__": { - "args": ["__prim__Seq", "__prim__int"], - "type": "bytearray", - "requires": ["__len__", "__val__"] - }, "__getitem__": { "args": ["bytearray", "__prim__int"], "type": "int", diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 63ad0e45a..a3579e8c6 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -42,6 +42,7 @@ THREAD_POST_PRED, THREAD_START_PRED, TUPLE_TYPE, + BYTEARRAY_TYPE, ) from nagini_translation.lib.errors import rules from nagini_translation.lib.program_nodes import ( @@ -492,6 +493,25 @@ def _translate_enumerate(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: return arg_stmt + [new_stmt, type_inhale, contents_inhale], new_list.ref(node, ctx) + def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: + elems = [] + for c in node.s: + lit = self.viper.IntLit(c, self.to_position(node, ctx), + self.no_info(ctx)) + elems.append(self.to_ref(lit, ctx)) + if elems: + seq = self.viper.ExplicitSeq(elems, self.to_position(node, ctx), + self.no_info(ctx)) + else: + seq = self.viper.EmptySeq(self.viper.Ref, + self.to_position(node, ctx), + self.no_info(ctx)) + bytearray_class = ctx.module.global_module.classes[BYTEARRAY_TYPE] + args = [seq, self.get_fresh_int_lit(ctx)] + result = self.get_function_call(bytearray_class, '__create__', args, + [None, None], node, ctx) + return [], result + def _translate_builtin_func(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: """ @@ -524,6 +544,8 @@ def _translate_builtin_func(self, node: ast.Call, return self._translate_type_func(node, ctx) elif func_name == 'cast': return self._translate_cast_func(node, ctx) + elif func_name == 'bytearray': + return self._translate_bytearray(node, ctx) else: raise UnsupportedException(node) diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 9f1a6b587..75a8f72f6 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -141,6 +141,7 @@ def _translate_only(self, node: ast.AST, ctx: Context, type in any way. """ method = 'translate_' + node.__class__.__name__ + print("Translation method " + method) visitor = getattr(self, method, self.translate_generic) if method in TAKES_IMPURE_ARGS: impure_arg = TAKES_IMPURE_ARGS[method] From f2465f0f4ca5e09a9dfd108b808ab681172b0e1a Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 16 Sep 2025 17:23:31 +0200 Subject: [PATCH 005/126] Working implementation for empty bytearray constructor --- .../resources/bytearray.sil | 8 ++-- src/nagini_translation/translators/call.py | 39 +++++++++++-------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 3aeefce2d..2ceba0eb5 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -5,18 +5,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -field bytearray_acc : Seq[Ref] +field bytearray_acc : Seq[Int] // TODO change type to Int method bytearray___init__() returns (res: Ref) ensures acc(res.bytearray_acc) - ensures res.bytearray_acc == Seq[Ref]() + ensures res.bytearray_acc == Seq[Int]() ensures typeof(res) == bytearray() ensures Low(res) { assume false } -function bytearray___val__(self: Ref) : Seq[Ref] +function bytearray___val__(self: Ref) : Seq[Int] decreases _ requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, wildcard) @@ -33,7 +33,7 @@ function bytearray___len__(self: Ref) : Int |self.bytearray_acc| } -function bytearray___create__(values: Seq[Ref], ctr: Int) : Ref +function bytearray___initInts__(values: Seq[Int], ctr: Int) : Ref decreases _ ensures typeof(result) == bytearray() ensures bytearray___val__(result) == values diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index a3579e8c6..d35adb00c 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -494,23 +494,30 @@ def _translate_enumerate(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: ctx) def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: - elems = [] - for c in node.s: - lit = self.viper.IntLit(c, self.to_position(node, ctx), - self.no_info(ctx)) - elems.append(self.to_ref(lit, ctx)) - if elems: - seq = self.viper.ExplicitSeq(elems, self.to_position(node, ctx), - self.no_info(ctx)) - else: - seq = self.viper.EmptySeq(self.viper.Ref, - self.to_position(node, ctx), - self.no_info(ctx)) + print("Translating bytearray") + stmts = [] + bytearray_class = ctx.module.global_module.classes[BYTEARRAY_TYPE] - args = [seq, self.get_fresh_int_lit(ctx)] - result = self.get_function_call(bytearray_class, '__create__', args, - [None, None], node, ctx) - return [], result + res_var = ctx.current_function.create_variable('bytearray', bytearray_class, self.translator) + targets = [res_var.ref()] + + if len(node.args) == 0: + call = self.get_method_call(bytearray_class, '__init__', [], [], targets, node, ctx) + stmts.extend(call) + + elif len(node.args) == 1: + print(node.args[0]) + + arg_type = self.get_type(node.args[0], ctx) + print("bytearray arg type: ", arg_type) + arg_stmt, arg_val = self.translate_expr(node.args[0], ctx) + stmts.extend(arg_stmt) + contents = arg_val + else: + raise UnsupportedException(node, 'bytearray() is currently only supported with at most one args.') + + result_var = res_var.ref(node, ctx) + return stmts, result_var def _translate_builtin_func(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: From 86db8f37a0adb1e662bb7f984d31005fd8a4c2a0 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 17 Sep 2025 18:11:25 +0200 Subject: [PATCH 006/126] Working bytearray from bytearray and append method bytearray from list of ints fails --- .../resources/builtins.json | 18 ++++--- .../resources/bytearray.sil | 35 ++++++++++-- src/nagini_translation/translators/call.py | 54 +++++++++++++------ .../translators/expression.py | 1 - 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 2ec76cf11..1eeb6315d 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -620,10 +620,19 @@ "type": null, "MustTerminate": true }, + "__initFromList__": { + "args": ["__prim__Seq"], + "type": null, + "MustTerminate": true + }, + "__initFromBytearray__": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true + }, "append": { - "args": ["bytearray", "int"], + "args": ["bytearray", "__prim__int"], "type": null, - "requires": ["__sil_seq__"], "MustTerminate": true }, "extend": { @@ -639,11 +648,6 @@ } }, "functions": { - "__create__": { - "args": ["__prim__Seq", "__prim__int"], - "type": "bytearray", - "requires": ["__val__"] - }, "__val__": { "args": ["bytearray"], "type": "__prim__Seq" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 2ceba0eb5..b90c7758b 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -field bytearray_acc : Seq[Int] // TODO change type to Int +field bytearray_acc : Seq[Int] method bytearray___init__() returns (res: Ref) ensures acc(res.bytearray_acc) @@ -16,6 +16,27 @@ method bytearray___init__() returns (res: Ref) assume false } +method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) + requires issubtype(typeof(other), bytearray()) + requires acc(other.bytearray_acc, 1/100) + ensures acc(other.bytearray_acc, 1/100) + ensures acc(res.bytearray_acc) + ensures res.bytearray_acc == other.bytearray_acc + ensures typeof(res) == bytearray() + ensures Low(res) +{ + assume false +} + +method bytearray___initFromList__(values: Seq[Int]) returns (res: Ref) + ensures acc(res.bytearray_acc) + ensures res.bytearray_acc == values + ensures typeof(res) == bytearray() + ensures Low(res) +{ + assume false +} + function bytearray___val__(self: Ref) : Seq[Int] decreases _ requires issubtype(typeof(self), bytearray()) @@ -33,10 +54,14 @@ function bytearray___len__(self: Ref) : Int |self.bytearray_acc| } -function bytearray___initInts__(values: Seq[Int], ctr: Int) : Ref - decreases _ - ensures typeof(result) == bytearray() - ensures bytearray___val__(result) == values +method bytearray_append(self: Ref, item: Int) returns () + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc) + ensures acc(self.bytearray_acc) + ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(item) +{ + assume false +} // function bytearray___getitem__(self: Ref, key: Ref): Ref // decreases _ diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index d35adb00c..bf8714f40 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -494,30 +494,52 @@ def _translate_enumerate(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: ctx) def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: - print("Translating bytearray") - stmts = [] - + """ + Translates a call to bytearray() + """ bytearray_class = ctx.module.global_module.classes[BYTEARRAY_TYPE] res_var = ctx.current_function.create_variable('bytearray', bytearray_class, self.translator) targets = [res_var.ref()] - + + position = self.to_position(node, ctx) + info = self.no_info(ctx) + result_var = res_var.ref(node, ctx) + if len(node.args) == 0: call = self.get_method_call(bytearray_class, '__init__', [], [], targets, node, ctx) - stmts.extend(call) - + return call, result_var + elif len(node.args) == 1: - print(node.args[0]) - arg_type = self.get_type(node.args[0], ctx) - print("bytearray arg type: ", arg_type) - arg_stmt, arg_val = self.translate_expr(node.args[0], ctx) - stmts.extend(arg_stmt) - contents = arg_val - else: - raise UnsupportedException(node, 'bytearray() is currently only supported with at most one args.') + + if arg_type.name == BYTEARRAY_TYPE: + method_name = '__initFromBytearray__' + + if arg_type.name == LIST_TYPE: + method_name = '__initFromList__' + + # sil_ref_seq = self.viper.SeqType(self.viper.Int) + # ref_seq = SilverType(sil_ref_seq, ctx.module) + # havoc_var = ctx.current_function.create_variable('havoc_seq', ref_seq, + # self.translator) + # seq_field = self.viper.Field('bytearray_acc', sil_ref_seq, position, info) + # content_field = self.viper.FieldAccess(result_var, seq_field, position, info) + # stmts.append(self.viper.FieldAssign(content_field, havoc_var.ref(), position, + # info)) + # arg_seq = self.get_sequence(arg_type, arg_val, None, node, ctx, position) + # res_seq = self.get_sequence(None, result_var, None, node, ctx, position) + # seq_equal = self.viper.EqCmp(arg_seq, res_seq, position, info) + # stmts.append(self.viper.Inhale(seq_equal, position, info)) - result_var = res_var.ref(node, ctx) - return stmts, result_var + if method_name: + print("Calling method " + method_name) + target_method = bytearray_class.get_method(method_name) + arg_stmts, arg_vals, arg_types = self.translate_args(target_method, node.args, node.keywords, node, ctx) + constr_call = self.get_method_call(bytearray_class, method_name, arg_vals, arg_types, targets, node, ctx) + return arg_stmts + constr_call, res_var.ref(node, ctx) + + raise UnsupportedException(node, 'Unsupported variant of bytearray().') + def _translate_builtin_func(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 75a8f72f6..9f1a6b587 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -141,7 +141,6 @@ def _translate_only(self, node: ast.AST, ctx: Context, type in any way. """ method = 'translate_' + node.__class__.__name__ - print("Translation method " + method) visitor = getattr(self, method, self.translate_generic) if method in TAKES_IMPURE_ARGS: impure_arg = TAKES_IMPURE_ARGS[method] From 10439f50856061642d14c5488b58950829037e82 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 18 Sep 2025 18:32:53 +0200 Subject: [PATCH 007/126] Extending bytearray support --- .../resources/builtins.json | 41 ++++-- .../resources/bytearray.sil | 126 +++++++++++------- .../sif/resources/builtins.json | 55 +++++--- 3 files changed, 147 insertions(+), 75 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 1eeb6315d..2b798a3a9 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -631,36 +631,55 @@ "MustTerminate": true }, "append": { - "args": ["bytearray", "__prim__int"], + "args": ["bytearray", "int"], "type": null, - "MustTerminate": true + "MustTerminate": true, + "requires": ["int___unbox__"] }, "extend": { "args": ["bytearray", "bytearray"], "type": null, "MustTerminate": true }, + "reverse": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true, + "requires": ["__len__"] + }, "__setitem__": { - "args": ["bytearray", "__prim__int", "int"], + "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__"] + "requires": ["__len__", "__getitem__", "int___unbox__"] + }, + "__iter__": { + "args": ["bytearray"], + "type": "Iterator", + "MustTerminate": true + }, + "__getitem_slice__": { + "args": ["bytearray", "slice"], + "type": "bytearray", + "display_name": "__getitem__", + "requires": ["__len__", "slice___start__", "slice___stop__"], + "MustTerminate": true } }, "functions": { - "__val__": { - "args": ["bytearray"], - "type": "__prim__Seq" - }, "__len__": { "args": ["bytearray"], "type": "__prim__int", "requires": ["__val__"] }, "__getitem__": { - "args": ["bytearray", "__prim__int"], - "type": "int", - "requires": ["__len__", "__val__"] + "args": ["bytearray", "int"], + "type": "__prim__int", + "requires": ["__len__", "int___unbox__"] + }, + "__sil_seq__": { + "args": ["bytearray"], + "type": "__prim__Seq" } }, "extends": "object" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index b90c7758b..efcca573b 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -18,8 +18,8 @@ method bytearray___init__() returns (res: Ref) method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) requires issubtype(typeof(other), bytearray()) - requires acc(other.bytearray_acc, 1/100) - ensures acc(other.bytearray_acc, 1/100) + requires acc(other.bytearray_acc, 1/1000) + ensures acc(other.bytearray_acc, 1/1000) ensures acc(res.bytearray_acc) ensures res.bytearray_acc == other.bytearray_acc ensures typeof(res) == bytearray() @@ -37,70 +37,100 @@ method bytearray___initFromList__(values: Seq[Int]) returns (res: Ref) assume false } -function bytearray___val__(self: Ref) : Seq[Int] +function bytearray___len__(self: Ref) : Int decreases _ requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, wildcard) + ensures result >= 0 { - self.bytearray_acc + |self.bytearray_acc| } -function bytearray___len__(self: Ref) : Int +function bytearray___getitem__(self: Ref, key: Ref): Int decreases _ requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, wildcard) - ensures result >= 0 + requires issubtype(typeof(key), int()) + requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) + requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) + ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) + // ensures 0 <= result < 256 + +method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) + requires issubtype(typeof(self), bytearray()) + requires issubtype(typeof(key), slice()) + requires acc(self.bytearray_acc, 1/1000) + ensures acc(self.bytearray_acc, 1/1000) + ensures acc(_res.bytearray_acc) + ensures typeof(_res) == bytearray() + ensures _res.bytearray_acc == self.bytearray_acc[slice___start__(key, bytearray___len__(self))..slice___stop__(key, bytearray___len__(self))] { - |self.bytearray_acc| + assume false } -method bytearray_append(self: Ref, item: Int) returns () +method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) + requires issubtype(typeof(key), int()) + requires @error("List index may be negative.")(int___unbox__(key) >= 0) + requires @error("List index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) + requires issubtype(typeof(value), int()) + requires 0 <= int___unbox__(value) < 256 ensures acc(self.bytearray_acc) - ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(item) + ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] + ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) { assume false -} +} -// function bytearray___getitem__(self: Ref, key: Ref): Ref -// decreases _ -// requires issubtype(typeof(self), bytearray()) -// requires issubtype(typeof(key), int()) -// requires acc(self.bytearray_acc, wildcard) -// requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) -// requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) -// ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) -// ensures issubtype(typeof(result), int()) +method bytearray_append(self: Ref, item: Ref) returns () + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc) + requires issubtype(typeof(item), int()) + requires 0 <= int___unbox__(item) < 256 + ensures acc(self.bytearray_acc) + ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) +{ + assume false +} + +// Actual type of other is Iterable[SupportsIndex] +method bytearray_extend(self: Ref, other: Ref) returns () + requires issubtype(typeof(self), bytearray()) + requires issubtype(typeof(other), bytearray()) + requires acc(self.bytearray_acc) + requires acc(other.bytearray_acc, 1/100) + ensures acc(self.bytearray_acc) + ensures acc(other.bytearray_acc, 1/100) + ensures self.bytearray_acc == old(self.bytearray_acc) ++ other.bytearray_acc + +method bytearray_reverse(self: Ref) returns () + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc) + ensures acc(self.bytearray_acc) + ensures old(bytearray___len__(self)) == bytearray___len__(self) + ensures forall i: Int :: {self.bytearray_acc[i]} ((i >= 0 && i < bytearray___len__(self)) ==> (self.bytearray_acc[i] == old(self.bytearray_acc[bytearray___len__(self) - 1 - i]))) -// method bytearray___setitem__(self: Ref, key: Int, item: Ref) returns () -// requires issubtype(typeof(self), bytearray()) -// requires acc(self.bytearray_acc) -// requires @error("Bytearray index may be negative.")(key >= 0) -// requires @error("Bytearray index may be out of bounds.")(key < bytearray___len__(self)) -// requires issubtype(typeof(item), int()) -// ensures acc(self.bytearray_acc) -// ensures self.bytearray_acc == old(self.bytearray_acc)[key := item] -// ensures (Low(key) && Low(item)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) -// { -// assume false -// } -// method bytearray_append(self: Ref, item: Ref) returns () -// requires issubtype(typeof(self), bytearray()) -// requires acc(self.bytearray_acc) -// requires issubtype(typeof(item), int()) -// ensures acc(self.bytearray_acc) -// ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(item) -// { -// assume false -// } +// TODO implement related functionality for Iterator on bytearray +method bytearray___iter__(self: Ref) returns (_res: Ref) + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, 1/ 20) + ensures _res != self + ensures acc(_res.bytearray_acc, 1 / 20) + ensures acc(self.bytearray_acc, 1 / 20) + ensures _res.bytearray_acc == self.bytearray_acc + ensures acc(_res.__container, write) && (_res.__container == self) + ensures acc(_res.__iter_index, write) && (_res.__iter_index == 0) + ensures acc(_res.__previous, write) && _res.__previous == Seq[Ref]() + ensures issubtype(typeof(_res), Iterator(int())) +{ + inhale false +} -// method list_extend(self: Ref, other: Ref) returns () -// requires issubtype(typeof(self), bytearray()) -// requires issubtype(typeof(other), bytearray()) -// requires acc(self.bytearray_acc) -// requires acc(other.bytearray_acc, 1/100) -// ensures acc(self.bytearray_acc) -// ensures acc(other.bytearray_acc, 1/100) -// ensures self.bytearray_acc == old(self.bytearray_acc) ++ other.bytearray_acc \ No newline at end of file +function bytearray___sil_seq__(self: Ref): Seq[Int] + decreases _ + requires acc(self.bytearray_acc, wildcard) +{ + self.bytearray_acc +} \ No newline at end of file diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 255c9bfbd..778b10620 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -614,43 +614,66 @@ "type": null, "MustTerminate": true }, + "__initFromList__": { + "args": ["__prim__Seq"], + "type": null, + "MustTerminate": true + }, + "__initFromBytearray__": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true + }, "append": { "args": ["bytearray", "int"], "type": null, - "requires": ["__sil_seq__"], - "MustTerminate": true + "MustTerminate": true, + "requires": ["int___unbox__"] }, "extend": { "args": ["bytearray", "bytearray"], "type": null, "MustTerminate": true }, + "reverse": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true, + "requires": ["__len__"] + }, "__setitem__": { - "args": ["bytearray", "__prim__int", "int"], + "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__"] - } - }, - "functions": { - "__create__": { - "args": ["__prim__Seq", "__prim__int"], - "type": "bytearray", - "requires": ["__val__"] + "requires": ["__len__", "__getitem__", "int___unbox__"] }, - "__val__": { + "__iter__": { "args": ["bytearray"], - "type": "__prim__Seq" + "type": "Iterator", + "MustTerminate": true }, + "__getitem_slice__": { + "args": ["bytearray", "slice"], + "type": "bytearray", + "display_name": "__getitem__", + "requires": ["__len__", "slice___start__", "slice___stop__"], + "MustTerminate": true + } + }, + "functions": { "__len__": { "args": ["bytearray"], "type": "__prim__int", "requires": ["__val__"] }, "__getitem__": { - "args": ["bytearray", "__prim__int"], - "type": "int", - "requires": ["__len__", "__val__"] + "args": ["bytearray", "int"], + "type": "__prim__int", + "requires": ["__len__", "int___unbox__"] + }, + "__sil_seq__": { + "args": ["bytearray"], + "type": "__prim__Seq" } }, "extends": "object" From bd4517c031c45f4df10fe094e458fb2f02199d45 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 19 Sep 2025 10:03:48 +0200 Subject: [PATCH 008/126] Add special predicate for access to bytearray --- src/nagini_contracts/contracts.py | 8 ++++++++ src/nagini_translation/lib/constants.py | 2 +- src/nagini_translation/translators/contract.py | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 68aadb8be..4104ec688 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -518,6 +518,13 @@ def dict_pred(d: object) -> bool: be folded or unfolded. """ +def bytearray_pred(d: object) -> bool: + """ + Special, predefined predicate that represents the permissions belonging + to a bytearray. To be used like normal predicates, except it does not need to + be folded or unfolded. + """ + def isNaN(f: float) -> bool: pass @@ -569,6 +576,7 @@ def isNaN(f: float) -> bool: 'list_pred', 'dict_pred', 'set_pred', + 'bytearray_pred', 'PSeq', 'PSet', 'PMultiset', diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 8d00b6272..a689a076a 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -30,7 +30,7 @@ THREADING = ['Thread'] -BUILTIN_PREDICATES = ['list_pred', 'set_pred', 'dict_pred', 'MayStart', 'ThreadPost'] +BUILTIN_PREDICATES = ['list_pred', 'set_pred', 'dict_pred', 'bytearray_pred', 'MayStart', 'ThreadPost'] FUNCTION_DOMAIN_NAME = 'Function' diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index c4bd36f0a..6f7e57e11 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -133,6 +133,7 @@ def _get_perm(self, node: ast.Call, ctx: Context) -> Expr: def translate_builtin_predicate(self, node: ast.Call, perm: Expr, args: List[Expr], ctx: Context) -> Expr: name = node.func.id + seq_int = self.viper.SeqType(self.viper.Int) seq_ref = self.viper.SeqType(self.viper.Ref) set_ref = self.viper.SetType(self.viper.Ref) map_ref_ref = self.viper.MapType(self.viper.Ref, self.viper.Ref) @@ -145,6 +146,8 @@ def translate_builtin_predicate(self, node: ast.Call, perm: Expr, return self._get_field_perm('set_acc', set_ref, perm, args[0], pos, ctx) elif name == 'dict_pred': return self._get_field_perm('dict_acc', map_ref_ref, perm, args[0], pos, ctx) + elif name == 'bytearray_pred': + return self._get_field_perm('bytearray_acc', seq_int, perm, args[0], pos, ctx) elif name == 'MayStart': return self.translate_may_start(node, args, perm, ctx) elif name == 'ThreadPost': From 7edab59db5ebc2c20edb3d62a603399296c38085 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 19 Sep 2025 12:04:39 +0200 Subject: [PATCH 009/126] Implement Iterator for bytearray --- src/nagini_translation/lib/viper_ast.py | 1 + src/nagini_translation/models/extractor.py | 2 +- src/nagini_translation/resources/bool.sil | 3 +- .../resources/builtins.json | 26 +++++++--- .../resources/bytearray.sil | 50 +++++++++++++++---- src/nagini_translation/translators/program.py | 4 ++ .../translators/statement.py | 11 +++- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/nagini_translation/lib/viper_ast.py b/src/nagini_translation/lib/viper_ast.py index ce7d61c18..27e50dbd9 100644 --- a/src/nagini_translation/lib/viper_ast.py +++ b/src/nagini_translation/lib/viper_ast.py @@ -207,6 +207,7 @@ def mark_class_used(self, name: str): self.used_names.add('list') self.used_names.add('dict') self.used_names.add('set') + self.used_names.add('bytearray') self.used_names.add(name) def DomainFuncApp(self, func_name, args, type_passed, diff --git a/src/nagini_translation/models/extractor.py b/src/nagini_translation/models/extractor.py index 77152666c..dfe3d541e 100644 --- a/src/nagini_translation/models/extractor.py +++ b/src/nagini_translation/models/extractor.py @@ -81,7 +81,7 @@ def extract_field_chunk(self, chunk, jvm, modules, model, target): value = None if field_name in ('__iter_index', '__previous', '__container'): return - if field_name in ('list_acc', 'set_acc', 'dict_acc', '_val', 'MustReleaseBounded', 'MustReleaseUnbounded'): + if field_name in ('list_acc', 'set_acc', 'dict_acc', 'bytearray_acc', '_val', 'MustReleaseBounded', 'MustReleaseUnbounded'): # Special handling, pyfield = field_name else: diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index aae5c7f15..0825aea95 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -12,7 +12,8 @@ function object___bool__(self: Ref) : Bool // this is not the case for collections. ensures self != null ==> (let t == (typeof(self)) in ((!issubtype(t, list(list_arg(t, 0))) && !issubtype(t, set(set_arg(t, 0))) && - !issubtype(t, dict(dict_arg(t, 0), dict_arg(t, 1)))) ==> result)) + !issubtype(t, dict(dict_arg(t, 0), dict_arg(t, 1))) && + !issubtype(t, bytearray())) ==> result)) function NoneType___bool__(self: Ref) : Bool decreases _ diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 2b798a3a9..134a7d12a 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -651,12 +651,13 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "int___unbox__"] + "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__"] }, "__iter__": { "args": ["bytearray"], "type": "Iterator", - "MustTerminate": true + "MustTerminate": true, + "requires": ["__sil_seq__"] }, "__getitem_slice__": { "args": ["bytearray", "slice"], @@ -669,17 +670,30 @@ "functions": { "__len__": { "args": ["bytearray"], - "type": "__prim__int", - "requires": ["__val__"] + "type": "__prim__int" }, "__getitem__": { "args": ["bytearray", "int"], "type": "__prim__int", - "requires": ["__len__", "int___unbox__"] + "requires": ["__len__", "bytearray___bounds_helper__", "int___unbox__"] + }, + "__contains__": { + "args": ["bytearray", "int"], + "type": "__prim__bool", + "requires": ["int___unbox__"] + }, + "__bool__": { + "args": ["bytearray"], + "type": "__prim__bool" + }, + "__bounds_helper__": { + "args": ["__prim__int"], + "type": "__prim__bool" }, "__sil_seq__": { "args": ["bytearray"], - "type": "__prim__Seq" + "type": "__prim__Seq", + "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__"] } }, "extends": "object" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index efcca573b..927e0b6a3 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -7,6 +7,12 @@ field bytearray_acc : Seq[Int] +function bytearray___bounds_helper__(value: Int): Bool + decreases _ +{ + 0 <= value < 256 +} + method bytearray___init__() returns (res: Ref) ensures acc(res.bytearray_acc) ensures res.bytearray_acc == Seq[Int]() @@ -46,6 +52,26 @@ function bytearray___len__(self: Ref) : Int |self.bytearray_acc| } +function bytearray___contains__(self: Ref, key: Ref): Bool + decreases _ + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, wildcard) + requires issubtype(typeof(key), int()) + ensures result == (int___unbox__(key) in self.bytearray_acc) + +function bytearray___bool__(self: Ref) : Bool + decreases _ + requires self != null ==> issubtype(typeof(self), bytearray()) + requires self != null ==> acc(self.bytearray_acc, wildcard) + ensures self == null ==> !result + ensures self != null ==> result == (|self.bytearray_acc| != 0) + +// function bytearray___hex__(self: Ref): Str +// decreases _ +// requires issubtype(typeof(self), bytearray()) +// requires acc(self.bytearray_acc, wildcard) +// ensures result == + function bytearray___getitem__(self: Ref, key: Ref): Int decreases _ requires issubtype(typeof(self), bytearray()) @@ -54,7 +80,7 @@ function bytearray___getitem__(self: Ref, key: Ref): Int requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) - // ensures 0 <= result < 256 + ensures bytearray___bounds_helper__(result) method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) requires issubtype(typeof(self), bytearray()) @@ -75,7 +101,7 @@ method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires @error("List index may be negative.")(int___unbox__(key) >= 0) requires @error("List index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) requires issubtype(typeof(value), int()) - requires 0 <= int___unbox__(value) < 256 + requires bytearray___bounds_helper__(int___unbox__(value)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) @@ -87,7 +113,7 @@ method bytearray_append(self: Ref, item: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) requires issubtype(typeof(item), int()) - requires 0 <= int___unbox__(item) < 256 + requires bytearray___bounds_helper__(int___unbox__(item)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) { @@ -117,9 +143,9 @@ method bytearray___iter__(self: Ref) returns (_res: Ref) requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, 1/ 20) ensures _res != self - ensures acc(_res.bytearray_acc, 1 / 20) + ensures acc(_res.list_acc, 1 / 20) ensures acc(self.bytearray_acc, 1 / 20) - ensures _res.bytearray_acc == self.bytearray_acc + ensures _res.list_acc == bytearray___sil_seq__(self) ensures acc(_res.__container, write) && (_res.__container == self) ensures acc(_res.__iter_index, write) && (_res.__iter_index == 0) ensures acc(_res.__previous, write) && _res.__previous == Seq[Ref]() @@ -128,9 +154,11 @@ method bytearray___iter__(self: Ref) returns (_res: Ref) inhale false } -function bytearray___sil_seq__(self: Ref): Seq[Int] - decreases _ - requires acc(self.bytearray_acc, wildcard) -{ - self.bytearray_acc -} \ No newline at end of file +function bytearray___sil_seq__(self: Ref): Seq[Ref] + decreases _ + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, wildcard) + ensures |result| == bytearray___len__(self) + ensures (forall i: Int :: { result[i] } i >= 0 && i < bytearray___len__(self) ==> result[i] == __prim__int___box__(self.bytearray_acc[i])) + ensures (forall i: Ref :: { (i in result) } (i in result) == (typeof(i) == int() && (int___unbox__(i) in self.bytearray_acc))) + ensures (forall i: Ref :: { (i in result) } (i in result) ==> bytearray___bounds_helper__(int___unbox__(i))) \ No newline at end of file diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index e02dfbae6..291fb4115 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -487,6 +487,10 @@ def _create_predefined_fields(self, self.viper.MapType(self.viper.Ref, self.viper.Ref), self.no_position(ctx), self.no_info(ctx))) + fields.append(self.viper.Field('bytearray_acc', + self.viper.SeqType(self.viper.Int), + self.no_position(ctx), + self.no_info(ctx))) fields.append(self.viper.Field('Measure$acc', self.viper.SeqType(self.viper.Ref), self.no_position(ctx), diff --git a/src/nagini_translation/translators/statement.py b/src/nagini_translation/translators/statement.py index e58c7aba6..d6e3bb7a1 100644 --- a/src/nagini_translation/translators/statement.py +++ b/src/nagini_translation/translators/statement.py @@ -11,6 +11,7 @@ BYTES_TYPE, COMBINED_NAME_ACCESSOR, DICT_TYPE, + BYTEARRAY_TYPE, END_LABEL, IGNORED_IMPORTS, IGNORED_MODULE_NAMES, @@ -453,6 +454,7 @@ def _create_for_loop_invariant(self, iter_var: PythonVar, seq_temp_var: PythonVa """ pos = self.to_position(node, ctx) info = self.no_info(ctx) + seq_int = self.viper.SeqType(self.viper.Int) seq_ref = self.viper.SeqType(self.viper.Ref) set_ref = self.viper.SetType(self.viper.Ref) map_ref_ref = self.viper.MapType(self.viper.Ref, self.viper.Ref) @@ -482,6 +484,13 @@ def _create_for_loop_invariant(self, iter_var: PythonVar, seq_temp_var: PythonVa frac_perm_120, pos, info) invariant.append(field_pred) + elif iterable_type.name == BYTEARRAY_TYPE: + acc_field = self.viper.Field('bytearray_acc', seq_int, pos, info) + field_acc = self.viper.FieldAccess(iterable, acc_field, pos, info) + field_pred = self.viper.FieldAccessPredicate(field_acc, + frac_perm_120, pos, + info) + invariant.append(field_pred) elif iterable_type.name == RANGE_TYPE: pass else: @@ -712,7 +721,7 @@ def translate_stmt_For(self, node: ast.For, ctx: Context) -> List[Stmt]: # Find type of the collection content we're iterating over. if iterable_type.name in (LIST_TYPE, DICT_TYPE, SET_TYPE): target_type = iterable_type.type_args[0] - elif iterable_type.name in (RANGE_TYPE, BYTES_TYPE): + elif iterable_type.name in (RANGE_TYPE, BYTES_TYPE, BYTEARRAY_TYPE): target_type = ctx.module.global_module.classes[INT_TYPE] else: raise UnsupportedException(node, 'unknown.iterable') From e9d713399e158dadc55450dd42318f765bc90845 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 19 Sep 2025 15:26:04 +0200 Subject: [PATCH 010/126] Update builtins.json --- .../sif/resources/builtins.json | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 778b10620..260630374 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -645,12 +645,13 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "int___unbox__"] + "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__"] }, "__iter__": { "args": ["bytearray"], "type": "Iterator", - "MustTerminate": true + "MustTerminate": true, + "requires": ["__sil_seq__"] }, "__getitem_slice__": { "args": ["bytearray", "slice"], @@ -663,17 +664,30 @@ "functions": { "__len__": { "args": ["bytearray"], - "type": "__prim__int", - "requires": ["__val__"] + "type": "__prim__int" }, "__getitem__": { "args": ["bytearray", "int"], "type": "__prim__int", - "requires": ["__len__", "int___unbox__"] + "requires": ["__len__", "bytearray___bounds_helper__", "int___unbox__"] + }, + "__contains__": { + "args": ["bytearray", "int"], + "type": "__prim__bool", + "requires": ["int___unbox__"] + }, + "__bool__": { + "args": ["bytearray"], + "type": "__prim__bool" + }, + "__bounds_helper__": { + "args": ["__prim__int"], + "type": "__prim__bool" }, "__sil_seq__": { "args": ["bytearray"], - "type": "__prim__Seq" + "type": "__prim__Seq", + "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__"] } }, "extends": "object" From 9160e1ea7c61fc3f88f45a944f70f1d9c51154c8 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 23 Sep 2025 15:06:41 +0200 Subject: [PATCH 011/126] Add lshift to bitwise operations --- src/nagini_translation/resources/bool.sil | 25 +++++++++++++++++++ .../resources/builtins.json | 14 +++++++++-- src/nagini_translation/resources/intbv.sil | 1 + 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 0825aea95..788bc3da3 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -118,6 +118,31 @@ function int___rxor__(self: Ref, other: Ref): Ref int___xor__(self, other) } +function int___lshift__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), int()) + requires issubtype(typeof(other), int()) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) + ensures result == + ((issubtype(typeof(self), bool()) && issubtype(typeof(other), bool())) ? + __prim__bool___box__(bool___unbox__(self) != bool___unbox__(other)) : + __prim__int___box__(fromBVInt(shlBVInt(toBVInt(int___unbox__(self)), toBVInt(int___unbox__(other)))))) + +function int___rlshift__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), int()) + requires issubtype(typeof(other), int()) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) +{ + int___lshift__(self, other) +} + function int___bool__(self: Ref) : Bool decreases _ requires self != null ==> issubtype(typeof(self), int()) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 134a7d12a..88c6afd58 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -318,7 +318,7 @@ "__ror__": { "args": ["int", "int"], "type": "int", - "requires": ["__and__"] + "requires": ["__or__"] }, "__xor__": { "args": ["int", "int"], @@ -328,7 +328,17 @@ "__rxor__": { "args": ["int", "int"], "type": "int", - "requires": ["__and__"] + "requires": ["__xor__"] + }, + "__lshift__": { + "args": ["int", "int"], + "type": "int", + "requires": ["__prim__int___box__", "int___unbox__", "__prim__bool___box__", "bool___unbox__"] + }, + "__rlshift__": { + "args": ["int", "int"], + "type": "int", + "requires": ["__lshift__"] } }, "extends": "float" diff --git a/src/nagini_translation/resources/intbv.sil b/src/nagini_translation/resources/intbv.sil index e425c76d8..7b2501402 100644 --- a/src/nagini_translation/resources/intbv.sil +++ b/src/nagini_translation/resources/intbv.sil @@ -16,4 +16,5 @@ domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function andBVInt(___intbv, ___intbv): ___intbv interpretation "bvand" function orBVInt(___intbv, ___intbv): ___intbv interpretation "bvor" function xorBVInt(___intbv, ___intbv): ___intbv interpretation "bvxor" + function shlBVInt(___intbv, ___intbv): ___intbv interpretation "bvshl" } \ No newline at end of file From 0fd24d60912d6dc3885ee7d962ee8ec3f454f4c2 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 23 Sep 2025 15:34:52 +0200 Subject: [PATCH 012/126] Adjust error messages for bytearray bounds --- src/nagini_translation/resources/bytearray.sil | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 927e0b6a3..1273172a0 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -98,10 +98,10 @@ method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) requires issubtype(typeof(key), int()) - requires @error("List index may be negative.")(int___unbox__(key) >= 0) - requires @error("List index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) + requires @error("Bytearray index may be negative.")(int___unbox__(key) >= 0) + requires @error("Bytearray index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) requires issubtype(typeof(value), int()) - requires bytearray___bounds_helper__(int___unbox__(value)) + requires @error("Provided value may be out of bounds.")bytearray___bounds_helper__(int___unbox__(value)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) @@ -113,7 +113,7 @@ method bytearray_append(self: Ref, item: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) requires issubtype(typeof(item), int()) - requires bytearray___bounds_helper__(int___unbox__(item)) + requires @error("Provided item may be out of bounds.")bytearray___bounds_helper__(int___unbox__(item)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) { From 9480b7be2b09885e5aff04615163512cd58c99c7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 24 Sep 2025 10:09:47 +0200 Subject: [PATCH 013/126] Get __initFromList__ working --- .../resources/builtins.json | 15 +++++-- .../resources/bytearray.sil | 44 +++++++------------ .../sif/resources/builtins.json | 15 +++++-- src/nagini_translation/translators/call.py | 3 ++ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 88c6afd58..0ac118fc8 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -630,10 +630,17 @@ "type": null, "MustTerminate": true }, + "__initFromInt__": { + "args": ["int"], + "type": null, + "MustTerminate": true, + "requires": ["int___unbox__"] + }, "__initFromList__": { - "args": ["__prim__Seq"], + "args": ["list"], "type": null, - "MustTerminate": true + "MustTerminate": true, + "requires": ["list", "list___getitem__", "list___len__", "bytearray___bounds_helper__", "int___unbox__", "__seq_ref_to_seq_int"] }, "__initFromBytearray__": { "args": ["bytearray"], @@ -661,7 +668,7 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__"] + "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__", "__prim__int___box__"] }, "__iter__": { "args": ["bytearray"], @@ -703,7 +710,7 @@ "__sil_seq__": { "args": ["bytearray"], "type": "__prim__Seq", - "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__"] + "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] } }, "extends": "object" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 1273172a0..cd602e46c 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -7,6 +7,7 @@ field bytearray_acc : Seq[Int] +// Bytearray only accepts values in the range [0, 255] function bytearray___bounds_helper__(value: Int): Bool decreases _ { @@ -18,9 +19,14 @@ method bytearray___init__() returns (res: Ref) ensures res.bytearray_acc == Seq[Int]() ensures typeof(res) == bytearray() ensures Low(res) -{ - assume false -} + +method bytearray___initFromInt__(length: Ref) returns (res: Ref) + requires issubtype(typeof(length), int()) + ensures typeof(res) == bytearray() + ensures acc(res.bytearray_acc) + ensures |res.bytearray_acc| == int___unbox__(length) + ensures (forall i: Int :: { res.bytearray_acc[i] } 0 <= i < int___unbox__(length) ==> res.bytearray_acc[i] == 0) + ensures Low(res) method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) requires issubtype(typeof(other), bytearray()) @@ -30,19 +36,17 @@ method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) ensures res.bytearray_acc == other.bytearray_acc ensures typeof(res) == bytearray() ensures Low(res) -{ - assume false -} -method bytearray___initFromList__(values: Seq[Int]) returns (res: Ref) +method bytearray___initFromList__(values: Ref) returns (res: Ref) + requires issubtype(typeof(values), list(int())) + requires acc(values.list_acc, 1/1000) + requires forall i: Int :: {values.list_acc[i]} ((0 <= i < list___len__(values)) ==> bytearray___bounds_helper__(int___unbox__(list___getitem__(values, __prim__int___box__(i))))) + ensures acc(values.list_acc, 1/1000) ensures acc(res.bytearray_acc) - ensures res.bytearray_acc == values ensures typeof(res) == bytearray() + ensures res.bytearray_acc == __seq_ref_to_seq_int(values.list_acc) ensures Low(res) -{ - assume false -} - + function bytearray___len__(self: Ref) : Int decreases _ requires issubtype(typeof(self), bytearray()) @@ -90,9 +94,6 @@ method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) ensures acc(_res.bytearray_acc) ensures typeof(_res) == bytearray() ensures _res.bytearray_acc == self.bytearray_acc[slice___start__(key, bytearray___len__(self))..slice___stop__(key, bytearray___len__(self))] -{ - assume false -} method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires issubtype(typeof(self), bytearray()) @@ -105,9 +106,6 @@ method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) -{ - assume false -} method bytearray_append(self: Ref, item: Ref) returns () requires issubtype(typeof(self), bytearray()) @@ -116,9 +114,6 @@ method bytearray_append(self: Ref, item: Ref) returns () requires @error("Provided item may be out of bounds.")bytearray___bounds_helper__(int___unbox__(item)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) -{ - assume false -} // Actual type of other is Iterable[SupportsIndex] method bytearray_extend(self: Ref, other: Ref) returns () @@ -137,8 +132,6 @@ method bytearray_reverse(self: Ref) returns () ensures old(bytearray___len__(self)) == bytearray___len__(self) ensures forall i: Int :: {self.bytearray_acc[i]} ((i >= 0 && i < bytearray___len__(self)) ==> (self.bytearray_acc[i] == old(self.bytearray_acc[bytearray___len__(self) - 1 - i]))) - -// TODO implement related functionality for Iterator on bytearray method bytearray___iter__(self: Ref) returns (_res: Ref) requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, 1/ 20) @@ -150,15 +143,12 @@ method bytearray___iter__(self: Ref) returns (_res: Ref) ensures acc(_res.__iter_index, write) && (_res.__iter_index == 0) ensures acc(_res.__previous, write) && _res.__previous == Seq[Ref]() ensures issubtype(typeof(_res), Iterator(int())) -{ - inhale false -} function bytearray___sil_seq__(self: Ref): Seq[Ref] decreases _ requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc, wildcard) ensures |result| == bytearray___len__(self) - ensures (forall i: Int :: { result[i] } i >= 0 && i < bytearray___len__(self) ==> result[i] == __prim__int___box__(self.bytearray_acc[i])) + ensures (forall i: Int :: { result[i] } 0 <= i < bytearray___len__(self) ==> result[i] == __prim__int___box__(self.bytearray_acc[i])) ensures (forall i: Ref :: { (i in result) } (i in result) == (typeof(i) == int() && (int___unbox__(i) in self.bytearray_acc))) ensures (forall i: Ref :: { (i in result) } (i in result) ==> bytearray___bounds_helper__(int___unbox__(i))) \ No newline at end of file diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 260630374..b4cffbddb 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -614,10 +614,17 @@ "type": null, "MustTerminate": true }, + "__initFromInt__": { + "args": ["int"], + "type": null, + "MustTerminate": true, + "requires": ["int___unbox__"] + }, "__initFromList__": { - "args": ["__prim__Seq"], + "args": ["list"], "type": null, - "MustTerminate": true + "MustTerminate": true, + "requires": ["list", "list___getitem__", "list___len__", "bytearray___bounds_helper__", "int___unbox__", "__seq_ref_to_seq_int"] }, "__initFromBytearray__": { "args": ["bytearray"], @@ -645,7 +652,7 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__"] + "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__", "__prim__int___box__"] }, "__iter__": { "args": ["bytearray"], @@ -687,7 +694,7 @@ "__sil_seq__": { "args": ["bytearray"], "type": "__prim__Seq", - "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__"] + "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] } }, "extends": "object" diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index bf8714f40..bc1dd1b1b 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -517,6 +517,9 @@ def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: if arg_type.name == LIST_TYPE: method_name = '__initFromList__' + + if arg_type.name == INT_TYPE: + method_name = '__initFromInt__' # sil_ref_seq = self.viper.SeqType(self.viper.Int) # ref_seq = SilverType(sil_ref_seq, ctx.module) From e0d18e17c65517b8dbde54c62d64d5910d3adb5d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 24 Sep 2025 10:29:11 +0200 Subject: [PATCH 014/126] Clean up call.py --- src/nagini_translation/translators/call.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index bc1dd1b1b..a727f7ea0 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -500,11 +500,10 @@ def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: bytearray_class = ctx.module.global_module.classes[BYTEARRAY_TYPE] res_var = ctx.current_function.create_variable('bytearray', bytearray_class, self.translator) targets = [res_var.ref()] + result_var = res_var.ref(node, ctx) - position = self.to_position(node, ctx) - info = self.no_info(ctx) - result_var = res_var.ref(node, ctx) - + # This could potentially be merged using the "display_name" field + # by extending the general code for selecting a specific __init__ call if len(node.args) == 0: call = self.get_method_call(bytearray_class, '__init__', [], [], targets, node, ctx) return call, result_var @@ -520,22 +519,8 @@ def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: if arg_type.name == INT_TYPE: method_name = '__initFromInt__' - - # sil_ref_seq = self.viper.SeqType(self.viper.Int) - # ref_seq = SilverType(sil_ref_seq, ctx.module) - # havoc_var = ctx.current_function.create_variable('havoc_seq', ref_seq, - # self.translator) - # seq_field = self.viper.Field('bytearray_acc', sil_ref_seq, position, info) - # content_field = self.viper.FieldAccess(result_var, seq_field, position, info) - # stmts.append(self.viper.FieldAssign(content_field, havoc_var.ref(), position, - # info)) - # arg_seq = self.get_sequence(arg_type, arg_val, None, node, ctx, position) - # res_seq = self.get_sequence(None, result_var, None, node, ctx, position) - # seq_equal = self.viper.EqCmp(arg_seq, res_seq, position, info) - # stmts.append(self.viper.Inhale(seq_equal, position, info)) if method_name: - print("Calling method " + method_name) target_method = bytearray_class.get_method(method_name) arg_stmts, arg_vals, arg_types = self.translate_args(target_method, node.args, node.keywords, node, ctx) constr_call = self.get_method_call(bytearray_class, method_name, arg_vals, arg_types, targets, node, ctx) From 27365c68c2c0c778fcd0e0fb174037cad0a87c60 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 25 Sep 2025 16:18:02 +0200 Subject: [PATCH 015/126] Add tests for bytearray --- setup.py | 4 +- src/nagini_translation/models/converter.py | 2 + .../functional/verification/test_bytearray.py | 78 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/functional/verification/test_bytearray.py diff --git a/setup.py b/setup.py index e92f691a1..db414ec4a 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,8 @@ 'toposort==1.5', 'jpype1==1.2.1', 'astunparse==1.6.2', - 'pytest==4.3.0', - 'pytest-xdist==1.27.0', + 'pytest', + 'pytest-xdist', 'z3-solver==4.8.7.0' ], entry_points={ diff --git a/src/nagini_translation/models/converter.py b/src/nagini_translation/models/converter.py index c066e0ce8..ea25afb9b 100644 --- a/src/nagini_translation/models/converter.py +++ b/src/nagini_translation/models/converter.py @@ -279,6 +279,8 @@ def convert_python_field(self, recv, field, value, heap_contents, target, target receiver_type = global_module.classes['list'] elif field == 'set_acc': receiver_type = global_module.classes['set'] + elif field == 'bytearray_acc': + receiver_type = global_module.classes['bytearray'] elif field == '_val': # This is a global variable. var_sil_name = str(recv.applicable().id()) diff --git a/tests/functional/verification/test_bytearray.py b/tests/functional/verification/test_bytearray.py new file mode 100644 index 000000000..1868a9dd8 --- /dev/null +++ b/tests/functional/verification/test_bytearray.py @@ -0,0 +1,78 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + + +def test_bytearray_constr() -> None: + a = bytearray() + assert len(a) == 0 + assert 6 not in a + #:: ExpectedOutput(assert.failed:assertion.false) + assert 2 in a + +def test_bytearray_constr_int() -> None: + a = bytearray(7) + assert len(a) == 7 + assert a[3] == 0 + assert 6 not in a + #:: ExpectedOutput(assert.failed:assertion.false) + assert 2 in a + +def test_bytearray_constr_list() -> None: + a = bytearray([2,3,4]) + assert len(a) == 3 + assert a[0] == 2 + assert a[1] == 3 + assert a[2] == 4 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert 5 in a + +def test_bytearray_constr_bytearray() -> None: + a = bytearray([2,3,4]) + b = bytearray(a) + + assert len(b) == 3 + assert b[0] == 2 + assert b[1] == 3 + assert b[2] == 4 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert 5 in b + +def test_byterray_append() -> None: + a = bytearray([2,3,4]) + a.append(5) + + assert len(a) == 4 + assert a[0] == 2 + assert a[1] == 3 + assert a[2] == 4 + assert a[3] == 5 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a[2] == 8 + +def test_byterray_extend() -> None: + a = bytearray([2,3,4]) + b = bytearray([5,6,7]) + a.extend(b) + + assert len(a) == 6 + assert a[4] == 6 + assert a[5] == 7 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a[3] == 8 + +def test_bytearray_reverse() -> None: + a = bytearray([2,3,4]) + a.reverse() + + assert len(a) == 3 + assert a[0] == 4 + assert a[1] == 3 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a[2] == 4 \ No newline at end of file From 32dbda1aa7dfb97c1257d8ead354411734d28b75 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 26 Sep 2025 09:44:05 +0200 Subject: [PATCH 016/126] Fix Low conditions --- src/nagini_translation/resources/bytearray.sil | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index cd602e46c..898a5efa1 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -26,7 +26,7 @@ method bytearray___initFromInt__(length: Ref) returns (res: Ref) ensures acc(res.bytearray_acc) ensures |res.bytearray_acc| == int___unbox__(length) ensures (forall i: Int :: { res.bytearray_acc[i] } 0 <= i < int___unbox__(length) ==> res.bytearray_acc[i] == 0) - ensures Low(res) + ensures Low(length) ==> Low(res) method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) requires issubtype(typeof(other), bytearray()) @@ -35,7 +35,7 @@ method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) ensures acc(res.bytearray_acc) ensures res.bytearray_acc == other.bytearray_acc ensures typeof(res) == bytearray() - ensures Low(res) + ensures Low(other) ==> Low(res) method bytearray___initFromList__(values: Ref) returns (res: Ref) requires issubtype(typeof(values), list(int())) @@ -45,7 +45,7 @@ method bytearray___initFromList__(values: Ref) returns (res: Ref) ensures acc(res.bytearray_acc) ensures typeof(res) == bytearray() ensures res.bytearray_acc == __seq_ref_to_seq_int(values.list_acc) - ensures Low(res) + ensures Low(values) ==> Low(res) function bytearray___len__(self: Ref) : Int decreases _ From c0ef41c14289c4f3c64e46125b615664fd9b1686 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 26 Sep 2025 09:44:18 +0200 Subject: [PATCH 017/126] Extend bytearray tests --- .../functional/verification/test_bytearray.py | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/tests/functional/verification/test_bytearray.py b/tests/functional/verification/test_bytearray.py index 1868a9dd8..949c63f81 100644 --- a/tests/functional/verification/test_bytearray.py +++ b/tests/functional/verification/test_bytearray.py @@ -28,7 +28,15 @@ def test_bytearray_constr_list() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert 5 in a + +def test_bytearray_constr_list_bounds_low() -> None: + #:: ExpectedOutput(call.precondition:assertion.false) + a = bytearray([-1,3,4]) +def test_bytearray_constr_list_bounds_high() -> None: + #:: ExpectedOutput(call.precondition:assertion.false) + a = bytearray([0,3,256]) + def test_bytearray_constr_bytearray() -> None: a = bytearray([2,3,4]) b = bytearray(a) @@ -40,7 +48,18 @@ def test_bytearray_constr_bytearray() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert 5 in b + +def test_bytearray_bool() -> None: + a = bytearray([0]) + b = bytearray(3) + c = bytearray() + assert a + assert b + + #:: ExpectedOutput(assert.failed:assertion.false) + assert c + def test_byterray_append() -> None: a = bytearray([2,3,4]) a.append(5) @@ -53,7 +72,39 @@ def test_byterray_append() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert a[2] == 8 + +def test_bytearray_append_bounds_low() -> None: + a = bytearray() + #:: ExpectedOutput(call.precondition:assertion.false) + a.append(-10) + +def test_bytearray_append_bounds_high() -> None: + a = bytearray() + #:: ExpectedOutput(call.precondition:assertion.false) + a.append(256) + +def test_bytearray_setitem() -> None: + a = bytearray([2,3,4]) + + a[0] = 10 + a[1] = 0 + a[2] = 255 + assert a[0] == 10 + assert a[1] == 0 + #:: ExpectedOutput(assert.failed:assertion.false) + assert a[2] == 254 + +def test_bytearray_setitem_bounds_low() -> None: + a = bytearray([0,128,255]) + #:: ExpectedOutput(call.precondition:assertion.false) + a[0] = -1 + +def test_bytearray_setitem_bounds_high() -> None: + a = bytearray([0,128,255]) + #:: ExpectedOutput(call.precondition:assertion.false) + a[0] = 256 + def test_byterray_extend() -> None: a = bytearray([2,3,4]) b = bytearray([5,6,7]) @@ -75,4 +126,44 @@ def test_bytearray_reverse() -> None: assert a[1] == 3 #:: ExpectedOutput(assert.failed:assertion.false) - assert a[2] == 4 \ No newline at end of file + assert a[2] == 4 + +def test_bytearray_getitem_slice() -> None: + a = bytearray([2,3,4]) + b = a[1:] + + assert len(b) == 2 + assert b[0] == 3 + assert b[1] == 4 + + c = a[:-1] + assert len(c) == 2 + #:: ExpectedOutput(assert.failed:assertion.false) + assert c[0] == 3 + + +def test_bytearray_perm(b: bytearray) -> None: + #:: ExpectedOutput(call.precondition:insufficient.permission) + b.append(6) + +def test_bytearray_bounds_low(b: bytearray) -> None: + Requires(bytearray_pred(b)) + Requires(len(b) > 1) + + assert b[0] >= 0 + #:: ExpectedOutput(assert.failed:assertion.false) + assert b[1] < 0 + +def test_bytearray_bounds_high(b: bytearray) -> None: + Requires(bytearray_pred(b)) + Requires(len(b) > 1) + + assert b[0] <= 255 + #:: ExpectedOutput(assert.failed:assertion.false) + assert b[1] > 255 + +def test_bytearray_iter_bounds(b: bytearray) -> None: + Requires(bytearray_pred(b)) + + for byte in b: + assert 0 <= byte and byte < 256 \ No newline at end of file From c66ffae0d30cce4f18b81707554ab0e0c8944640 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 26 Sep 2025 10:52:34 +0200 Subject: [PATCH 018/126] Add tests for lshift and fix negative inputs --- src/nagini_translation/resources/bool.sil | 12 ++--- .../verification/test_bitwise_op.py | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 788bc3da3..c0c13e221 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -124,12 +124,12 @@ function int___lshift__(self: Ref, other: Ref): Ref requires issubtype(typeof(other), int()) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= _INT_MIN) + requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) - ensures result == - ((issubtype(typeof(self), bool()) && issubtype(typeof(other), bool())) ? - __prim__bool___box__(bool___unbox__(self) != bool___unbox__(other)) : - __prim__int___box__(fromBVInt(shlBVInt(toBVInt(int___unbox__(self)), toBVInt(int___unbox__(other)))))) + ensures result == (let val == (int___unbox__(self)) in val >= 0 ? + __prim__int___box__(fromBVInt(shlBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : + __prim__int___box__(-fromBVInt(shlBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) + ) function int___rlshift__(self: Ref, other: Ref): Ref decreases _ @@ -137,7 +137,7 @@ function int___rlshift__(self: Ref, other: Ref): Ref requires issubtype(typeof(other), int()) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= _INT_MIN) + requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { int___lshift__(self, other) diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 6b65f3caf..1cf17bb4e 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -171,4 +171,46 @@ def xor_4(a: int, b: bool, c: int) -> None: Requires(a > -100 and a < 100) Requires(c >= -128 and c < 129) #:: ExpectedOutput(application.precondition:assertion.false) - intint = a ^ c \ No newline at end of file + intint = a ^ c + +def lshift_1(b: int) -> None: + Requires(b >=0 and b <= 127) + + a = 1 + shift = a << b + + if b == 0: + assert shift == a * 1 + if b == 1: + assert shift == a * 2 + if b == 2: + assert shift == a * 4 + if b == 3: + assert shift == a * 8 + if b == 4: + assert shift == a * 16 + if b == 5: + assert shift == a * 32 + if b == 6: + assert shift == a * 64 + if b == 7: + assert shift == a * 128 + +def lshift_general(a: int, b: int) -> None: + Requires(a > -100 and a < 100) + Requires(b >=0 and b <= 127) + + shift = a << b + + # Unfortunately we cannot prove the equivalence shift == a * (2**b) + if b == 0: + assert shift == a + if b == 1: + assert shift == a * 2 + +def lshift_neg(a: int, b: int) -> None: + Requires(a > -100 and a < 100) + Requires(b >= -128 and b <= 127) + + #:: ExpectedOutput(application.precondition:assertion.false) + shift = a << b \ No newline at end of file From 93689d9eabb5ccbc149f7c91c42a355ccf412cdf Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 26 Sep 2025 13:36:58 +0200 Subject: [PATCH 019/126] fix: spelling mistake --- src/nagini_translation/lib/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 3986f02d7..737cec5d5 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -284,7 +284,7 @@ def _do_get_type(node: ast.AST, containers: List[ContainerInterface], elif node.value is None: return module.global_module.classes['NoneType'] else: - raise UnsupportedException(node.value, f"Unsupported contant value type {type(node.value)}") + raise UnsupportedException(node.value, f"Unsupported constant value type {type(node.value)}") if isinstance(node, ast.Num): if isinstance(node.n, int): return module.global_module.classes[INT_TYPE] From 45f55310972d78fd7c4caab4d061090ae25e70ab Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 26 Sep 2025 15:04:15 +0200 Subject: [PATCH 020/126] Add dummy implementation for formatted strings --- src/nagini_translation/lib/constants.py | 1 + src/nagini_translation/lib/resolver.py | 2 ++ .../resources/builtins.json | 8 ++++++ src/nagini_translation/resources/str.sil | 11 ++++++++ .../sif/resources/builtins.json | 9 +++++++ .../translators/expression.py | 27 +++++++++++++++++++ tests/functional/verification/test_string.py | 18 ++++++++++++- 7 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index a689a076a..b9bdd5183 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -257,6 +257,7 @@ '__str__', '__len__', '__bool__', + '__format__', '__getitem__', '__setitem__', diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 737cec5d5..6e46f031a 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -285,6 +285,8 @@ def _do_get_type(node: ast.AST, containers: List[ContainerInterface], return module.global_module.classes['NoneType'] else: raise UnsupportedException(node.value, f"Unsupported constant value type {type(node.value)}") + if isinstance(node, (ast.JoinedStr, ast.FormattedValue)): + return module.global_module.classes[STRING_TYPE] if isinstance(node, ast.Num): if isinstance(node.n, int): return module.global_module.classes[INT_TYPE] diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 0ac118fc8..123a7b8cd 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -552,6 +552,14 @@ "__mod__": { "args": ["str", "tuple"], "type": "str" + }, + "format": { + "args": ["str", "object", "object"], + "type": "str" + }, + "__format__": { + "args": ["str", "str"], + "type": "str" } }, "methods": { diff --git a/src/nagini_translation/resources/str.sil b/src/nagini_translation/resources/str.sil index 32d477206..034b6fa40 100644 --- a/src/nagini_translation/resources/str.sil +++ b/src/nagini_translation/resources/str.sil @@ -48,6 +48,17 @@ function str___mod__(self: Ref, other: Ref): Ref requires issubtype(typeof(self), str()) ensures issubtype(typeof(result), str()) +function str_format(self: Ref, args: Ref, kwargs: Ref): Ref + decreases _ + requires issubtype(typeof(self), str()) + ensures issubtype(typeof(result), str()) + +function str___format__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), str()) + requires issubtype(typeof(other), str()) + ensures issubtype(typeof(result), str()) + method str_split(self: Ref) returns (res: Ref) decreases _ requires issubtype(typeof(self), str()) diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index b4cffbddb..90b48d6f3 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -536,6 +536,15 @@ "__mod__": { "args": ["str", "tuple"], "type": "str" + }, + "format": { + "args": ["str", "object", "object"], + "type": "str", + "MustTerminate": true + }, + "__format__": { + "args": ["str", "str"], + "type": "str" } }, "methods": { diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 9f1a6b587..b1c66d9de 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -403,6 +403,33 @@ def translate_string(self, s: str, node: ast.AST, ctx: Context) -> Expr: node, ctx) return call + def translate_JoinedStr(self, node: ast.JoinedStr, ctx: Context) -> StmtsAndExpr: + """ + Dummy implementation for ast.JoinedStr, only translates contained expressions. + Provides no guarantees about resulting value. + """ + stmts = [] + exps = [] + for val in node.values: + val_stmt, val_exp = self.translate_expr(val, ctx) + stmts += val_stmt + exps.append(val_exp) + + str_class = ctx.module.global_module.classes[STRING_TYPE] + res_var = ctx.current_function.create_variable('joined_str', str_class, self.translator) + result_var = res_var.ref(node, ctx) + position = self.to_position(node, ctx) + stmts.append(self.viper.Inhale(self.type_check(result_var, str_class, position, ctx), + position, self.no_info(ctx))) + return stmts, result_var + + def translate_FormattedValue(self, node: ast.FormattedValue, ctx: Context) -> StmtsAndExpr: + """ + Dummy implementation for ast.FormattedValue, only translates contained expression. + Does not apply given formatting rules + """ + stmt, exp = self.translate_expr(node.value, ctx) + return stmt, exp def translate_Bytes(self, node: ast.Bytes, ctx: Context) -> StmtsAndExpr: elems = [] diff --git a/tests/functional/verification/test_string.py b/tests/functional/verification/test_string.py index de23598e0..64d102595 100644 --- a/tests/functional/verification/test_string.py +++ b/tests/functional/verification/test_string.py @@ -29,4 +29,20 @@ def main() -> None: my_string2 = "a" Assert(my_string2 == "a") #:: ExpectedOutput(assert.failed:assertion.false) - Assert(my_string2 == "b") \ No newline at end of file + Assert(my_string2 == "b") + + +def test_str_format_wrong1() -> None: + a = "".format() + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a == " ") + +def test_str_format_wrong2() -> None: + a = "{0}".format(2) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a == "3") + +def test_fstr_wrong() -> None: + a = f"{8}" + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a == "3") \ No newline at end of file From 8b369b1f10dd426f5e68c9a8413439c791e28665 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 29 Sep 2025 11:16:05 +0200 Subject: [PATCH 021/126] Partial support for right shift --- src/nagini_translation/resources/bool.sil | 27 ++++++++++++++ .../resources/builtins.json | 10 +++++ src/nagini_translation/resources/intbv.sil | 1 + .../verification/test_bitwise_op.py | 37 ++++++++++++++++++- 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index c0c13e221..c321e0cac 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -143,6 +143,33 @@ function int___rlshift__(self: Ref, other: Ref): Ref int___lshift__(self, other) } +function int___rshift__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), int()) + requires issubtype(typeof(other), int()) + // requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) + ensures result == (let val == (int___unbox__(self)) in val >= 0 ? + __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : + __prim__int___box__(-fromBVInt(shrBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) // TODO This case is wrong + ) + +function int___rrshift__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), int()) + requires issubtype(typeof(other), int()) + // requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) +{ + int___rshift__(self, other) +} + function int___bool__(self: Ref) : Bool decreases _ requires self != null ==> issubtype(typeof(self), int()) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 123a7b8cd..d388ac3eb 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -339,6 +339,16 @@ "args": ["int", "int"], "type": "int", "requires": ["__lshift__"] + }, + "__rshift__": { + "args": ["int", "int"], + "type": "int", + "requires": ["__prim__int___box__", "int___unbox__", "__prim__bool___box__", "bool___unbox__"] + }, + "__rrshift__": { + "args": ["int", "int"], + "type": "int", + "requires": ["__rshift__"] } }, "extends": "float" diff --git a/src/nagini_translation/resources/intbv.sil b/src/nagini_translation/resources/intbv.sil index 7b2501402..cf7f27bff 100644 --- a/src/nagini_translation/resources/intbv.sil +++ b/src/nagini_translation/resources/intbv.sil @@ -17,4 +17,5 @@ domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function orBVInt(___intbv, ___intbv): ___intbv interpretation "bvor" function xorBVInt(___intbv, ___intbv): ___intbv interpretation "bvxor" function shlBVInt(___intbv, ___intbv): ___intbv interpretation "bvshl" + function shrBVInt(___intbv, ___intbv): ___intbv interpretation "bvlshr" } \ No newline at end of file diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 1cf17bb4e..34c3f680b 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -213,4 +213,39 @@ def lshift_neg(a: int, b: int) -> None: Requires(b >= -128 and b <= 127) #:: ExpectedOutput(application.precondition:assertion.false) - shift = a << b \ No newline at end of file + shift = a << b + +def rshift_1(b: int) -> None: + Requires(b >=0 and b <= 127) + + a = 127 + shift = a >> b + + if b == 0: + assert shift == a // 1 + if b == 1: + assert shift == a // 2 + if b == 2: + assert shift == a // 4 + if b == 3: + assert shift == a // 8 + if b == 4: + assert shift == a // 16 + if b == 5: + assert shift == a // 32 + if b == 6: + assert shift == a // 64 + if b == 7: + assert shift == a // 128 + +def rshift_general(a: int, b: int) -> None: + Requires(a >= 0 and a < 100) + Requires(b >=0 and b <= 127) + + shift = a >> b + + # Unfortunately we cannot prove the equivalence shift == a // (2 ** b) + if b == 0: + assert shift == a + if b == 1: + assert shift == a // 2 \ No newline at end of file From bbe42190c35417914935ccdb5f4b20f05b790666 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 29 Sep 2025 13:12:25 +0200 Subject: [PATCH 022/126] Disable right shift of negative numbers --- src/nagini_translation/resources/bool.sil | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index c321e0cac..f55d30ab7 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -152,10 +152,11 @@ function int___rshift__(self: Ref, other: Ref): Ref requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) - ensures result == (let val == (int___unbox__(self)) in val >= 0 ? - __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : - __prim__int___box__(-fromBVInt(shrBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) // TODO This case is wrong - ) + // ensures result == (let val == (int___unbox__(self)) in val >= 0 ? + // __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : + // __prim__int___box__(-fromBVInt(shrBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) // TODO This case is wrong + // ) + ensures result == (let val == (int___unbox__(self)) in __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other)))))) function int___rrshift__(self: Ref, other: Ref): Ref decreases _ From 6a838172c519d15b425da5838658c5f59bb8f140 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 30 Sep 2025 16:03:20 +0200 Subject: [PATCH 023/126] Attempting to enable Unfold statements in pure functions --- src/nagini_translation/translators/pure.py | 62 +++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/translators/pure.py b/src/nagini_translation/translators/pure.py index 4cd413997..31579f42a 100644 --- a/src/nagini_translation/translators/pure.py +++ b/src/nagini_translation/translators/pure.py @@ -51,6 +51,20 @@ def __init__(self, cond: List, expr: ast.AST, node: ast.AST): self.names = {} +class UnfoldWrapper: + """ + Represents an unfolding of predicate pred, to be executed under conditions conds. + """ + + def __init__(self, conds: List, pred: ast.AST, node: ast.AST): + self.cond = conds + self.pred = pred + self.node = node + self.names = {} + self.var = None + + + class NotWrapper: """ Represents a negation of the condition cond. @@ -68,7 +82,7 @@ def __init__(self, op: ast.BinOp, rhs: ast.AST): self.op = op self.rhs = rhs -Wrapper = Union[AssignWrapper, ReturnWrapper] +Wrapper = Union[AssignWrapper, ReturnWrapper, UnfoldWrapper] class PureTranslator(CommonTranslator): @@ -90,6 +104,9 @@ def translate_pure_Expr(self, conds: List, node: ast.Expr, return [] if isinstance(node.value, ast.Call) and get_func_name(node.value) in CONTRACT_WRAPPER_FUNCS: raise InvalidProgramException(node, 'invalid.contract.position') + if isinstance(node.value, ast.Call) and get_func_name(node.value) == 'Unfold': + wrapper = UnfoldWrapper(conds, node.value, node) + return [wrapper] raise UnsupportedException(node) def translate_pure_If(self, conds: List, node: ast.If, @@ -213,6 +230,46 @@ def _translate_assign_wrapper(self, wrapper: Wrapper, previous: Expr, return self.viper.Let(wrapper.var.decl, val, previous, position, info) + def _translate_unfold_wrapper(self, wrapper: Wrapper, previous: Expr, + function: PythonMethod, + ctx: Context) -> Expr: + info = self.no_info(ctx) + position = self.to_position(wrapper.node, ctx) + if not previous: + raise InvalidProgramException(function.node, + 'function.return.missing') + + if len(wrapper.pred.args) != 1: + raise InvalidProgramException(wrapper.pred, 'invalid.contract.call') + if not isinstance(wrapper.pred.args[0], ast.Call): + raise InvalidProgramException(wrapper.pred, 'invalid.contract.call') + if get_func_name(wrapper.pred.args[0]) in ('Acc', 'Rd'): + pred_call = wrapper.pred.args[0].args[0] + else: + pred_call = wrapper.pred.args[0] + target_pred = self.get_target(pred_call, ctx) + if (target_pred and + (not isinstance(target_pred, PythonMethod) or not target_pred.predicate)): + raise InvalidProgramException(wrapper.pred, 'invalid.contract.call') + if target_pred and target_pred.contract_only: + raise InvalidProgramException(wrapper.pred, 'abstract.predicate.fold') + pred_stmt, pred = self.translate_expr(wrapper.pred.args[0], ctx, + self.viper.Bool, True) + if pred_stmt: + raise InvalidProgramException(wrapper.node, 'purity.violated') + + unfolding = self.viper.Unfolding(pred, previous, position, info) + + if wrapper.cond: + cond = self._translate_condition(wrapper.cond, + wrapper.names, ctx) + + new_val = self.viper.CondExp(cond, unfolding, previous, position, + info) + return new_val + else: + return unfolding + def _translate_wrapper_expr(self, wrapper: Wrapper, ctx: Context) -> Expr: info = self.no_info(ctx) @@ -246,6 +303,9 @@ def _translate_wrapper(self, wrapper: Wrapper, previous: Expr, elif isinstance(wrapper, AssignWrapper): return self._translate_assign_wrapper(wrapper, previous, function, ctx) + elif isinstance(wrapper, UnfoldWrapper): + return self._translate_unfold_wrapper(wrapper, previous, + function, ctx) else: raise UnsupportedException(wrapper) From dd5af6297e55459688a1f96e279449367aec6cc0 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 1 Oct 2025 10:56:35 +0200 Subject: [PATCH 024/126] Checking type of assign during _assign_with_subscript Fixes #237 --- src/nagini_translation/translators/statement.py | 6 +++++- tests/functional/translation/issues/00237.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/functional/translation/issues/00237.py diff --git a/src/nagini_translation/translators/statement.py b/src/nagini_translation/translators/statement.py index d6e3bb7a1..4f08051f0 100644 --- a/src/nagini_translation/translators/statement.py +++ b/src/nagini_translation/translators/statement.py @@ -1179,7 +1179,11 @@ def _assign_with_subscript(self, lhs: ast.Tuple, rhs: Expr, node: ast.AST, List[Expr]]: # Special treatment for subscript; instead of an assignment, we # need to call a setitem method. - if isinstance(node.targets[0].slice, ast.Slice): + if isinstance(node, ast.Assign): + target = node.targets[0] + elif isinstance(node, (ast.AnnAssign, ast.AugAssign)): + target = node.target + if isinstance(target.slice, ast.Slice): raise UnsupportedException(node, 'assignment to slice') position = self.to_position(node, ctx) target_cls = self.get_type(lhs.value, ctx) diff --git a/tests/functional/translation/issues/00237.py b/tests/functional/translation/issues/00237.py new file mode 100644 index 000000000..385c451eb --- /dev/null +++ b/tests/functional/translation/issues/00237.py @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from typing import List +from nagini_contracts.contracts import * + +a: List[int] = [1] +a[0] += 1 + From 921418b80973841d3d490107d075220385d95697 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 1 Oct 2025 11:04:28 +0200 Subject: [PATCH 025/126] Ignore sign bit for bitvector operations --- src/nagini_translation/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index c73597835..59181a995 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -65,8 +65,8 @@ def parse_sil_file(sil_path: str, bv_path: str, bv_size: int, jvm, float_option: with open(sil_path, 'r') as file: text = file.read() with open(bv_path, 'r') as file: - int_min = -(2 ** (bv_size - 1)) - int_max = 2 ** (bv_size - 1) - 1 + int_min = -(2 ** (bv_size)) + int_max = 2 ** (bv_size) - 1 text += "\n" + file.read().replace("NBITS", str(bv_size)).replace("INT_MIN_VAL", str(int_min)).replace("INT_MAX_VAL", str(int_max)) if float_option == "real": text = text.replace("float.sil", "float_real.sil") @@ -482,6 +482,7 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di line = str(e.node.lineno) col = str(e.node.col_offset) print(issue + ' (' + python_file + '@' + line + '.' + col + ')') + traceback.print_exc() if isinstance(e, TypeException): for msg in e.messages: parts = TYPE_ERROR_MATCHER.match(msg) @@ -493,6 +494,7 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di msg = parts['msg'] line = parts['line'] print('Type error: ' + msg + ' (' + file + '@' + line + '.0)') + traceback.print_exc() else: print(msg) return False From dba26d2a509428319d60c385b81de0c0beb93919 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 2 Oct 2025 13:51:21 +0200 Subject: [PATCH 026/126] Adjust tests for larget bitsize --- tests/functional/verification/test_bitwise_op.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 34c3f680b..9c3027672 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -97,7 +97,7 @@ def and_3a(a: int, b: bool, c: int) -> None: def and_4(a: int, b: bool, c: int) -> None: Requires(a > -100 and a < 100) - Requires(c > -130 and c < 127) + Requires(c > -260 and c < 127) #:: ExpectedOutput(application.precondition:assertion.false) intint = a & c @@ -133,7 +133,7 @@ def or_3a(a: int, b: bool, c: int) -> None: def or_4(a: int, b: bool, c: int) -> None: Requires(a > -100 and a < 100) - Requires(c > -130 and c < 127) + Requires(c > -260 and c < 127) #:: ExpectedOutput(application.precondition:assertion.false) intint = a | c @@ -169,7 +169,7 @@ def xor_3a(a: int, b: bool, c: int) -> None: def xor_4(a: int, b: bool, c: int) -> None: Requires(a > -100 and a < 100) - Requires(c >= -128 and c < 129) + Requires(c >= -128 and c < 257) #:: ExpectedOutput(application.precondition:assertion.false) intint = a ^ c From b60a507f7590a237b2d99806b6ff94dc26f56b0c Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 2 Oct 2025 16:07:42 +0200 Subject: [PATCH 027/126] Add eq content check for bytearray --- .../resources/builtins.json | 5 +++ .../resources/bytearray.sil | 16 +++++++++- .../translators/contract.py | 3 +- .../functional/verification/test_bytearray.py | 32 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index d388ac3eb..c30e6ad70 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -721,6 +721,11 @@ "args": ["bytearray"], "type": "__prim__bool" }, + "__eq__": { + "args": ["bytearray", "object"], + "type": "__prim__bool", + "requires": ["__len__"] + }, "__bounds_helper__": { "args": ["__prim__int"], "type": "__prim__bool" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 898a5efa1..4cdee3114 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -46,7 +46,7 @@ method bytearray___initFromList__(values: Ref) returns (res: Ref) ensures typeof(res) == bytearray() ensures res.bytearray_acc == __seq_ref_to_seq_int(values.list_acc) ensures Low(values) ==> Low(res) - + function bytearray___len__(self: Ref) : Int decreases _ requires issubtype(typeof(self), bytearray()) @@ -70,6 +70,20 @@ function bytearray___bool__(self: Ref) : Bool ensures self == null ==> !result ensures self != null ==> result == (|self.bytearray_acc| != 0) +// Currently only supports comparing to another bytearray +function bytearray___eq__(self: Ref, other: Ref): Bool + decreases _ + requires issubtype(typeof(self), bytearray()) + requires acc(self.bytearray_acc, wildcard) + requires issubtype(typeof(other), bytearray()) + requires acc(other.bytearray_acc, wildcard) + ensures result <==> + (bytearray___len__(self) == bytearray___len__(other) && + (forall i: Int :: { self.bytearray_acc[i] } + { other.bytearray_acc[i] } + 0 <= i && i < bytearray___len__(self) + ==> self.bytearray_acc[i] == other.bytearray_acc[i] )) + // function bytearray___hex__(self: Ref): Str // decreases _ // requires issubtype(typeof(self), bytearray()) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 6f7e57e11..f314898f4 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -24,6 +24,7 @@ PSEQ_TYPE, PSET_TYPE, RANGE_TYPE, + BYTEARRAY_TYPE, THREAD_DOMAIN, THREAD_POST_PRED, THREAD_START_PRED, @@ -685,7 +686,7 @@ def translate_to_sequence(self, node: ast.Call, # iterable (which gives no information about order for unordered types). seq_call = self.get_sequence(coll_type, arg, None, node, ctx) seq_class = ctx.module.global_module.classes[PSEQ_TYPE] - if coll_type.name == RANGE_TYPE: + if coll_type.name == RANGE_TYPE or coll_type.name == BYTEARRAY_TYPE: type_arg = ctx.module.global_module.classes[INT_TYPE] else: type_arg = coll_type.type_args[0] diff --git a/tests/functional/verification/test_bytearray.py b/tests/functional/verification/test_bytearray.py index 949c63f81..ace6a4917 100644 --- a/tests/functional/verification/test_bytearray.py +++ b/tests/functional/verification/test_bytearray.py @@ -60,6 +60,38 @@ def test_bytearray_bool() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert c +def test_bytearray_eq1() -> None: + a = bytearray([1,2,3]) + b = bytearray([1,2,3]) + + assert a == b + +def test_bytearray_eq2() -> None: + a = bytearray([1,2,3]) + b = bytearray([2,2,3]) + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a == b + +def test_bytearray_eq_client1(b1: bytearray, b2: bytearray) -> None: + Requires(bytearray_pred(b1)) + Requires(bytearray_pred(b2)) + Requires(b1 == b2) + Requires(len(b1) > 0) + + assert b1[0] == b2[0] + +def test_bytearray_eq_client2(b1: bytearray, b2: bytearray) -> None: + Requires(bytearray_pred(b1)) + Requires(bytearray_pred(b2)) + Requires(b1 == b2) + Requires(len(b1) > 0) + + b1[0] = 42 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert b1[0] == b2[0] + def test_byterray_append() -> None: a = bytearray([2,3,4]) a.append(5) From ab16a01c3f9bef0b5731c343dfc67fa6fc47b907 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 7 Oct 2025 11:17:56 +0200 Subject: [PATCH 028/126] Simplify bytearray___eq__ --- src/nagini_translation/resources/bytearray.sil | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 4cdee3114..00965b0f1 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -77,12 +77,7 @@ function bytearray___eq__(self: Ref, other: Ref): Bool requires acc(self.bytearray_acc, wildcard) requires issubtype(typeof(other), bytearray()) requires acc(other.bytearray_acc, wildcard) - ensures result <==> - (bytearray___len__(self) == bytearray___len__(other) && - (forall i: Int :: { self.bytearray_acc[i] } - { other.bytearray_acc[i] } - 0 <= i && i < bytearray___len__(self) - ==> self.bytearray_acc[i] == other.bytearray_acc[i] )) + ensures result <==> self.bytearray_acc == other.bytearray_acc // function bytearray___hex__(self: Ref): Str // decreases _ From 961e7fcc5014007463de21455c204089b5b36032 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 8 Oct 2025 08:41:06 +0200 Subject: [PATCH 029/126] Progress on adding PIntSeq --- src/nagini_contracts/contracts.py | 68 ++++++++++++++++++- src/nagini_translation/lib/constants.py | 2 + .../lib/silver_nodes/types.py | 17 +++++ src/nagini_translation/resources/pytype.sil | 1 + src/nagini_translation/resources/str.sil | 10 ++- src/nagini_translation/translators/type.py | 1 + 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 37d092515..8c8e3d6fe 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -29,7 +29,7 @@ CONTRACT_FUNCS = ['Assume', 'Assert', 'Old', 'Result', 'ResultT', 'Implies', 'Forall', 'IOForall', 'Forall2', 'Forall3', 'Forall6', 'Exists', 'Low', 'LowVal', 'LowEvent', 'Declassify', 'TerminatesSif', 'Acc', 'Rd', 'Wildcard', 'Fold', 'Unfold', 'Unfolding', 'Previous', - 'RaisedException', 'PSeq', 'PSet', 'ToSeq', 'ToMS', 'MaySet', 'MayCreate', + 'RaisedException', 'PSeq', 'PIntSeq', 'PSet', 'ToSeq', 'ToMS', 'MaySet', 'MayCreate', 'getMethod', 'getArg', 'getOld', 'arg', 'Joinable', 'MayStart', 'Let', 'PMultiset', 'LowExit', 'Refute', 'isNaN', 'Reveal'] @@ -260,6 +260,66 @@ def __iter__(self) -> Iterator[T]: can be used as arguments for Forall. """ +class PIntSeq(Sized, Iterable[int]): + """ + A PIntSeq represents a pure sequence of instances of int, and + is translated to native Viper sequences. + """ + + def __init__(self, *args: int) -> None: + """ + ``PIntSeq(a, b, c)`` creates a PIntSeq instance containing the objects + a, b and c in that order. + """ + + def __contains__(self, item: object) -> bool: + """ + True iff this PIntSeq contains the given object (not taking ``__eq__`` + into account). + """ + + def __getitem__(self, item: int) -> int: + """ + Returns the item at the given position. + """ + + def __len__(self) -> int: + """ + Returns the length of this PIntSeq. + """ + + def __add__(self, other: 'PIntSeq') -> 'PIntSeq': + """ + Concatenates two PIntSeqs to get a new PIntSeq. + """ + + def take(self, until: int) -> 'PIntSeq': + """ + Returns a new PIntSeq containing all elements starting + from the beginning until the given index. ``PIntSeq(3,2,5,6).take(3)`` + is equal to ``PIntSeq(3,2,5)``. + """ + + def drop(self, until: int) -> 'PIntSeq': + """ + Returns a new PIntSeq containing all elements starting + from the given index (i.e., drops all elements until that index). + ``PIntSeq(2,3,5,6).drop(2)`` is equal to ``PIntSeq(5,6)``. + """ + + def update(self, index: int, new_val: int) -> 'PIntSeq': + """ + Returns a new PIntSeq, containing the same elements + except for the element at index ``index``, which is replaced by + ``new_val``. + """ + + def __iter__(self) -> Iterator[int]: + """ + PIntSeqs can be quantified over; this is only here so thatPIntSeqs + can be used as arguments for Forall. + """ + def Previous(it: T) -> PSeq[T]: """ Within the body of a loop 'for x in xs', Previous(x) represents the list of @@ -351,6 +411,12 @@ def ToSeq(l: Iterable[T]) -> PSeq[T]: Converts the given iterable of a built-in type (list, set, dict, range) to a pure PSeq. """ + +def ToPIntSeq(l: Iterable[int]) -> PIntSeq: + """ + Converts the given iterable of a compatible built-in type (bytearray) to + a pure PIntSeq. + """ def ToMS(s: PSeq[T]) -> PMultiset[T]: diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index b9bdd5183..dc7bd6157 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -289,6 +289,8 @@ PSEQ_TYPE = 'PSeq' +PINTSEQ_TYPE = 'PIntSeq' + PSET_TYPE = 'PSet' PMSET_TYPE = 'PMultiset' diff --git a/src/nagini_translation/lib/silver_nodes/types.py b/src/nagini_translation/lib/silver_nodes/types.py index 025863975..ebba2c814 100644 --- a/src/nagini_translation/lib/silver_nodes/types.py +++ b/src/nagini_translation/lib/silver_nodes/types.py @@ -164,3 +164,20 @@ def translate(self, translator: 'AbstractTranslator', ctx: 'Context', elements = [element.translate(translator, ctx, position, info) for element in self._elements] return translator.viper.ExplicitSeq(elements, position, info) + +class PIntSeq: + """A helper class for generating Silver sequences.""" + + def __init__(self, elements: List['Expression']) -> None: + self._elements = elements + + def translate(self, translator: 'AbstractTranslator', ctx: 'Context', + position: Position, info: Info) -> Expr: + """Translate to Silver sequence.""" + if not self._elements: + typ = self._type.translate(translator) + return translator.viper.EmptySeq(typ, position, info) + else: + elements = [element.translate(translator, ctx, position, info) + for element in self._elements] + return translator.viper.ExplicitSeq(elements, position, info) diff --git a/src/nagini_translation/resources/pytype.sil b/src/nagini_translation/resources/pytype.sil index 55ae05d18..f3572dca9 100644 --- a/src/nagini_translation/resources/pytype.sil +++ b/src/nagini_translation/resources/pytype.sil @@ -35,6 +35,7 @@ domain PyType { unique function bool(): PyType unique function bytes(): PyType unique function bytearray(): PyType + unique function PIntSeq(): PyType unique function range_0(): PyType unique function slice(): PyType unique function str(): PyType diff --git a/src/nagini_translation/resources/str.sil b/src/nagini_translation/resources/str.sil index 034b6fa40..e5a3ea406 100644 --- a/src/nagini_translation/resources/str.sil +++ b/src/nagini_translation/resources/str.sil @@ -48,16 +48,14 @@ function str___mod__(self: Ref, other: Ref): Ref requires issubtype(typeof(self), str()) ensures issubtype(typeof(result), str()) -function str_format(self: Ref, args: Ref, kwargs: Ref): Ref - decreases _ +method str_format(self: Ref, args: Ref, kwargs: Ref) returns (_res: Ref) requires issubtype(typeof(self), str()) - ensures issubtype(typeof(result), str()) + ensures issubtype(typeof(_res), str()) -function str___format__(self: Ref, other: Ref): Ref - decreases _ +method str___format__(self: Ref, other: Ref) returns (_res: Ref) requires issubtype(typeof(self), str()) requires issubtype(typeof(other), str()) - ensures issubtype(typeof(result), str()) + ensures issubtype(typeof(_res), str()) method str_split(self: Ref) returns (res: Ref) decreases _ diff --git a/src/nagini_translation/translators/type.py b/src/nagini_translation/translators/type.py index 431c2712c..4552593bf 100644 --- a/src/nagini_translation/translators/type.py +++ b/src/nagini_translation/translators/type.py @@ -44,6 +44,7 @@ def builtins(self): return {'builtins.int': self.viper.Int, 'builtins.bool': self.viper.Bool, 'builtins.PSeq': self.viper.SeqType(self.viper.Ref), + 'builtins.PSeqInt': self.viper.SeqType(self.viper.Int), 'builtins.PSet': self.viper.SetType(self.viper.Ref), 'builtins.PMultiset': self.viper.MultisetType(self.viper.Ref), } From 2fd7ba7f8b69da289b76cc7ae448da577a05948d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 8 Oct 2025 13:55:06 +0200 Subject: [PATCH 030/126] Add PIntSeq --- src/nagini_contracts/contracts.py | 6 +- src/nagini_translation/lib/program_nodes.py | 3 + src/nagini_translation/lib/resolver.py | 5 +- .../lib/silver_nodes/types.py | 3 +- src/nagini_translation/models/converter.py | 8 ++ src/nagini_translation/resources/all.sil | 1 + .../resources/builtins.json | 69 ++++++++++++++++ src/nagini_translation/resources/intseq.sil | 78 +++++++++++++++++++ src/nagini_translation/sif/resources/all.sil | 1 + src/nagini_translation/translators/common.py | 36 +++++++++ .../translators/contract.py | 44 ++++++++++- tests/functional/verification/test_pintseq.py | 43 ++++++++++ 12 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 src/nagini_translation/resources/intseq.sil create mode 100644 tests/functional/verification/test_pintseq.py diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 8c8e3d6fe..5e7586359 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -29,7 +29,7 @@ CONTRACT_FUNCS = ['Assume', 'Assert', 'Old', 'Result', 'ResultT', 'Implies', 'Forall', 'IOForall', 'Forall2', 'Forall3', 'Forall6', 'Exists', 'Low', 'LowVal', 'LowEvent', 'Declassify', 'TerminatesSif', 'Acc', 'Rd', 'Wildcard', 'Fold', 'Unfold', 'Unfolding', 'Previous', - 'RaisedException', 'PSeq', 'PIntSeq', 'PSet', 'ToSeq', 'ToMS', 'MaySet', 'MayCreate', + 'RaisedException', 'PSeq', 'PIntSeq', 'PSet', 'ToSeq', 'ToIntSeq', 'ToMS', 'MaySet', 'MayCreate', 'getMethod', 'getArg', 'getOld', 'arg', 'Joinable', 'MayStart', 'Let', 'PMultiset', 'LowExit', 'Refute', 'isNaN', 'Reveal'] @@ -412,7 +412,7 @@ def ToSeq(l: Iterable[T]) -> PSeq[T]: a pure PSeq. """ -def ToPIntSeq(l: Iterable[int]) -> PIntSeq: +def ToIntSeq(l: Iterable[int]) -> PIntSeq: """ Converts the given iterable of a compatible built-in type (bytearray) to a pure PIntSeq. @@ -665,9 +665,11 @@ def isNaN(f: float) -> bool: 'set_pred', 'bytearray_pred', 'PSeq', + 'PIntSeq', 'PSet', 'PMultiset', 'ToSeq', + 'ToIntSeq', 'ToMS', 'MaySet', 'MayCreate', diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index d0b161e45..b44f57d0c 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -26,6 +26,7 @@ PRIMITIVE_SET_TYPE, PRIMITIVES, PSEQ_TYPE, + PINTSEQ_TYPE, PSET_TYPE, RESULT_NAME, STRING_TYPE, @@ -720,6 +721,8 @@ def try_box(self) -> 'PythonClass': boxed_name = PMSET_TYPE if boxed_name == 'Seq': boxed_name = PSEQ_TYPE + if boxed_name == 'PIntSeq': + boxed_name = PINTSEQ_TYPE return self.module.classes[boxed_name] return self diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index f685e69cd..5fd960a2f 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -22,6 +22,7 @@ RIGHT_OPERATOR_FUNCTIONS, PMSET_TYPE, PSEQ_TYPE, + PINTSEQ_TYPE, PSET_TYPE, RANGE_TYPE, SET_TYPE, @@ -487,6 +488,8 @@ def _get_call_type(node: ast.Call, module: PythonModule, seq_class = module.global_module.classes[PSEQ_TYPE] content_type = _get_iteration_type(arg_type, module, node) return GenericType(seq_class, [content_type]) + elif node.func.id == 'ToIntSeq': + return module.global_module.classes[PINTSEQ_TYPE] elif node.func.id == 'ToMS': arg_type = get_type(node.args[0], containers, container) ms_class = module.global_module.classes[PMSET_TYPE] @@ -594,7 +597,7 @@ def _get_subscript_type(value_type: PythonType, module: PythonModule, # FIXME: This is very unfortunate, but right now we cannot handle this # generically, so we have to hard code these two cases for the moment. return value_type.type_args[1] - elif value_type.name in (RANGE_TYPE, BYTES_TYPE): + elif value_type.name in (RANGE_TYPE, BYTES_TYPE, BYTEARRAY_TYPE, PINTSEQ_TYPE): return module.global_module.classes[INT_TYPE] elif value_type.name == PSEQ_TYPE: return value_type.type_args[0] diff --git a/src/nagini_translation/lib/silver_nodes/types.py b/src/nagini_translation/lib/silver_nodes/types.py index ebba2c814..63ddd6291 100644 --- a/src/nagini_translation/lib/silver_nodes/types.py +++ b/src/nagini_translation/lib/silver_nodes/types.py @@ -175,8 +175,7 @@ def translate(self, translator: 'AbstractTranslator', ctx: 'Context', position: Position, info: Info) -> Expr: """Translate to Silver sequence.""" if not self._elements: - typ = self._type.translate(translator) - return translator.viper.EmptySeq(typ, position, info) + return translator.viper.EmptySeq(self.viper.Int, position, info) else: elements = [element.translate(translator, ctx, position, info) for element in self._elements] diff --git a/src/nagini_translation/models/converter.py b/src/nagini_translation/models/converter.py index ea25afb9b..293775607 100644 --- a/src/nagini_translation/models/converter.py +++ b/src/nagini_translation/models/converter.py @@ -20,6 +20,7 @@ UNBOX_INT = 'int___unbox__%limited' UNBOX_BOOL = 'bool___unbox__%limited' UNBOX_PSEQ = 'PSeq___sil_seq__%limited' +UNBOX_PINTSEQ = 'PIntSeq___val__%limited' TYPEOF = 'typeof' SNAP_TO = '$SortWrappers.' SEQ_LENGTH = 'seq_ref_length' @@ -515,6 +516,8 @@ def convert_value(self, val, t: PythonType, name: str = None): return self.convert_bool_value(val) elif t.python_class.name == 'PSeq': return self.convert_pseq_value(val, t, name) + elif t.python_class.name == 'PIntSeq': + return self.convert_pintseq_value(val, name) elif t.python_class.is_adt: return self.convert_adt_value(val, t) elif isinstance(t, GenericType) and t.python_class.name == 'tuple': @@ -627,6 +630,11 @@ def convert_pseq_value(self, val, t: PythonType, name): sequence_info = self.convert_sequence_value(sequence, t.type_args[0], name) return 'Sequence: {{ {} }}'.format(', '.join(['{} -> {}'.format(k, v) for k, v in sequence_info.items()])) + def convert_pintseq_value(self, val, name): + sequence = self.get_func_value(UNBOX_PINTSEQ, (UNIT, val)) + sequence_info = self.convert_sequence_value(sequence, type(int), name) + return 'Sequence: {{ {} }}'.format(', '.join(['{} -> {}'.format(k, v) for k, v in sequence_info.items()])) + def convert_int_value(self, val): if self.ref_has_type(val, 'bool'): return self.convert_bool_value(val) diff --git a/src/nagini_translation/resources/all.sil b/src/nagini_translation/resources/all.sil index 3df036c45..6d0fce2ba 100644 --- a/src/nagini_translation/resources/all.sil +++ b/src/nagini_translation/resources/all.sil @@ -32,6 +32,7 @@ import "measures.sil" import "pytype.sil" import "range.sil" import "seq.sil" +import "intseq.sil" import "pset.sil" import "set_dict.sil" import "slice.sil" diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index c30e6ad70..c52cf1baf 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -911,6 +911,75 @@ "type_vars": 1, "extends": "object" }, +"PIntSeq": { + "functions": { + "__create__": { + "args": ["__prim__Seq"], + "type": "PIntSeq", + "requires": ["__val__"] + }, + "__unbox__": { + "args": ["PIntSeq"], + "type": "__prim__Seq", + "requires": [] + }, + "__contains__": { + "args": ["PIntSeq", "__prim__int"], + "type": "__prim__bool", + "requires": ["__val__"] + }, + "__getitem__": { + "args": ["PIntSeq", "int"], + "type": "__prim__int", + "requires": ["__val__", "__len__", "int___unbox__"] + }, + "__sil_seq__": { + "args": ["PIntSeq"], + "type": "__prim__Seq", + "requires": ["__val__", "__seq_ref_to_seq_int"] + }, + "__seq_ref_to_seq_int__": { + "args": ["__prim__Seq"], + "type": "__prim__Seq", + "requires": ["__seq_ref_to_seq_int"] + }, + "__val__": { + "args": ["PIntSeq"], + "type": "__prim__Seq" + }, + "__len__": { + "args": ["PIntSeq"], + "type": "__prim__int", + "requires": ["__val__"] + }, + "take": { + "args": ["PIntSeq", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "drop": { + "args": ["PIntSeq", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "update": { + "args": ["PIntSeq", "__prim__int", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "__add__": { + "args": ["PIntSeq", "PIntSeq"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "__eq__": { + "args": ["PIntSeq", "PIntSeq"], + "type": "__prim__bool", + "requires": ["__val__"] + } + }, + "extends": "object" +}, "PSet": { "functions": { "__create__": { diff --git a/src/nagini_translation/resources/intseq.sil b/src/nagini_translation/resources/intseq.sil new file mode 100644 index 000000000..d9d814006 --- /dev/null +++ b/src/nagini_translation/resources/intseq.sil @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 ETH Zurich + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +function PIntSeq___val__(self: Ref): Seq[Int] + decreases _ + requires issubtype(typeof(self), PIntSeq()) + +function PIntSeq___len__(self: Ref): Int + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures result == |PIntSeq___val__(self)| + +function PIntSeq___create__(values: Seq[Int]): Ref + decreases _ + ensures typeof(result) == PIntSeq() + ensures PIntSeq___val__(result) == values + +function PIntSeq___contains__(self: Ref, item: Int): Bool + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures result == (item in PIntSeq___val__(self)) + +function PIntSeq___getitem__(self: Ref, index: Ref): Int + decreases _ + requires issubtype(typeof(self), PIntSeq()) + requires issubtype(typeof(index), int()) + requires @error("Index may be out of bounds.")(let ln == (PIntSeq___len__(self)) in + @error("Index may be out of bounds.")((int___unbox__(index) < 0 ==> int___unbox__(index) >= -ln) && (int___unbox__(index) >= 0 ==> int___unbox__(index) < ln))) + ensures result == (int___unbox__(index) >= 0 ? PIntSeq___val__(self)[int___unbox__(index)] : PIntSeq___val__(self)[PIntSeq___len__(self) + int___unbox__(index)]) + +function PIntSeq_take(self: Ref, no: Int): Ref + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures result == PIntSeq___create__(PIntSeq___val__(self)[..no]) + +function PIntSeq_drop(self: Ref, no: Int): Ref + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures result == PIntSeq___create__(PIntSeq___val__(self)[no..]) + +function PIntSeq_update(self: Ref, index: Int, val: Int): Ref + decreases _ + requires issubtype(typeof(self), PIntSeq()) + requires index >= 0 && index < PIntSeq___len__(self) + ensures result == PIntSeq___create__(PIntSeq___val__(self)[index := val]) + +function PIntSeq___add__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), PIntSeq()) + requires issubtype(typeof(other), PIntSeq()) + ensures result == PIntSeq___create__(PIntSeq___val__(self) ++ PIntSeq___val__(other)) + +function PIntSeq___eq__(self: Ref, other: Ref): Bool + decreases _ + requires issubtype(typeof(self), PIntSeq()) + requires issubtype(typeof(other), PIntSeq()) + ensures result == (PIntSeq___val__(self) == PIntSeq___val__(other)) + ensures result ==> self == other // extensionality + ensures result == object___eq__(self, other) + + +function PIntSeq___sil_seq__(self: Ref): Seq[Ref] + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures PIntSeq___val__(self) == __seq_ref_to_seq_int(result) + + +// Helper function to wrap generic __sil_seq__ calls for conversion to PIntSeq +function PIntSeq___seq_ref_to_seq_int__(sr: Seq[Ref]): Seq[Int] + decreases _ +{ + __seq_ref_to_seq_int(sr) +} \ No newline at end of file diff --git a/src/nagini_translation/sif/resources/all.sil b/src/nagini_translation/sif/resources/all.sil index 1b9fddb5c..d625d57e4 100644 --- a/src/nagini_translation/sif/resources/all.sil +++ b/src/nagini_translation/sif/resources/all.sil @@ -26,6 +26,7 @@ import "../../resources/measures.sil" import "../../resources/pytype.sil" import "../../resources/range.sil" import "../../resources/seq.sil" +import "../../resources/intseq.sil" import "../../resources/pset.sil" import "../../resources/set_dict.sil" import "../../resources/slice.sil" diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index f359e8097..663219460 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -17,6 +17,7 @@ INT_TYPE, IS_DEFINED_FUNC, LIST_TYPE, + BYTEARRAY_TYPE, MAIN_METHOD_NAME, MAY_SET_PRED, NAME_DOMAIN, @@ -25,6 +26,7 @@ PRIMITIVE_INT_TYPE, RANGE_TYPE, PSEQ_TYPE, + PINTSEQ_TYPE, PSET_TYPE, SET_TYPE, SINGLE_NAME, @@ -634,6 +636,40 @@ def get_sequence(self, receiver: PythonType, arg: Expr, arg_type: PythonType, return self.get_function_call(receiver, '__sil_seq__', [arg], [arg_type], node, ctx, position) + def get_int_sequence(self, receiver: PythonType, arg: Expr, + node: ast.AST, ctx: Context, + position: Position = None) -> Expr: + """ + Returns a sequence (Viper type Seq[Int]) representing the contents of arg. + Defaults to type___sil_seq__, but used simpler expressions for known types + to improve performance/triggering. + """ + position = position if position else self.to_position(node, ctx) + info = self.no_info(ctx) + int_type = INT_TYPE + if not isinstance(receiver, UnionType) or isinstance(receiver, OptionalType): + if receiver.name == BYTEARRAY_TYPE: + seq_int = self.viper.SeqType(self.viper.Int) + field = self.viper.Field('bytearray_acc', seq_int, position, info) + res = self.viper.FieldAccess(arg, field, position, info) + return res + if receiver.name == PINTSEQ_TYPE: + if (isinstance(arg, self.viper.ast.FuncApp) and + arg.funcname() == 'PIntSeq___create__'): + args = self.viper.to_list(arg.args()) + return args[0] + int_seq_op = getattr(receiver.cls, '__sil_int_seq__', None) + if callable(int_seq_op): + self.get_function_call(receiver, '__sil_int_seq__', [arg], [None], + node, ctx, position) + + # Fallback to getting a Seq[Ref] and then converting to Seq[Int] + pintseq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] + seq_ref_exp = self.get_function_call(receiver, '__sil_seq__', [arg], [None], + node, ctx, position) + return self.get_function_call(pintseq_class, '__seq_ref_to_seq_int__', [seq_ref_exp], [None], + node, ctx, position) + def _get_function_call(self, receiver: PythonType, func_name: str, args: List[Expr], arg_types: List[PythonType], node: ast.AST, diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index b1bbb181b..6a0421375 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -22,6 +22,7 @@ PMSET_TYPE, PRIMITIVES, PSEQ_TYPE, + PINTSEQ_TYPE, PSET_TYPE, RANGE_TYPE, BYTEARRAY_TYPE, @@ -721,6 +722,18 @@ def translate_to_sequence(self, node: ast.Call, [seq_call, type_lit], [None, None], node, ctx) return stmt, result + + def translate_to_int_sequence(self, node: ast.Call, + ctx: Context) -> StmtsAndExpr: + coll_type = self.get_type(node.args[0], ctx) + stmt, arg = self.translate_expr(node.args[0], ctx) + + seq_call = self.get_int_sequence(coll_type, arg, node, ctx) + seq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] + result = self.get_function_call(seq_class, '__create__', + [seq_call], [None], + node, ctx) + return stmt, result def translate_sequence(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: @@ -749,6 +762,31 @@ def translate_sequence(self, node: ast.Call, [result, type_lit], [None, None], node, ctx) return val_stmts, result + + def translate_int_sequence(self, node: ast.Call, + ctx: Context) -> StmtsAndExpr: + intseq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] + viper_type = self.viper.Int + val_stmts = [] + if node.args: + vals = [] + for arg in node.args: + arg_stmt, arg_val = self.translate_expr(arg, ctx, + target_type=viper_type) + val_stmts += arg_stmt + vals.append(arg_val) + result = self.viper.ExplicitSeq(vals, self.to_position(node, + ctx), + self.no_info(ctx)) + else: + result = self.viper.EmptySeq(viper_type, + self.to_position(node, ctx), + self.no_info(ctx)) + + result = self.get_function_call(intseq_class, '__create__', + [result], [None], node, + ctx) + return val_stmts, result def translate_pset(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: set_type = self.get_type(node, ctx) @@ -1138,12 +1176,16 @@ def translate_contractfunc_call(self, node: ast.Call, ctx: Context, return self.translate_let(node, ctx, impure) elif func_name == PSEQ_TYPE: return self.translate_sequence(node, ctx) + elif func_name == PINTSEQ_TYPE: + return self.translate_int_sequence(node, ctx) elif func_name == PSET_TYPE: return self.translate_pset(node, ctx) elif func_name == PMSET_TYPE: return self.translate_mset(node, ctx) elif func_name == 'ToSeq': return self.translate_to_sequence(node, ctx) + elif func_name == 'ToIntSeq': + return self.translate_to_int_sequence(node, ctx) elif func_name == 'ToMS': return self.translate_to_multiset(node, ctx) elif func_name == 'Joinable': @@ -1159,4 +1201,4 @@ def translate_contractfunc_call(self, node: ast.Call, ctx: Context, elif func_name == 'arg': raise InvalidProgramException(node, 'invalid.arg.use') else: - raise UnsupportedException(node) + raise UnsupportedException(node, func_name) diff --git a/tests/functional/verification/test_pintseq.py b/tests/functional/verification/test_pintseq.py new file mode 100644 index 000000000..beab68a29 --- /dev/null +++ b/tests/functional/verification/test_pintseq.py @@ -0,0 +1,43 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + + +def test_seq() -> None: + no_ints = PIntSeq() + assert len(no_ints) == 0 + ints = PIntSeq(1, 2, 3) + + assert 3 in ints and 1 in ints + assert 4 not in ints + assert ints[1] == 2 + assert len(ints) == 3 + ints2 = ints + ints + assert len(ints2) == 6 + assert ints2[3] == 1 + ints3 = ints2.take(4) + assert len(ints3) == 4 + assert ints3[1] == ints2[1] + ints4 = ints.update(0, 3) + assert 1 not in ints4 + assert ints4[0] == 3 + ints5 = ints.drop(2) + assert len(ints5) == 1 + assert ints5[0] == 3 + #:: ExpectedOutput(assert.failed:assertion.false) + assert False + + +def test_list_ToIntSeq() -> None: + a = [1,2,3] + assert ToIntSeq(a) == PIntSeq(1,2,3) + #:: ExpectedOutput(assert.failed:assertion.false) + assert False + + +def test_bytearray_ToIntSeq() -> None: + a = bytearray([1,2,3]) + assert ToIntSeq(a) == PIntSeq(1,2,3) + #:: ExpectedOutput(assert.failed:assertion.false) + assert False \ No newline at end of file From c6f906b6699ebcb61787c79e1ec3f8e0343de7e3 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 8 Oct 2025 14:14:56 +0200 Subject: [PATCH 031/126] Add helper method range to PSeq and PIntSeq --- src/nagini_contracts/contracts.py | 13 +++ .../resources/builtins.json | 11 +++ src/nagini_translation/resources/intseq.sil | 5 ++ src/nagini_translation/resources/seq.sil | 5 ++ .../sif/resources/builtins.json | 80 +++++++++++++++++++ tests/functional/verification/test_pintseq.py | 13 +++ tests/functional/verification/test_pseq.py | 13 +++ 7 files changed, 140 insertions(+) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 5e7586359..282f5932b 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -247,6 +247,13 @@ def drop(self, until: int) -> 'PSeq[T]': ``PSeq(2,3,5,6).drop(2)`` is equal to ``PSeq(5,6)``. """ + def range(self, start: int, end: int) -> 'PSeq[T]': + """ + Returns a new PSeq of the same type containg all elements + in the range [start, end[\n + (i.e. ``PSeq(2,3,5,6).range(1,3)`` is equal to ``PSeq(3,5)`` ) + """ + def update(self, index: int, new_val: T) -> 'PSeq[T]': """ Returns a new sequence of the same type, containing the same elements @@ -306,6 +313,12 @@ def drop(self, until: int) -> 'PIntSeq': from the given index (i.e., drops all elements until that index). ``PIntSeq(2,3,5,6).drop(2)`` is equal to ``PIntSeq(5,6)``. """ + + def range(self, start: int, end: int) -> 'PIntSeq': + """ + Returns a new PIntSeq containg all elements in the range [start, end[\n + (i.e. ``PIntSeq(2,3,5,6).range(1,3)`` is equal to ``PIntSeq(3,5)`` ) + """ def update(self, index: int, new_val: int) -> 'PIntSeq': """ diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index c52cf1baf..84bd13b43 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -892,6 +892,12 @@ "generic_type": -2, "requires": ["__sil_seq__", "__create__"] }, + "range": { + "args": ["PSeq", "__prim__int", "__prim__int"], + "type": "PSeq", + "generic_type": -2, + "requires": ["__sil_seq__", "__create__"] + }, "update": { "args": ["PSeq", "__prim__int", "object"], "type": "PSeq", @@ -962,6 +968,11 @@ "type": "PIntSeq", "requires": ["__val__", "__create__"] }, + "range": { + "args": ["PIntSeq", "__prim__int", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, "update": { "args": ["PIntSeq", "__prim__int", "__prim__int"], "type": "PIntSeq", diff --git a/src/nagini_translation/resources/intseq.sil b/src/nagini_translation/resources/intseq.sil index d9d814006..4b19a5353 100644 --- a/src/nagini_translation/resources/intseq.sil +++ b/src/nagini_translation/resources/intseq.sil @@ -43,6 +43,11 @@ function PIntSeq_drop(self: Ref, no: Int): Ref requires issubtype(typeof(self), PIntSeq()) ensures result == PIntSeq___create__(PIntSeq___val__(self)[no..]) +function PIntSeq_range(self: Ref, start: Int, end: Int): Ref + decreases _ + requires issubtype(typeof(self), PIntSeq()) + ensures result == PIntSeq___create__(PIntSeq___val__(self)[start..end]) + function PIntSeq_update(self: Ref, index: Int, val: Int): Ref decreases _ requires issubtype(typeof(self), PIntSeq()) diff --git a/src/nagini_translation/resources/seq.sil b/src/nagini_translation/resources/seq.sil index 38e1fd88d..d1f399570 100644 --- a/src/nagini_translation/resources/seq.sil +++ b/src/nagini_translation/resources/seq.sil @@ -44,6 +44,11 @@ function PSeq_drop(self: Ref, no: Int): Ref requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) ensures result == PSeq___create__(PSeq___sil_seq__(self)[no..], PSeq_arg(typeof(self), 0)) +function PSeq_range(self: Ref, start: Int, end: Int): Ref + decreases _ + requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) + ensures result == PSeq___create__(PSeq___sil_seq__(self)[start..end], PSeq_arg(typeof(self), 0)) + function PSeq_update(self: Ref, index: Int, val: Ref): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 90b48d6f3..0ba2d71d7 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -862,6 +862,12 @@ "generic_type": -2, "requires": ["__sil_seq__", "__create__"] }, + "range": { + "args": ["PSeq", "__prim__int", "__prim__int"], + "type": "PSeq", + "generic_type": -2, + "requires": ["__sil_seq__", "__create__"] + }, "update": { "args": ["PSeq", "__prim__int", "object"], "type": "PSeq", @@ -881,6 +887,80 @@ "type_vars": 1, "extends": "object" }, +"PIntSeq": { + "functions": { + "__create__": { + "args": ["__prim__Seq"], + "type": "PIntSeq", + "requires": ["__val__"] + }, + "__unbox__": { + "args": ["PIntSeq"], + "type": "__prim__Seq", + "requires": [] + }, + "__contains__": { + "args": ["PIntSeq", "__prim__int"], + "type": "__prim__bool", + "requires": ["__val__"] + }, + "__getitem__": { + "args": ["PIntSeq", "int"], + "type": "__prim__int", + "requires": ["__val__", "__len__", "int___unbox__"] + }, + "__sil_seq__": { + "args": ["PIntSeq"], + "type": "__prim__Seq", + "requires": ["__val__", "__seq_ref_to_seq_int"] + }, + "__seq_ref_to_seq_int__": { + "args": ["__prim__Seq"], + "type": "__prim__Seq", + "requires": ["__seq_ref_to_seq_int"] + }, + "__val__": { + "args": ["PIntSeq"], + "type": "__prim__Seq" + }, + "__len__": { + "args": ["PIntSeq"], + "type": "__prim__int", + "requires": ["__val__"] + }, + "take": { + "args": ["PIntSeq", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "drop": { + "args": ["PIntSeq", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "range": { + "args": ["PIntSeq", "__prim__int", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "update": { + "args": ["PIntSeq", "__prim__int", "__prim__int"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "__add__": { + "args": ["PIntSeq", "PIntSeq"], + "type": "PIntSeq", + "requires": ["__val__", "__create__"] + }, + "__eq__": { + "args": ["PIntSeq", "PIntSeq"], + "type": "__prim__bool", + "requires": ["__val__"] + } + }, + "extends": "object" +}, "PSet": { "functions": { "__create__": { diff --git a/tests/functional/verification/test_pintseq.py b/tests/functional/verification/test_pintseq.py index beab68a29..3cd74d92c 100644 --- a/tests/functional/verification/test_pintseq.py +++ b/tests/functional/verification/test_pintseq.py @@ -28,6 +28,19 @@ def test_seq() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert False +def test_range() -> None: + ints = PIntSeq(1,3,5,6,8) + r = ints.range(1, 3) + + assert len(ints) == 5 + assert len(r) == 2 + assert 5 in r + assert r[0] == 3 + assert 1 not in r + assert 8 not in r + + #:: ExpectedOutput(assert.failed:assertion.false) + assert r[1] == 6 def test_list_ToIntSeq() -> None: a = [1,2,3] diff --git a/tests/functional/verification/test_pseq.py b/tests/functional/verification/test_pseq.py index 9a257c105..92c586264 100644 --- a/tests/functional/verification/test_pseq.py +++ b/tests/functional/verification/test_pseq.py @@ -35,6 +35,19 @@ def test_seq() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert False +def test_range() -> None: + ints = PSeq(1,3,5,6,8) + r = ints.range(1, 3) + + assert len(ints) == 5 + assert len(r) == 2 + assert 5 in r + assert r[0] == 3 + assert 1 not in r + assert 8 not in r + + #:: ExpectedOutput(assert.failed:assertion.false) + assert r[1] == 6 def test_list_ToSeq() -> None: a = [1,2,3] From 429aab4cc6e93f50802d6ac2484b5926442c6c4c Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 10 Oct 2025 13:23:36 +0200 Subject: [PATCH 032/126] Add special implementation to ToIntSeq for bytearray and bytes --- src/nagini_translation/resources/bool.sil | 6 +++++ .../resources/builtins.json | 23 +++++++++++-------- .../resources/bytearray.sil | 18 +++++---------- src/nagini_translation/resources/intseq.sil | 6 +++++ .../translators/contract.py | 7 +++++- tests/functional/verification/test_pintseq.py | 12 +++++++++- 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index f55d30ab7..471e0de05 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -342,6 +342,12 @@ function int___int__(self: Ref): Ref requires issubtype(typeof(self), int()) ensures result == self +function int___byte__bounds__(value: Int): Bool + decreases _ +{ + 0 <= value < 256 +} + domain __ObjectEquality { function object___eq__(Ref, Ref): Bool diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 84bd13b43..277b560a3 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -349,6 +349,10 @@ "args": ["int", "int"], "type": "int", "requires": ["__rshift__"] + }, + "__byte__bounds__": { + "args": ["__prim__int"], + "type": "__prim__bool" } }, "extends": "float" @@ -658,7 +662,7 @@ "args": ["list"], "type": null, "MustTerminate": true, - "requires": ["list", "list___getitem__", "list___len__", "bytearray___bounds_helper__", "int___unbox__", "__seq_ref_to_seq_int"] + "requires": ["list", "list___getitem__", "list___len__", "int___byte__bounds__", "int___unbox__", "__seq_ref_to_seq_int"] }, "__initFromBytearray__": { "args": ["bytearray"], @@ -686,7 +690,7 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__", "__prim__int___box__"] + "requires": ["__len__", "__getitem__", "int___byte__bounds__", "int___unbox__", "__prim__int___box__"] }, "__iter__": { "args": ["bytearray"], @@ -710,7 +714,7 @@ "__getitem__": { "args": ["bytearray", "int"], "type": "__prim__int", - "requires": ["__len__", "bytearray___bounds_helper__", "int___unbox__"] + "requires": ["__len__", "int___byte__bounds__", "int___unbox__"] }, "__contains__": { "args": ["bytearray", "int"], @@ -726,14 +730,10 @@ "type": "__prim__bool", "requires": ["__len__"] }, - "__bounds_helper__": { - "args": ["__prim__int"], - "type": "__prim__bool" - }, "__sil_seq__": { "args": ["bytearray"], "type": "__prim__Seq", - "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] + "requires": ["int___byte__bounds__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] } }, "extends": "object" @@ -922,7 +922,12 @@ "__create__": { "args": ["__prim__Seq"], "type": "PIntSeq", - "requires": ["__val__"] + "requires": ["__val__", "int___byte__bounds__"] + }, + "__create__bytes__": { + "args": ["__prim__Seq"], + "type": "PIntSeq", + "requires": ["__val__", "int___byte__bounds__"] }, "__unbox__": { "args": ["PIntSeq"], diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 00965b0f1..4765bdec7 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -5,14 +5,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -field bytearray_acc : Seq[Int] - // Bytearray only accepts values in the range [0, 255] -function bytearray___bounds_helper__(value: Int): Bool - decreases _ -{ - 0 <= value < 256 -} +field bytearray_acc : Seq[Int] method bytearray___init__() returns (res: Ref) ensures acc(res.bytearray_acc) @@ -40,7 +34,7 @@ method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) method bytearray___initFromList__(values: Ref) returns (res: Ref) requires issubtype(typeof(values), list(int())) requires acc(values.list_acc, 1/1000) - requires forall i: Int :: {values.list_acc[i]} ((0 <= i < list___len__(values)) ==> bytearray___bounds_helper__(int___unbox__(list___getitem__(values, __prim__int___box__(i))))) + requires forall i: Int :: {values.list_acc[i]} ((0 <= i < list___len__(values)) ==> int___byte__bounds__(int___unbox__(list___getitem__(values, __prim__int___box__(i))))) ensures acc(values.list_acc, 1/1000) ensures acc(res.bytearray_acc) ensures typeof(res) == bytearray() @@ -93,7 +87,7 @@ function bytearray___getitem__(self: Ref, key: Ref): Int requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) - ensures bytearray___bounds_helper__(result) + ensures int___byte__bounds__(result) method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) requires issubtype(typeof(self), bytearray()) @@ -111,7 +105,7 @@ method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires @error("Bytearray index may be negative.")(int___unbox__(key) >= 0) requires @error("Bytearray index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) requires issubtype(typeof(value), int()) - requires @error("Provided value may be out of bounds.")bytearray___bounds_helper__(int___unbox__(value)) + requires @error("Provided value may be out of bounds.")int___byte__bounds__(int___unbox__(value)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) @@ -120,7 +114,7 @@ method bytearray_append(self: Ref, item: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) requires issubtype(typeof(item), int()) - requires @error("Provided item may be out of bounds.")bytearray___bounds_helper__(int___unbox__(item)) + requires @error("Provided item may be out of bounds.")int___byte__bounds__(int___unbox__(item)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) @@ -160,4 +154,4 @@ function bytearray___sil_seq__(self: Ref): Seq[Ref] ensures |result| == bytearray___len__(self) ensures (forall i: Int :: { result[i] } 0 <= i < bytearray___len__(self) ==> result[i] == __prim__int___box__(self.bytearray_acc[i])) ensures (forall i: Ref :: { (i in result) } (i in result) == (typeof(i) == int() && (int___unbox__(i) in self.bytearray_acc))) - ensures (forall i: Ref :: { (i in result) } (i in result) ==> bytearray___bounds_helper__(int___unbox__(i))) \ No newline at end of file + ensures (forall i: Ref :: { (i in result) } (i in result) ==> int___byte__bounds__(int___unbox__(i))) \ No newline at end of file diff --git a/src/nagini_translation/resources/intseq.sil b/src/nagini_translation/resources/intseq.sil index 4b19a5353..df781a6e4 100644 --- a/src/nagini_translation/resources/intseq.sil +++ b/src/nagini_translation/resources/intseq.sil @@ -20,6 +20,12 @@ function PIntSeq___create__(values: Seq[Int]): Ref ensures typeof(result) == PIntSeq() ensures PIntSeq___val__(result) == values +function PIntSeq___create__bytes__(values: Seq[Int]): Ref + decreases _ + ensures typeof(result) == PIntSeq() + ensures PIntSeq___val__(result) == values + ensures forall i:Int :: i in values ==> int___byte__bounds__(i) + function PIntSeq___contains__(self: Ref, item: Int): Bool decreases _ requires issubtype(typeof(self), PIntSeq()) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 6a0421375..a841d914d 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -26,6 +26,7 @@ PSET_TYPE, RANGE_TYPE, BYTEARRAY_TYPE, + BYTES_TYPE, THREAD_DOMAIN, THREAD_POST_PRED, THREAD_START_PRED, @@ -730,7 +731,11 @@ def translate_to_int_sequence(self, node: ast.Call, seq_call = self.get_int_sequence(coll_type, arg, node, ctx) seq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] - result = self.get_function_call(seq_class, '__create__', + if coll_type.name == BYTEARRAY_TYPE or coll_type.name == BYTES_TYPE: + call_name = '__create__bytes__' + else: + call_name = '__create__' + result = self.get_function_call(seq_class, call_name, [seq_call], [None], node, ctx) return stmt, result diff --git a/tests/functional/verification/test_pintseq.py b/tests/functional/verification/test_pintseq.py index 3cd74d92c..3731af742 100644 --- a/tests/functional/verification/test_pintseq.py +++ b/tests/functional/verification/test_pintseq.py @@ -53,4 +53,14 @@ def test_bytearray_ToIntSeq() -> None: a = bytearray([1,2,3]) assert ToIntSeq(a) == PIntSeq(1,2,3) #:: ExpectedOutput(assert.failed:assertion.false) - assert False \ No newline at end of file + assert False + +def test_bytearray_bounds(b_array: bytearray) -> None: + Requires(bytearray_pred(b_array)) + Requires(len(b_array) > 2) + seq = ToIntSeq(b_array) + + assert 0 <= seq[0] and seq[0] <= 0xFF + + #:: ExpectedOutput(assert.failed:assertion.false) + assert seq[1] >= 256 \ No newline at end of file From cea14aa323ef67e67bb8b0f38e17ab079b1687e7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 10 Oct 2025 14:47:04 +0200 Subject: [PATCH 033/126] Repurpose PIntSeq as PByteSeq --- src/nagini_contracts/contracts.py | 50 +++++----- src/nagini_translation/lib/constants.py | 2 +- src/nagini_translation/lib/program_nodes.py | 6 +- src/nagini_translation/lib/resolver.py | 8 +- .../lib/silver_nodes/types.py | 2 +- src/nagini_translation/models/converter.py | 10 +- src/nagini_translation/resources/all.sil | 2 +- src/nagini_translation/resources/bool.sil | 2 +- .../resources/builtins.json | 58 ++++++------ .../resources/bytearray.sil | 10 +- src/nagini_translation/resources/byteseq.sil | 92 +++++++++++++++++++ src/nagini_translation/resources/intseq.sil | 89 ------------------ src/nagini_translation/resources/pytype.sil | 2 +- .../sif/resources/builtins.json | 38 ++++---- src/nagini_translation/translators/common.py | 10 +- .../translators/contract.py | 14 +-- .../{test_pintseq.py => test_pbyteseq.py} | 30 ++++-- 17 files changed, 221 insertions(+), 204 deletions(-) create mode 100644 src/nagini_translation/resources/byteseq.sil delete mode 100644 src/nagini_translation/resources/intseq.sil rename tests/functional/verification/{test_pintseq.py => test_pbyteseq.py} (68%) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 282f5932b..9505542c5 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -29,7 +29,7 @@ CONTRACT_FUNCS = ['Assume', 'Assert', 'Old', 'Result', 'ResultT', 'Implies', 'Forall', 'IOForall', 'Forall2', 'Forall3', 'Forall6', 'Exists', 'Low', 'LowVal', 'LowEvent', 'Declassify', 'TerminatesSif', 'Acc', 'Rd', 'Wildcard', 'Fold', 'Unfold', 'Unfolding', 'Previous', - 'RaisedException', 'PSeq', 'PIntSeq', 'PSet', 'ToSeq', 'ToIntSeq', 'ToMS', 'MaySet', 'MayCreate', + 'RaisedException', 'PSeq', 'PByteSeq', 'PSet', 'ToSeq', 'ToByteSeq', 'ToMS', 'MaySet', 'MayCreate', 'getMethod', 'getArg', 'getOld', 'arg', 'Joinable', 'MayStart', 'Let', 'PMultiset', 'LowExit', 'Refute', 'isNaN', 'Reveal'] @@ -267,21 +267,21 @@ def __iter__(self) -> Iterator[T]: can be used as arguments for Forall. """ -class PIntSeq(Sized, Iterable[int]): +class PByteSeq(Sized, Iterable[int]): """ - A PIntSeq represents a pure sequence of instances of int, and + A PByteSeq represents a pure sequence of instances of int, and is translated to native Viper sequences. """ def __init__(self, *args: int) -> None: """ - ``PIntSeq(a, b, c)`` creates a PIntSeq instance containing the objects + ``PByteSeq(a, b, c)`` creates a PByteSeq instance containing the objects a, b and c in that order. """ def __contains__(self, item: object) -> bool: """ - True iff this PIntSeq contains the given object (not taking ``__eq__`` + True iff this PByteSeq contains the given object (not taking ``__eq__`` into account). """ @@ -292,44 +292,44 @@ def __getitem__(self, item: int) -> int: def __len__(self) -> int: """ - Returns the length of this PIntSeq. + Returns the length of this PByteSeq. """ - def __add__(self, other: 'PIntSeq') -> 'PIntSeq': + def __add__(self, other: 'PByteSeq') -> 'PByteSeq': """ - Concatenates two PIntSeqs to get a new PIntSeq. + Concatenates two PByteSeqs to get a new PByteSeq. """ - def take(self, until: int) -> 'PIntSeq': + def take(self, until: int) -> 'PByteSeq': """ - Returns a new PIntSeq containing all elements starting - from the beginning until the given index. ``PIntSeq(3,2,5,6).take(3)`` - is equal to ``PIntSeq(3,2,5)``. + Returns a new PByteSeq containing all elements starting + from the beginning until the given index. ``PByteSeq(3,2,5,6).take(3)`` + is equal to ``PByteSeq(3,2,5)``. """ - def drop(self, until: int) -> 'PIntSeq': + def drop(self, until: int) -> 'PByteSeq': """ - Returns a new PIntSeq containing all elements starting + Returns a new PByteSeq containing all elements starting from the given index (i.e., drops all elements until that index). - ``PIntSeq(2,3,5,6).drop(2)`` is equal to ``PIntSeq(5,6)``. + ``PByteSeq(2,3,5,6).drop(2)`` is equal to ``PByteSeq(5,6)``. """ - def range(self, start: int, end: int) -> 'PIntSeq': + def range(self, start: int, end: int) -> 'PByteSeq': """ - Returns a new PIntSeq containg all elements in the range [start, end[\n - (i.e. ``PIntSeq(2,3,5,6).range(1,3)`` is equal to ``PIntSeq(3,5)`` ) + Returns a new PByteSeq containg all elements in the range [start, end[\n + (i.e. ``PByteSeq(2,3,5,6).range(1,3)`` is equal to ``PByteSeq(3,5)`` ) """ - def update(self, index: int, new_val: int) -> 'PIntSeq': + def update(self, index: int, new_val: int) -> 'PByteSeq': """ - Returns a new PIntSeq, containing the same elements + Returns a new PByteSeq, containing the same elements except for the element at index ``index``, which is replaced by ``new_val``. """ def __iter__(self) -> Iterator[int]: """ - PIntSeqs can be quantified over; this is only here so thatPIntSeqs + PByteSeqs can be quantified over; this is only here so thatPByteSeqs can be used as arguments for Forall. """ @@ -425,10 +425,10 @@ def ToSeq(l: Iterable[T]) -> PSeq[T]: a pure PSeq. """ -def ToIntSeq(l: Iterable[int]) -> PIntSeq: +def ToByteSeq(l: Iterable[int]) -> PByteSeq: """ Converts the given iterable of a compatible built-in type (bytearray) to - a pure PIntSeq. + a pure PByteSeq. """ @@ -678,11 +678,11 @@ def isNaN(f: float) -> bool: 'set_pred', 'bytearray_pred', 'PSeq', - 'PIntSeq', + 'PByteSeq', 'PSet', 'PMultiset', 'ToSeq', - 'ToIntSeq', + 'ToByteSeq', 'ToMS', 'MaySet', 'MayCreate', diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index dc7bd6157..6975b0b06 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -289,7 +289,7 @@ PSEQ_TYPE = 'PSeq' -PINTSEQ_TYPE = 'PIntSeq' +PBYTESEQ_TYPE = 'PByteSeq' PSET_TYPE = 'PSet' diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index b44f57d0c..4c9e07589 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -26,7 +26,7 @@ PRIMITIVE_SET_TYPE, PRIMITIVES, PSEQ_TYPE, - PINTSEQ_TYPE, + PBYTESEQ_TYPE, PSET_TYPE, RESULT_NAME, STRING_TYPE, @@ -721,8 +721,8 @@ def try_box(self) -> 'PythonClass': boxed_name = PMSET_TYPE if boxed_name == 'Seq': boxed_name = PSEQ_TYPE - if boxed_name == 'PIntSeq': - boxed_name = PINTSEQ_TYPE + if boxed_name == 'PByteSeq': + boxed_name = PBYTESEQ_TYPE return self.module.classes[boxed_name] return self diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 5fd960a2f..7f50ae618 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -22,7 +22,7 @@ RIGHT_OPERATOR_FUNCTIONS, PMSET_TYPE, PSEQ_TYPE, - PINTSEQ_TYPE, + PBYTESEQ_TYPE, PSET_TYPE, RANGE_TYPE, SET_TYPE, @@ -488,8 +488,8 @@ def _get_call_type(node: ast.Call, module: PythonModule, seq_class = module.global_module.classes[PSEQ_TYPE] content_type = _get_iteration_type(arg_type, module, node) return GenericType(seq_class, [content_type]) - elif node.func.id == 'ToIntSeq': - return module.global_module.classes[PINTSEQ_TYPE] + elif node.func.id == 'ToByteSeq': + return module.global_module.classes[PBYTESEQ_TYPE] elif node.func.id == 'ToMS': arg_type = get_type(node.args[0], containers, container) ms_class = module.global_module.classes[PMSET_TYPE] @@ -597,7 +597,7 @@ def _get_subscript_type(value_type: PythonType, module: PythonModule, # FIXME: This is very unfortunate, but right now we cannot handle this # generically, so we have to hard code these two cases for the moment. return value_type.type_args[1] - elif value_type.name in (RANGE_TYPE, BYTES_TYPE, BYTEARRAY_TYPE, PINTSEQ_TYPE): + elif value_type.name in (RANGE_TYPE, BYTES_TYPE, BYTEARRAY_TYPE, PBYTESEQ_TYPE): return module.global_module.classes[INT_TYPE] elif value_type.name == PSEQ_TYPE: return value_type.type_args[0] diff --git a/src/nagini_translation/lib/silver_nodes/types.py b/src/nagini_translation/lib/silver_nodes/types.py index 63ddd6291..92c69d2cc 100644 --- a/src/nagini_translation/lib/silver_nodes/types.py +++ b/src/nagini_translation/lib/silver_nodes/types.py @@ -165,7 +165,7 @@ def translate(self, translator: 'AbstractTranslator', ctx: 'Context', for element in self._elements] return translator.viper.ExplicitSeq(elements, position, info) -class PIntSeq: +class PByteSeq: """A helper class for generating Silver sequences.""" def __init__(self, elements: List['Expression']) -> None: diff --git a/src/nagini_translation/models/converter.py b/src/nagini_translation/models/converter.py index 293775607..a0adf5919 100644 --- a/src/nagini_translation/models/converter.py +++ b/src/nagini_translation/models/converter.py @@ -20,7 +20,7 @@ UNBOX_INT = 'int___unbox__%limited' UNBOX_BOOL = 'bool___unbox__%limited' UNBOX_PSEQ = 'PSeq___sil_seq__%limited' -UNBOX_PINTSEQ = 'PIntSeq___val__%limited' +UNBOX_PBYTESEQ = 'PByteSeq___val__%limited' TYPEOF = 'typeof' SNAP_TO = '$SortWrappers.' SEQ_LENGTH = 'seq_ref_length' @@ -516,8 +516,8 @@ def convert_value(self, val, t: PythonType, name: str = None): return self.convert_bool_value(val) elif t.python_class.name == 'PSeq': return self.convert_pseq_value(val, t, name) - elif t.python_class.name == 'PIntSeq': - return self.convert_pintseq_value(val, name) + elif t.python_class.name == 'PByteSeq': + return self.convert_PByteSeq_value(val, name) elif t.python_class.is_adt: return self.convert_adt_value(val, t) elif isinstance(t, GenericType) and t.python_class.name == 'tuple': @@ -630,8 +630,8 @@ def convert_pseq_value(self, val, t: PythonType, name): sequence_info = self.convert_sequence_value(sequence, t.type_args[0], name) return 'Sequence: {{ {} }}'.format(', '.join(['{} -> {}'.format(k, v) for k, v in sequence_info.items()])) - def convert_pintseq_value(self, val, name): - sequence = self.get_func_value(UNBOX_PINTSEQ, (UNIT, val)) + def convert_PByteSeq_value(self, val, name): + sequence = self.get_func_value(UNBOX_PBYTESEQ, (UNIT, val)) sequence_info = self.convert_sequence_value(sequence, type(int), name) return 'Sequence: {{ {} }}'.format(', '.join(['{} -> {}'.format(k, v) for k, v in sequence_info.items()])) diff --git a/src/nagini_translation/resources/all.sil b/src/nagini_translation/resources/all.sil index 6d0fce2ba..a1e7fc00e 100644 --- a/src/nagini_translation/resources/all.sil +++ b/src/nagini_translation/resources/all.sil @@ -32,7 +32,7 @@ import "measures.sil" import "pytype.sil" import "range.sil" import "seq.sil" -import "intseq.sil" +import "byteseq.sil" import "pset.sil" import "set_dict.sil" import "slice.sil" diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 471e0de05..9b4a77fbf 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -342,7 +342,7 @@ function int___int__(self: Ref): Ref requires issubtype(typeof(self), int()) ensures result == self -function int___byte__bounds__(value: Int): Bool +function int___byte_bounds__(value: Int): Bool decreases _ { 0 <= value < 256 diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 277b560a3..a5641ca0c 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -662,7 +662,7 @@ "args": ["list"], "type": null, "MustTerminate": true, - "requires": ["list", "list___getitem__", "list___len__", "int___byte__bounds__", "int___unbox__", "__seq_ref_to_seq_int"] + "requires": ["list", "list___getitem__", "list___len__", "int___byte_bounds__", "int___unbox__", "__seq_ref_to_seq_int"] }, "__initFromBytearray__": { "args": ["bytearray"], @@ -690,7 +690,7 @@ "args": ["bytearray", "int", "int"], "type": null, "MustTerminate": true, - "requires": ["__len__", "__getitem__", "int___byte__bounds__", "int___unbox__", "__prim__int___box__"] + "requires": ["__len__", "__getitem__", "int___byte_bounds__", "int___unbox__", "__prim__int___box__"] }, "__iter__": { "args": ["bytearray"], @@ -714,7 +714,7 @@ "__getitem__": { "args": ["bytearray", "int"], "type": "__prim__int", - "requires": ["__len__", "int___byte__bounds__", "int___unbox__"] + "requires": ["__len__", "int___byte_bounds__", "int___unbox__"] }, "__contains__": { "args": ["bytearray", "int"], @@ -733,7 +733,7 @@ "__sil_seq__": { "args": ["bytearray"], "type": "__prim__Seq", - "requires": ["int___byte__bounds__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] + "requires": ["int___byte_bounds__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] } }, "extends": "object" @@ -917,35 +917,35 @@ "type_vars": 1, "extends": "object" }, -"PIntSeq": { +"PByteSeq": { "functions": { "__create__": { "args": ["__prim__Seq"], - "type": "PIntSeq", - "requires": ["__val__", "int___byte__bounds__"] + "type": "PByteSeq", + "requires": ["__val__", "int___byte_bounds__"] }, - "__create__bytes__": { + "__from_bytes__": { "args": ["__prim__Seq"], - "type": "PIntSeq", - "requires": ["__val__", "int___byte__bounds__"] + "type": "PByteSeq", + "requires": ["__val__"] }, "__unbox__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq", "requires": [] }, "__contains__": { - "args": ["PIntSeq", "__prim__int"], + "args": ["PByteSeq", "__prim__int"], "type": "__prim__bool", "requires": ["__val__"] }, "__getitem__": { - "args": ["PIntSeq", "int"], + "args": ["PByteSeq", "int"], "type": "__prim__int", - "requires": ["__val__", "__len__", "int___unbox__"] + "requires": ["__val__", "__len__", "int___unbox__", "int___byte_bounds__"] }, "__sil_seq__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq", "requires": ["__val__", "__seq_ref_to_seq_int"] }, @@ -955,41 +955,41 @@ "requires": ["__seq_ref_to_seq_int"] }, "__val__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq" }, "__len__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__int", "requires": ["__val__"] }, "take": { - "args": ["PIntSeq", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "drop": { - "args": ["PIntSeq", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "range": { - "args": ["PIntSeq", "__prim__int", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "update": { - "args": ["PIntSeq", "__prim__int", "__prim__int"], - "type": "PIntSeq", - "requires": ["__val__", "__create__"] + "args": ["PByteSeq", "__prim__int", "__prim__int"], + "type": "PByteSeq", + "requires": ["__val__", "__create__", "int___byte_bounds__"] }, "__add__": { - "args": ["PIntSeq", "PIntSeq"], - "type": "PIntSeq", + "args": ["PByteSeq", "PByteSeq"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "__eq__": { - "args": ["PIntSeq", "PIntSeq"], + "args": ["PByteSeq", "PByteSeq"], "type": "__prim__bool", "requires": ["__val__"] } diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 4765bdec7..e0cf154cf 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -34,7 +34,7 @@ method bytearray___initFromBytearray__(other: Ref) returns (res: Ref) method bytearray___initFromList__(values: Ref) returns (res: Ref) requires issubtype(typeof(values), list(int())) requires acc(values.list_acc, 1/1000) - requires forall i: Int :: {values.list_acc[i]} ((0 <= i < list___len__(values)) ==> int___byte__bounds__(int___unbox__(list___getitem__(values, __prim__int___box__(i))))) + requires forall i: Int :: {values.list_acc[i]} ((0 <= i < list___len__(values)) ==> int___byte_bounds__(int___unbox__(list___getitem__(values, __prim__int___box__(i))))) ensures acc(values.list_acc, 1/1000) ensures acc(res.bytearray_acc) ensures typeof(res) == bytearray() @@ -87,7 +87,7 @@ function bytearray___getitem__(self: Ref, key: Ref): Int requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) < 0 ==> int___unbox__(key) >= -ln)) requires @error("Bytearray index may be out of bounds.")(let ln == (bytearray___len__(self)) in (int___unbox__(key) >= 0 ==> int___unbox__(key) < ln)) ensures result == (int___unbox__(key) >= 0 ? self.bytearray_acc[int___unbox__(key)] : self.bytearray_acc[bytearray___len__(self) + int___unbox__(key)]) - ensures int___byte__bounds__(result) + ensures int___byte_bounds__(result) method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) requires issubtype(typeof(self), bytearray()) @@ -105,7 +105,7 @@ method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () requires @error("Bytearray index may be negative.")(int___unbox__(key) >= 0) requires @error("Bytearray index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) requires issubtype(typeof(value), int()) - requires @error("Provided value may be out of bounds.")int___byte__bounds__(int___unbox__(value)) + requires @error("Provided value may be out of bounds.")int___byte_bounds__(int___unbox__(value)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) @@ -114,7 +114,7 @@ method bytearray_append(self: Ref, item: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) requires issubtype(typeof(item), int()) - requires @error("Provided item may be out of bounds.")int___byte__bounds__(int___unbox__(item)) + requires @error("Provided item may be out of bounds.")int___byte_bounds__(int___unbox__(item)) ensures acc(self.bytearray_acc) ensures self.bytearray_acc == old(self.bytearray_acc) ++ Seq(int___unbox__(item)) @@ -154,4 +154,4 @@ function bytearray___sil_seq__(self: Ref): Seq[Ref] ensures |result| == bytearray___len__(self) ensures (forall i: Int :: { result[i] } 0 <= i < bytearray___len__(self) ==> result[i] == __prim__int___box__(self.bytearray_acc[i])) ensures (forall i: Ref :: { (i in result) } (i in result) == (typeof(i) == int() && (int___unbox__(i) in self.bytearray_acc))) - ensures (forall i: Ref :: { (i in result) } (i in result) ==> int___byte__bounds__(int___unbox__(i))) \ No newline at end of file + ensures (forall i: Ref :: { (i in result) } (i in result) ==> int___byte_bounds__(int___unbox__(i))) \ No newline at end of file diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil new file mode 100644 index 000000000..4e4924173 --- /dev/null +++ b/src/nagini_translation/resources/byteseq.sil @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019 ETH Zurich + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + + +function PByteSeq___val__(self: Ref): Seq[Int] + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures forall i: Int :: {i in result} (i in result) ==> int___byte_bounds__(i) + +function PByteSeq___len__(self: Ref): Int + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures result == |PByteSeq___val__(self)| + +function PByteSeq___create__(values: Seq[Int]): Ref + decreases _ + requires forall i: Int :: {i in values} (i in values) ==> int___byte_bounds__(i) + ensures typeof(result) == PByteSeq() + ensures PByteSeq___val__(result) == values + +function PByteSeq___from_bytes__(values: Seq[Int]): Ref + decreases _ + ensures typeof(result) == PByteSeq() + ensures PByteSeq___val__(result) == values + +function PByteSeq___contains__(self: Ref, item: Int): Bool + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures result == (item in PByteSeq___val__(self)) + +function PByteSeq___getitem__(self: Ref, index: Ref): Int + decreases _ + requires issubtype(typeof(self), PByteSeq()) + requires issubtype(typeof(index), int()) + requires @error("Index may be out of bounds.")(let ln == (PByteSeq___len__(self)) in + @error("Index may be out of bounds.")((int___unbox__(index) < 0 ==> int___unbox__(index) >= -ln) && (int___unbox__(index) >= 0 ==> int___unbox__(index) < ln))) + ensures result == (int___unbox__(index) >= 0 ? PByteSeq___val__(self)[int___unbox__(index)] : PByteSeq___val__(self)[PByteSeq___len__(self) + int___unbox__(index)]) + ensures int___byte_bounds__(result) + +function PByteSeq_take(self: Ref, no: Int): Ref + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures result == PByteSeq___create__(PByteSeq___val__(self)[..no]) + +function PByteSeq_drop(self: Ref, no: Int): Ref + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures result == PByteSeq___create__(PByteSeq___val__(self)[no..]) + +function PByteSeq_range(self: Ref, start: Int, end: Int): Ref + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures result == PByteSeq___create__(PByteSeq___val__(self)[start..end]) + +function PByteSeq_update(self: Ref, index: Int, val: Int): Ref + decreases _ + requires issubtype(typeof(self), PByteSeq()) + requires index >= 0 && index < PByteSeq___len__(self) + ensures int___byte_bounds__(val) + ensures result == PByteSeq___create__(PByteSeq___val__(self)[index := val]) + +function PByteSeq___add__(self: Ref, other: Ref): Ref + decreases _ + requires issubtype(typeof(self), PByteSeq()) + requires issubtype(typeof(other), PByteSeq()) + ensures result == PByteSeq___create__(PByteSeq___val__(self) ++ PByteSeq___val__(other)) + +function PByteSeq___eq__(self: Ref, other: Ref): Bool + decreases _ + requires issubtype(typeof(self), PByteSeq()) + requires issubtype(typeof(other), PByteSeq()) + ensures result == (PByteSeq___val__(self) == PByteSeq___val__(other)) + ensures result ==> self == other // extensionality + ensures result == object___eq__(self, other) + + +function PByteSeq___sil_seq__(self: Ref): Seq[Ref] + decreases _ + requires issubtype(typeof(self), PByteSeq()) + ensures PByteSeq___val__(self) == __seq_ref_to_seq_int(result) + + +// Helper function to wrap generic __sil_seq__ calls for conversion to PByteSeq +function PByteSeq___seq_ref_to_seq_int__(sr: Seq[Ref]): Seq[Int] + decreases _ +{ + __seq_ref_to_seq_int(sr) +} \ No newline at end of file diff --git a/src/nagini_translation/resources/intseq.sil b/src/nagini_translation/resources/intseq.sil deleted file mode 100644 index df781a6e4..000000000 --- a/src/nagini_translation/resources/intseq.sil +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2019 ETH Zurich - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - - -function PIntSeq___val__(self: Ref): Seq[Int] - decreases _ - requires issubtype(typeof(self), PIntSeq()) - -function PIntSeq___len__(self: Ref): Int - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures result == |PIntSeq___val__(self)| - -function PIntSeq___create__(values: Seq[Int]): Ref - decreases _ - ensures typeof(result) == PIntSeq() - ensures PIntSeq___val__(result) == values - -function PIntSeq___create__bytes__(values: Seq[Int]): Ref - decreases _ - ensures typeof(result) == PIntSeq() - ensures PIntSeq___val__(result) == values - ensures forall i:Int :: i in values ==> int___byte__bounds__(i) - -function PIntSeq___contains__(self: Ref, item: Int): Bool - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures result == (item in PIntSeq___val__(self)) - -function PIntSeq___getitem__(self: Ref, index: Ref): Int - decreases _ - requires issubtype(typeof(self), PIntSeq()) - requires issubtype(typeof(index), int()) - requires @error("Index may be out of bounds.")(let ln == (PIntSeq___len__(self)) in - @error("Index may be out of bounds.")((int___unbox__(index) < 0 ==> int___unbox__(index) >= -ln) && (int___unbox__(index) >= 0 ==> int___unbox__(index) < ln))) - ensures result == (int___unbox__(index) >= 0 ? PIntSeq___val__(self)[int___unbox__(index)] : PIntSeq___val__(self)[PIntSeq___len__(self) + int___unbox__(index)]) - -function PIntSeq_take(self: Ref, no: Int): Ref - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures result == PIntSeq___create__(PIntSeq___val__(self)[..no]) - -function PIntSeq_drop(self: Ref, no: Int): Ref - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures result == PIntSeq___create__(PIntSeq___val__(self)[no..]) - -function PIntSeq_range(self: Ref, start: Int, end: Int): Ref - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures result == PIntSeq___create__(PIntSeq___val__(self)[start..end]) - -function PIntSeq_update(self: Ref, index: Int, val: Int): Ref - decreases _ - requires issubtype(typeof(self), PIntSeq()) - requires index >= 0 && index < PIntSeq___len__(self) - ensures result == PIntSeq___create__(PIntSeq___val__(self)[index := val]) - -function PIntSeq___add__(self: Ref, other: Ref): Ref - decreases _ - requires issubtype(typeof(self), PIntSeq()) - requires issubtype(typeof(other), PIntSeq()) - ensures result == PIntSeq___create__(PIntSeq___val__(self) ++ PIntSeq___val__(other)) - -function PIntSeq___eq__(self: Ref, other: Ref): Bool - decreases _ - requires issubtype(typeof(self), PIntSeq()) - requires issubtype(typeof(other), PIntSeq()) - ensures result == (PIntSeq___val__(self) == PIntSeq___val__(other)) - ensures result ==> self == other // extensionality - ensures result == object___eq__(self, other) - - -function PIntSeq___sil_seq__(self: Ref): Seq[Ref] - decreases _ - requires issubtype(typeof(self), PIntSeq()) - ensures PIntSeq___val__(self) == __seq_ref_to_seq_int(result) - - -// Helper function to wrap generic __sil_seq__ calls for conversion to PIntSeq -function PIntSeq___seq_ref_to_seq_int__(sr: Seq[Ref]): Seq[Int] - decreases _ -{ - __seq_ref_to_seq_int(sr) -} \ No newline at end of file diff --git a/src/nagini_translation/resources/pytype.sil b/src/nagini_translation/resources/pytype.sil index f3572dca9..b912f0ac6 100644 --- a/src/nagini_translation/resources/pytype.sil +++ b/src/nagini_translation/resources/pytype.sil @@ -35,7 +35,7 @@ domain PyType { unique function bool(): PyType unique function bytes(): PyType unique function bytearray(): PyType - unique function PIntSeq(): PyType + unique function PByteSeq(): PyType unique function range_0(): PyType unique function slice(): PyType unique function str(): PyType diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 0ba2d71d7..fd1d2410e 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -887,30 +887,30 @@ "type_vars": 1, "extends": "object" }, -"PIntSeq": { +"PByteSeq": { "functions": { "__create__": { "args": ["__prim__Seq"], - "type": "PIntSeq", + "type": "PByteSeq", "requires": ["__val__"] }, "__unbox__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq", "requires": [] }, "__contains__": { - "args": ["PIntSeq", "__prim__int"], + "args": ["PByteSeq", "__prim__int"], "type": "__prim__bool", "requires": ["__val__"] }, "__getitem__": { - "args": ["PIntSeq", "int"], + "args": ["PByteSeq", "int"], "type": "__prim__int", "requires": ["__val__", "__len__", "int___unbox__"] }, "__sil_seq__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq", "requires": ["__val__", "__seq_ref_to_seq_int"] }, @@ -920,41 +920,41 @@ "requires": ["__seq_ref_to_seq_int"] }, "__val__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__Seq" }, "__len__": { - "args": ["PIntSeq"], + "args": ["PByteSeq"], "type": "__prim__int", "requires": ["__val__"] }, "take": { - "args": ["PIntSeq", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "drop": { - "args": ["PIntSeq", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "range": { - "args": ["PIntSeq", "__prim__int", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "update": { - "args": ["PIntSeq", "__prim__int", "__prim__int"], - "type": "PIntSeq", + "args": ["PByteSeq", "__prim__int", "__prim__int"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "__add__": { - "args": ["PIntSeq", "PIntSeq"], - "type": "PIntSeq", + "args": ["PByteSeq", "PByteSeq"], + "type": "PByteSeq", "requires": ["__val__", "__create__"] }, "__eq__": { - "args": ["PIntSeq", "PIntSeq"], + "args": ["PByteSeq", "PByteSeq"], "type": "__prim__bool", "requires": ["__val__"] } diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 663219460..60b5e52cb 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -26,7 +26,7 @@ PRIMITIVE_INT_TYPE, RANGE_TYPE, PSEQ_TYPE, - PINTSEQ_TYPE, + PBYTESEQ_TYPE, PSET_TYPE, SET_TYPE, SINGLE_NAME, @@ -653,9 +653,9 @@ def get_int_sequence(self, receiver: PythonType, arg: Expr, field = self.viper.Field('bytearray_acc', seq_int, position, info) res = self.viper.FieldAccess(arg, field, position, info) return res - if receiver.name == PINTSEQ_TYPE: + if receiver.name == PBYTESEQ_TYPE: if (isinstance(arg, self.viper.ast.FuncApp) and - arg.funcname() == 'PIntSeq___create__'): + arg.funcname() == 'PByteSeq___create__'): args = self.viper.to_list(arg.args()) return args[0] int_seq_op = getattr(receiver.cls, '__sil_int_seq__', None) @@ -664,10 +664,10 @@ def get_int_sequence(self, receiver: PythonType, arg: Expr, node, ctx, position) # Fallback to getting a Seq[Ref] and then converting to Seq[Int] - pintseq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] + PByteSeq_class = ctx.module.global_module.classes[PBYTESEQ_TYPE] seq_ref_exp = self.get_function_call(receiver, '__sil_seq__', [arg], [None], node, ctx, position) - return self.get_function_call(pintseq_class, '__seq_ref_to_seq_int__', [seq_ref_exp], [None], + return self.get_function_call(PByteSeq_class, '__seq_ref_to_seq_int__', [seq_ref_exp], [None], node, ctx, position) def _get_function_call(self, receiver: PythonType, diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index a841d914d..22ca2226d 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -22,7 +22,7 @@ PMSET_TYPE, PRIMITIVES, PSEQ_TYPE, - PINTSEQ_TYPE, + PBYTESEQ_TYPE, PSET_TYPE, RANGE_TYPE, BYTEARRAY_TYPE, @@ -730,9 +730,9 @@ def translate_to_int_sequence(self, node: ast.Call, stmt, arg = self.translate_expr(node.args[0], ctx) seq_call = self.get_int_sequence(coll_type, arg, node, ctx) - seq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] - if coll_type.name == BYTEARRAY_TYPE or coll_type.name == BYTES_TYPE: - call_name = '__create__bytes__' + seq_class = ctx.module.global_module.classes[PBYTESEQ_TYPE] + if coll_type.name == BYTEARRAY_TYPE: + call_name = '__from_bytes__' else: call_name = '__create__' result = self.get_function_call(seq_class, call_name, @@ -770,7 +770,7 @@ def translate_sequence(self, node: ast.Call, def translate_int_sequence(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: - intseq_class = ctx.module.global_module.classes[PINTSEQ_TYPE] + intseq_class = ctx.module.global_module.classes[PBYTESEQ_TYPE] viper_type = self.viper.Int val_stmts = [] if node.args: @@ -1181,7 +1181,7 @@ def translate_contractfunc_call(self, node: ast.Call, ctx: Context, return self.translate_let(node, ctx, impure) elif func_name == PSEQ_TYPE: return self.translate_sequence(node, ctx) - elif func_name == PINTSEQ_TYPE: + elif func_name == PBYTESEQ_TYPE: return self.translate_int_sequence(node, ctx) elif func_name == PSET_TYPE: return self.translate_pset(node, ctx) @@ -1189,7 +1189,7 @@ def translate_contractfunc_call(self, node: ast.Call, ctx: Context, return self.translate_mset(node, ctx) elif func_name == 'ToSeq': return self.translate_to_sequence(node, ctx) - elif func_name == 'ToIntSeq': + elif func_name == 'ToByteSeq': return self.translate_to_int_sequence(node, ctx) elif func_name == 'ToMS': return self.translate_to_multiset(node, ctx) diff --git a/tests/functional/verification/test_pintseq.py b/tests/functional/verification/test_pbyteseq.py similarity index 68% rename from tests/functional/verification/test_pintseq.py rename to tests/functional/verification/test_pbyteseq.py index 3731af742..030981d2b 100644 --- a/tests/functional/verification/test_pintseq.py +++ b/tests/functional/verification/test_pbyteseq.py @@ -5,9 +5,9 @@ def test_seq() -> None: - no_ints = PIntSeq() + no_ints = PByteSeq() assert len(no_ints) == 0 - ints = PIntSeq(1, 2, 3) + ints = PByteSeq(1, 2, 3) assert 3 in ints and 1 in ints assert 4 not in ints @@ -28,8 +28,22 @@ def test_seq() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert False +def test_byteseq_bounds_low(b: PByteSeq) -> None: + Requires(len(b) > 1) + + assert b[0] >= 0 + #:: ExpectedOutput(assert.failed:assertion.false) + assert b[1] < 0 + +def test_byteseq_bounds_high(b: PByteSeq) -> None: + Requires(len(b) > 1) + + assert b[0] <= 255 + #:: ExpectedOutput(assert.failed:assertion.false) + assert b[1] > 255 + def test_range() -> None: - ints = PIntSeq(1,3,5,6,8) + ints = PByteSeq(1,3,5,6,8) r = ints.range(1, 3) assert len(ints) == 5 @@ -42,23 +56,23 @@ def test_range() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert r[1] == 6 -def test_list_ToIntSeq() -> None: +def test_list_ToByteSeq() -> None: a = [1,2,3] - assert ToIntSeq(a) == PIntSeq(1,2,3) + assert ToByteSeq(a) == PByteSeq(1,2,3) #:: ExpectedOutput(assert.failed:assertion.false) assert False -def test_bytearray_ToIntSeq() -> None: +def test_bytearray_ToByteSeq() -> None: a = bytearray([1,2,3]) - assert ToIntSeq(a) == PIntSeq(1,2,3) + assert ToByteSeq(a) == PByteSeq(1,2,3) #:: ExpectedOutput(assert.failed:assertion.false) assert False def test_bytearray_bounds(b_array: bytearray) -> None: Requires(bytearray_pred(b_array)) Requires(len(b_array) > 2) - seq = ToIntSeq(b_array) + seq = ToByteSeq(b_array) assert 0 <= seq[0] and seq[0] <= 0xFF From 0cc53f0498e1a4674f06c346c09c1d18ad2fa0f4 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 14 Oct 2025 16:06:24 +0200 Subject: [PATCH 034/126] Use prim_int for bytearray setitem --- src/nagini_translation/resources/builtins.json | 2 +- src/nagini_translation/resources/bytearray.sil | 9 ++++----- src/nagini_translation/sif/resources/builtins.json | 9 +++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index a5641ca0c..ea80afb7c 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -687,7 +687,7 @@ "requires": ["__len__"] }, "__setitem__": { - "args": ["bytearray", "int", "int"], + "args": ["bytearray", "__prim__int", "int"], "type": null, "MustTerminate": true, "requires": ["__len__", "__getitem__", "int___byte_bounds__", "int___unbox__", "__prim__int___box__"] diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index e0cf154cf..37fed4193 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -98,16 +98,15 @@ method bytearray___getitem_slice__(self: Ref, key: Ref) returns (_res: Ref) ensures typeof(_res) == bytearray() ensures _res.bytearray_acc == self.bytearray_acc[slice___start__(key, bytearray___len__(self))..slice___stop__(key, bytearray___len__(self))] -method bytearray___setitem__(self: Ref, key: Ref, value: Ref) returns () +method bytearray___setitem__(self: Ref, key: Int, value: Ref) returns () requires issubtype(typeof(self), bytearray()) requires acc(self.bytearray_acc) - requires issubtype(typeof(key), int()) - requires @error("Bytearray index may be negative.")(int___unbox__(key) >= 0) - requires @error("Bytearray index may be out of bounds.")(int___unbox__(key) < bytearray___len__(self)) + requires @error("Bytearray index may be negative.")(key >= 0) + requires @error("Bytearray index may be out of bounds.")(key < bytearray___len__(self)) requires issubtype(typeof(value), int()) requires @error("Provided value may be out of bounds.")int___byte_bounds__(int___unbox__(value)) ensures acc(self.bytearray_acc) - ensures self.bytearray_acc == old(self.bytearray_acc)[int___unbox__(key) := int___unbox__(value)] + ensures self.bytearray_acc == old(self.bytearray_acc)[key := int___unbox__(value)] ensures (Low(key) && Low(value)) ==> (forall i: Ref :: {bytearray___getitem__(self, i)} ((issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < bytearray___len__(self) && Low(old(bytearray___getitem__(self, i)))) ==> Low(bytearray___getitem__(self, i)))) method bytearray_append(self: Ref, item: Ref) returns () diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index fd1d2410e..00146fe9e 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -890,6 +890,11 @@ "PByteSeq": { "functions": { "__create__": { + "args": ["__prim__Seq"], + "type": "PByteSeq", + "requires": ["__val__", "int___byte_bounds__"] + }, + "__from_bytes__": { "args": ["__prim__Seq"], "type": "PByteSeq", "requires": ["__val__"] @@ -907,7 +912,7 @@ "__getitem__": { "args": ["PByteSeq", "int"], "type": "__prim__int", - "requires": ["__val__", "__len__", "int___unbox__"] + "requires": ["__val__", "__len__", "int___unbox__", "int___byte_bounds__"] }, "__sil_seq__": { "args": ["PByteSeq"], @@ -946,7 +951,7 @@ "update": { "args": ["PByteSeq", "__prim__int", "__prim__int"], "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "requires": ["__val__", "__create__", "int___byte_bounds__"] }, "__add__": { "args": ["PByteSeq", "PByteSeq"], From f1d3f53444367964100391b2aaf43d3fd1f680bb Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 14 Oct 2025 16:06:48 +0200 Subject: [PATCH 035/126] Use multiplication for lshift of at most 8 bits --- src/nagini_translation/resources/bool.sil | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 9b4a77fbf..10d95c807 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -126,10 +126,24 @@ function int___lshift__(self: Ref, other: Ref): Ref requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) - ensures result == (let val == (int___unbox__(self)) in val >= 0 ? - __prim__int___box__(fromBVInt(shlBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : - __prim__int___box__(-fromBVInt(shlBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) +{ + __prim__int___box__( + let shift_amt == (int___unbox__(other)) in + let val == (int___unbox__(self)) in + shift_amt == 0 ? val : + shift_amt == 1 ? val * 2 : + shift_amt == 2 ? val * 4 : + shift_amt == 3 ? val * 8 : + shift_amt == 4 ? val * 16 : + shift_amt == 5 ? val * 32 : + shift_amt == 6 ? val * 64 : + shift_amt == 7 ? val * 128 : + shift_amt == 8 ? val * 256 : + val >= 0 ? fromBVInt(shlBVInt(toBVInt(val), toBVInt(shift_amt))) : + -fromBVInt(shlBVInt(toBVInt(-val), toBVInt(shift_amt))) ) +} + function int___rlshift__(self: Ref, other: Ref): Ref decreases _ From a36755e9d037637233979dd709bb12737086cee7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 15 Oct 2025 10:38:24 +0200 Subject: [PATCH 036/126] Allow preprocessing of comments --- src/nagini_translation/analyzer.py | 27 +++++++++++++++++++++++++++ src/nagini_translation/lib/config.py | 12 ++++++++++++ src/nagini_translation/main.py | 15 +++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index b339ff669..6dcad5208 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -8,6 +8,7 @@ import ast import logging import os +import re import nagini_contracts.io_builtins import nagini_contracts.lock import tokenize @@ -101,6 +102,9 @@ def __init__(self, types: TypeInfo, path: str, selected: Set[str]): self.deferred_tasks = [] self.has_all_low = False self.enable_obligations = False + # Set to enable preprocessing + self.enable_preprocessing = False + self.comment_pattern = "#@nagini" def initialize_io_analyzer(self) -> None: self.io_operation_analyzer = IOOperationAnalyzer( @@ -119,6 +123,24 @@ def node_factory(self): self._node_factory = ProgramNodeFactory() return self._node_factory + def preprocess_text(self, text: str, comment_prefix: str) -> str: + """ + Preprocesses the file text by transforming special comments into code. + Comments starting with the specified prefix will be converted to regular code. + """ + # Pattern: (whitespace)(comment_pattern)(optional space)(rest of line) + escaped_prefix = re.escape(comment_prefix) + pattern = re.compile(r'^(\s*)' + escaped_prefix + r' ?(.*)') + + def process_line(line: str) -> str: + match = pattern.match(line) + if match: + # Return indentation + code + return match.group(1) + match.group(2) + return line + + return '\n'.join(process_line(line) for line in text.split('\n')) + def define_new(self, container: Union[PythonModule, PythonClass], name: str, node: ast.AST) -> None: """ @@ -154,6 +176,11 @@ def collect_imports(self, abs_path: str) -> None: return with tokenize.open(abs_path) as file: text = file.read() + + # Preprocess the text to transform special comments into code + if self.enable_preprocessing: + text = self.preprocess_text(text, self.comment_pattern) + parse_result = ast.parse(text) try: mark_text_ranges(parse_result, text) diff --git a/src/nagini_translation/lib/config.py b/src/nagini_translation/lib/config.py index c9a4a8677..fd8ae273f 100644 --- a/src/nagini_translation/lib/config.py +++ b/src/nagini_translation/lib/config.py @@ -324,6 +324,16 @@ def set_verifier(v: str): Test configuration. """ +enable_preprocessing = False +""" +Enable Preprocessing of files. +""" + +comment_pattern = "" +""" +Comment pattern to preprocess. +""" + __all__ = ( 'classpath', @@ -333,4 +343,6 @@ def set_verifier(v: str): 'mypy_dir', 'obligation_config', 'set_verifier', + 'enable_preprocessing', + 'comment_pattern' ) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 59181a995..e1b9ec08b 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -132,6 +132,8 @@ def translate(path: str, jvm: JVM, bv_size: int, selected: Set[str] = set(), bas return None analyzer = Analyzer(types, path, selected) + analyzer.enable_preprocessing = config.enable_preprocessing + analyzer.comment_pattern = config.comment_pattern main_module = analyzer.module with open(os.path.join(builtins_index_path, 'builtins.json'), 'r') as file: analyzer.add_native_silver_builtins(json.loads(file.read())) @@ -356,6 +358,17 @@ def main() -> None: type=int, default=8 ) + parser.add_argument( + '--comment-pattern', + help='Preprocess comments with pattern', + type=str, + default="#@nagini" + ) + parser.add_argument( + '--preprocess', + action='store_true', + help='Enable preprocessing', + ) args = parser.parse_args() config.classpath = args.viper_jar_path @@ -363,6 +376,8 @@ def main() -> None: config.z3_path = args.z3 config.mypy_path = args.mypy_path config.set_verifier(args.verifier) + config.enable_preprocessing = args.preprocess + config.comment_pattern = args.comment_pattern if args.ignore_obligations: if args.force_obligations: parser.error('incompatible arguments: --ignore-obligations and --force-obligations') From 268ae52b2225f16550f430de630545b611d2f9e7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 17 Oct 2025 09:51:23 +0200 Subject: [PATCH 037/126] Add decreases clause to functions generated for constants --- src/nagini_translation/translators/program.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 089adcdef..4be98ec74 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -196,6 +196,7 @@ def create_global_var_function(self, var: PythonVar, body = None else: body = None + posts.append(self.viper.DecreasesWildcard(None, position, self.no_info(ctx))) return self.viper.Function(var.sil_name, [], type, [], posts, body, self.to_position(var.node, ctx), self.no_info(ctx)) From 9bdba4c4fa91ab5ec89cbf61539c3f476a59bfb7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 22 Oct 2025 12:01:24 +0200 Subject: [PATCH 038/126] Vscode task to debug verification --- .vscode/launch.json | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 37cdaf977..76ede1777 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -61,23 +61,19 @@ }, { "name": "Debug Nagini verifying Python", - "type": "python", + "type": "debugpy", "request": "launch", - "stopOnEntry": false, - "pythonPath": "${workspaceRoot}/nagini/env/bin/python3", - "program": "${workspaceRoot}/nagini/env/bin/nagini", + "program": "${workspaceFolder}/src/nagini_translation/main.py", "args": [ "${file}" ], - "cwd": "${workspaceRoot}", - "env": {}, - "envFile": "${workspaceRoot}/.env", + "cwd": "${workspaceFolder}", "console": "integratedTerminal", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "env": { + "PYTHONPATH": "${workspaceFolder}/src" + } }, { "name": "Translate Python to Viper", From 3200da5d25ec21c822daae11a30e87caf3eaf4d8 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 27 Oct 2025 09:32:15 +0100 Subject: [PATCH 039/126] Use any version of pytest --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cfafb8afd..be6cba45c 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ 'toposort==1.5', 'jpype1==1.2.1', 'astunparse==1.6.2', - 'pytest==4.3.0', + 'pytest', 'z3-solver==4.8.7.0', 'setuptools==68.2.0' ], From 9ce7e1c8d65075b72ffdbd92dd7efce1d4d79eec Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 27 Oct 2025 09:32:34 +0100 Subject: [PATCH 040/126] WIP bitwise operations --- src/nagini_contracts/contracts.py | 14 ++++++++ src/nagini_translation/resources/bool.sil | 6 ++++ .../resources/builtins.json | 16 ++++++++- src/nagini_translation/resources/byteseq.sil | 36 ++++++++++++++++++- src/nagini_translation/resources/intbv.sil | 3 ++ .../verification/test_bitwise_op.py | 5 +++ 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 9505542c5..74ad29c5e 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -332,6 +332,20 @@ def __iter__(self) -> Iterator[int]: PByteSeqs can be quantified over; this is only here so thatPByteSeqs can be used as arguments for Forall. """ + + @staticmethod + def int_set_bit(value: int, position: int, bit: bool) -> int: + """ + Helper method to set a specific bit of an integer. + Position starts at least significant bit + """ + + @staticmethod + def int_get_bit(value: int, position: int) -> bool: + """ + Helper method to get a specific bit of an integer. + Position starts at least significant bit + """ def Previous(it: T) -> PSeq[T]: """ diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 10d95c807..e92616847 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -191,6 +191,12 @@ function int___bool__(self: Ref) : Bool ensures self == null ==> !result ensures self != null ==> result == (int___unbox__(self) != 0) +function int_bit_length(self: Ref): Ref + decreases _ + requires self != null ==> issubtype(typeof(self), int()) + ensures typeof(result) == int() + // TODO add postcondition for value + function __prim__int___box__(prim: Int): Ref decreases _ ensures typeof(result) == int() diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index ea80afb7c..356d44cfc 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -350,7 +350,11 @@ "type": "int", "requires": ["__rshift__"] }, - "__byte__bounds__": { + "bit_length": { + "args": ["int"], + "type": "int" + }, + "__byte_bounds__": { "args": ["__prim__int"], "type": "__prim__bool" } @@ -992,6 +996,16 @@ "args": ["PByteSeq", "PByteSeq"], "type": "__prim__bool", "requires": ["__val__"] + }, + "int_set_bit": { + "args": ["int", "int", "__prim__bool"], + "type": "int", + "requires": ["__prim__int___box__", "int___unbox__"] + }, + "int_get_bit": { + "args": ["int", "int"], + "type": "__prim__bool", + "requires": ["__prim__int___box__", "int___unbox__"] } }, "extends": "object" diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index 4e4924173..b86245c59 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -89,4 +89,38 @@ function PByteSeq___seq_ref_to_seq_int__(sr: Seq[Ref]): Seq[Int] decreases _ { __seq_ref_to_seq_int(sr) -} \ No newline at end of file +} + +// Position starts from least significant bit +function PByteSeq_int_set_bit(value: Ref, position: Ref, bit: Bool): Ref + decreases _ + requires issubtype(typeof(value), int()) + requires issubtype(typeof(position), int()) + requires @error("set_bit is only supported for positive values")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) <= _INT_MAX) + requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) + ensures issubtype(typeof(result), int()) +{ + let val == (toBVInt(int___unbox__(value))) in + let mask == (shlBVInt(toBVInt(1), toBVInt(int___unbox__(position)))) in + __prim__int___box__(fromBVInt( bit ? + orBVInt(val, mask) : + andBVInt(val, notBVInt(mask)) + )) +} + +// Position starts from least significant bit +// function PByteSeq_int_get_bit(self: Ref, position: Ref): Bool +// decreases _ +// requires issubtype(typeof(value), int()) +// requires issubtype(typeof(position), int()) +// requires @error("set_bit is only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) +// requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) +// requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) +// requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) +// { +// let val == (toBVInt(int___unbox__(self))) in +// let pos == (int___unbox__(position)) in +// extractBVInt(pos, pos, val) == toBVInt(1) +// } \ No newline at end of file diff --git a/src/nagini_translation/resources/intbv.sil b/src/nagini_translation/resources/intbv.sil index cf7f27bff..e0554c44c 100644 --- a/src/nagini_translation/resources/intbv.sil +++ b/src/nagini_translation/resources/intbv.sil @@ -7,6 +7,7 @@ // File template: NBITS is to be replaced by the number of bits, INT_MAX_VAL and INT_MIN_VAL by the actual values. +define _BITOPS_SIZE (NBITS) define _INT_MAX (INT_MAX_VAL) define _INT_MIN (INT_MIN_VAL) @@ -18,4 +19,6 @@ domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function xorBVInt(___intbv, ___intbv): ___intbv interpretation "bvxor" function shlBVInt(___intbv, ___intbv): ___intbv interpretation "bvshl" function shrBVInt(___intbv, ___intbv): ___intbv interpretation "bvlshr" + function notBVInt(___intbv): ___intbv interpretation "bvnot" + function extractBVInt(i: Int, j: Int, ___intbv): ___intbv interpretation "Extract" } \ No newline at end of file diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 9c3027672..ad0beb453 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -172,7 +172,12 @@ def xor_4(a: int, b: bool, c: int) -> None: Requires(c >= -128 and c < 257) #:: ExpectedOutput(application.precondition:assertion.false) intint = a ^ c + +def set_bit() -> None: + a = PByteSeq.update(PByteSeq(0), 7, True) + assert a == 128 + def lshift_1(b: int) -> None: Requires(b >=0 and b <= 127) From 5022a111d9a95243464fda794f308ea26bedceb8 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 27 Oct 2025 14:23:15 +0100 Subject: [PATCH 041/126] Implement PByteSeq_int_get_bit with direct handling on bit vectors --- src/nagini_translation/resources/byteseq.sil | 34 ++++++++------ src/nagini_translation/resources/intbv.sil | 15 +++++- .../verification/test_bitwise_op.py | 4 -- tests/functional/verification/test_byte_op.py | 46 +++++++++++++++++++ 4 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 tests/functional/verification/test_byte_op.py diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index b86245c59..f2d4bc6f2 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -111,16 +111,24 @@ function PByteSeq_int_set_bit(value: Ref, position: Ref, bit: Bool): Ref } // Position starts from least significant bit -// function PByteSeq_int_get_bit(self: Ref, position: Ref): Bool -// decreases _ -// requires issubtype(typeof(value), int()) -// requires issubtype(typeof(position), int()) -// requires @error("set_bit is only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) -// requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) -// requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) -// requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) -// { -// let val == (toBVInt(int___unbox__(self))) in -// let pos == (int___unbox__(position)) in -// extractBVInt(pos, pos, val) == toBVInt(1) -// } \ No newline at end of file +function PByteSeq_int_get_bit(value: Ref, position: Ref): Bool + decreases _ + requires issubtype(typeof(value), int()) + requires issubtype(typeof(position), int()) + requires @error("set_bit is only supported for positive values")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) <= _INT_MAX) + requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) +{ + let val == (toBVInt(int___unbox__(value))) in + let pos == (int___unbox__(position)) in ( + pos == 0 ? extractBVInt_0_0(val) : + pos == 1 ? extractBVInt_1_1(val) : + pos == 2 ? extractBVInt_2_2(val) : + pos == 3 ? extractBVInt_3_3(val) : + pos == 4 ? extractBVInt_4_4(val) : + pos == 5 ? extractBVInt_5_5(val) : + pos == 6 ? extractBVInt_6_6(val) : + extractBVInt_7_7(val)) == toBVInt1(1) + +} \ No newline at end of file diff --git a/src/nagini_translation/resources/intbv.sil b/src/nagini_translation/resources/intbv.sil index e0554c44c..7e3c1b753 100644 --- a/src/nagini_translation/resources/intbv.sil +++ b/src/nagini_translation/resources/intbv.sil @@ -11,6 +11,10 @@ define _BITOPS_SIZE (NBITS) define _INT_MAX (INT_MAX_VAL) define _INT_MIN (INT_MIN_VAL) +domain ___intbv1 interpretation (SMTLIB: "(_ BitVec 1)", Boogie: "bv1") { + function toBVInt1(i: Int): ___intbv1 interpretation "(_ int2bv 1)" +} + domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function toBVInt(i: Int): ___intbv interpretation "(_ int2bv NBITS)" function fromBVInt(___intbv): Int interpretation "bv2int" @@ -20,5 +24,14 @@ domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function shlBVInt(___intbv, ___intbv): ___intbv interpretation "bvshl" function shrBVInt(___intbv, ___intbv): ___intbv interpretation "bvlshr" function notBVInt(___intbv): ___intbv interpretation "bvnot" - function extractBVInt(i: Int, j: Int, ___intbv): ___intbv interpretation "Extract" + + // Current implementation of `interpretation` does not allow for the required syntax + function extractBVInt_0_0(___intbv): ___intbv1 interpretation "(_ extract 0 0)" + function extractBVInt_1_1(___intbv): ___intbv1 interpretation "(_ extract 1 1)" + function extractBVInt_2_2(___intbv): ___intbv1 interpretation "(_ extract 2 2)" + function extractBVInt_3_3(___intbv): ___intbv1 interpretation "(_ extract 3 3)" + function extractBVInt_4_4(___intbv): ___intbv1 interpretation "(_ extract 4 4)" + function extractBVInt_5_5(___intbv): ___intbv1 interpretation "(_ extract 5 5)" + function extractBVInt_6_6(___intbv): ___intbv1 interpretation "(_ extract 6 6)" + function extractBVInt_7_7(___intbv): ___intbv1 interpretation "(_ extract 7 7)" } \ No newline at end of file diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index ad0beb453..2ef386ed4 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -172,10 +172,6 @@ def xor_4(a: int, b: bool, c: int) -> None: Requires(c >= -128 and c < 257) #:: ExpectedOutput(application.precondition:assertion.false) intint = a ^ c - -def set_bit() -> None: - a = PByteSeq.update(PByteSeq(0), 7, True) - assert a == 128 def lshift_1(b: int) -> None: diff --git a/tests/functional/verification/test_byte_op.py b/tests/functional/verification/test_byte_op.py new file mode 100644 index 000000000..daa56e67f --- /dev/null +++ b/tests/functional/verification/test_byte_op.py @@ -0,0 +1,46 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + +def set_bit() -> None: + a = PByteSeq.int_set_bit(0, 7, True) + assert a == 128 + + a = PByteSeq.int_set_bit(a, 6, True) + assert a == 192 + + a = PByteSeq.int_set_bit(a, 7, False) + assert a == 64 + + a = PByteSeq.int_set_bit(a, 0, True) + #:: ExpectedOutput(assert.failed:assertion.false) + assert a == 64 + +def get_bit1() -> None: + a = 255 + assert PByteSeq.int_get_bit(a, 0) + assert PByteSeq.int_get_bit(a, 1) + assert PByteSeq.int_get_bit(a, 2) + assert PByteSeq.int_get_bit(a, 3) + assert PByteSeq.int_get_bit(a, 4) + assert PByteSeq.int_get_bit(a, 5) + assert PByteSeq.int_get_bit(a, 6) + assert PByteSeq.int_get_bit(a, 7) + +def get_bit2(pos: int) -> None: + Requires(0 <= pos and pos < 8) + a = 15 + + if pos < 4: + assert PByteSeq.int_get_bit(a, pos) + else: + #:: ExpectedOutput(assert.failed:assertion.false) + assert PByteSeq.int_get_bit(a, 4) + +def set_get_bit(val: int, pos: int, bit: bool) -> None: + Requires(0 <= val and val <= 255) + Requires(0 <= pos and pos < 8) + + mod_val = PByteSeq.int_set_bit(val, pos, bit) + assert PByteSeq.int_get_bit(mod_val, pos) == bit \ No newline at end of file From d4d205fc4c172a8e6888cc3ec95f5d963a44ba93 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 28 Oct 2025 15:08:40 +0100 Subject: [PATCH 042/126] Simplify bit and byte operations --- src/nagini_translation/resources/bool.sil | 31 ++++++++---- src/nagini_translation/resources/byteseq.sil | 13 +---- src/nagini_translation/resources/intbv.sil | 14 ------ .../verification/test_bitwise_op.py | 47 +++++-------------- 4 files changed, 36 insertions(+), 69 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index e92616847..f2ee7aba8 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -161,23 +161,36 @@ function int___rshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - // requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + // requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) - // ensures result == (let val == (int___unbox__(self)) in val >= 0 ? - // __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other))))) : - // __prim__int___box__(-fromBVInt(shrBVInt(toBVInt(-val), toBVInt(int___unbox__(other))))) // TODO This case is wrong - // ) - ensures result == (let val == (int___unbox__(self)) in __prim__int___box__(fromBVInt(shrBVInt(toBVInt(val), toBVInt(int___unbox__(other)))))) +{ + __prim__int___box__( + let shift_amt == (int___unbox__(other)) in + let val == (int___unbox__(self)) in + shift_amt == 0 ? val : + shift_amt == 1 ? val / 2 : + shift_amt == 2 ? val / 4 : + shift_amt == 3 ? val / 8 : + shift_amt == 4 ? val / 16 : + shift_amt == 5 ? val / 32 : + shift_amt == 6 ? val / 64 : + shift_amt == 7 ? val / 128 : + shift_amt == 8 ? val / 256 : + val >= 0 ? fromBVInt(shrBVInt(toBVInt(val), toBVInt(shift_amt))) : + -fromBVInt(shrBVInt(toBVInt(-val), toBVInt(shift_amt))) + ) +} + function int___rrshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - // requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) + // requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index f2d4bc6f2..e77ec190c 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -120,15 +120,6 @@ function PByteSeq_int_get_bit(value: Ref, position: Ref): Bool requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) { - let val == (toBVInt(int___unbox__(value))) in - let pos == (int___unbox__(position)) in ( - pos == 0 ? extractBVInt_0_0(val) : - pos == 1 ? extractBVInt_1_1(val) : - pos == 2 ? extractBVInt_2_2(val) : - pos == 3 ? extractBVInt_3_3(val) : - pos == 4 ? extractBVInt_4_4(val) : - pos == 5 ? extractBVInt_5_5(val) : - pos == 6 ? extractBVInt_6_6(val) : - extractBVInt_7_7(val)) == toBVInt1(1) - + let mask == (shlBVInt(toBVInt(1), toBVInt(int___unbox__(position)))) in + andBVInt(toBVInt(int___unbox__(value)), mask) == mask } \ No newline at end of file diff --git a/src/nagini_translation/resources/intbv.sil b/src/nagini_translation/resources/intbv.sil index 7e3c1b753..bcf626bbf 100644 --- a/src/nagini_translation/resources/intbv.sil +++ b/src/nagini_translation/resources/intbv.sil @@ -11,10 +11,6 @@ define _BITOPS_SIZE (NBITS) define _INT_MAX (INT_MAX_VAL) define _INT_MIN (INT_MIN_VAL) -domain ___intbv1 interpretation (SMTLIB: "(_ BitVec 1)", Boogie: "bv1") { - function toBVInt1(i: Int): ___intbv1 interpretation "(_ int2bv 1)" -} - domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function toBVInt(i: Int): ___intbv interpretation "(_ int2bv NBITS)" function fromBVInt(___intbv): Int interpretation "bv2int" @@ -24,14 +20,4 @@ domain ___intbv interpretation (SMTLIB: "(_ BitVec NBITS)", Boogie: "bvNBITS") { function shlBVInt(___intbv, ___intbv): ___intbv interpretation "bvshl" function shrBVInt(___intbv, ___intbv): ___intbv interpretation "bvlshr" function notBVInt(___intbv): ___intbv interpretation "bvnot" - - // Current implementation of `interpretation` does not allow for the required syntax - function extractBVInt_0_0(___intbv): ___intbv1 interpretation "(_ extract 0 0)" - function extractBVInt_1_1(___intbv): ___intbv1 interpretation "(_ extract 1 1)" - function extractBVInt_2_2(___intbv): ___intbv1 interpretation "(_ extract 2 2)" - function extractBVInt_3_3(___intbv): ___intbv1 interpretation "(_ extract 3 3)" - function extractBVInt_4_4(___intbv): ___intbv1 interpretation "(_ extract 4 4)" - function extractBVInt_5_5(___intbv): ___intbv1 interpretation "(_ extract 5 5)" - function extractBVInt_6_6(___intbv): ___intbv1 interpretation "(_ extract 6 6)" - function extractBVInt_7_7(___intbv): ___intbv1 interpretation "(_ extract 7 7)" } \ No newline at end of file diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 2ef386ed4..e40a630ca 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -172,16 +172,16 @@ def xor_4(a: int, b: bool, c: int) -> None: Requires(c >= -128 and c < 257) #:: ExpectedOutput(application.precondition:assertion.false) intint = a ^ c - - -def lshift_1(b: int) -> None: + +def lshift_general(a: int, b: int) -> None: + Requires(a > -128 and a <= 127) Requires(b >=0 and b <= 127) - a = 1 shift = a << b + # Unfortunately we cannot prove the equivalence shift == a * (2**b) if b == 0: - assert shift == a * 1 + assert shift == a if b == 1: assert shift == a * 2 if b == 2: @@ -196,34 +196,23 @@ def lshift_1(b: int) -> None: assert shift == a * 64 if b == 7: assert shift == a * 128 - -def lshift_general(a: int, b: int) -> None: - Requires(a > -100 and a < 100) - Requires(b >=0 and b <= 127) - - shift = a << b - - # Unfortunately we cannot prove the equivalence shift == a * (2**b) - if b == 0: - assert shift == a - if b == 1: - assert shift == a * 2 def lshift_neg(a: int, b: int) -> None: - Requires(a > -100 and a < 100) + Requires(a > -128 and a <= 127) Requires(b >= -128 and b <= 127) #:: ExpectedOutput(application.precondition:assertion.false) shift = a << b - -def rshift_1(b: int) -> None: + +def rshift_general(a: int, b: int) -> None: + Requires(a >= -128 and a <= 127) Requires(b >=0 and b <= 127) - a = 127 shift = a >> b + # Unfortunately we cannot prove the equivalence shift == a // (2 ** b) if b == 0: - assert shift == a // 1 + assert shift == a if b == 1: assert shift == a // 2 if b == 2: @@ -237,16 +226,4 @@ def rshift_1(b: int) -> None: if b == 6: assert shift == a // 64 if b == 7: - assert shift == a // 128 - -def rshift_general(a: int, b: int) -> None: - Requires(a >= 0 and a < 100) - Requires(b >=0 and b <= 127) - - shift = a >> b - - # Unfortunately we cannot prove the equivalence shift == a // (2 ** b) - if b == 0: - assert shift == a - if b == 1: - assert shift == a // 2 \ No newline at end of file + assert shift == a // 128 \ No newline at end of file From f70176d5c639b6467d52d91d6b95b8a074a13028 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 11 Nov 2025 11:24:52 +0100 Subject: [PATCH 043/126] Providing a simple (not sound yet) way to mark parameters as primitives --- src/nagini_contracts/contracts.py | 6 ++ src/nagini_translation/analyzer.py | 8 +++ .../verification/test_opaque_function.py | 63 +++++++++++++++++++ .../verification/test_primitives.py | 44 +++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 tests/functional/verification/test_opaque_function.py create mode 100644 tests/functional/verification/test_primitives.py diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 942931f93..0a28c0246 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -200,6 +200,10 @@ def TerminatesSif(cond: bool, rank: int) -> bool: """ pass +PBool = bool + +PInt = int + class PSeq(Generic[T], Sized, Iterable[T]): """ A PSeq[T] represents a pure sequence of instances of subtypes of T, and @@ -590,6 +594,8 @@ def isNaN(f: float) -> bool: 'list_pred', 'dict_pred', 'set_pred', + 'PBool', + 'PInt', 'PSeq', 'PSet', 'PMultiset', diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index b339ff669..67dad93e5 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -877,6 +877,14 @@ def visit_Lambda(self, node: ast.Lambda) -> None: def visit_arg(self, node: ast.arg) -> None: assert self.current_function is not None node_type = self.typeof(node) + if isinstance(node.annotation, ast.Name) and node.annotation.id in ('PInt', 'PBool'): + if node.annotation.id == 'PInt': + assert node_type.name == 'int' + node_type = node_type.module.classes['__prim__int'] + elif node.annotation.id == 'PBool': + assert node_type.name == 'bool' + node_type = node_type.module.classes['__prim__bool'] + self.current_function.args[node.arg] = \ self.node_factory.create_python_var(node.arg, node, node_type) # If we just introduced new type variables, create the expression that diff --git a/tests/functional/verification/test_opaque_function.py b/tests/functional/verification/test_opaque_function.py new file mode 100644 index 000000000..9f4f8655b --- /dev/null +++ b/tests/functional/verification/test_opaque_function.py @@ -0,0 +1,63 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + + +@Pure +@Opaque +def plusFour(i: int) -> int: + Ensures(Result() > i) + return i + 4 + +def client1() -> None: + a = plusFour(2) + Assert(a > 2) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a > 3) + +def client2() -> None: + a = Reveal(plusFour(2)) + Assert(a > 2) + Assert(a > 3) + Assert(a == 6) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a > 7) + +@Pure +def plusFiveA(i: int) -> int: + Ensures(Result() > i + 1) + return plusFour(i) + 1 + +@Pure +def plusFiveB(i: int) -> int: + #:: ExpectedOutput(postcondition.violated:assertion.false) + Ensures(Result() == i + 5) + return plusFour(i) + 1 + +@Pure +def plusFiveC(i: int) -> int: + Ensures(Result() == i + 5) + return Reveal(plusFour(i)) + 1 + + +class MyClass: + @Pure + @Opaque + def plusFour(self, i: int) -> int: + Ensures(Result() > i) + return i + 4 + + def client1(self) -> None: + a = self.plusFour(2) + Assert(a > 2) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a > 3) + + def client2(self) -> None: + a = Reveal(self.plusFour(2)) + Assert(a > 2) + Assert(a > 3) + Assert(a == 6) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(a > 7) \ No newline at end of file diff --git a/tests/functional/verification/test_primitives.py b/tests/functional/verification/test_primitives.py new file mode 100644 index 000000000..ab0defb27 --- /dev/null +++ b/tests/functional/verification/test_primitives.py @@ -0,0 +1,44 @@ +from nagini_contracts.contracts import * + +@Opaque +@Pure +def positive1(i1: int) -> bool: + return i1 > 0 + +@Opaque +@Pure +def positive2(i1: PInt) -> bool: + return i1 > 0 + +def client1(i1: int, i2: int) -> None: + if i1 == i2: + if positive1(i1): + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(positive1(i2)) + +def client2(i1: int, i2: int) -> None: + if i1 == i2: + if positive2(i1): + Assert(positive2(i2)) + + +@Opaque +@Pure +def true1(i1: bool) -> bool: + return i1 + +@Opaque +@Pure +def true2(i1: PBool) -> bool: + return i1 + + +def bclient1(i1: bool, i2: bool) -> None: + if i1 == i2: + if true1(i1): + Assert(true1(i2)) + +def bclient2(i1: bool, i2: bool) -> None: + if i1 == i2: + if true2(i1): + Assert(true2(i2)) \ No newline at end of file From 7155b224923b50f9cced7b565f94ea39cf317e5e Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 11 Nov 2025 11:39:13 +0100 Subject: [PATCH 044/126] Remove range for now --- src/nagini_contracts/contracts.py | 13 ------------- src/nagini_translation/resources/builtins.json | 11 ----------- src/nagini_translation/resources/byteseq.sil | 5 ----- src/nagini_translation/resources/seq.sil | 5 ----- src/nagini_translation/sif/resources/all.sil | 2 +- 5 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 74ad29c5e..e3648cfc8 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -247,13 +247,6 @@ def drop(self, until: int) -> 'PSeq[T]': ``PSeq(2,3,5,6).drop(2)`` is equal to ``PSeq(5,6)``. """ - def range(self, start: int, end: int) -> 'PSeq[T]': - """ - Returns a new PSeq of the same type containg all elements - in the range [start, end[\n - (i.e. ``PSeq(2,3,5,6).range(1,3)`` is equal to ``PSeq(3,5)`` ) - """ - def update(self, index: int, new_val: T) -> 'PSeq[T]': """ Returns a new sequence of the same type, containing the same elements @@ -313,12 +306,6 @@ def drop(self, until: int) -> 'PByteSeq': from the given index (i.e., drops all elements until that index). ``PByteSeq(2,3,5,6).drop(2)`` is equal to ``PByteSeq(5,6)``. """ - - def range(self, start: int, end: int) -> 'PByteSeq': - """ - Returns a new PByteSeq containg all elements in the range [start, end[\n - (i.e. ``PByteSeq(2,3,5,6).range(1,3)`` is equal to ``PByteSeq(3,5)`` ) - """ def update(self, index: int, new_val: int) -> 'PByteSeq': """ diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 356d44cfc..38a9c496d 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -896,12 +896,6 @@ "generic_type": -2, "requires": ["__sil_seq__", "__create__"] }, - "range": { - "args": ["PSeq", "__prim__int", "__prim__int"], - "type": "PSeq", - "generic_type": -2, - "requires": ["__sil_seq__", "__create__"] - }, "update": { "args": ["PSeq", "__prim__int", "object"], "type": "PSeq", @@ -977,11 +971,6 @@ "type": "PByteSeq", "requires": ["__val__", "__create__"] }, - "range": { - "args": ["PByteSeq", "__prim__int", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] - }, "update": { "args": ["PByteSeq", "__prim__int", "__prim__int"], "type": "PByteSeq", diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index e77ec190c..b24156a1d 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -51,11 +51,6 @@ function PByteSeq_drop(self: Ref, no: Int): Ref requires issubtype(typeof(self), PByteSeq()) ensures result == PByteSeq___create__(PByteSeq___val__(self)[no..]) -function PByteSeq_range(self: Ref, start: Int, end: Int): Ref - decreases _ - requires issubtype(typeof(self), PByteSeq()) - ensures result == PByteSeq___create__(PByteSeq___val__(self)[start..end]) - function PByteSeq_update(self: Ref, index: Int, val: Int): Ref decreases _ requires issubtype(typeof(self), PByteSeq()) diff --git a/src/nagini_translation/resources/seq.sil b/src/nagini_translation/resources/seq.sil index d1f399570..38e1fd88d 100644 --- a/src/nagini_translation/resources/seq.sil +++ b/src/nagini_translation/resources/seq.sil @@ -44,11 +44,6 @@ function PSeq_drop(self: Ref, no: Int): Ref requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) ensures result == PSeq___create__(PSeq___sil_seq__(self)[no..], PSeq_arg(typeof(self), 0)) -function PSeq_range(self: Ref, start: Int, end: Int): Ref - decreases _ - requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) - ensures result == PSeq___create__(PSeq___sil_seq__(self)[start..end], PSeq_arg(typeof(self), 0)) - function PSeq_update(self: Ref, index: Int, val: Ref): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) diff --git a/src/nagini_translation/sif/resources/all.sil b/src/nagini_translation/sif/resources/all.sil index d625d57e4..3bd46e69c 100644 --- a/src/nagini_translation/sif/resources/all.sil +++ b/src/nagini_translation/sif/resources/all.sil @@ -26,7 +26,7 @@ import "../../resources/measures.sil" import "../../resources/pytype.sil" import "../../resources/range.sil" import "../../resources/seq.sil" -import "../../resources/intseq.sil" +import "../../resources/byteseq.sil" import "../../resources/pset.sil" import "../../resources/set_dict.sil" import "../../resources/slice.sil" From 3f39df767315325b8827b9833a2050f5022aea44 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 11 Nov 2025 23:32:11 +0100 Subject: [PATCH 045/126] Fix requirements for PByteSeq --- src/nagini_translation/resources/builtins.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 38a9c496d..7a890efab 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -954,7 +954,8 @@ }, "__val__": { "args": ["PByteSeq"], - "type": "__prim__Seq" + "type": "__prim__Seq", + "requires": ["int___byte_bounds__"] }, "__len__": { "args": ["PByteSeq"], From 5cc4cbfc3adfdec820ffeede05f016ffa2043f40 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 12 Nov 2025 11:01:59 +0100 Subject: [PATCH 046/126] Allow unrestricted lshift and rshift for shift values <= 8 --- src/nagini_translation/resources/bool.sil | 18 ++++----- .../verification/test_bitwise_op.py | 40 ++++++++++++++++++- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index f2ee7aba8..0f8acb1dd 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -122,8 +122,8 @@ function int___lshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -149,8 +149,8 @@ function int___rlshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -161,9 +161,8 @@ function int___rshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - // requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -189,9 +188,8 @@ function int___rrshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= _INT_MIN) - // requires @error("Right shift is currently only supported for positive values")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index e40a630ca..31ab67908 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -177,6 +177,20 @@ def lshift_general(a: int, b: int) -> None: Requires(a > -128 and a <= 127) Requires(b >=0 and b <= 127) + shift = a << b + + if b <= 8: + lshift_unlimited(a, b) + + if b == 9: + assert shift == a * 512 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert shift == 1 + +def lshift_unlimited(a: int, b: int) -> None: + Requires(b >= 0 and b <= 8) + shift = a << b # Unfortunately we cannot prove the equivalence shift == a * (2**b) @@ -196,6 +210,11 @@ def lshift_general(a: int, b: int) -> None: assert shift == a * 64 if b == 7: assert shift == a * 128 + if b == 8: + assert shift == a * 256 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert shift == 1 def lshift_neg(a: int, b: int) -> None: Requires(a > -128 and a <= 127) @@ -210,6 +229,20 @@ def rshift_general(a: int, b: int) -> None: shift = a >> b + if b <= 8: + rshift_unlimited(a, b) + + if b == 9: + assert shift == a // 512 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert shift == 1 + +def rshift_unlimited(a: int, b: int) -> None: + Requires(b >= 0 and b <= 8) + + shift = a >> b + # Unfortunately we cannot prove the equivalence shift == a // (2 ** b) if b == 0: assert shift == a @@ -226,4 +259,9 @@ def rshift_general(a: int, b: int) -> None: if b == 6: assert shift == a // 64 if b == 7: - assert shift == a // 128 \ No newline at end of file + assert shift == a // 128 + if b == 8: + assert shift == a // 256 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert shift == 1 From de18898753e5e5df2471d20c4f9409d7abe3c3ba Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 26 Nov 2025 13:07:31 +0100 Subject: [PATCH 047/126] Remove unused byte operations --- src/nagini_contracts/contracts.py | 14 ------ .../resources/builtins.json | 10 ---- src/nagini_translation/resources/byteseq.sil | 33 ------------- tests/functional/verification/test_byte_op.py | 46 ------------------- 4 files changed, 103 deletions(-) delete mode 100644 tests/functional/verification/test_byte_op.py diff --git a/src/nagini_contracts/contracts.py b/src/nagini_contracts/contracts.py index 431c3d2d8..a83ba6bab 100644 --- a/src/nagini_contracts/contracts.py +++ b/src/nagini_contracts/contracts.py @@ -323,20 +323,6 @@ def __iter__(self) -> Iterator[int]: PByteSeqs can be quantified over; this is only here so thatPByteSeqs can be used as arguments for Forall. """ - - @staticmethod - def int_set_bit(value: int, position: int, bit: bool) -> int: - """ - Helper method to set a specific bit of an integer. - Position starts at least significant bit - """ - - @staticmethod - def int_get_bit(value: int, position: int) -> bool: - """ - Helper method to get a specific bit of an integer. - Position starts at least significant bit - """ def Previous(it: T) -> PSeq[T]: """ diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 7a890efab..969f71bcd 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -986,16 +986,6 @@ "args": ["PByteSeq", "PByteSeq"], "type": "__prim__bool", "requires": ["__val__"] - }, - "int_set_bit": { - "args": ["int", "int", "__prim__bool"], - "type": "int", - "requires": ["__prim__int___box__", "int___unbox__"] - }, - "int_get_bit": { - "args": ["int", "int"], - "type": "__prim__bool", - "requires": ["__prim__int___box__", "int___unbox__"] } }, "extends": "object" diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index b24156a1d..e0aaf4eb0 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -84,37 +84,4 @@ function PByteSeq___seq_ref_to_seq_int__(sr: Seq[Ref]): Seq[Int] decreases _ { __seq_ref_to_seq_int(sr) -} - -// Position starts from least significant bit -function PByteSeq_int_set_bit(value: Ref, position: Ref, bit: Bool): Ref - decreases _ - requires issubtype(typeof(value), int()) - requires issubtype(typeof(position), int()) - requires @error("set_bit is only supported for positive values")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) <= _INT_MAX) - requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) - ensures issubtype(typeof(result), int()) -{ - let val == (toBVInt(int___unbox__(value))) in - let mask == (shlBVInt(toBVInt(1), toBVInt(int___unbox__(position)))) in - __prim__int___box__(fromBVInt( bit ? - orBVInt(val, mask) : - andBVInt(val, notBVInt(mask)) - )) -} - -// Position starts from least significant bit -function PByteSeq_int_get_bit(value: Ref, position: Ref): Bool - decreases _ - requires issubtype(typeof(value), int()) - requires issubtype(typeof(position), int()) - requires @error("set_bit is only supported for positive values")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(value), bool()) ==> int___unbox__(value) <= _INT_MAX) - requires @error("Negative position.")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) >= 0) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(position), bool()) ==> int___unbox__(position) < _BITOPS_SIZE) -{ - let mask == (shlBVInt(toBVInt(1), toBVInt(int___unbox__(position)))) in - andBVInt(toBVInt(int___unbox__(value)), mask) == mask } \ No newline at end of file diff --git a/tests/functional/verification/test_byte_op.py b/tests/functional/verification/test_byte_op.py deleted file mode 100644 index daa56e67f..000000000 --- a/tests/functional/verification/test_byte_op.py +++ /dev/null @@ -1,46 +0,0 @@ -# Any copyright is dedicated to the Public Domain. -# http://creativecommons.org/publicdomain/zero/1.0/ - -from nagini_contracts.contracts import * - -def set_bit() -> None: - a = PByteSeq.int_set_bit(0, 7, True) - assert a == 128 - - a = PByteSeq.int_set_bit(a, 6, True) - assert a == 192 - - a = PByteSeq.int_set_bit(a, 7, False) - assert a == 64 - - a = PByteSeq.int_set_bit(a, 0, True) - #:: ExpectedOutput(assert.failed:assertion.false) - assert a == 64 - -def get_bit1() -> None: - a = 255 - assert PByteSeq.int_get_bit(a, 0) - assert PByteSeq.int_get_bit(a, 1) - assert PByteSeq.int_get_bit(a, 2) - assert PByteSeq.int_get_bit(a, 3) - assert PByteSeq.int_get_bit(a, 4) - assert PByteSeq.int_get_bit(a, 5) - assert PByteSeq.int_get_bit(a, 6) - assert PByteSeq.int_get_bit(a, 7) - -def get_bit2(pos: int) -> None: - Requires(0 <= pos and pos < 8) - a = 15 - - if pos < 4: - assert PByteSeq.int_get_bit(a, pos) - else: - #:: ExpectedOutput(assert.failed:assertion.false) - assert PByteSeq.int_get_bit(a, 4) - -def set_get_bit(val: int, pos: int, bit: bool) -> None: - Requires(0 <= val and val <= 255) - Requires(0 <= pos and pos < 8) - - mod_val = PByteSeq.int_set_bit(val, pos, bit) - assert PByteSeq.int_get_bit(mod_val, pos) == bit \ No newline at end of file From e830d8e0d6c29e35888e37572810be473c7dc0ee Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 27 Nov 2025 15:15:48 +0100 Subject: [PATCH 048/126] Apply preprocessing before mypy akin to analyzer --- src/nagini_translation/analyzer.py | 36 ++------------------------ src/nagini_translation/lib/typeinfo.py | 8 +++--- src/nagini_translation/lib/util.py | 29 +++++++++++++++++++++ src/nagini_translation/main.py | 4 ++- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 61dfde507..b9826f1bc 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -8,10 +8,8 @@ import ast import logging import os -import re import nagini_contracts.io_builtins import nagini_contracts.lock -import tokenize from collections import OrderedDict from nagini_contracts.contracts import CONTRACT_FUNCS, CONTRACT_WRAPPER_FUNCS @@ -63,6 +61,7 @@ get_parent_of_type, InvalidProgramException, is_io_existential, + read_source_file, UnsupportedException, ) from nagini_translation.lib.views import PythonModuleView @@ -123,24 +122,6 @@ def node_factory(self): self._node_factory = ProgramNodeFactory() return self._node_factory - def preprocess_text(self, text: str, comment_prefix: str) -> str: - """ - Preprocesses the file text by transforming special comments into code. - Comments starting with the specified prefix will be converted to regular code. - """ - # Pattern: (whitespace)(comment_pattern)(optional space)(rest of line) - escaped_prefix = re.escape(comment_prefix) - pattern = re.compile(r'^(\s*)' + escaped_prefix + r' ?(.*)') - - def process_line(line: str) -> str: - match = pattern.match(line) - if match: - # Return indentation + code - return match.group(1) + match.group(2) - return line - - return '\n'.join(process_line(line) for line in text.split('\n')) - def define_new(self, container: Union[PythonModule, PythonClass], name: str, node: ast.AST) -> None: """ @@ -174,13 +155,8 @@ def collect_imports(self, abs_path: str) -> None: # This is a module that corresponds to a directory, so it has no # contents of its own. return - with tokenize.open(abs_path) as file: - text = file.read() - - # Preprocess the text to transform special comments into code - if self.enable_preprocessing: - text = self.preprocess_text(text, self.comment_pattern) + text = read_source_file(abs_path) parse_result = ast.parse(text) try: mark_text_ranges(parse_result, text) @@ -904,14 +880,6 @@ def visit_Lambda(self, node: ast.Lambda) -> None: def visit_arg(self, node: ast.arg) -> None: assert self.current_function is not None node_type = self.typeof(node) - if isinstance(node.annotation, ast.Name) and node.annotation.id in ('PInt', 'PBool'): - if node.annotation.id == 'PInt': - assert node_type.name == 'int' - node_type = node_type.module.classes['__prim__int'] - elif node.annotation.id == 'PBool': - assert node_type.name == 'bool' - node_type = node_type.module.classes['__prim__bool'] - self.current_function.args[node.arg] = \ self.node_factory.create_python_var(node.arg, node, node_type) # If we just introduced new type variables, create the expression that diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 129c47abe..408c37571 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -296,10 +296,10 @@ def _create_options(self, strict_optional: bool): # enable it like this return result - def check(self, filename: str, base_dir: str = None) -> bool: + def check(self, filename: str, base_dir: str = None, text: Optional[str] = None) -> bool: """ Typechecks the given file and collects all type information needed for - the translation to Viper + the translation to Viper. Optionally pass preprocessed text content. """ def report_errors(errors: List[str]) -> None: @@ -320,7 +320,7 @@ def report_errors(errors: List[str]) -> None: try: options_strict = self._create_options(True) res_strict = mypy.build.build( - [BuildSource(filename, module_name, None, base_dir=base_dir)], + [BuildSource(filename, module_name, text, base_dir=base_dir)], options_strict ) @@ -329,7 +329,7 @@ def report_errors(errors: List[str]) -> None: # s.t. we don't get overapproximated none-related errors. options_non_strict = self._create_options(False) res_non_strict = mypy.build.build( - [BuildSource(filename, module_name, None, base_dir=base_dir)], + [BuildSource(filename, module_name, text, base_dir=base_dir)], options_non_strict ) if res_non_strict.errors: diff --git a/src/nagini_translation/lib/util.py b/src/nagini_translation/lib/util.py index 92cb4a12d..165b2960a 100644 --- a/src/nagini_translation/lib/util.py +++ b/src/nagini_translation/lib/util.py @@ -7,6 +7,9 @@ import ast import astunparse +import re +import tokenize +from nagini_translation.lib import config from typing import ( Any, @@ -23,6 +26,32 @@ V = TypeVar('V') +def preprocess_text(text: str, comment_prefix: str) -> str: + """ + Preprocesses the file text by transforming special comments into code. + Comments starting with the specified prefix will be converted to regular code. + """ + # Pattern: (whitespace)(comment_prefix)(optional space)(rest of line) + escaped_prefix = re.escape(comment_prefix) + pattern = re.compile(r'^(\s*)' + escaped_prefix + r' ?(.*)') + + def process_line(line: str) -> str: + match = pattern.match(line) + if match: + # Return indentation + code + return match.group(1) + match.group(2) + return line + + return '\n'.join(process_line(line) for line in text.split('\n')) + +def read_source_file(path: str) -> str: + with tokenize.open(path) as file: + text = file.read() + + if config.enable_preprocessing: + text = preprocess_text(text, config.comment_pattern) + return text + def flatten(lists: List[List[T]]) -> List[T]: """ Flattens a list of lists into a flat list diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index e1b9ec08b..2aa9056c7 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -21,6 +21,7 @@ from nagini_translation.analyzer import Analyzer from nagini_translation.sif_translator import SIFTranslator from nagini_translation.lib import config +from nagini_translation.lib.util import read_source_file from nagini_translation.lib.constants import DEFAULT_SERVER_SOCKET from nagini_translation.lib.errors import error_manager from nagini_translation.lib.jvmaccess import ( @@ -126,8 +127,9 @@ def translate(path: str, jvm: JVM, bv_size: int, selected: Set[str] = set(), bas raise Exception('Viper not found on classpath.') if sif and not viper_ast.is_extension_available(): raise Exception('Viper AST SIF extension not found on classpath.') + types = TypeInfo() - type_correct = types.check(path, base_dir) + type_correct = types.check(path, base_dir, text=read_source_file(path)) if not type_correct: return None From 143196824405c50dc0e57a8946dc7eea414ee29a Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 28 Nov 2025 12:08:17 +0100 Subject: [PATCH 049/126] Reapply primitive type usage for arguments --- src/nagini_translation/analyzer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index dbafacd19..22dedb164 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -877,6 +877,14 @@ def visit_Lambda(self, node: ast.Lambda) -> None: def visit_arg(self, node: ast.arg) -> None: assert self.current_function is not None node_type = self.typeof(node) + if isinstance(node.annotation, ast.Name) and node.annotation.id in ('PInt', 'PBool'): + if node.annotation.id == 'PInt': + assert node_type.name == 'int' + node_type = node_type.module.classes['__prim__int'] + elif node.annotation.id == 'PBool': + assert node_type.name == 'bool' + node_type = node_type.module.classes['__prim__bool'] + self.current_function.args[node.arg] = \ self.node_factory.create_python_var(node.arg, node, node_type) # If we just introduced new type variables, create the expression that From 5a98926e0ce91d9b51ca134379c6d4feb1f790bf Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 28 Nov 2025 13:27:24 +0100 Subject: [PATCH 050/126] Fix merge issues --- .../resources/builtins.json | 140 +++++++++++++----- .../resources/bytearray.sil | 4 + .../sif/resources/builtins.json | 82 ++++------ 3 files changed, 137 insertions(+), 89 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 3b5507022..2d62a9129 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -305,23 +305,19 @@ }, "__lshift__": { "args": ["int", "int"], - "type": "int", - "requires": ["__prim__int___box__", "int___unbox__", "__prim__bool___box__", "bool___unbox__"] + "type": "int" }, "__rlshift__": { "args": ["int", "int"], - "type": "int", - "requires": ["__lshift__"] + "type": "int" }, "__rshift__": { "args": ["int", "int"], - "type": "int", - "requires": ["__prim__int___box__", "int___unbox__", "__prim__bool___box__", "bool___unbox__"] + "type": "int" }, "__rrshift__": { "args": ["int", "int"], - "type": "int", - "requires": ["__rshift__"] + "type": "int" }, "bit_length": { "args": ["int"], @@ -584,6 +580,92 @@ }, "extends": "object" }, +"bytearray": { + "methods": { + "__init__": { + "args": [], + "type": null, + "MustTerminate": true + }, + "__initFromInt__": { + "args": ["int"], + "type": null, + "MustTerminate": true + }, + "__initFromList__": { + "args": ["list"], + "type": null, + "MustTerminate": true + }, + "__initFromBytearray__": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true + }, + "append": { + "args": ["bytearray", "int"], + "type": null, + "MustTerminate": true + }, + "extend": { + "args": ["bytearray", "bytearray"], + "type": null, + "MustTerminate": true + }, + "reverse": { + "args": ["bytearray"], + "type": null, + "MustTerminate": true + }, + "__setitem__": { + "args": ["bytearray", "__prim__int", "int"], + "type": null, + "MustTerminate": true + }, + "__iter__": { + "args": ["bytearray"], + "type": "Iterator", + "MustTerminate": true + }, + "__getitem_slice__": { + "args": ["bytearray", "slice"], + "type": "bytearray", + "display_name": "__getitem__", + "MustTerminate": true + } + }, + "functions": { + "hex": { + "args": ["bytearray"], + "type": "str" + }, + "__len__": { + "args": ["bytearray"], + "type": "__prim__int" + }, + "__getitem__": { + "args": ["bytearray", "int"], + "type": "__prim__int" + }, + "__contains__": { + "args": ["bytearray", "int"], + "type": "__prim__bool" + }, + "__bool__": { + "args": ["bytearray"], + "type": "__prim__bool" + }, + "__eq__": { + "args": ["bytearray", "object"], + "type": "__prim__bool" + }, + "__sil_seq__": { + "args": ["bytearray"], + "type": "__prim__Seq" + } + }, + "extends": "object" +}, "tuple": { "functions": { "__create0__": { @@ -734,73 +816,59 @@ "functions": { "__create__": { "args": ["__prim__Seq"], - "type": "PByteSeq", - "requires": ["__val__", "int___byte_bounds__"] + "type": "PByteSeq" }, "__from_bytes__": { "args": ["__prim__Seq"], - "type": "PByteSeq", - "requires": ["__val__"] + "type": "PByteSeq" }, "__unbox__": { "args": ["PByteSeq"], - "type": "__prim__Seq", - "requires": [] + "type": "__prim__Seq" }, "__contains__": { "args": ["PByteSeq", "__prim__int"], - "type": "__prim__bool", - "requires": ["__val__"] + "type": "__prim__bool" }, "__getitem__": { "args": ["PByteSeq", "int"], - "type": "__prim__int", - "requires": ["__val__", "__len__", "int___unbox__", "int___byte_bounds__"] + "type": "__prim__int" }, "__sil_seq__": { "args": ["PByteSeq"], - "type": "__prim__Seq", - "requires": ["__val__", "__seq_ref_to_seq_int"] + "type": "__prim__Seq" }, "__seq_ref_to_seq_int__": { "args": ["__prim__Seq"], - "type": "__prim__Seq", - "requires": ["__seq_ref_to_seq_int"] + "type": "__prim__Seq" }, "__val__": { "args": ["PByteSeq"], - "type": "__prim__Seq", - "requires": ["int___byte_bounds__"] + "type": "__prim__Seq" }, "__len__": { "args": ["PByteSeq"], - "type": "__prim__int", - "requires": ["__val__"] + "type": "__prim__int" }, "take": { "args": ["PByteSeq", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "drop": { "args": ["PByteSeq", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "update": { "args": ["PByteSeq", "__prim__int", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__", "int___byte_bounds__"] + "type": "PByteSeq" }, "__add__": { "args": ["PByteSeq", "PByteSeq"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "__eq__": { "args": ["PByteSeq", "PByteSeq"], - "type": "__prim__bool", - "requires": ["__val__"] + "type": "__prim__bool" } }, "extends": "object" diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 37fed4193..2a764fc43 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -41,6 +41,10 @@ method bytearray___initFromList__(values: Ref) returns (res: Ref) ensures res.bytearray_acc == __seq_ref_to_seq_int(values.list_acc) ensures Low(values) ==> Low(res) +function bytearray_hex(self: Ref): Ref + requires issubtype(typeof(self), bytearray()) + ensures issubtype(typeof(res), str()) + function bytearray___len__(self: Ref) : Int decreases _ requires issubtype(typeof(self), bytearray()) diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 8aff4c919..3f11cf6cf 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -562,14 +562,12 @@ "__initFromInt__": { "args": ["int"], "type": null, - "MustTerminate": true, - "requires": ["int___unbox__"] + "MustTerminate": true }, "__initFromList__": { "args": ["list"], "type": null, - "MustTerminate": true, - "requires": ["list", "list___getitem__", "list___len__", "bytearray___bounds_helper__", "int___unbox__", "__seq_ref_to_seq_int"] + "MustTerminate": true }, "__initFromBytearray__": { "args": ["bytearray"], @@ -579,8 +577,7 @@ "append": { "args": ["bytearray", "int"], "type": null, - "MustTerminate": true, - "requires": ["int___unbox__"] + "MustTerminate": true }, "extend": { "args": ["bytearray", "bytearray"], @@ -590,56 +587,53 @@ "reverse": { "args": ["bytearray"], "type": null, - "MustTerminate": true, - "requires": ["__len__"] + "MustTerminate": true }, "__setitem__": { - "args": ["bytearray", "int", "int"], + "args": ["bytearray", "__prim__int", "int"], "type": null, - "MustTerminate": true, - "requires": ["__len__", "__getitem__", "bytearray___bounds_helper__", "int___unbox__", "__prim__int___box__"] + "MustTerminate": true }, "__iter__": { "args": ["bytearray"], "type": "Iterator", - "MustTerminate": true, - "requires": ["__sil_seq__"] + "MustTerminate": true }, "__getitem_slice__": { "args": ["bytearray", "slice"], "type": "bytearray", "display_name": "__getitem__", - "requires": ["__len__", "slice___start__", "slice___stop__"], "MustTerminate": true } }, "functions": { + "hex": { + "args": ["bytearray"], + "type": "str" + }, "__len__": { "args": ["bytearray"], "type": "__prim__int" }, "__getitem__": { "args": ["bytearray", "int"], - "type": "__prim__int", - "requires": ["__len__", "bytearray___bounds_helper__", "int___unbox__"] + "type": "__prim__int" }, "__contains__": { "args": ["bytearray", "int"], - "type": "__prim__bool", - "requires": ["int___unbox__"] + "type": "__prim__bool" }, "__bool__": { "args": ["bytearray"], "type": "__prim__bool" }, - "__bounds_helper__": { - "args": ["__prim__int"], + "__eq__": { + "args": ["bytearray", "object"], "type": "__prim__bool" }, "__sil_seq__": { "args": ["bytearray"], - "type": "__prim__Seq", - "requires": ["bytearray___bounds_helper__", "__len__", "__prim__int___box__", "int___unbox__", "__prim__int___box__"] + "type": "__prim__Seq" } }, "extends": "object" @@ -800,38 +794,31 @@ "functions": { "__create__": { "args": ["__prim__Seq"], - "type": "PByteSeq", - "requires": ["__val__", "int___byte_bounds__"] + "type": "PByteSeq" }, "__from_bytes__": { "args": ["__prim__Seq"], - "type": "PByteSeq", - "requires": ["__val__"] + "type": "PByteSeq" }, "__unbox__": { "args": ["PByteSeq"], - "type": "__prim__Seq", - "requires": [] + "type": "__prim__Seq" }, "__contains__": { "args": ["PByteSeq", "__prim__int"], - "type": "__prim__bool", - "requires": ["__val__"] + "type": "__prim__bool" }, "__getitem__": { "args": ["PByteSeq", "int"], - "type": "__prim__int", - "requires": ["__val__", "__len__", "int___unbox__", "int___byte_bounds__"] + "type": "__prim__int" }, "__sil_seq__": { "args": ["PByteSeq"], - "type": "__prim__Seq", - "requires": ["__val__", "__seq_ref_to_seq_int"] + "type": "__prim__Seq" }, "__seq_ref_to_seq_int__": { "args": ["__prim__Seq"], - "type": "__prim__Seq", - "requires": ["__seq_ref_to_seq_int"] + "type": "__prim__Seq" }, "__val__": { "args": ["PByteSeq"], @@ -839,38 +826,27 @@ }, "__len__": { "args": ["PByteSeq"], - "type": "__prim__int", - "requires": ["__val__"] + "type": "__prim__int" }, "take": { "args": ["PByteSeq", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "drop": { "args": ["PByteSeq", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] - }, - "range": { - "args": ["PByteSeq", "__prim__int", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "update": { "args": ["PByteSeq", "__prim__int", "__prim__int"], - "type": "PByteSeq", - "requires": ["__val__", "__create__", "int___byte_bounds__"] + "type": "PByteSeq" }, "__add__": { "args": ["PByteSeq", "PByteSeq"], - "type": "PByteSeq", - "requires": ["__val__", "__create__"] + "type": "PByteSeq" }, "__eq__": { "args": ["PByteSeq", "PByteSeq"], - "type": "__prim__bool", - "requires": ["__val__"] + "type": "__prim__bool" } }, "extends": "object" From 997648b5128ec079346fe366f24b163f042d8773 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 28 Nov 2025 15:19:48 +0100 Subject: [PATCH 051/126] Fix bytearray hex and adjust test --- src/nagini_translation/resources/bytearray.sil | 4 +++- tests/functional/verification/test_bytearray.py | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/resources/bytearray.sil b/src/nagini_translation/resources/bytearray.sil index 2a764fc43..98ed1aec4 100644 --- a/src/nagini_translation/resources/bytearray.sil +++ b/src/nagini_translation/resources/bytearray.sil @@ -42,8 +42,10 @@ method bytearray___initFromList__(values: Ref) returns (res: Ref) ensures Low(values) ==> Low(res) function bytearray_hex(self: Ref): Ref + decreases _ requires issubtype(typeof(self), bytearray()) - ensures issubtype(typeof(res), str()) + requires acc(self.bytearray_acc, wildcard) + ensures typeof(result) == str() function bytearray___len__(self: Ref) : Int decreases _ diff --git a/tests/functional/verification/test_bytearray.py b/tests/functional/verification/test_bytearray.py index ace6a4917..e79fac036 100644 --- a/tests/functional/verification/test_bytearray.py +++ b/tests/functional/verification/test_bytearray.py @@ -198,4 +198,12 @@ def test_bytearray_iter_bounds(b: bytearray) -> None: Requires(bytearray_pred(b)) for byte in b: - assert 0 <= byte and byte < 256 \ No newline at end of file + assert 0 <= byte and byte < 256 + +def test_bytearray_hex(b: bytearray) -> None: + Requires(bytearray_pred(b)) + + value = b.hex() + + #:: ExpectedOutput(assert.failed:assertion.false) + assert value == "" \ No newline at end of file From 5f7f5edd45f29c23f7bda0ac6c8e889f76416b3d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 2 Dec 2025 13:55:08 +0100 Subject: [PATCH 052/126] Add primitive int version for shift --- src/nagini_translation/resources/bool.sil | 92 +++++++++++++------ .../resources/builtins.json | 16 ++++ 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 4a55a7d26..984fa06b8 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -118,6 +118,43 @@ function int___rxor__(self: Ref, other: Ref): Ref int___xor__(self, other) } +function __shift_factor(amount: Int): Int + decreases _ + requires 0 <= amount && amount <= 8 +{ + amount == 0 ? 1 : + amount == 1 ? 2 : + amount == 2 ? 4 : + amount == 3 ? 8 : + amount == 4 ? 16 : + amount == 5 ? 32 : + amount == 6 ? 64 : + amount == 7 ? 128 : + 256 +} + +function __prim__int___lshift__(self: Int, other: Int): Int + decreases _ + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Negative shift count.")(other >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) +{ + 0 <= other <= 8 ? self * __shift_factor(other) : + self >= 0 ? fromBVInt(shlBVInt(toBVInt(self), toBVInt(other))) : + -fromBVInt(shlBVInt(toBVInt(-self), toBVInt(other))) +} + +function __prim__int___rlshift__(self: Int, other: Int): Int + decreases _ + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Negative shift count.")(other >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) +{ + __prim__int___lshift__(self, other) +} + function int___lshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) @@ -127,21 +164,7 @@ function int___lshift__(self: Ref, other: Ref): Ref requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { - __prim__int___box__( - let shift_amt == (int___unbox__(other)) in - let val == (int___unbox__(self)) in - shift_amt == 0 ? val : - shift_amt == 1 ? val * 2 : - shift_amt == 2 ? val * 4 : - shift_amt == 3 ? val * 8 : - shift_amt == 4 ? val * 16 : - shift_amt == 5 ? val * 32 : - shift_amt == 6 ? val * 64 : - shift_amt == 7 ? val * 128 : - shift_amt == 8 ? val * 256 : - val >= 0 ? fromBVInt(shlBVInt(toBVInt(val), toBVInt(shift_amt))) : - -fromBVInt(shlBVInt(toBVInt(-val), toBVInt(shift_amt))) - ) + __prim__int___box__(__prim__int___lshift__(int___unbox__(self), int___unbox__(other))) } @@ -157,6 +180,29 @@ function int___rlshift__(self: Ref, other: Ref): Ref int___lshift__(self, other) } +function __prim__int___rshift__(self: Int, other: Int): Int + decreases _ + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Negative shift count.")(other >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) +{ + + 0 <= other <= 8 ? self / __shift_factor(other) : + self >= 0 ? fromBVInt(shrBVInt(toBVInt(self), toBVInt(other))) : + -fromBVInt(shrBVInt(toBVInt(-self), toBVInt(other))) +} + +function __prim__int___rrshift__(self: Int, other: Int): Int + decreases _ + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Negative shift count.")(other >= 0) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) +{ + __prim__int___rshift__(self, other) +} + function int___rshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) @@ -166,21 +212,7 @@ function int___rshift__(self: Ref, other: Ref): Ref requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { - __prim__int___box__( - let shift_amt == (int___unbox__(other)) in - let val == (int___unbox__(self)) in - shift_amt == 0 ? val : - shift_amt == 1 ? val / 2 : - shift_amt == 2 ? val / 4 : - shift_amt == 3 ? val / 8 : - shift_amt == 4 ? val / 16 : - shift_amt == 5 ? val / 32 : - shift_amt == 6 ? val / 64 : - shift_amt == 7 ? val / 128 : - shift_amt == 8 ? val / 256 : - val >= 0 ? fromBVInt(shrBVInt(toBVInt(val), toBVInt(shift_amt))) : - -fromBVInt(shrBVInt(toBVInt(-val), toBVInt(shift_amt))) - ) + __prim__int___box__(__prim__int___rshift__(int___unbox__(self), int___unbox__(other))) } diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index c7c559006..f97e84791 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -747,6 +747,22 @@ "__box__": { "args": ["__prim__int"], "type": "int" + }, + "__lshift__": { + "args": ["__prim__int", "__prim__int"], + "type": "__prim__int" + }, + "__rlshift__": { + "args": ["__prim__int", "__prim__int"], + "type": "__prim__int" + }, + "__rshift__": { + "args": ["__prim__int", "__prim__int"], + "type": "__prim__int" + }, + "__rrshift__": { + "args": ["__prim__int", "__prim__int"], + "type": "__prim__int" } } }, From f41d77418409a4e8b7a765c2610c7d6dfee6e5a3 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 3 Dec 2025 15:00:54 +0100 Subject: [PATCH 053/126] Support basic frozen dataclasses List access not yet working --- src/nagini_translation/analyzer.py | 156 ++++++++++++++++-- src/nagini_translation/lib/constants.py | 1 + src/nagini_translation/lib/program_nodes.py | 2 + src/nagini_translation/main.py | 9 +- src/nagini_translation/translators/method.py | 18 +- src/nagini_translation/translators/program.py | 2 +- .../functional/translation/test_dataclass.py | 15 ++ .../functional/verification/test_dataclass.py | 79 +++++++++ 8 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 tests/functional/translation/test_dataclass.py create mode 100644 tests/functional/verification/test_dataclass.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index da0475b1e..a0b4dbd06 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -579,10 +579,63 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: if cls.python_class not in cls.superclass.python_class.direct_subclasses: cls.superclass.python_class.direct_subclasses.append(cls.python_class) - for member in node.body: + if self.is_dataclass(node): + cls.dataclass = True + if self.is_frozen_dataclass(node): + cls.frozen = True + + if cls.dataclass and not cls.frozen: + raise UnsupportedException(node, 'Non frozen dataclass currently not supported') + + for member in node.body.copy(): self.visit(member, node) + if cls.dataclass and "__init__" not in cls.methods.keys(): + self._add_dataclass_init_method(node) + self.current_class = None + def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: + """Adds the implicit __init__ method for dataclasses""" + assert self.current_class != None + + args: list[ast.arg] = [] + stmts: list[ast.stmt] = [] + + # Parse fields, add implicit args and post conditions + args.append(self._create_arg_ast(node, 'self', None)) + for name, field in self.current_class.fields.items(): + args.append(self._create_arg_ast(node, name, field.type.name)) + stmts.append(self._create_eq_postcondition(node, name, name)) + + ast_arguments = ast.arguments([], args, None, [], [], None, []) + + # Could add implicit field assignments for non-frozen dataclass + + # Add decorators + decorator_list: list[ast.expr] = [self._create_name_ast('ContractOnly', node)] + + function_def = ast.FunctionDef('__init__', ast_arguments, stmts, decorator_list, returns=None, lineno=node.lineno, col_offset=0) + self.visit(function_def, node) + node.body.append(function_def) + return + + def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> ast.arg: + name_node = None + if type_name != None: + name_node = self._create_name_ast(type_name, node) + return ast.arg(arg, name_node, lineno=node.lineno, col_offset=0) + + def _create_eq_postcondition(self, node, attribute: str, arg: str) -> ast.stmt: + compare = ast.Compare( + ast.Attribute(self._create_name_ast('self', node), attribute, ast.Load(), lineno=node.lineno, col_offset=0), + ops=[ast.Eq()], + comparators=[self._create_name_ast(arg, node)], + lineno=node.lineno, col_offset=0) + return ast.Expr(ast.Call(self._create_name_ast('Ensures', node), [compare], [], lineno=node.lineno, col_offset=0)) + + def _create_name_ast(self, id: str, node) -> ast.Name: + return ast.Name(id, ast.Load(), lineno=node.lineno, col_offset=0) + def _is_illegal_magic_method_name(self, name: str) -> bool: """ Anything that could potentially be a magic method, i.e. anything that @@ -1128,6 +1181,33 @@ def todo(): if isinstance(var, PythonGlobalVar): self.track_access(node, var) self.deferred_tasks.append(todo) + return + elif self.current_class.dataclass and self.current_class.frozen: + # Node is a field of a frozen dataclass + if isinstance(node.ctx, ast.Load): + return + + # Add type info for self in this context, can retrieve the correct type from __init__.self + context = tuple([self.module.type_prefix, self.current_class.name, node.id, 'self']) + self_type, _ = self.module.get_type([self.current_class.name, '__init__'], 'self') + self.module.types.all_types[context] = self_type + + # Create a property for this field + ast_arguments = ast.arguments([], [self._create_arg_ast(node, 'self', None)], None, [], [], None, []) + stmts = [ast.Expr(ast.Call(self._create_name_ast('Decreases', node), [ast.Constant(None)], []))] + decorator_list: list[ast.expr] = [ast.Name('property'), ast.Name('ContractOnly')] + function_def = ast.FunctionDef(node.id, ast_arguments, stmts, decorator_list, returns=node._parent.annotation, lineno=node.lineno, col_offset=0) + self.visit(function_def, self.current_class.node) + + # Adjust the class body + assign = node._parent + self.current_class.node.body.remove(assign) + self.current_class.node.body.append(function_def) + + if(assign.value != None): + raise UnsupportedException(assign, 'Default value for dataclass fields not supported') + # func.result = assign.value # Temporarily set value, because it will be used as default + return else: # Node is a static field. @@ -1308,7 +1388,7 @@ def convert_type(self, mypy_type, node) -> PythonType: msg = f'Type could not be fully inferred (this usually means that a type argument is unknown)' raise InvalidProgramException(node, 'partial.type', message=msg) else: - msg = 'Unsupported type: {}'.format(mypy_type.__class__.__name__) + msg = 'Unsupported type: {} for node {}'.format(mypy_type.__class__.__name__, node.id) raise UnsupportedException(node, desc=msg) return result @@ -1472,6 +1552,7 @@ def typeof(self, node: ast.AST) -> PythonType: return method.type else: raise UnsupportedException(node) + def _get_basic_name(self, node: Union[ast.Name, ast.Attribute]) -> str: """ @@ -1545,13 +1626,21 @@ def visit_Try(self, node: ast.Try) -> None: self.stmt_container.labels.append(finally_name) self.visit_default(node) - def _incompatible_decorators(self, decorators) -> bool: + def _class_incompatible_decorators(self, decorators: set[str]) -> bool: + return ((('dataclass' in decorators) and (len(decorators) != 1)) or + (('dataclass' not in decorators) and (len(decorators) > 0)) + ) + + def _class_unsupported_decorator_keywords(self, decorator: str, keyword: str) -> bool: + return ((('dataclass' == decorator) and (keyword != 'frozen'))) + + def _function_incompatible_decorators(self, decorators) -> bool: return ((('Predicate' in decorators) and ('Pure' in decorators)) or (('Opaque' in decorators) and ('Pure' not in decorators)) or (('Predicate' in decorators) and ('Inline' in decorators)) or (('Inline' in decorators) and ('Pure' in decorators)) or (('IOOperation' in decorators) and (len(decorators) != 1)) or - (('property' in decorators) and (len(decorators) != 1)) or + (('property' in decorators) and not(len(decorators) == 1 or (len(decorators) == 2 and 'ContractOnly' in decorators))) or (('AllLow' in decorators) and ('PreservesLow' in decorators)) or ((('AllLow' in decorators) or ('PreservesLow' in decorators)) and ( ('Predicate' in decorators) or ('Pure' in decorators))) @@ -1563,7 +1652,7 @@ def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: respective decorator. """ decorators = {d.id for d in func.decorator_list if isinstance(d, ast.Name)} - if self._incompatible_decorators(decorators): + if self._function_incompatible_decorators(decorators): raise InvalidProgramException(func, "decorators.incompatible") result = 'ContractOnly' in decorators return result @@ -1590,35 +1679,68 @@ def is_contract_only(self, func: ast.FunctionDef) -> bool: result = result or (not selected) return result - def has_decorator(self, func: ast.FunctionDef, decorator: str) -> bool: + def __resolve_decorator(self, decorator: ast.expr) -> Tuple[bool, str]: + if isinstance(decorator, ast.Name): + return (True, decorator.id) + elif isinstance(decorator, ast.Call): + return self.__resolve_decorator(decorator.func) + return (False, "") + + def __get_decorators(self, decorator_list: list[ast.expr]) -> set[str]: + return {res[1] for d in decorator_list if (res := self.__resolve_decorator(d))[0]} + + def __decorator_has_keyword_value(self, decorator_list: list[ast.expr], decorator: str, keyword: str, value) -> bool: + for d in decorator_list: + if isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == decorator: + for k in d.keywords: + if self._class_unsupported_decorator_keywords(decorator, k.arg): + raise UnsupportedException(d, "keyword unsupported") + + if k.arg == keyword and isinstance(k.value, ast.Constant): + return k.value.value == value + return False + + def class_has_decorator(self, cls: ast.ClassDef, decorator: str) -> bool: + decorators = self.__get_decorators(cls.decorator_list) + if self._class_incompatible_decorators(decorators): + raise InvalidProgramException(cls, "decorators.incompatible") + return decorator in decorators + + def is_dataclass(self, cls: ast.ClassDef) -> bool: + return self.class_has_decorator(cls, 'dataclass') + + def is_frozen_dataclass(self, cls: ast.ClassDef) -> bool: + return self.__decorator_has_keyword_value(cls.decorator_list, 'dataclass', 'frozen', True) + + def function_has_decorator(self, func: ast.FunctionDef, decorator: str) -> bool: decorators = {d.id for d in func.decorator_list if isinstance(d, ast.Name)} - if self._incompatible_decorators(decorators): + if self._function_incompatible_decorators(decorators): raise InvalidProgramException(func, "decorators.incompatible") return decorator in decorators def is_pure(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'Pure') + return self.function_has_decorator(func, 'Pure') def is_opaque(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'Opaque') + return self.function_has_decorator(func, 'Opaque') def is_predicate(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'Predicate') + return self.function_has_decorator(func, 'Predicate') def is_inline_method(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'Inline') + return self.function_has_decorator(func, 'Inline') def is_static_method(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'staticmethod') + return self.function_has_decorator(func, 'staticmethod') def is_class_method(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'classmethod') + return self.function_has_decorator(func, 'classmethod') def is_io_operation(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'IOOperation') + return self.function_has_decorator(func, 'IOOperation') def is_property_getter(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'property') + return self.function_has_decorator(func, 'property') def is_property_setter(self, func: ast.FunctionDef) -> bool: setter_decorator = [d for d in func.decorator_list @@ -1630,7 +1752,7 @@ def is_property_setter(self, func: ast.FunctionDef) -> bool: return self.current_class.fields[setter_decorator[0].value.id] def is_all_low(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'AllLow') + return self.function_has_decorator(func, 'AllLow') def preserves_low(self, func: ast.FunctionDef) -> bool: - return self.has_decorator(func, 'PreservesLow') + return self.function_has_decorator(func, 'PreservesLow') diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index bb54c17d6..26b244e08 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -360,6 +360,7 @@ IGNORED_IMPORTS = {'_importlib_modulespec', 'abc', 'builtins', + 'dataclasses', 'nagini_contracts', 'nagini_contracts.adt', 'nagini_contracts.contracts', diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index e19779ccd..7ac3e606e 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -378,6 +378,8 @@ def __init__(self, name: str, superscope: PythonScope, self.static_fields = OrderedDict() self.type = None # infer, domain type self.interface = interface + self.dataclass = False + self.frozen = False self.defined = False self._has_classmethod = False self.type_vars = OrderedDict() diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index b330a4f02..8be73f06f 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -494,11 +494,12 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di issue = 'Not supported: ' if e.args[0]: issue += e.args[0] - else: + elif e.node != None: issue += astunparse.unparse(e.node) - line = str(e.node.lineno) - col = str(e.node.col_offset) - print(issue + ' (' + python_file + '@' + line + '.' + col + ')') + if e.node != None: + line = str(e.node.lineno) + col = str(e.node.col_offset) + print(issue + ' (' + python_file + '@' + line + '.' + col + ')') traceback.print_exc() if isinstance(e, TypeException): for msg in e.messages: diff --git a/src/nagini_translation/translators/method.py b/src/nagini_translation/translators/method.py index 822de1e58..aa1ec6262 100644 --- a/src/nagini_translation/translators/method.py +++ b/src/nagini_translation/translators/method.py @@ -329,16 +329,18 @@ def translate_function(self, func: PythonMethod, check = self.type_check(result, func.type, res_type_pos, ctx) posts = [check] + posts - statements = func.node.body - start, end = get_body_indices(statements) - # Translate body - actual_body = statements[start:end] - if (func.contract_only or - (len(actual_body) == 1 and isinstance(actual_body[0], ast.Expr) and - isEllipsis(actual_body[0].value))): + if func.contract_only: body = None else: - body = self.translate_exprs(actual_body, func, ctx) + statements = func.node.body + start, end = get_body_indices(statements) + # Translate body + actual_body = statements[start:end] + if ((len(actual_body) == 1 and isinstance(actual_body[0], ast.Expr) and + isEllipsis(actual_body[0].value))): + body = None + else: + body = self.translate_exprs(actual_body, func, ctx) ctx.current_function = old_function name = func.sil_name diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 74b18d7ef..6fa9e76dc 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1292,7 +1292,7 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, self.track_dependencies(selected_names, selected, func, ctx) functions.append(self.translate_function(func, ctx)) func_constants.append(self.translate_function_constant(func, ctx)) - if func.overrides and not ((func_name in ('__str__', '__bool__') and + if func.overrides and not ((func_name in ('__str__', '__bool__', '__eq__') and func.overrides.cls.name == 'object') or (func_name in ('__getitem__',) and func.overrides.cls.name == 'dict')): # We allow overriding certain methods, since the basic versions diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py new file mode 100644 index 000000000..4ea1c70b8 --- /dev/null +++ b/tests/functional/translation/test_dataclass.py @@ -0,0 +1,15 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass + +@dataclass(frozen=True) +class foo: + num: int + name: str + obj: list[int] + +@dataclass(frozen=True) +class A: + data: foo \ No newline at end of file diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py new file mode 100644 index 000000000..f4374d1e7 --- /dev/null +++ b/tests/functional/verification/test_dataclass.py @@ -0,0 +1,79 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass + +@dataclass(frozen=True) +class A: + data: int + + @Pure + def __eq__(self, other: object) -> bool: + if not isinstance(other, A): + return False + + return self.data == other.data + +@dataclass(frozen=True) +class B: + field: A + + @Pure + def __eq__(self, other: object) -> bool: + if not isinstance(other, B): + return False + + return self.field == other.field + +@dataclass(frozen=True) +class C: + fields: list[A] + + +def test_1(val: int) -> None: + a = A(val) + + assert a.data == val + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a.data == 2 + +# def test_2() -> None: +# a1 = A(0) +# a2 = A(3) +# a3 = A(42) +# c = C([a1, a2, a3]) + +# assert len(c.fields) == 3 +# assert c.fields[0].data == 0 + +# c.fields.append(A(20)) +# assert len(c.fields) == 4 +# assert c.fields[3].data == 20 + +# #:: ExpectedOutput(assert.failed:assertion.false) +# assert c.fields[1].data == c.fields[2].data + +def test_eq_1(val: int) -> None: + a1 = A(val) + a2 = A(val) + a3 = A(0) + + assert a1 == a2 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a1 == a3 + +def test_eq_2(a1: A, a2: A) -> None: + b1 = B(a1) + b2 = B(a1) + b3 = B(a2) + + assert b1 == b2 + + if a1 == a2: + assert b1 == b3 + else: + #:: ExpectedOutput(assert.failed:assertion.false) + assert b1 == b3 \ No newline at end of file From acb2577c5818138f568603a7a8d0aa3fbe010709 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 4 Dec 2025 10:04:23 +0100 Subject: [PATCH 054/126] Remove legacy preprocessing flag --- src/nagini_translation/analyzer.py | 2 -- src/nagini_translation/main.py | 1 - 2 files changed, 3 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index a0b4dbd06..db45c7963 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -102,8 +102,6 @@ def __init__(self, types: TypeInfo, path: str, selected: Set[str]): self.deferred_tasks = [] self.has_all_low = False self.enable_obligations = False - # Set to enable preprocessing - self.enable_preprocessing = False self.comment_pattern = "#@nagini" def initialize_io_analyzer(self) -> None: diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 8be73f06f..65c54eda8 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -134,7 +134,6 @@ def translate(path: str, jvm: JVM, bv_size: int, selected: Set[str] = set(), bas return None analyzer = Analyzer(types, path, selected) - analyzer.enable_preprocessing = config.enable_preprocessing analyzer.comment_pattern = config.comment_pattern main_module = analyzer.module with open(os.path.join(builtins_index_path, 'builtins.json'), 'r') as file: From 5bd21ffc8b3115fab43362ea551bbcd8af0b51f2 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 6 Dec 2025 10:59:38 +0100 Subject: [PATCH 055/126] IntEnum WIP --- src/nagini_translation/analyzer.py | 74 ++++++++++++++----- src/nagini_translation/lib/constants.py | 1 + src/nagini_translation/lib/program_nodes.py | 1 + tests/functional/translation/test_enum.py | 9 +++ .../functional/verification/test_enum_int.py | 22 ++++++ 5 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 tests/functional/translation/test_enum.py create mode 100644 tests/functional/verification/test_enum_int.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index db45c7963..f4649b0d9 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -25,6 +25,7 @@ CALLABLE_TYPE, IGNORED_IMPORTS, INT_TYPE, + PRIMITIVE_INT_TYPE, LEGAL_MAGIC_METHODS, LITERALS, MYPY_SUPERCLASSES, @@ -576,7 +577,8 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: cls.superclass = self.find_or_create_class(OBJECT_TYPE) if cls.python_class not in cls.superclass.python_class.direct_subclasses: cls.superclass.python_class.direct_subclasses.append(cls.python_class) - + if cls.superclass.name == "IntEnum": + cls.enum = True if self.is_dataclass(node): cls.dataclass = True if self.is_frozen_dataclass(node): @@ -603,7 +605,9 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: args.append(self._create_arg_ast(node, 'self', None)) for name, field in self.current_class.fields.items(): args.append(self._create_arg_ast(node, name, field.type.name)) - stmts.append(self._create_eq_postcondition(node, name, name)) + stmts.append(self._create_eq_postcondition(node, + ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), + self._create_name_ast(name, node))) ast_arguments = ast.arguments([], args, None, [], [], None, []) @@ -623,11 +627,11 @@ def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> as name_node = self._create_name_ast(type_name, node) return ast.arg(arg, name_node, lineno=node.lineno, col_offset=0) - def _create_eq_postcondition(self, node, attribute: str, arg: str) -> ast.stmt: + def _create_eq_postcondition(self, node, left: ast.expr, right: ast.expr) -> ast.stmt: compare = ast.Compare( - ast.Attribute(self._create_name_ast('self', node), attribute, ast.Load(), lineno=node.lineno, col_offset=0), + left, ops=[ast.Eq()], - comparators=[self._create_name_ast(arg, node)], + comparators=[right], lineno=node.lineno, col_offset=0) return ast.Expr(ast.Call(self._create_name_ast('Ensures', node), [compare], [], lineno=node.lineno, col_offset=0)) @@ -1206,31 +1210,44 @@ def todo(): raise UnsupportedException(assign, 'Default value for dataclass fields not supported') # func.result = assign.value # Temporarily set value, because it will be used as default + if node.id in self.current_class.fields: + del self.current_class.fields[node.id] + + return + elif self.current_class.superclass.name == "IntEnum": + # Node is an enum member. Basically a static field that returns an instance of the enum instead + if isinstance(node.ctx, ast.Load): + return + + node_type = self.typeof(node) + if node_type.name != INT_TYPE: + raise InvalidProgramException(node, 'invalid literal for int() with base 10') + + assign = node._parent + if (not isinstance(assign, ast.Assign) + or len(assign.targets) != 1): + msg = ('only simple assignments and reads allowed for ' + 'enum members') + raise UnsupportedException(assign, msg) + + value = ast.Call(ast.Attribute(self._create_name_ast(self.current_class.name, assign), "__box__", ast.Load(), lineno=assign.lineno, col_offset=0), + [assign.value], [], lineno=assign.lineno, col_offset=0) + self.create_static_field(node, self.current_class, value) return else: # Node is a static field. if isinstance(node.ctx, ast.Load): return - cls = self.typeof(node) - self.define_new(self.current_class, node.id, node) - var = self.node_factory.create_static_field(node.id, node, cls, - self.module, - self.current_class) + assign = node._parent if (not isinstance(assign, ast.Assign) or len(assign.targets) != 1): msg = ('only simple assignments and reads allowed for ' - 'static fields') + 'static fields') raise UnsupportedException(assign, msg) - var.value = assign.value - self.current_class.static_fields[node.id] = var - if node.id in self.current_class.fields: - # It's possible that we encountered a read of this field - # before seeing the definition, assumed it's a normal - # (non-static) field, and created the field. We remove it - # again now that we now it's actually static. - del self.current_class.fields[node.id] - self.track_access(node, var) + + cls = self.typeof(node) + self.create_static_field(node, cls, assign.value) return # We're in a function if isinstance(node.ctx, ast.Store): @@ -1282,6 +1299,23 @@ def todo(): self.track_access(node, var) + def create_static_field(self, node: ast.Name, type_: PythonType, val: ast.expr) -> None: + assert self.current_class != None + + self.define_new(self.current_class, node.id, node) + var = self.node_factory.create_static_field(node.id, node, type_, + self.module, + self.current_class) + var.value = val + self.current_class.static_fields[node.id] = var + if node.id in self.current_class.fields: + # It's possible that we encountered a read of this field + # before seeing the definition, assumed it's a normal + # (non-static) field, and created the field. We remove it + # again now that we now it's actually static. + del self.current_class.fields[node.id] + self.track_access(node, var) + def visit_Attribute(self, node: ast.Attribute) -> None: """ Tracks field accesses to find out which fields exist. diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 26b244e08..15e0551ec 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -361,6 +361,7 @@ 'abc', 'builtins', 'dataclasses', + 'enum', 'nagini_contracts', 'nagini_contracts.adt', 'nagini_contracts.contracts', diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 7ac3e606e..8707806b6 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -380,6 +380,7 @@ def __init__(self, name: str, superscope: PythonScope, self.interface = interface self.dataclass = False self.frozen = False + self.enum = False self.defined = False self._has_classmethod = False self.type_vars = OrderedDict() diff --git a/tests/functional/translation/test_enum.py b/tests/functional/translation/test_enum.py new file mode 100644 index 000000000..0e1f9bc00 --- /dev/null +++ b/tests/functional/translation/test_enum.py @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from enum import IntEnum + +class flag(IntEnum): + success = 0 + failure = 1 \ No newline at end of file diff --git a/tests/functional/verification/test_enum_int.py b/tests/functional/verification/test_enum_int.py new file mode 100644 index 000000000..01ff022a3 --- /dev/null +++ b/tests/functional/verification/test_enum_int.py @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from enum import IntEnum + +# class flag(int): +# pass + +# def test() -> None: +# f = flag(1) +# assert f == flag(1) + +class flag(IntEnum): + success = 0 + failure = 1 + +def test() -> None: + f = flag(1) + + assert f == flag(1) + assert f == 1 \ No newline at end of file From eebb1c37c57be0606d38d88c179f3c6480ff647b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 9 Dec 2025 22:50:50 +0100 Subject: [PATCH 056/126] WIP --- src/nagini_translation/analyzer.py | 7 +-- src/nagini_translation/translators/program.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index f4649b0d9..16edaa16b 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1230,9 +1230,10 @@ def todo(): 'enum members') raise UnsupportedException(assign, msg) - value = ast.Call(ast.Attribute(self._create_name_ast(self.current_class.name, assign), "__box__", ast.Load(), lineno=assign.lineno, col_offset=0), - [assign.value], [], lineno=assign.lineno, col_offset=0) - self.create_static_field(node, self.current_class, value) + # value = ast.Call(ast.Attribute(self._create_name_ast(self.current_class.name, assign), "__box__", ast.Load(), lineno=assign.lineno, col_offset=0), + # [assign.value], [], lineno=assign.lineno, col_offset=0) + # self.create_static_field(node, self.current_class, value) + self.create_static_field(node, node_type, assign.value) return else: # Node is a static field. diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 6fa9e76dc..6a7918bc5 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1165,6 +1165,50 @@ def create_adts_domains_and_functions(self, adts: List[PythonClass], return domains, functions + def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> list[Function]: + """Create __box__ and __unbox__ functions for IntEnum. Other enum types currently not supported""" + + pos = self.to_position(enum, ctx) + info = self.no_info(ctx) + + ## Create box function (Int -> Ref) + int_val_use = self.viper.LocalVar('value', self.viper.Int, pos, info) + int_val_decl = self.viper.LocalVarDecl('value', self.viper.Int, pos, info) + postconds = [] + result = self.viper.Result(self.viper.Ref, pos, info) + postconds.append(self.type_factory.type_check(result, enum, pos, ctx)) + + # Ensure boxing is injective: forall other: Int :: (other == value) == (box(other) == result) + other_int_use = self.viper.LocalVar('___other', self.viper.Int, pos, info) + other_int_decl = self.viper.LocalVarDecl('___other', self.viper.Int, pos, info) + box_func_name = enum.sil_name + '__box__' + other_object = self.viper.FuncApp(box_func_name, [other_int_use], pos, info, self.viper.Ref) + other_is_result = self.viper.EqCmp(other_object, result, pos, info) + args_equal = self.viper.EqCmp(int_val_use, other_int_use, pos, info) + both_equal = self.viper.EqCmp(args_equal, other_is_result, pos, info) + trigger = self.viper.Trigger([other_object], pos, info) + quant = self.viper.Forall([other_int_decl], [trigger], both_equal, pos, info) + postconds.append(quant) + + terminates_wildcard = self.viper.DecreasesWildcard(None, pos, info) + yield self.viper.Function(box_func_name, + [int_val_decl], self.viper.Ref, [terminates_wildcard], postconds, + None, pos, info) + + ## Create unbox function (Ref -> Int) + preconds = [terminates_wildcard] + postconds = [] + ref_use = self.viper.LocalVar('ref', self.viper.Ref, pos, info) + preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx)) + result = self.viper.Result(self.viper.Int, pos, info) + box_func = self.viper.FuncApp(box_func_name, [result], pos, info, self.viper.Ref) + postconds.append(self.viper.EqCmp(box_func, ref_use, pos, info)) + ref_decl = self.viper.LocalVarDecl('ref', self.viper.Ref, pos, info) + yield self.viper.Function(enum.sil_name + '__unbox__', + [ref_decl], self.viper.Int, preconds, postconds, + None, pos, info) + + def translate_program(self, modules: List[PythonModule], sil_progs: Program, ctx: Context, selected: Set[str] = None, ignore_global: bool = False) -> Program: @@ -1350,6 +1394,10 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, predicate_families[cpred].append(pred) else: predicate_families[cpred] = [pred] + if cls.enum: + enum_functions = list(self._create_enum_func_box_and_unbox(cls, ctx)) + functions.extend(enum_functions) + ctx.current_class = old_class if not ignore_global: From 2d819f3d6c1f9079796c218ced8cdda5605d8e2b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 10 Dec 2025 14:53:14 +0100 Subject: [PATCH 057/126] Fix dataclass for arrays --- src/nagini_translation/analyzer.py | 11 ++++----- .../functional/verification/test_dataclass.py | 24 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 16edaa16b..6f34865e3 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -605,9 +605,9 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: args.append(self._create_arg_ast(node, 'self', None)) for name, field in self.current_class.fields.items(): args.append(self._create_arg_ast(node, name, field.type.name)) - stmts.append(self._create_eq_postcondition(node, + stmts.append(self._create_comp_postcondition(node, ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), - self._create_name_ast(name, node))) + self._create_name_ast(name, node), ast.Is())) ast_arguments = ast.arguments([], args, None, [], [], None, []) @@ -627,10 +627,10 @@ def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> as name_node = self._create_name_ast(type_name, node) return ast.arg(arg, name_node, lineno=node.lineno, col_offset=0) - def _create_eq_postcondition(self, node, left: ast.expr, right: ast.expr) -> ast.stmt: + def _create_comp_postcondition(self, node, left: ast.expr, right: ast.expr, op: ast.cmpop) -> ast.stmt: compare = ast.Compare( left, - ops=[ast.Eq()], + ops=[op], comparators=[right], lineno=node.lineno, col_offset=0) return ast.Expr(ast.Call(self._create_name_ast('Ensures', node), [compare], [], lineno=node.lineno, col_offset=0)) @@ -1210,9 +1210,6 @@ def todo(): raise UnsupportedException(assign, 'Default value for dataclass fields not supported') # func.result = assign.value # Temporarily set value, because it will be used as default - if node.id in self.current_class.fields: - del self.current_class.fields[node.id] - return elif self.current_class.superclass.name == "IntEnum": # Node is an enum member. Basically a static field that returns an instance of the enum instead diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py index f4374d1e7..8a97b15de 100644 --- a/tests/functional/verification/test_dataclass.py +++ b/tests/functional/verification/test_dataclass.py @@ -39,21 +39,21 @@ def test_1(val: int) -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert a.data == 2 -# def test_2() -> None: -# a1 = A(0) -# a2 = A(3) -# a3 = A(42) -# c = C([a1, a2, a3]) +def test_2() -> None: + a1 = A(0) + a2 = A(3) + a3 = A(42) + c = C([a1, a2, a3]) -# assert len(c.fields) == 3 -# assert c.fields[0].data == 0 + assert len(c.fields) == 3 + assert c.fields[0].data == 0 -# c.fields.append(A(20)) -# assert len(c.fields) == 4 -# assert c.fields[3].data == 20 + c.fields.append(A(20)) + assert len(c.fields) == 4 + assert c.fields[3].data == 20 -# #:: ExpectedOutput(assert.failed:assertion.false) -# assert c.fields[1].data == c.fields[2].data + #:: ExpectedOutput(assert.failed:assertion.false) + assert c.fields[1].data == c.fields[2].data def test_eq_1(val: int) -> None: a1 = A(val) From 807c10ff09bbbbfa939dae93fcb9c54540226178 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 12 Dec 2025 08:54:01 +0100 Subject: [PATCH 058/126] IntEnum WIP --- src/nagini_translation/translators/program.py | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 6a7918bc5..1160c1594 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -145,7 +145,11 @@ def create_static_field_function(self, root: PythonVar, ctx.current_class = field.cls ctx.module = field.cls.module # Compute the field value - stmt, value = self.translate_expr(field.value, ctx) + if cls.enum: + stmt, value = self.translate_expr(field.value, ctx, self.viper.Int) + value = self.viper.FuncApp(cls.name + '__box__', [value], position, info, self.viper.Ref) + else: + stmt, value = self.translate_expr(field.value, ctx) if stmt: raise InvalidProgramException('purity.violated', field.node) field_position = self.to_position(field.node, ctx) @@ -1166,45 +1170,55 @@ def create_adts_domains_and_functions(self, adts: List[PythonClass], return domains, functions def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> list[Function]: - """Create __box__ and __unbox__ functions for IntEnum. Other enum types currently not supported""" - + """Create __box__ and __unbox__ functions for IntEnum. Other enum types currently not supported.""" + pos = self.to_position(enum, ctx) info = self.no_info(ctx) + terminates_wildcard = self.viper.DecreasesWildcard(None, pos, info) + + box_func_name = enum.sil_name + '__box__' + unbox_func_name = enum.sil_name + '__unbox__' ## Create box function (Int -> Ref) int_val_use = self.viper.LocalVar('value', self.viper.Int, pos, info) int_val_decl = self.viper.LocalVarDecl('value', self.viper.Int, pos, info) - postconds = [] result = self.viper.Result(self.viper.Ref, pos, info) - postconds.append(self.type_factory.type_check(result, enum, pos, ctx)) + preconds = [terminates_wildcard] + postconds = [] - # Ensure boxing is injective: forall other: Int :: (other == value) == (box(other) == result) - other_int_use = self.viper.LocalVar('___other', self.viper.Int, pos, info) - other_int_decl = self.viper.LocalVarDecl('___other', self.viper.Int, pos, info) - box_func_name = enum.sil_name + '__box__' - other_object = self.viper.FuncApp(box_func_name, [other_int_use], pos, info, self.viper.Ref) - other_is_result = self.viper.EqCmp(other_object, result, pos, info) - args_equal = self.viper.EqCmp(int_val_use, other_int_use, pos, info) - both_equal = self.viper.EqCmp(args_equal, other_is_result, pos, info) - trigger = self.viper.Trigger([other_object], pos, info) - quant = self.viper.Forall([other_int_decl], [trigger], both_equal, pos, info) - postconds.append(quant) + postconds.append(self.type_factory.type_check(result, enum, pos, ctx, True)) + + unbox_func = self.viper.FuncApp(unbox_func_name, [result], pos, info, self.viper.Int) + postconds.append(self.viper.EqCmp(unbox_func, int_val_use, pos, info)) - terminates_wildcard = self.viper.DecreasesWildcard(None, pos, info) yield self.viper.Function(box_func_name, - [int_val_decl], self.viper.Ref, [terminates_wildcard], postconds, + [int_val_decl], self.viper.Ref, preconds, postconds, None, pos, info) ## Create unbox function (Ref -> Int) + ref_use = self.viper.LocalVar('box', self.viper.Ref, pos, info) + ref_decl = self.viper.LocalVarDecl('box', self.viper.Ref, pos, info) + result = self.viper.Result(self.viper.Int, pos, info) preconds = [terminates_wildcard] postconds = [] - ref_use = self.viper.LocalVar('ref', self.viper.Ref, pos, info) - preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx)) - result = self.viper.Result(self.viper.Int, pos, info) - box_func = self.viper.FuncApp(box_func_name, [result], pos, info, self.viper.Ref) - postconds.append(self.viper.EqCmp(box_func, ref_use, pos, info)) - ref_decl = self.viper.LocalVarDecl('ref', self.viper.Ref, pos, info) - yield self.viper.Function(enum.sil_name + '__unbox__', + + preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx, True)) + + # Add forall postcondition + i_var_use = self.viper.LocalVar('i', self.viper.Ref, pos, info) + i_var_decl = self.viper.LocalVarDecl('i', self.viper.Ref, pos, info) + obj_eq_check = self.viper.EqCmp(ref_use, i_var_use, pos, info) + type_check = self.type_factory.type_check(i_var_use, enum, pos, ctx, True) + condition = self.viper.And(obj_eq_check, type_check, pos, info) + unbox_eq = self.viper.EqCmp(self.viper.FuncApp(unbox_func_name, [i_var_use], pos, info, self.viper.Int), + result, pos, info) + implication = self.viper.Implies(condition, unbox_eq, pos, info) + + trigger = self.viper.Trigger([obj_eq_check, unbox_eq], pos, info) + forall_postcond = self.viper.Forall([i_var_decl], [trigger], implication, pos, info) + postconds.append(forall_postcond) + + yield self.viper.Function(unbox_func_name, [ref_decl], self.viper.Int, preconds, postconds, None, pos, info) @@ -1276,6 +1290,12 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, while current_field.overrides: current_field = current_field.overrides static_fields.setdefault(current_field, []).append(cls) + if cls.enum: + enum_functions = list(self._create_enum_func_box_and_unbox(cls, ctx)) + functions.extend(enum_functions) + if module is not module.global_module: + for function in enum_functions: + all_names.append(function.name()) ctx.current_class = None # Translate default args @@ -1394,9 +1414,6 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, predicate_families[cpred].append(pred) else: predicate_families[cpred] = [pred] - if cls.enum: - enum_functions = list(self._create_enum_func_box_and_unbox(cls, ctx)) - functions.extend(enum_functions) ctx.current_class = old_class From f12af0e5661eaec58347a97d7f721858f2881df7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 12 Dec 2025 16:47:12 +0100 Subject: [PATCH 059/126] Working IntEnum for equality checks on instances --- src/nagini_translation/analyzer.py | 6 +----- src/nagini_translation/translators/program.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 6f34865e3..ae4033304 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1226,11 +1226,7 @@ def todo(): msg = ('only simple assignments and reads allowed for ' 'enum members') raise UnsupportedException(assign, msg) - - # value = ast.Call(ast.Attribute(self._create_name_ast(self.current_class.name, assign), "__box__", ast.Load(), lineno=assign.lineno, col_offset=0), - # [assign.value], [], lineno=assign.lineno, col_offset=0) - # self.create_static_field(node, self.current_class, value) - self.create_static_field(node, node_type, assign.value) + self.create_static_field(node, self.current_class, assign.value) return else: # Node is a static field. diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 1160c1594..2d48695f8 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1204,17 +1204,19 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx, True)) - # Add forall postcondition + ## Add forall postcondition i_var_use = self.viper.LocalVar('i', self.viper.Ref, pos, info) i_var_decl = self.viper.LocalVarDecl('i', self.viper.Ref, pos, info) - obj_eq_check = self.viper.EqCmp(ref_use, i_var_use, pos, info) + # obj_eq_check = self.viper.EqCmp(ref_use, i_var_use, pos, info) # TODO should be object___eq__ + obj_eq_check = self.viper.DomainFuncApp('object___eq__', [ref_use, i_var_use], self.viper.Bool, pos, info, '__ObjectEquality') type_check = self.type_factory.type_check(i_var_use, enum, pos, ctx, True) - condition = self.viper.And(obj_eq_check, type_check, pos, info) - unbox_eq = self.viper.EqCmp(self.viper.FuncApp(unbox_func_name, [i_var_use], pos, info, self.viper.Int), - result, pos, info) + condition = self.viper.And(obj_eq_check, type_check, pos, info) + unbox_apply = self.viper.FuncApp(unbox_func_name, [i_var_use], pos, info, self.viper.Int) + unbox_eq = self.viper.EqCmp(unbox_apply, result, pos, info) implication = self.viper.Implies(condition, unbox_eq, pos, info) - trigger = self.viper.Trigger([obj_eq_check, unbox_eq], pos, info) + # trigger = self.viper.Trigger([unbox_apply], pos, info) + trigger = self.viper.Trigger([obj_eq_check, unbox_apply], pos, info) forall_postcond = self.viper.Forall([i_var_decl], [trigger], implication, pos, info) postconds.append(forall_postcond) From a3223754319f2aaaa8012509abcbfca4bb0a05ad Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 15 Dec 2025 21:41:31 +0100 Subject: [PATCH 060/126] Use box function for construction of enum --- src/nagini_translation/analyzer.py | 3 ++- src/nagini_translation/lib/program_nodes.py | 1 + src/nagini_translation/translators/call.py | 18 ++++++++++++++++++ .../translators/expression.py | 2 ++ src/nagini_translation/translators/program.py | 8 +++----- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index ae4033304..597bf27cb 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -579,6 +579,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: cls.superclass.python_class.direct_subclasses.append(cls.python_class) if cls.superclass.name == "IntEnum": cls.enum = True + cls.enum_type = INT_TYPE if self.is_dataclass(node): cls.dataclass = True if self.is_frozen_dataclass(node): @@ -1203,7 +1204,7 @@ def todo(): # Adjust the class body assign = node._parent - self.current_class.node.body.remove(assign) + self.current_class.node.body.remove(assign) # TODO is this necessary? self.current_class.node.body.append(function_def) if(assign.value != None): diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 8707806b6..8d4c7064d 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -381,6 +381,7 @@ def __init__(self, name: str, superscope: PythonScope, self.dataclass = False self.frozen = False self.enum = False + self.enum_type = None self.defined = False self._has_classmethod = False self.type_vars = OrderedDict() diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index fc7e9f197..877be064c 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -209,6 +209,21 @@ def translate_adt_cons(self, cons: PythonClass, args: List[FuncApp], return box_func + def translate_enum_cons(self, enum: PythonClass, args: List[FuncApp], + pos: Position, ctx: Context) -> Expr: + """ + Cosntruct Enums via a sequence of constructor calls and + boxing/unboxing calls. + """ + assert len(args) == 1 + + info = self.no_info(ctx) + args[0] = self.to_type(args[0], self.viper.Int, ctx) + box_func_name = enum.sil_name + '__box__' + box_func = self.viper.FuncApp(box_func_name, args, pos, info, self.viper.Ref) + return box_func + + def _is_lock_subtype(self, cls: PythonClass) -> bool: if cls is None: return False @@ -230,6 +245,9 @@ def translate_constructor_call(self, target_class: PythonClass, if target_class.is_adt: return arg_stmts, self.translate_adt_cons(target_class, args, pos, ctx) + if target_class.enum: + return arg_stmts, self.translate_enum_cons(target_class, args, pos, ctx) + res_var = ctx.current_function.create_variable(target_class.name + '_res', target_class, diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 2938eeaa3..1489901f7 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -1174,6 +1174,8 @@ def translate_Compare(self, node: ast.Compare, position = self.to_position(node, ctx) info = self.no_info(ctx) + # TODO add handling for IntEnum + if self._is_primitive_operation(node.ops[0], left_type, right_type): result = self._translate_primitive_operation(left, right, left_type, node.ops[0], position, diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 2d48695f8..3cff640f5 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1179,7 +1179,7 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li box_func_name = enum.sil_name + '__box__' unbox_func_name = enum.sil_name + '__unbox__' - ## Create box function (Int -> Ref) + # Create box function (Int -> Ref) int_val_use = self.viper.LocalVar('value', self.viper.Int, pos, info) int_val_decl = self.viper.LocalVarDecl('value', self.viper.Int, pos, info) result = self.viper.Result(self.viper.Ref, pos, info) @@ -1195,7 +1195,7 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li [int_val_decl], self.viper.Ref, preconds, postconds, None, pos, info) - ## Create unbox function (Ref -> Int) + # Create unbox function (Ref -> Int) ref_use = self.viper.LocalVar('box', self.viper.Ref, pos, info) ref_decl = self.viper.LocalVarDecl('box', self.viper.Ref, pos, info) result = self.viper.Result(self.viper.Int, pos, info) @@ -1204,10 +1204,9 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx, True)) - ## Add forall postcondition + # Add forall postcondition i_var_use = self.viper.LocalVar('i', self.viper.Ref, pos, info) i_var_decl = self.viper.LocalVarDecl('i', self.viper.Ref, pos, info) - # obj_eq_check = self.viper.EqCmp(ref_use, i_var_use, pos, info) # TODO should be object___eq__ obj_eq_check = self.viper.DomainFuncApp('object___eq__', [ref_use, i_var_use], self.viper.Bool, pos, info, '__ObjectEquality') type_check = self.type_factory.type_check(i_var_use, enum, pos, ctx, True) condition = self.viper.And(obj_eq_check, type_check, pos, info) @@ -1215,7 +1214,6 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li unbox_eq = self.viper.EqCmp(unbox_apply, result, pos, info) implication = self.viper.Implies(condition, unbox_eq, pos, info) - # trigger = self.viper.Trigger([unbox_apply], pos, info) trigger = self.viper.Trigger([obj_eq_check, unbox_apply], pos, info) forall_postcond = self.viper.Forall([i_var_decl], [trigger], implication, pos, info) postconds.append(forall_postcond) From a389d381b2ea7c44a6f486b9025168ab6f4f1bf7 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 16 Dec 2025 11:16:15 +0100 Subject: [PATCH 061/126] Resolve mypy LiteralType --- src/nagini_translation/analyzer.py | 2 ++ src/nagini_translation/lib/typeinfo.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 597bf27cb..a9ea743c8 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1357,6 +1357,8 @@ def convert_type(self, mypy_type, node) -> PythonType: """ Converts an internal mypy type to a PythonType. """ + if (self.types.is_literal_type(mypy_type)): + mypy_type = mypy_type.fallback if (self.types.is_void_type(mypy_type) or self.types.is_none_type(mypy_type)): result = None diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 681f816d1..d9c01d51a 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -455,6 +455,9 @@ def is_normal_type(self, type: mypy.types.Type) -> bool: def is_instance_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.Instance) + def is_literal_type(self, type: mypy.types.Type) -> bool: + return isinstance(type, mypy.types.LiteralType) # TODO use correct type here + def is_tuple_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.TupleType) From 7ab982701b7c5784f089c12ec4d5baa10386655a Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 16 Dec 2025 15:29:07 +0100 Subject: [PATCH 062/126] Mark IntEnum as subclass of int. Ideally, we could mark it as subclass of both int and Enum --- src/nagini_translation/resources/builtins.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index f97e84791..ea5e7c3f6 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -1092,6 +1092,9 @@ }, "extends": "object" }, +"IntEnum": { + "extends": "int" +}, "global": { "functions": { "max": { From 9a3bc02ef3f84b3b6a9a82754427bb2fe91a03db Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 16 Dec 2025 15:47:56 +0100 Subject: [PATCH 063/126] Add precondition for box function of IntEnum --- src/nagini_translation/lib/constants.py | 2 ++ src/nagini_translation/lib/typeinfo.py | 2 +- src/nagini_translation/translators/program.py | 14 +++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 15e0551ec..2fd6af879 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -378,6 +378,8 @@ '_importlib_modulespec': [], 'abc': [], 'builtins': [], + 'dataclasses': [], + 'enum': [], 'nagini_contracts': [], 'nagini_contracts.contracts': [], 'nagini_contracts.io_contracts': [], diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index d9c01d51a..f3dbae330 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -456,7 +456,7 @@ def is_instance_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.Instance) def is_literal_type(self, type: mypy.types.Type) -> bool: - return isinstance(type, mypy.types.LiteralType) # TODO use correct type here + return isinstance(type, mypy.types.LiteralType) def is_tuple_type(self, type: mypy.types.Type) -> bool: return isinstance(type, mypy.types.TupleType) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 3cff640f5..043d4a785 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1184,12 +1184,24 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li int_val_decl = self.viper.LocalVarDecl('value', self.viper.Int, pos, info) result = self.viper.Result(self.viper.Ref, pos, info) preconds = [terminates_wildcard] - postconds = [] + # Add precondition for allowed values + enum_value_precond = self.viper.FalseLit(pos, info) + for field_name in enum.static_fields: + field = enum.get_static_field(field_name) + if field and field.value: + _, enum_value_expr = self.translate_expr(field.value, ctx, self.viper.Int) + value_check = self.viper.EqCmp(int_val_use, enum_value_expr, pos, info) + enum_value_precond = self.viper.Or(enum_value_precond, value_check, pos, info) + preconds.append(enum_value_precond) + + postconds = [] postconds.append(self.type_factory.type_check(result, enum, pos, ctx, True)) unbox_func = self.viper.FuncApp(unbox_func_name, [result], pos, info, self.viper.Int) postconds.append(self.viper.EqCmp(unbox_func, int_val_use, pos, info)) + int_unbox_func = self.viper.FuncApp('int___unbox__', [result], pos, info, self.viper.Int) + postconds.append(self.viper.EqCmp(int_unbox_func, int_val_use, pos, info)) yield self.viper.Function(box_func_name, [int_val_decl], self.viper.Ref, preconds, postconds, From 886852fde34abd73dac92f04a88351c339a31530 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 17 Dec 2025 10:24:40 +0100 Subject: [PATCH 064/126] Add tests for named parameters --- tests/functional/translation/test_dataclass.py | 7 ++++++- tests/functional/verification/test_dataclass.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py index 4ea1c70b8..166ec75e1 100644 --- a/tests/functional/translation/test_dataclass.py +++ b/tests/functional/translation/test_dataclass.py @@ -12,4 +12,9 @@ class foo: @dataclass(frozen=True) class A: - data: foo \ No newline at end of file + data: foo + +def test_cons() -> None: + f1 = foo(1, "hello", []) + + f2 = foo(num=2, name="hello", obj=[]) \ No newline at end of file diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py index 8a97b15de..a228fa015 100644 --- a/tests/functional/verification/test_dataclass.py +++ b/tests/functional/verification/test_dataclass.py @@ -30,6 +30,11 @@ def __eq__(self, other: object) -> bool: class C: fields: list[A] +@dataclass(frozen=True) +class D: + value: int + length: int + text: str def test_1(val: int) -> None: a = A(val) @@ -54,7 +59,16 @@ def test_2() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert c.fields[1].data == c.fields[2].data - + +def test_named_param(val: int, length: int) -> None: + d = D(length=length, value=val, text="") + + assert d.value == val + assert d.text == "" + + #:: ExpectedOutput(assert.failed:assertion.false) + assert d.length == 2 + def test_eq_1(val: int) -> None: a1 = A(val) a2 = A(val) From 54b4d2281c003aec3054cf03a075e61f550c5433 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 18 Dec 2025 10:21:54 +0100 Subject: [PATCH 065/126] Fix failing tests due to illegal id access --- src/nagini_translation/analyzer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 00658475f..68c7fdd3c 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1420,7 +1420,12 @@ def convert_type(self, mypy_type, node) -> PythonType: msg = f'Type could not be fully inferred (this usually means that a type argument is unknown)' raise InvalidProgramException(node, 'partial.type', message=msg) else: - msg = 'Unsupported type: {} for node {}'.format(mypy_type.__class__.__name__, node.id) + name = "" + if hasattr(node, 'id'): + name = node.id + elif hasattr(node, 'name'): + name = node.name + msg = 'Unsupported type: {} for node {}'.format(mypy_type.__class__.__name__, name) raise UnsupportedException(node, desc=msg) return result @@ -1584,7 +1589,6 @@ def typeof(self, node: ast.AST) -> PythonType: return method.type else: raise UnsupportedException(node) - def _get_basic_name(self, node: Union[ast.Name, ast.Attribute]) -> str: """ From e9883b55af6d1d43b7a2d77799b9989dcd411d81 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 18 Dec 2025 14:36:25 +0100 Subject: [PATCH 066/126] Workaround for preprocessing --- src/nagini_translation/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 65c54eda8..d896ab2bf 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -129,7 +129,8 @@ def translate(path: str, jvm: JVM, bv_size: int, selected: Set[str] = set(), bas raise Exception('Viper AST SIF extension not found on classpath.') types = TypeInfo() - type_correct = types.check(path, base_dir, text=read_source_file(path)) + preprocessed_text = read_source_file(path) if config.enable_preprocessing else None + type_correct = types.check(path, base_dir, text=preprocessed_text) if not type_correct: return None From 61c356f43adced634ad791d52207b986b9e8361d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 19 Dec 2025 11:24:02 +0100 Subject: [PATCH 067/126] Allow default values for dataclass fields --- src/nagini_translation/analyzer.py | 23 +++++++++--- .../functional/translation/test_dataclass.py | 6 +++- .../verification/test_dataclass_defaults.py | 35 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 tests/functional/verification/test_dataclass_defaults.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 68c7fdd3c..0db369e8c 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -603,6 +603,7 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: assert self.current_class != None args: list[ast.arg] = [] + defaults: list[ast.expr] = [] stmts: list[ast.stmt] = [] # Parse fields, add implicit args and post conditions @@ -612,8 +613,11 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: stmts.append(self._create_comp_postcondition(node, ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), self._create_name_ast(name, node), ast.Is())) + if field.result != None: + defaults.append(field.result) + field.result = None - ast_arguments = ast.arguments([], args, None, [], [], None, []) + ast_arguments = ast.arguments([], args, None, [], [], None, defaults) # Could add implicit field assignments for non-frozen dataclass @@ -1207,12 +1211,21 @@ def todo(): # Adjust the class body assign = node._parent - self.current_class.node.body.remove(assign) # TODO is this necessary? + self.current_class.node.body.remove(assign) self.current_class.node.body.append(function_def) - if(assign.value != None): - raise UnsupportedException(assign, 'Default value for dataclass fields not supported') - # func.result = assign.value # Temporarily set value, because it will be used as default + if not ((isinstance(assign, ast.Assign) and len(assign.targets) == 1) or + (isinstance(assign, ast.AnnAssign) and assign.simple == 1)): + msg = ('only simple assignments and reads allowed for ' + 'dataclass fields') + raise UnsupportedException(assign, msg) + + if assign.value != None: + if not isinstance(assign.value, ast.Constant): + raise UnsupportedException(assign, 'Only constants allowed for datafield default value') + + # Temporarily set value, because it will be used as default + self.current_class.fields[node.id].result = assign.value return elif self.current_class.superclass.name == "IntEnum": diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py index 4ea1c70b8..087beaba3 100644 --- a/tests/functional/translation/test_dataclass.py +++ b/tests/functional/translation/test_dataclass.py @@ -12,4 +12,8 @@ class foo: @dataclass(frozen=True) class A: - data: foo \ No newline at end of file + data: foo + +@dataclass(frozen=True) +class B: + num: int = 2 \ No newline at end of file diff --git a/tests/functional/verification/test_dataclass_defaults.py b/tests/functional/verification/test_dataclass_defaults.py new file mode 100644 index 000000000..cd1f5817c --- /dev/null +++ b/tests/functional/verification/test_dataclass_defaults.py @@ -0,0 +1,35 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass + +@dataclass(frozen=True) +class A: + num: int = 2 + num2: int = 10 + +@dataclass(frozen=True) +class B: + num: int + my_field: int = 5 + +def test_default_vals1() -> None: + a = A() + + assert a.num == 2 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a.num == 3 + +def test_default_vals2(val: int) -> None: + b = B(val) + + assert b.num == val + assert b.my_field == 5 + + b2 = B(val, val) + assert b2.num == b2.my_field + + #:: ExpectedOutput(assert.failed:assertion.false) + assert b2.my_field == 5 \ No newline at end of file From 62f9feabb3ec9f5a3732e60677321fdd9064d858 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 22 Dec 2025 10:27:10 +0100 Subject: [PATCH 068/126] Add IntEnum to list of allowed extendable builtins --- src/nagini_translation/lib/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index be06c4693..a2826bfe0 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -32,7 +32,8 @@ 'object', 'Exception', 'Lock', - 'int' + 'int', + 'IntEnum' ] THREADING = ['Thread'] From d58b358336f8333244551708e396ff2d9191052b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 2 Jan 2026 16:34:02 +0100 Subject: [PATCH 069/126] Consider dataclass __post_init__ function --- src/nagini_translation/analyzer.py | 1 + src/nagini_translation/lib/constants.py | 1 + src/nagini_translation/lib/program_nodes.py | 1 + src/nagini_translation/translators/call.py | 41 +++++++---- .../verification/test_dataclass_override.py | 68 +++++++++++++++++++ 5 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 tests/functional/verification/test_dataclass_override.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 0db369e8c..b63eaf710 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -627,6 +627,7 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: function_def = ast.FunctionDef('__init__', ast_arguments, stmts, decorator_list, returns=None, lineno=node.lineno, col_offset=0) self.visit(function_def, node) node.body.append(function_def) + self.current_class.implicit_init = True return def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> ast.arg: diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index a2826bfe0..5467b5b43 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -260,6 +260,7 @@ '__ror__', '__init__', + '__post_init__', '__enter__', '__exit__', '__str__', diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 8d4c7064d..9456a3bb2 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -379,6 +379,7 @@ def __init__(self, name: str, superscope: PythonScope, self.type = None # infer, domain type self.interface = interface self.dataclass = False + self.implicit_init = False self.frozen = False self.enum = False self.enum_type = None diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 1ed06b6ec..c97ca2cb7 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -336,22 +336,35 @@ def translate_constructor_call(self, target_class: PythonClass, if target: - target_class = target.cls - targets = [] - if target.declared_exceptions: - error_var = self.get_error_var(node, ctx) - targets.append(error_var) - method_name = target_class.get_method('__init__').sil_name - init = self.create_method_call_node( - ctx, method_name, args, targets, self.to_position(node, ctx), - self.no_info(ctx), target_method=target, target_node=node) - stmts.extend(init) - if target.declared_exceptions: - catchers = self.create_exception_catchers(error_var, - ctx.actual_function.try_blocks, node, ctx) - stmts = stmts + catchers + init_stmts = self._translate_init_call(target, args, node, ctx) + stmts.extend(init_stmts) + + # If the init method was created implicitly, we have to check for __post_init__ + if target_class.dataclass and target_class.implicit_init: + target = target_class.get_method('__post_init__') + if target: + post_init_stmts = self._translate_init_call(target, [res_var.ref()], node, ctx, '__post_init__') + stmts.extend(post_init_stmts) + return arg_stmts + defined_check + stmts, res_var.ref() + def _translate_init_call(self, target: PythonMethod, args: list, node: ast.Call, ctx: Context, name = '__init__') -> list: + target_class = target.cls + targets = [] + + if target.declared_exceptions: + error_var = self.get_error_var(node, ctx) + targets.append(error_var) + method_name = target_class.get_method(name).sil_name + stmts = self.create_method_call_node( + ctx, method_name, args, targets, self.to_position(node, ctx), + self.no_info(ctx), target_method=target, target_node=node) + if target.declared_exceptions: + catchers = self.create_exception_catchers(error_var, + ctx.actual_function.try_blocks, node, ctx) + stmts = stmts + catchers + return stmts + def _translate_list(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: contents = None stmts = [] diff --git a/tests/functional/verification/test_dataclass_override.py b/tests/functional/verification/test_dataclass_override.py new file mode 100644 index 000000000..d7a4751da --- /dev/null +++ b/tests/functional/verification/test_dataclass_override.py @@ -0,0 +1,68 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from typing import Optional, TypeVar, Generic +from dataclasses import dataclass + +T = TypeVar('T') + +@dataclass(frozen=True) +class ResultDataclass(Generic[T]): + success: bool + error_code: int = 0 + data: Optional[T] = None + + @Pure + def __bool__(self) -> bool: + return self.success + + def __post_init__(self) -> None: + Requires(Implies(self.success, self.data != None)) + Requires((self.success and self.error_code <= 0) or (not self.success and self.error_code > 0)) + + if not self.success and self.error_code <= 0: + raise Exception() + + if self.success and self.error_code > 0: + raise Exception() + +def test_bool() -> None: + res = ResultDataclass(True, 0, 'data') + + assert res.success + assert res + + #:: ExpectedOutput(assert.failed:assertion.false) + assert res.error_code == 1 + +def test_bool2(res: ResultDataclass[int]) -> None: + + if res.success: + assert res + + #:: ExpectedOutput(assert.failed:assertion.false) + assert res + +def test_init_False() -> None: + res = ResultDataclass(False, 1, '') + + #:: ExpectedOutput(call.precondition:assertion.false) + res = ResultDataclass(False, 0, None) + +def test_init_code() -> None: + res = ResultDataclass(True, 0, 'data') + + #:: ExpectedOutput(call.precondition:assertion.false) + res = ResultDataclass(True, 1, 'data') + +def test_init_None() -> None: + res = ResultDataclass(True, 0, 'data') + + #:: ExpectedOutput(call.precondition:assertion.false) + res = ResultDataclass(True, 0, None) + +def test_data(res: ResultDataclass[str]) -> None: + if res.success: + #:: ExpectedOutput(assert.failed:assertion.false) + assert isinstance(res.data, str) \ No newline at end of file From 2e67938a5a7385a95bf66a9c7116b9ddd004edd6 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 5 Jan 2026 16:26:25 +0100 Subject: [PATCH 070/126] Check for existing fields on superclass --- src/nagini_translation/lib/program_nodes.py | 6 ++-- .../verification/test_property_inherited.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/functional/verification/test_property_inherited.py diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 9456a3bb2..e477193ea 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -494,10 +494,10 @@ def add_field(self, name: str, node: ast.AST, type: 'PythonType') -> 'PythonField': """ Adds a field with the given name and type if it doesn't exist yet in - this class. + this class or a superclass. """ - if name in self.fields: - field = self.fields[name] + field = self.get_field(name) + if field != None: assert self.types_match(field.type.try_box(), type.try_box()) elif name in self.static_fields: field = self.static_fields[name] diff --git a/tests/functional/verification/test_property_inherited.py b/tests/functional/verification/test_property_inherited.py new file mode 100644 index 000000000..214699b2d --- /dev/null +++ b/tests/functional/verification/test_property_inherited.py @@ -0,0 +1,32 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + +class A: + def __init__(self, val: int) -> None: + Ensures(Acc(self._field)) # type: ignore + Ensures(self.field == val) + self._field = val + + @property + def field(self) -> int: + Requires(Acc(self._field)) + return self._field + +class B(A): + + def __init__(self, val: int) -> None: + Ensures(Acc(self._field)) + Ensures(self.field == val) + super().__init__(val) + + @Pure + def non_zero(self) -> bool: + Requires(Acc(self._field)) + return self.field != 0 + +def test() -> None: + b = B(5) + + assert b.non_zero() \ No newline at end of file From dfade3c9c4bd73297b3388cac81100b39e0bd139 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 6 Jan 2026 15:37:12 +0100 Subject: [PATCH 071/126] Fix constructor call for type argument change --- src/nagini_translation/translators/call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 5fa5b4660..320a5dc8e 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -245,7 +245,7 @@ def translate_constructor_call(self, target_class: PythonClass, if target_class.python_class.is_adt: return arg_stmts, self.translate_adt_cons(target_class, args, pos, ctx) - if target_class.enum: + if target_class.python_class.enum: return arg_stmts, self.translate_enum_cons(target_class, args, pos, ctx) res_var = ctx.current_function.create_variable(target_class.name + @@ -340,7 +340,7 @@ def translate_constructor_call(self, target_class: PythonClass, stmts.extend(init_stmts) # If the init method was created implicitly, we have to check for __post_init__ - if target_class.dataclass and target_class.implicit_init: + if target_class.python_class.dataclass and target_class.python_class.implicit_init: target = target_class.get_method('__post_init__') if target: post_init_stmts = self._translate_init_call(target, [res_var.ref()], node, ctx, '__post_init__') From e1952077b20ae936b477ff4cd67ee4968aa5242f Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 8 Jan 2026 09:46:00 +0100 Subject: [PATCH 072/126] Extend fix for #266 to union types --- src/nagini_translation/analyzer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 37192e940..4701fbda1 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1414,7 +1414,7 @@ def convert_type(self, mypy_type, node, bound_type_vars: Dict[str, PythonType] = result = GenericType(self.module.global_module.classes[TUPLE_TYPE], args) elif self.types.is_union_type(mypy_type): - return self._convert_union_type(mypy_type, node) + return self._convert_union_type(mypy_type, node, bound_type_vars) elif self.types.is_type_var(mypy_type): return self._convert_type_var(mypy_type, node, bound_type_vars) elif self.types.is_type_type(mypy_type): @@ -1464,8 +1464,8 @@ def _convert_normal_type(self, mypy_type) -> PythonType: def _convert_callable_type(self, mypy_type, node) -> PythonType: return self.find_or_create_class(CALLABLE_TYPE, module=self.module.global_module) - def _convert_union_type(self, mypy_type, node) -> PythonType: - args = [self.convert_type(arg_type, node) + def _convert_union_type(self, mypy_type, node, bound_type_vars: Dict[str, PythonType] = None) -> PythonType: + args = [self.convert_type(arg_type, node, bound_type_vars) for arg_type in mypy_type.items if not self.types.is_any_type_from_error(arg_type)] optional = False From de5fedaa14dfef9011270bfca923e38fd484213b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 9 Jan 2026 10:58:01 +0100 Subject: [PATCH 073/126] Extend shift operations to 64 bit, support int.bit_length() --- src/nagini_translation/resources/bool.sil | 100 ++++++++++++++---- .../resources/builtins.json | 2 +- .../verification/test_bitwise_op.py | 24 ++++- 3 files changed, 101 insertions(+), 25 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 5e21e52fb..900aa873a 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -118,7 +118,7 @@ function int___rxor__(self: Ref, other: Ref): Ref int___xor__(self, other) } -function __shift_factor(amount: Int): Int +function __shift_factor8(amount: Int): Int decreases _ requires 0 <= amount && amount <= 8 { @@ -133,22 +133,40 @@ function __shift_factor(amount: Int): Int 256 } +function __shift_factor32(amount: Int): Int + decreases _ + requires 0 <= amount && amount <= 32 +{ + amount <= 8 ? __shift_factor8(amount) : + amount <= 16 ? __shift_factor8(amount - 8) * 256 : + amount <= 24 ? __shift_factor8(amount - 16) * 256 * 256 : + __shift_factor8(amount - 24) * 256 * 256 * 256 +} + +function __shift_factor64(amount: Int): Int + decreases _ + requires 0 <= amount && amount <= 64 +{ + amount <= 32 ? __shift_factor32(amount) : + __shift_factor32(amount - 32) * __shift_factor32(32) +} + function __prim__int___lshift__(self: Int, other: Int): Int decreases _ - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self <= _INT_MAX) requires @error("Negative shift count.")(other >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { - 0 <= other <= 8 ? self * __shift_factor(other) : + 0 <= other <= 8 ? self * __shift_factor64(other) : self >= 0 ? fromBVInt(shlBVInt(toBVInt(self), toBVInt(other))) : -fromBVInt(shlBVInt(toBVInt(-self), toBVInt(other))) } function __prim__int___rlshift__(self: Int, other: Int): Int decreases _ - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self <= _INT_MAX) requires @error("Negative shift count.")(other >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { @@ -159,8 +177,8 @@ function int___lshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -172,8 +190,8 @@ function int___rlshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -182,21 +200,21 @@ function int___rlshift__(self: Ref, other: Ref): Ref function __prim__int___rshift__(self: Int, other: Int): Int decreases _ - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self <= _INT_MAX) requires @error("Negative shift count.")(other >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { - 0 <= other <= 8 ? self / __shift_factor(other) : + 0 <= other <= 8 ? self / __shift_factor64(other) : self >= 0 ? fromBVInt(shrBVInt(toBVInt(self), toBVInt(other))) : -fromBVInt(shrBVInt(toBVInt(-self), toBVInt(other))) } function __prim__int___rrshift__(self: Int, other: Int): Int decreases _ - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 8 || self <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= 64 || self <= _INT_MAX) requires @error("Negative shift count.")(other >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { @@ -207,8 +225,8 @@ function int___rshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -220,8 +238,8 @@ function int___rrshift__(self: Ref, other: Ref): Ref decreases _ requires issubtype(typeof(self), int()) requires issubtype(typeof(other), int()) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) >= _INT_MIN) - requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 8 || int___unbox__(self) <= _INT_MAX) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) >= _INT_MIN) + requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(self), bool()) ==> int___unbox__(other) <= 64 || int___unbox__(self) <= _INT_MAX) requires @error("Negative shift count.")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(!issubtype(typeof(other), bool()) ==> int___unbox__(other) <= _INT_MAX) { @@ -234,11 +252,47 @@ function int___bool__(self: Ref) : Bool ensures self == null ==> !result ensures self != null ==> result == (int___unbox__(self) != 0) -function int_bit_length(self: Ref): Ref +function __int_bit_length8(val: Int): Int decreases _ - requires self != null ==> issubtype(typeof(self), int()) - ensures typeof(result) == int() - // TODO add postcondition for value + requires 0 <= val && val < 256 +{ + val < 1 ? 0 : + val < 2 ? 1 : + val < 4 ? 2 : + val < 8 ? 3 : + val < 16 ? 4 : + val < 32 ? 5 : + val < 64 ? 6 : + val < 128 ? 7 : + 8 +} + +function __int_bit_length32(val: Int): Int + decreases _ + requires 0 <= val && val < __shift_factor32(32) +{ + val < __shift_factor32(8) ? __int_bit_length8(val) : + val < __shift_factor32(16) ? __int_bit_length8(val / __shift_factor32(8)) + 8 : + val < __shift_factor32(24) ? __int_bit_length8(val / __shift_factor32(16)) + 16 : + __int_bit_length8(val / __shift_factor32(24)) + 24 +} + +function __int_bit_length64(val: Int): Int + decreases _ + requires 0 <= val && val < __shift_factor64(64) +{ + val < __shift_factor32(32) ? __int_bit_length32(val) : + __int_bit_length32(val / __shift_factor32(32)) + 32 +} + +function int_bit_length(self: Ref): Int + decreases _ + requires issubtype(typeof(self), int()) + requires @error("bit_length only supported up to 64 bits")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) < __shift_factor64(64)) + requires @error("bit_length only supported up to 64 bits")(!issubtype(typeof(self), bool()) ==> int___unbox__(self) > -__shift_factor64(64)) +{ + __int_bit_length64(abs(int___unbox__(self))) +} function __prim__int___box__(prim: Int): Ref decreases _ diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index ceae461b8..ec49da0c2 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -330,7 +330,7 @@ }, "bit_length": { "args": ["int"], - "type": "int" + "type": "__prim__int" }, "__byte_bounds__": { "args": ["__prim__int"], diff --git a/tests/functional/verification/test_bitwise_op.py b/tests/functional/verification/test_bitwise_op.py index 31ab67908..3337f0d73 100644 --- a/tests/functional/verification/test_bitwise_op.py +++ b/tests/functional/verification/test_bitwise_op.py @@ -189,7 +189,7 @@ def lshift_general(a: int, b: int) -> None: assert shift == 1 def lshift_unlimited(a: int, b: int) -> None: - Requires(b >= 0 and b <= 8) + Requires(b >= 0 and b <= 64) shift = a << b @@ -212,6 +212,10 @@ def lshift_unlimited(a: int, b: int) -> None: assert shift == a * 128 if b == 8: assert shift == a * 256 + if b == 32: + assert shift == a * 4_294_967_296 + if b == 33: + assert shift == a * 8_589_934_592 #:: ExpectedOutput(assert.failed:assertion.false) assert shift == 1 @@ -265,3 +269,21 @@ def rshift_unlimited(a: int, b: int) -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert shift == 1 + +def int_bit_length() -> None: + + assert (0).bit_length() == 0 + assert (1).bit_length() == 1 + assert (3).bit_length() == 2 + assert (7).bit_length() == 3 + assert (15).bit_length() == 4 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert (3245).bit_length() == 6 + +def int_bit_length_general(a: int) -> None: + Requires(a >= 0 and a < (1 << 64)) + + if a > 2: + assert a.bit_length() == (a >> 1).bit_length() + 1 + \ No newline at end of file From c936624029ce9d91acd64f65fd3c54e2598d6b5b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 12 Jan 2026 00:21:01 +0100 Subject: [PATCH 074/126] Actually use extension to shift operations --- src/nagini_translation/resources/bool.sil | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 900aa873a..702f4a887 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -158,7 +158,7 @@ function __prim__int___lshift__(self: Int, other: Int): Int requires @error("Negative shift count.")(other >= 0) requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { - 0 <= other <= 8 ? self * __shift_factor64(other) : + 0 <= other <= 64 ? self * __shift_factor64(other) : self >= 0 ? fromBVInt(shlBVInt(toBVInt(self), toBVInt(other))) : -fromBVInt(shlBVInt(toBVInt(-self), toBVInt(other))) } @@ -206,7 +206,7 @@ function __prim__int___rshift__(self: Int, other: Int): Int requires @error("Bitwise operations on ints can only be performed in the range set by the --int-bitops-size setting (default: 8 bits).")(other <= _INT_MAX) { - 0 <= other <= 8 ? self / __shift_factor64(other) : + 0 <= other <= 64 ? self / __shift_factor64(other) : self >= 0 ? fromBVInt(shrBVInt(toBVInt(self), toBVInt(other))) : -fromBVInt(shrBVInt(toBVInt(-self), toBVInt(other))) } From cf91f9c9f575b282488a9d5c49e36a25f535de2b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 17 Jan 2026 11:33:17 +0100 Subject: [PATCH 075/126] Mark abstractmethods as ContractOnly --- src/nagini_translation/analyzer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 45a558640..fdec37019 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1711,7 +1711,9 @@ def _function_incompatible_decorators(self, decorators) -> bool: (('property' in decorators) and not(len(decorators) == 1 or (len(decorators) == 2 and 'ContractOnly' in decorators))) or (('AllLow' in decorators) and ('PreservesLow' in decorators)) or ((('AllLow' in decorators) or ('PreservesLow' in decorators)) and ( - ('Predicate' in decorators) or ('Pure' in decorators))) + ('Predicate' in decorators) or ('Pure' in decorators))) or + (('abstractmethod' in decorators) and # Python actually allows this, but only in the correct order + (('staticmethod' in decorators) or ('classmethod' in decorators))) ) def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: @@ -1722,7 +1724,7 @@ def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: decorators = {d.id for d in func.decorator_list if isinstance(d, ast.Name)} if self._function_incompatible_decorators(decorators): raise InvalidProgramException(func, "decorators.incompatible") - result = 'ContractOnly' in decorators + result = 'ContractOnly' in decorators or self.is_abstract_method(func) return result def is_contract_only(self, func: ast.FunctionDef) -> bool: @@ -1804,6 +1806,9 @@ def is_static_method(self, func: ast.FunctionDef) -> bool: def is_class_method(self, func: ast.FunctionDef) -> bool: return self.function_has_decorator(func, 'classmethod') + def is_abstract_method(self, func: ast.FunctionDef) -> bool: + return self.function_has_decorator(func, 'abstractmethod') + def is_io_operation(self, func: ast.FunctionDef) -> bool: return self.function_has_decorator(func, 'IOOperation') From 0d6f0eaeee940f5077b238b3455dfea5e638c0a5 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 19 Jan 2026 13:03:03 +0100 Subject: [PATCH 076/126] Print more info for Analyzer type exception --- src/nagini_translation/analyzer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index fdec37019..7003b13fe 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1439,6 +1439,10 @@ def convert_type(self, mypy_type, node, bound_type_vars: Dict[str, PythonType] = name = node.id elif hasattr(node, 'name'): name = node.name + elif isinstance(node, ast.Attribute): + name = node.attr + if hasattr(node.value, 'id'): + name = node.value.id + "." + name msg = 'Unsupported type: {} for node {}'.format(mypy_type.__class__.__name__, name) raise UnsupportedException(node, desc=msg) return result From 0140c1edd5f2d8106ef18f87d7ebd68a4d1ff677 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Mon, 19 Jan 2026 17:02:58 +0100 Subject: [PATCH 077/126] Fixing #275 and #276 --- src/nagini_translation/analyzer.py | 15 ++++++++--- src/nagini_translation/lib/program_nodes.py | 2 +- src/nagini_translation/lib/resolver.py | 5 ++++ tests/functional/verification/issues/00275.py | 27 +++++++++++++++++++ tests/functional/verification/issues/00276.py | 23 ++++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 tests/functional/verification/issues/00275.py create mode 100644 tests/functional/verification/issues/00276.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 1bf57b39f..4c70a7a4d 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1421,18 +1421,27 @@ def typeof(self, node: ast.AST) -> PythonType: type, _ = self.module.get_type(context, node.attr) set_of_types.add(self.convert_type(type, node)) return UnionType(list(set_of_types)) if len(set_of_types) > 1 else set_of_types.pop() + contexts = [] if isinstance(receiver, OptionalType): - context = [receiver.optional_type.name] + contexts.append([receiver.optional_type.name]) + rec_super = receiver.optional_type.superclass module = receiver.optional_type.module else: - context = [receiver.name] + contexts.append([receiver.name]) + rec_super = receiver.superclass module = receiver.module + while rec_super is not None: + contexts.append([rec_super.name]) + rec_super = rec_super.superclass bound_type_vars = None if isinstance(receiver, GenericType) or (isinstance(receiver, OptionalType) and isinstance(receiver.optional_type, GenericType)): gt = receiver if isinstance(receiver, GenericType) else receiver.optional_type bound_type_vars = zip(gt.cls.type_vars.keys(), gt.type_args) bound_type_vars = {k: v for (k, v) in bound_type_vars} - type, _ = module.get_type(context, node.attr) + for context in contexts: + type, _ = module.get_type(context, node.attr) + if type: + break return self.convert_type(type, node, bound_type_vars) elif isinstance(node, ast.arg): # Special case for cls parameter of classmethods; for those, we diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index 759412c37..25ce17153 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -826,7 +826,7 @@ def has_function(self, name: str) -> bool: types_set = self.get_types() - {None} result = len(types_set) > 0 for type in types_set: - result = result and type.has_function(name) + result = result and type.python_class.has_function(name) return result else: return self.cls.has_function(name) diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index bc6088305..3b31ee32d 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -228,6 +228,11 @@ def _do_get_type(node: ast.AST, containers: List[ContainerInterface], name_list = list(rectype.python_class.type_vars.keys()) index = name_list.index(target.type.name) return rectype.type_args[index] + if isinstance(node, ast.Attribute) and target.type.contains_type_var(): + rec_type = _do_get_type(node.value, containers, container) + type_subs = rec_type.get_bound_type_vars() + subst = target.type.substitute(type_subs) + return subst return target.type if isinstance(target, PythonField): result = target.type diff --git a/tests/functional/verification/issues/00275.py b/tests/functional/verification/issues/00275.py new file mode 100644 index 000000000..fcf9bd601 --- /dev/null +++ b/tests/functional/verification/issues/00275.py @@ -0,0 +1,27 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * + +class A: + + @property + def seg(self) -> PSeq[int]: + return PSeq(1,2,3) + +class B(A): + def test_is(self) -> None: + Ensures(self.seg is Old(self.seg)) + pass + + +class A2: + def __init__(self) -> None: + self.urgh = 5 + + +class B2(A2): + def test_is(self) -> None: + Ensures(Acc(self.urgh)) + Ensures(self.urgh is self.urgh) + Assume(False) \ No newline at end of file diff --git a/tests/functional/verification/issues/00276.py b/tests/functional/verification/issues/00276.py new file mode 100644 index 000000000..23e66c1f8 --- /dev/null +++ b/tests/functional/verification/issues/00276.py @@ -0,0 +1,23 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from typing import Generic, Optional, TypeVar + +T = TypeVar('T') +class A(Generic[T]): + + def __init__(self, val: Optional[T] = None) -> None: + Ensures(Acc(self._value) and self._value is val) # type: ignore + self._value = val + + @property + def value(self) -> Optional[T]: + Requires(Acc(self._value)) + return self._value + +def test_produce_seq() -> A[PSeq[int]]: + Ensures(Acc(Result()._value)) + Ensures(isinstance(Result().value, PSeq)) + Ensures(len(Result().value) == 3) + return A[PSeq[int]](PSeq(1,2,3)) \ No newline at end of file From fa9fde298a2adf0af52fd3028aceba8a6f62a5e6 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Mon, 19 Jan 2026 17:11:49 +0100 Subject: [PATCH 078/126] Same fix for function calls --- src/nagini_translation/lib/resolver.py | 15 ++++---------- tests/functional/verification/issues/00276.py | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/nagini_translation/lib/resolver.py b/src/nagini_translation/lib/resolver.py index 3b31ee32d..3a864d4bf 100644 --- a/src/nagini_translation/lib/resolver.py +++ b/src/nagini_translation/lib/resolver.py @@ -214,20 +214,13 @@ def _do_get_type(node: ast.AST, containers: List[ContainerInterface], if isinstance(target, PythonVarBase): return target.get_specific_type(node) if isinstance(target, PythonMethod): - if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and target.type.contains_type_var(): rec_target = get_target(node.func.value, containers, container) if not isinstance(rec_target, PythonModule): rectype = get_type(node.func.value, containers, container) - if target.generic_type != -1: - if target.generic_type == -2: - return rectype - return rectype.type_args[target.generic_type] - if isinstance(target.type, TypeVar): - while rectype.python_class is not target.cls: - rectype = rectype.superclass - name_list = list(rectype.python_class.type_vars.keys()) - index = name_list.index(target.type.name) - return rectype.type_args[index] + type_subs = rectype.get_bound_type_vars() + subst = target.type.substitute(type_subs) + return subst if isinstance(node, ast.Attribute) and target.type.contains_type_var(): rec_type = _do_get_type(node.value, containers, container) type_subs = rec_type.get_bound_type_vars() diff --git a/tests/functional/verification/issues/00276.py b/tests/functional/verification/issues/00276.py index 23e66c1f8..add8f80b9 100644 --- a/tests/functional/verification/issues/00276.py +++ b/tests/functional/verification/issues/00276.py @@ -20,4 +20,22 @@ def test_produce_seq() -> A[PSeq[int]]: Ensures(Acc(Result()._value)) Ensures(isinstance(Result().value, PSeq)) Ensures(len(Result().value) == 3) - return A[PSeq[int]](PSeq(1,2,3)) \ No newline at end of file + return A[PSeq[int]](PSeq(1,2,3)) + + +class A2(Generic[T]): + + def __init__(self, val: Optional[T] = None) -> None: + Ensures(Acc(self._value) and self._value is val) # type: ignore + self._value = val + + @Pure + def value(self) -> Optional[T]: + Requires(Acc(self._value)) + return self._value + +def test_produce_seq2() -> A2[PSeq[int]]: + Ensures(Acc(Result()._value)) + Ensures(isinstance(Result().value(), PSeq)) + Ensures(len(Result().value()) == 3) + return A2[PSeq[int]](PSeq(1,2,3)) \ No newline at end of file From 0661758e701e71c982a9f3d4b3f85958feb4c5cd Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 23 Jan 2026 09:38:41 +0100 Subject: [PATCH 079/126] Fix error with non-existing method_name --- src/nagini_translation/translators/call.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 320a5dc8e..1ac2a1764 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -548,7 +548,8 @@ def _translate_bytearray(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: bytearray_class = ctx.module.global_module.classes[BYTEARRAY_TYPE] res_var = ctx.current_function.create_variable('bytearray', bytearray_class, self.translator) targets = [res_var.ref()] - result_var = res_var.ref(node, ctx) + result_var = res_var.ref(node, ctx) + method_name = None # This could potentially be merged using the "display_name" field # by extending the general code for selecting a specific __init__ call From c1a98c96898940900f50d62580c72fd57617d691 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 23 Jan 2026 14:43:36 +0100 Subject: [PATCH 080/126] Fix context for self in dataclass properties --- src/nagini_translation/analyzer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 6fc4f18c2..a96d6a3ab 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1208,7 +1208,9 @@ def todo(): return # Add type info for self in this context, can retrieve the correct type from __init__.self - context = tuple([self.module.type_prefix, self.current_class.name, node.id, 'self']) + prefix = self.module.type_prefix.split('.') if self.module.type_prefix else [] + prefix.extend([self.current_class.name, node.id, 'self']) + context = tuple(prefix) self_type, _ = self.module.get_type([self.current_class.name, '__init__'], 'self') self.module.types.all_types[context] = self_type @@ -1452,7 +1454,9 @@ def convert_type(self, mypy_type, node, bound_type_vars: Dict[str, PythonType] = name = node.attr if hasattr(node.value, 'id'): name = node.value.id + "." + name - msg = 'Unsupported type: {} for node {}'.format(mypy_type.__class__.__name__, name) + elif isinstance(node, ast.arg): + name = node.arg + msg = 'Unsupported type: {} for node {} of type {}'.format(mypy_type.__class__.__name__, name, type(node)) raise UnsupportedException(node, desc=msg) return result From aaee70f6d7afc15d483a2befc467461bdfc9c3e6 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 23 Jan 2026 18:21:14 +0100 Subject: [PATCH 081/126] New approach for preprocessing mode --- src/nagini_translation/lib/typeinfo.py | 40 ++++++++++++++++++++++++-- src/nagini_translation/main.py | 4 +-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 884a14a11..1dd7baf30 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -12,6 +12,7 @@ import os from mypy.build import BuildSource +from mypy.fscache import FileSystemCache from nagini_translation.lib import config from nagini_translation.lib.constants import IGNORED_IMPORTS, LITERALS from nagini_translation.mypy_patches.visitor import TraverserVisitor @@ -19,6 +20,7 @@ from nagini_translation.lib.util import ( construct_lambda_prefix, + read_source_file, ) from typing import List, Optional @@ -265,6 +267,37 @@ def visit_comparison_expr(self, o: mypy.nodes.ComparisonExpr): if 'is' not in o.operators and 'is not' not in o.operators: super().visit_comparison_expr(o) +class PreprocessingFileSystemCache(FileSystemCache): + """ + Slightly adjusted FileSystemCache that invokes the custom read_source_file + to read file data. + """ + def read(self, path: str) -> bytes: + if path in self.read_cache: + return self.read_cache[path] + if path in self.read_error_cache: + raise self.read_error_cache[path] + + # Need to stat first so that the contents of file are from no + # earlier instant than the mtime reported by self.stat(). + self.stat(path) + + dirname, basename = os.path.split(path) + dirname = os.path.normpath(dirname) + # Check the fake cache. + if basename == '__init__.py' and dirname in self.fake_package_cache: + data = b'' + else: + try: + text = read_source_file(path) + data = text.encode() + except OSError as err: + self.read_error_cache[path] = err + raise + + self.read_cache[path] = data + self.hash_cache[path] = mypy.util.hash_digest(data) + return data class TypeInfo: """ @@ -363,15 +396,16 @@ def my_find_cache_meta(id, path, mgr): # In Python 3.9 or newer, we use the incremental mode, and we have to monkey-patch mypy. mypy.build.find_cache_meta = my_find_cache_meta - sources = [BuildSource(filename, module_name, text, base_dir=base_dir)] + sources = [BuildSource(filename, module_name, base_dir=base_dir)] - res_strict = mypy.build.build(sources, options_strict) + fscache = PreprocessingFileSystemCache() + res_strict = mypy.build.build(sources, options_strict, fscache=fscache) if res_strict.errors: # Run mypy a second time with strict optional checking disabled, # s.t. we don't get overapproximated none-related errors. options_non_strict = self._create_options(False) - res_non_strict = mypy.build.build(sources, options_non_strict) + res_non_strict = mypy.build.build(sources, options_non_strict, fscache=fscache) if res_non_strict.errors: report_errors(res_non_strict.errors) relevant_files = [next(iter(res_strict.graph))] diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index d896ab2bf..21473bbda 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -21,7 +21,6 @@ from nagini_translation.analyzer import Analyzer from nagini_translation.sif_translator import SIFTranslator from nagini_translation.lib import config -from nagini_translation.lib.util import read_source_file from nagini_translation.lib.constants import DEFAULT_SERVER_SOCKET from nagini_translation.lib.errors import error_manager from nagini_translation.lib.jvmaccess import ( @@ -129,8 +128,7 @@ def translate(path: str, jvm: JVM, bv_size: int, selected: Set[str] = set(), bas raise Exception('Viper AST SIF extension not found on classpath.') types = TypeInfo() - preprocessed_text = read_source_file(path) if config.enable_preprocessing else None - type_correct = types.check(path, base_dir, text=preprocessed_text) + type_correct = types.check(path, base_dir) if not type_correct: return None From 23907a3ac3b2411e27ba2b82870c0a4560518f76 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 24 Jan 2026 10:18:22 +0100 Subject: [PATCH 082/126] Ingoring preprocessing for the moment --- src/nagini_translation/lib/typeinfo.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/lib/typeinfo.py b/src/nagini_translation/lib/typeinfo.py index 1dd7baf30..d2877ff45 100644 --- a/src/nagini_translation/lib/typeinfo.py +++ b/src/nagini_translation/lib/typeinfo.py @@ -398,14 +398,15 @@ def my_find_cache_meta(id, path, mgr): sources = [BuildSource(filename, module_name, base_dir=base_dir)] - fscache = PreprocessingFileSystemCache() - res_strict = mypy.build.build(sources, options_strict, fscache=fscache) + # fscache = PreprocessingFileSystemCache() + # Ignoring preprocessing for now + res_strict = mypy.build.build(sources, options_strict) if res_strict.errors: # Run mypy a second time with strict optional checking disabled, # s.t. we don't get overapproximated none-related errors. options_non_strict = self._create_options(False) - res_non_strict = mypy.build.build(sources, options_non_strict, fscache=fscache) + res_non_strict = mypy.build.build(sources, options_non_strict) if res_non_strict.errors: report_errors(res_non_strict.errors) relevant_files = [next(iter(res_strict.graph))] From f4d1b476644f7d468eb18996a7daf1f4f49948ca Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 26 Jan 2026 11:01:02 +0100 Subject: [PATCH 083/126] Add base-dir when launch from vscode --- .vscode/launch.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 76ede1777..a4c801a3c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -65,6 +65,7 @@ "request": "launch", "program": "${workspaceFolder}/src/nagini_translation/main.py", "args": [ + "--base-dir=${fileDirname}", "${file}" ], "cwd": "${workspaceFolder}", From 6a8dc85d48b4d8dad51c42b04e9519faef5541ba Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 28 Jan 2026 08:59:45 +0100 Subject: [PATCH 084/126] Clean up legacy handling for abstract methods --- src/nagini_translation/analyzer.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index a96d6a3ab..73705f34a 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1737,9 +1737,7 @@ def _function_incompatible_decorators(self, decorators) -> bool: (('property' in decorators) and not(len(decorators) == 1 or (len(decorators) == 2 and 'ContractOnly' in decorators))) or (('AllLow' in decorators) and ('PreservesLow' in decorators)) or ((('AllLow' in decorators) or ('PreservesLow' in decorators)) and ( - ('Predicate' in decorators) or ('Pure' in decorators))) or - (('abstractmethod' in decorators) and # Python actually allows this, but only in the correct order - (('staticmethod' in decorators) or ('classmethod' in decorators))) + ('Predicate' in decorators) or ('Pure' in decorators))) ) def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: @@ -1750,7 +1748,6 @@ def is_declared_contract_only(self, func: ast.FunctionDef) -> bool: decorators = {d.id for d in func.decorator_list if isinstance(d, ast.Name)} if self._function_incompatible_decorators(decorators): raise InvalidProgramException(func, "decorators.incompatible") - result = 'ContractOnly' in decorators or self.is_abstract_method(func) result = 'ContractOnly' in decorators or 'abstractmethod' in decorators return result @@ -1832,9 +1829,6 @@ def is_static_method(self, func: ast.FunctionDef) -> bool: def is_class_method(self, func: ast.FunctionDef) -> bool: return self.function_has_decorator(func, 'classmethod') - - def is_abstract_method(self, func: ast.FunctionDef) -> bool: - return self.function_has_decorator(func, 'abstractmethod') def is_io_operation(self, func: ast.FunctionDef) -> bool: return self.function_has_decorator(func, 'IOOperation') From e62ea999537cc5fd4f4fde669888d7ab70b53232 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 3 Feb 2026 10:09:40 +0100 Subject: [PATCH 085/126] Slightly strength sequence take and drop functions --- src/nagini_translation/resources/seq.sil | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nagini_translation/resources/seq.sil b/src/nagini_translation/resources/seq.sil index 38e1fd88d..f494849bf 100644 --- a/src/nagini_translation/resources/seq.sil +++ b/src/nagini_translation/resources/seq.sil @@ -38,11 +38,13 @@ function PSeq_take(self: Ref, no: Int): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) ensures result == PSeq___create__(PSeq___sil_seq__(self)[..no], PSeq_arg(typeof(self), 0)) + ensures no == PSeq___len__(self) ==> result == self function PSeq_drop(self: Ref, no: Int): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) ensures result == PSeq___create__(PSeq___sil_seq__(self)[no..], PSeq_arg(typeof(self), 0)) + ensures no == 0 ==> result == self function PSeq_update(self: Ref, index: Int, val: Ref): Ref decreases _ From d1bc6c12cd8ba18ca033616302edea6c5d6ae577 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 4 Feb 2026 08:26:05 +0100 Subject: [PATCH 086/126] Add handling for hash --- src/nagini_translation/lib/constants.py | 2 ++ src/nagini_translation/resources/bool.sil | 4 +++- src/nagini_translation/resources/builtins.json | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/lib/constants.py b/src/nagini_translation/lib/constants.py index 5467b5b43..9df4d076e 100644 --- a/src/nagini_translation/lib/constants.py +++ b/src/nagini_translation/lib/constants.py @@ -264,9 +264,11 @@ '__enter__', '__exit__', '__str__', + '__repr__', '__len__', '__bool__', '__format__', + '__hash__', '__getitem__', '__setitem__', diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 702f4a887..c26613c7b 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -463,7 +463,7 @@ function int___byte_bounds__(value: Int): Bool decreases _ { 0 <= value < 256 -} +} domain __ObjectEquality { function object___eq__(Ref, Ref): Bool @@ -534,6 +534,8 @@ method sorted(r: Ref) returns (rs: Ref) ensures list___len__(r) > 1 ==> forall i: Int :: { r.list_acc[i] } i >= 0 && i < list___len__(r) ==> int___unbox__(list___getitem__(r, __prim__int___box__(i))) >= int___unbox__(list___getitem__(rs, __prim__int___box__(0))) ensures list___len__(r) > 1 ==> forall i: Int :: { r.list_acc[i] } i >= 0 && i < list___len__(r) ==> int___unbox__(list___getitem__(r, __prim__int___box__(i))) <= int___unbox__(list___getitem__(rs, __prim__int___box__(list___len__(r) - 1))) +method hash(r: Ref) returns (h: Ref) + ensures issubtype(typeof(h), int()) function sum(r: Ref): Int requires issubtype(typeof(r), list(int())) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index ec49da0c2..5d8c3aa65 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -1136,6 +1136,11 @@ "args": ["list"], "type": "list", "MustTerminate": true + }, + "hash": { + "args": ["object"], + "type": "int", + "MustTerminate": true } } } From efd52166b024f62153d7c6011c512ed4e73d6748 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 4 Feb 2026 14:20:42 +0100 Subject: [PATCH 087/126] Launch files with parent directory as base-dir --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a4c801a3c..6a224085d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -65,7 +65,7 @@ "request": "launch", "program": "${workspaceFolder}/src/nagini_translation/main.py", "args": [ - "--base-dir=${fileDirname}", + "--base-dir=${fileDirname}/..", "${file}" ], "cwd": "${workspaceFolder}", From 59793347bf0cd06bb5d2c4c1eeb8c03749af3c8c Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 5 Feb 2026 10:22:52 +0100 Subject: [PATCH 088/126] Fix pbyteseq --- src/nagini_translation/models/converter.py | 3 ++- tests/functional/verification/test_pbyteseq.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/models/converter.py b/src/nagini_translation/models/converter.py index 8848d5209..0b0219d3e 100644 --- a/src/nagini_translation/models/converter.py +++ b/src/nagini_translation/models/converter.py @@ -632,7 +632,8 @@ def convert_pseq_value(self, val, t: PythonType, name): def convert_PByteSeq_value(self, val, name): sequence = self.get_func_value(UNBOX_PBYTESEQ, (UNIT, val)) - sequence_info = self.convert_sequence_value(sequence, type(int), name) + int_type = self.modules[0].global_module.classes['int'] + sequence_info = self.convert_sequence_value(sequence, int_type, name) return 'Sequence: {{ {} }}'.format(', '.join(['{} -> {}'.format(k, v) for k, v in sequence_info.items()])) def convert_int_value(self, val): diff --git a/tests/functional/verification/test_pbyteseq.py b/tests/functional/verification/test_pbyteseq.py index 030981d2b..76dfea5a5 100644 --- a/tests/functional/verification/test_pbyteseq.py +++ b/tests/functional/verification/test_pbyteseq.py @@ -44,7 +44,7 @@ def test_byteseq_bounds_high(b: PByteSeq) -> None: def test_range() -> None: ints = PByteSeq(1,3,5,6,8) - r = ints.range(1, 3) + r = ints.drop(1).take(2) assert len(ints) == 5 assert len(r) == 2 From ea42cc409857acbffc1ee16864fd1f391722433a Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 5 Feb 2026 10:23:21 +0100 Subject: [PATCH 089/126] Copy changes for pseq to pbyteseq --- src/nagini_translation/resources/byteseq.sil | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nagini_translation/resources/byteseq.sil b/src/nagini_translation/resources/byteseq.sil index e0aaf4eb0..0dd656810 100644 --- a/src/nagini_translation/resources/byteseq.sil +++ b/src/nagini_translation/resources/byteseq.sil @@ -45,11 +45,13 @@ function PByteSeq_take(self: Ref, no: Int): Ref decreases _ requires issubtype(typeof(self), PByteSeq()) ensures result == PByteSeq___create__(PByteSeq___val__(self)[..no]) + ensures no == PByteSeq___len__(self) ==> result == self function PByteSeq_drop(self: Ref, no: Int): Ref decreases _ requires issubtype(typeof(self), PByteSeq()) ensures result == PByteSeq___create__(PByteSeq___val__(self)[no..]) + ensures no == 0 ==> result == self function PByteSeq_update(self: Ref, index: Int, val: Int): Ref decreases _ From 811d36a4f35f408e48f90a9d4e4776c8817c4754 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 5 Feb 2026 11:53:59 +0100 Subject: [PATCH 090/126] Add simple byteseq interop test --- tests/functional/verification/test_pbyteseq.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/functional/verification/test_pbyteseq.py b/tests/functional/verification/test_pbyteseq.py index 76dfea5a5..460bc3e98 100644 --- a/tests/functional/verification/test_pbyteseq.py +++ b/tests/functional/verification/test_pbyteseq.py @@ -1,6 +1,7 @@ # Any copyright is dedicated to the Public Domain. # http://creativecommons.org/publicdomain/zero/1.0/ +from typing import List from nagini_contracts.contracts import * @@ -77,4 +78,13 @@ def test_bytearray_bounds(b_array: bytearray) -> None: assert 0 <= seq[0] and seq[0] <= 0xFF #:: ExpectedOutput(assert.failed:assertion.false) - assert seq[1] >= 256 \ No newline at end of file + assert seq[1] >= 256 + +def test_list_interop(b_array: bytearray) -> None: + Requires(bytearray_pred(b_array)) + l = list(b_array) + + byteseq_direct = ToByteSeq(b_array) + byteseq = ToByteSeq(l) + + assert byteseq_direct == byteseq \ No newline at end of file From 5f40542aa2cf76fd9c68193d7d5c49ec1bfd6d6d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 6 Feb 2026 11:03:55 +0100 Subject: [PATCH 091/126] Merge __str__ changes, add __repr__ --- src/nagini_translation/resources/builtins.json | 12 +++++++++--- src/nagini_translation/resources/references.sil | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 5d8c3aa65..20e5a2c4a 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -9,13 +9,19 @@ "args": ["object", "object"], "type": "__prim__bool" }, + "__cast__": { + "args": ["type", "object"], + "type": "object" + } + }, + "methods": { "__str__": { "args": ["object"], "type": "str" }, - "__cast__": { - "args": ["type", "object"], - "type": "object" + "__repr__": { + "args": ["object"], + "type": "str" } } }, diff --git a/src/nagini_translation/resources/references.sil b/src/nagini_translation/resources/references.sil index bd944557b..0315917a0 100644 --- a/src/nagini_translation/resources/references.sil +++ b/src/nagini_translation/resources/references.sil @@ -6,6 +6,16 @@ */ - function object___str__(self: Ref) : Ref +method object___str__(self: Ref) returns (res: Ref) + ensures issubtype(typeof(res), str()) + ensures str___val__(res) == object___str_val__(self) + +function object___str_val__(self: Ref): Seq[Int] decreases _ - ensures issubtype(typeof(result), str()) \ No newline at end of file + +method object___repr__(self: Ref) returns (res: Ref) + ensures issubtype(typeof(res), str()) + ensures str___val__(res) == object___repr_val__(self) + +function object___repr_val__(self: Ref): Seq[Int] + decreases _ \ No newline at end of file From 9a3a82a2fef2545a207010fd68c348ea187bd620 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Wed, 11 Feb 2026 14:04:20 +0100 Subject: [PATCH 092/126] Stronger type information about ADTs --- src/nagini_translation/analyzer.py | 2 + src/nagini_translation/translators/program.py | 2 +- tests/functional/verification/test_adt_4.py | 38 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/functional/verification/test_adt_4.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index aae9e0b35..570a807ed 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1228,6 +1228,8 @@ def visit_Attribute(self, node: ast.Attribute) -> None: self.track_access(node, real_target) else: receiver = self.typeof(node.value) + if isinstance(receiver, PythonClass) and receiver.is_adt: + return if (isinstance(receiver, UnionType) and not isinstance(receiver, OptionalType)): for type in receiver.get_types() - {None}: diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index a66c4d2ba..e5ac823c4 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1048,7 +1048,7 @@ def _create_adt_func_box_and_unbox(self, adt: PythonClass, adt_type: DomainType, self.type_factory.type_type(), pos, info, self.type_factory.type_domain) typeof_eq = self.viper.EqCmp(typeof_call, const_call, pos, info) - postconds.append(self.viper.Implies(is_cons_call, typeof_eq, pos, info)) + postconds.append(self.viper.EqCmp(is_cons_call, typeof_eq, pos, info)) other_object = self.viper.FuncApp(box_func_name, [adt_other_use], pos, info, self.viper.Ref, [adt_other_decl]) other_is_result = self.viper.EqCmp(self.viper.Result(self.viper.Ref, pos, info), diff --git a/tests/functional/verification/test_adt_4.py b/tests/functional/verification/test_adt_4.py new file mode 100644 index 000000000..61c2360d9 --- /dev/null +++ b/tests/functional/verification/test_adt_4.py @@ -0,0 +1,38 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + + +from nagini_contracts.contracts import * +from nagini_contracts.adt import ADT +from typing import ( + NamedTuple, +) + +class Segment_ADT(ADT): + pass + + +class Segment(Segment_ADT, NamedTuple('Segment', [('length', int), ('value', int)])): + pass + + +@Pure +def segment_eq_lemma(s1: Segment, s2: Segment) -> bool: + Requires(s1.length == s2.length) + Requires(s1.value == s2.value) + Ensures(s1 == s2) + return True + +@Pure +def segment_eq_lemma_2(s1: Segment, s2: Segment) -> bool: + Requires(s1.length is s2.length) + Requires(s1.value is s2.value) + Ensures(s1 == s2) + return True + +@Pure +def segment_eq_lemma_3(s1: Segment, s2: Segment) -> bool: + Requires(s1.length == s2.length) + #:: ExpectedOutput(postcondition.violated:assertion.false) + Ensures(s1 == s2) + return True \ No newline at end of file From 2a9e2e5772a829e8aaf75e4b99b5b84f5d024290 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Wed, 11 Feb 2026 14:06:44 +0100 Subject: [PATCH 093/126] Better tests --- tests/functional/verification/test_adt_2.py | 2 ++ tests/functional/verification/test_adt_3.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/functional/verification/test_adt_2.py b/tests/functional/verification/test_adt_2.py index 22a20ec75..918e5c0aa 100644 --- a/tests/functional/verification/test_adt_2.py +++ b/tests/functional/verification/test_adt_2.py @@ -32,6 +32,8 @@ def common_use_of_ADTs()-> None: assert cast(Leaf, t_3.left).elem == 5 assert cast(Leaf, t_3.right).elem == 6 + #:: ExpectedOutput(assert.failed:assertion.false) + assert False def check_type_is_known(l: Leaf) -> int: diff --git a/tests/functional/verification/test_adt_3.py b/tests/functional/verification/test_adt_3.py index 17cf05c40..923dd420d 100644 --- a/tests/functional/verification/test_adt_3.py +++ b/tests/functional/verification/test_adt_3.py @@ -51,6 +51,8 @@ def common_use_of_ADTs()-> None: Assert(type(cast(Node, polymorphic_tree).right) is Leaf) Assert(type(cast(Leaf, cast(Node, polymorphic_tree).right).fruit) is Grape) + #:: ExpectedOutput(assert.failed:assertion.false) + Assert(False) # Ordinary class class Property: From 23f65da002bfda5da32df559a1a244685dbf3b9f Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 12 Feb 2026 09:02:13 +0100 Subject: [PATCH 094/126] Disallow extending enumerations, which isn't caught by mypy --- src/nagini_translation/analyzer.py | 2 ++ tests/functional/translation/test_enum.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index e0240eb0e..4cc1d1530 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -583,6 +583,8 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: cls.superclass = self.find_or_create_class(OBJECT_TYPE) if cls.python_class not in cls.superclass.python_class.direct_subclasses: cls.superclass.python_class.direct_subclasses.append(cls.python_class) + if cls.superclass.python_class.enum: + raise InvalidProgramException(node, 'Cannot extend enumeration') if cls.superclass.name == "IntEnum": cls.enum = True cls.enum_type = INT_TYPE diff --git a/tests/functional/translation/test_enum.py b/tests/functional/translation/test_enum.py index 0e1f9bc00..67e748506 100644 --- a/tests/functional/translation/test_enum.py +++ b/tests/functional/translation/test_enum.py @@ -6,4 +6,8 @@ class flag(IntEnum): success = 0 - failure = 1 \ No newline at end of file + failure = 1 + +#:: ExpectedOutput(type.error:Cannot extend enumeration) +class sub_flag(flag): + unknown = 3 \ No newline at end of file From f6b8006065ab01f105f36cc6fe1a723f0fa49684 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 12 Feb 2026 09:03:32 +0100 Subject: [PATCH 095/126] Adjust expected error type --- tests/functional/translation/test_enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/translation/test_enum.py b/tests/functional/translation/test_enum.py index 67e748506..91e68332f 100644 --- a/tests/functional/translation/test_enum.py +++ b/tests/functional/translation/test_enum.py @@ -8,6 +8,6 @@ class flag(IntEnum): success = 0 failure = 1 -#:: ExpectedOutput(type.error:Cannot extend enumeration) +#:: ExpectedOutput(invalid.program:Cannot extend enumeration) class sub_flag(flag): unknown = 3 \ No newline at end of file From 88daee906e1898872651bdd1b94f6e6f394c7c33 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 12 Feb 2026 10:18:06 +0100 Subject: [PATCH 096/126] Expand handling for IntEnum --- src/nagini_translation/translators/common.py | 20 ++++-- .../translators/expression.py | 18 ++++-- src/nagini_translation/translators/program.py | 10 +++ .../translators/type_domain_factory.py | 3 + .../functional/verification/test_enum_int.py | 62 ++++++++++++++++--- 5 files changed, 93 insertions(+), 20 deletions(-) diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 6be64b4c9..161c99030 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -188,11 +188,13 @@ def to_bool(self, e: Expr, ctx: Context, node: ast.AST = None) -> Expr: position=e.pos()) return result - def to_int(self, e: Expr, ctx: Context) -> Expr: + def to_int(self, e: Expr, ctx: Context, + python_type: 'PythonType' = None) -> Expr: """ Converts the given expression to an expression of the Silver type Int if it isn't already, either by unboxing a reference or undoing a - previous boxing operation. + previous boxing operation. When python_type is an enum, uses the + enum-specific unbox function to preserve value range information. """ # Avoid wrapping non-pure expressions (leads to errors within Silver's # Consistency object) @@ -206,10 +208,16 @@ def to_int(self, e: Expr, ctx: Context) -> Expr: e.funcname() == '__prim__int___box__'): return e.args().head() result = e - int_type = ctx.module.global_module.classes[INT_TYPE] - result = self.get_function_call(int_type, '__unbox__', - [result], [None], None, ctx, - position=e.pos()) + if python_type and python_type.python_class.enum and python_type.python_class.enum_type == INT_TYPE: + unbox_name = python_type.sil_name + '__unbox__' + result = self.viper.FuncApp(unbox_name, [result], + e.pos(), self.no_info(ctx), + self.viper.Int) + else: + int_type = ctx.module.global_module.classes[INT_TYPE] + result = self.get_function_call(int_type, '__unbox__', + [result], [None], None, ctx, + position=e.pos()) return result def unwrap(self, e: Expr) -> Expr: diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index d2e9e290b..f0666e6a6 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -1177,18 +1177,24 @@ def translate_Compare(self, node: ast.Compare, position = self.to_position(node, ctx) info = self.no_info(ctx) - # TODO add handling for IntEnum + if isinstance(node.ops[0], ast.Is): + return (stmts, self.viper.EqCmp(left, right, position, info)) + elif isinstance(node.ops[0], ast.IsNot): + return (stmts, self.viper.NeCmp(left, right, position, info)) + + # Unbox IntEnum to int + if left_type.python_class.enum and left_type.python_class.enum_type == INT_TYPE: + left = self.to_int(left, ctx, left_type) + left_type = ctx.module.global_module.classes[INT_TYPE] + if right_type.python_class.enum and right_type.python_class.enum_type == INT_TYPE: + right = self.to_int(right, ctx, right_type) + right_type = ctx.module.global_module.classes[INT_TYPE] if self._is_primitive_operation(node.ops[0], left_type, right_type): result = self._translate_primitive_operation(left, right, left_type, node.ops[0], position, ctx) return stmts, result - - if isinstance(node.ops[0], ast.Is): - return (stmts, self.viper.EqCmp(left, right, position, info)) - elif isinstance(node.ops[0], ast.IsNot): - return (stmts, self.viper.NeCmp(left, right, position, info)) elif isinstance(node.ops[0], (ast.In, ast.NotIn)): contains_stmts, contains_expr = self._translate_contains( left, right, left_type, right_type, node, ctx) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 50e6e6e0c..66ec827f9 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1217,6 +1217,16 @@ def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> li preconds.append(self.type_factory.type_check(ref_use, enum, pos, ctx, True)) + # Add postcondition constraining the result to valid enum values + enum_value_postcond = self.viper.FalseLit(pos, info) + for field_name in enum.static_fields: + field = enum.get_static_field(field_name) + if field and field.value: + _, enum_value_expr = self.translate_expr(field.value, ctx, self.viper.Int) + value_check = self.viper.EqCmp(result, enum_value_expr, pos, info) + enum_value_postcond = self.viper.Or(enum_value_postcond, value_check, pos, info) + postconds.append(enum_value_postcond) + # Add forall postcondition i_var_use = self.viper.LocalVar('i', self.viper.Ref, pos, info) i_var_decl = self.viper.LocalVarDecl('i', self.viper.Ref, pos, info) diff --git a/src/nagini_translation/translators/type_domain_factory.py b/src/nagini_translation/translators/type_domain_factory.py index 6082137a5..1f0fc8c97 100644 --- a/src/nagini_translation/translators/type_domain_factory.py +++ b/src/nagini_translation/translators/type_domain_factory.py @@ -829,6 +829,9 @@ def type_check(self, lhs: 'Expr', type: 'PythonType', just_part = self.subtype_check(type_func, type.cls, position, ctx, concrete=concrete) return self.viper.Or(none_part, just_part, position, info) + # Enums cannot be subclassed, so we can use exact type equality + if type.python_class.enum: + concrete = True return self.subtype_check(type_func, type, position, ctx, concrete=concrete) diff --git a/tests/functional/verification/test_enum_int.py b/tests/functional/verification/test_enum_int.py index 01ff022a3..a30e98f7e 100644 --- a/tests/functional/verification/test_enum_int.py +++ b/tests/functional/verification/test_enum_int.py @@ -4,19 +4,65 @@ from nagini_contracts.contracts import * from enum import IntEnum -# class flag(int): -# pass - -# def test() -> None: -# f = flag(1) -# assert f == flag(1) - class flag(IntEnum): success = 0 failure = 1 -def test() -> None: +class flag2(IntEnum): + success = 0 + failure = 2 + +def test_value() -> None: f = flag(1) assert f == flag(1) + assert f == 1 + assert f == flag.failure + + assert flag.success == 0 + assert flag.success == flag(0) + assert flag.success == False + assert flag2(0) == flag(0) + + #:: ExpectedOutput(assert.failed:assertion.false) + assert flag.success == flag.failure + +def test_comparison() -> None: + f0 = flag(0) + f1 = flag(0) + f2 = flag2(0) + + assert f0 == f1 + assert f0 is f1 + + assert f1 == f2 + #:: ExpectedOutput(assert.failed:assertion.false) + assert f1 is f2 + +def test_value3() -> None: + assert flag.success == flag2.success + + #:: ExpectedOutput(assert.failed:assertion.false) + assert flag.failure == flag2.failure + +def test_contraints(f: flag) -> None: + assert 0 <= f + assert f <= 1 + +def test_contraints2(f: flag2) -> None: + assert f == 0 or f == 2 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert f == 0 + +def test_contraints3(f: flag2) -> None: + assert f != 1 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert f == 3 + +def test_precond(f: flag) -> None: + Requires(f == flag.success) + + #:: ExpectedOutput(assert.failed:assertion.false) assert f == 1 \ No newline at end of file From 4cc62c49f03b880a33b277efe947fca09a84389e Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 16 Feb 2026 10:59:14 +0100 Subject: [PATCH 097/126] Allow ast.Attribute usage for default value in dataclasses --- src/nagini_translation/analyzer.py | 4 +-- .../translation/test_dataclass_defaults.py | 16 ++++++++++++ .../verification/test_dataclass_defaults.py | 26 +++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tests/functional/translation/test_dataclass_defaults.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 4cc1d1530..0d3b10dec 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -1235,8 +1235,8 @@ def todo(): raise UnsupportedException(assign, msg) if assign.value != None: - if not isinstance(assign.value, ast.Constant): - raise UnsupportedException(assign, 'Only constants allowed for datafield default value') + if not isinstance(assign.value, (ast.Constant, ast.Attribute)): + raise UnsupportedException(assign, 'Illegal default value for datafield creation') # Temporarily set value, because it will be used as default self.current_class.fields[node.id].result = assign.value diff --git a/tests/functional/translation/test_dataclass_defaults.py b/tests/functional/translation/test_dataclass_defaults.py new file mode 100644 index 000000000..88332fc98 --- /dev/null +++ b/tests/functional/translation/test_dataclass_defaults.py @@ -0,0 +1,16 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from dataclasses import dataclass +from nagini_contracts.contracts import * +from enum import IntEnum + +class Color_Enum(IntEnum): + red = 0 + green = 1 + blue = 2 + yellow = 3 + +@dataclass(frozen=True) +class MyClass(): + color: Color_Enum = Color_Enum.red \ No newline at end of file diff --git a/tests/functional/verification/test_dataclass_defaults.py b/tests/functional/verification/test_dataclass_defaults.py index cd1f5817c..d0ac07667 100644 --- a/tests/functional/verification/test_dataclass_defaults.py +++ b/tests/functional/verification/test_dataclass_defaults.py @@ -1,6 +1,7 @@ # Any copyright is dedicated to the Public Domain. # http://creativecommons.org/publicdomain/zero/1.0/ +from enum import IntEnum from nagini_contracts.contracts import * from dataclasses import dataclass @@ -13,7 +14,17 @@ class A: class B: num: int my_field: int = 5 - + +class Color_Enum(IntEnum): + red = 0 + green = 1 + blue = 2 + yellow = 3 + +@dataclass(frozen=True) +class C: + color: Color_Enum = Color_Enum.green + def test_default_vals1() -> None: a = A() @@ -32,4 +43,15 @@ def test_default_vals2(val: int) -> None: assert b2.num == b2.my_field #:: ExpectedOutput(assert.failed:assertion.false) - assert b2.my_field == 5 \ No newline at end of file + assert b2.my_field == 5 + +def test_default_val_enum() -> None: + c = C() + + assert c.color == Color_Enum.green + + c2 = C(Color_Enum.yellow) + assert c2.color == Color_Enum.yellow + + # :: ExpectedOutput(assert.failed:assertion.false) + assert c.color == c2.color \ No newline at end of file From 4dea086ccc399fee141bb9c81afbe47b8a6fdf5d Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 16 Feb 2026 12:01:55 +0100 Subject: [PATCH 098/126] Use enum unbox function for int conversion call --- src/nagini_translation/translators/call.py | 4 ++++ tests/functional/verification/test_enum_int.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 1ac2a1764..0822e880c 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -142,6 +142,10 @@ def _translate_int(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: assert len(node.args) == 1 stmt, target = self.translate_expr(node.args[0], ctx) arg_type = self.get_type(node.args[0], ctx) + if arg_type.enum and arg_type.enum_type == INT_TYPE: + unboxed = self.to_int(target, ctx, arg_type) + boxed = self.to_ref(unboxed, ctx) + return stmt, boxed str_stmt, str_val = self.get_func_or_method_call(arg_type, '__int__', [target], [None], node, ctx) return stmt + str_stmt, str_val diff --git a/tests/functional/verification/test_enum_int.py b/tests/functional/verification/test_enum_int.py index a30e98f7e..9625e2dbf 100644 --- a/tests/functional/verification/test_enum_int.py +++ b/tests/functional/verification/test_enum_int.py @@ -49,6 +49,9 @@ def test_contraints(f: flag) -> None: assert 0 <= f assert f <= 1 + assert 0 <= int(f) + assert int(f) <= 1 + def test_contraints2(f: flag2) -> None: assert f == 0 or f == 2 From 9acf51afc971e3936a89172c1212b7ebe0499bbe Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 16 Feb 2026 17:54:33 +0100 Subject: [PATCH 099/126] Workaround for Union types --- src/nagini_translation/translators/call.py | 3 ++- src/nagini_translation/translators/common.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 0822e880c..c5ebb57fc 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -142,7 +142,8 @@ def _translate_int(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: assert len(node.args) == 1 stmt, target = self.translate_expr(node.args[0], ctx) arg_type = self.get_type(node.args[0], ctx) - if arg_type.enum and arg_type.enum_type == INT_TYPE: + # hasattr check as workaround for Union types, which are not properly covered + if hasattr(arg_type, "enum") and arg_type.enum and arg_type.enum_type == INT_TYPE: unboxed = self.to_int(target, ctx, arg_type) boxed = self.to_ref(unboxed, ctx) return stmt, boxed diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 161c99030..185dee5e3 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -193,8 +193,7 @@ def to_int(self, e: Expr, ctx: Context, """ Converts the given expression to an expression of the Silver type Int if it isn't already, either by unboxing a reference or undoing a - previous boxing operation. When python_type is an enum, uses the - enum-specific unbox function to preserve value range information. + previous boxing operation. """ # Avoid wrapping non-pure expressions (leads to errors within Silver's # Consistency object) From b79a51cb670d21ee4d7152a8a8a7938ab1fefe9c Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 16 Feb 2026 18:53:57 +0100 Subject: [PATCH 100/126] Remove legacy range test --- tests/functional/verification/test_pseq.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/functional/verification/test_pseq.py b/tests/functional/verification/test_pseq.py index 92c586264..7962329b8 100644 --- a/tests/functional/verification/test_pseq.py +++ b/tests/functional/verification/test_pseq.py @@ -35,20 +35,6 @@ def test_seq() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert False -def test_range() -> None: - ints = PSeq(1,3,5,6,8) - r = ints.range(1, 3) - - assert len(ints) == 5 - assert len(r) == 2 - assert 5 in r - assert r[0] == 3 - assert 1 not in r - assert 8 not in r - - #:: ExpectedOutput(assert.failed:assertion.false) - assert r[1] == 6 - def test_list_ToSeq() -> None: a = [1,2,3] assert ToSeq(a) == PSeq(1,2,3) From a731704675f90edca690232baa9c2ad3f46fd66c Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 16 Feb 2026 18:55:01 +0100 Subject: [PATCH 101/126] Add reverse postcondition for int sequence conversion --- src/nagini_translation/resources/bool.sil | 1 + tests/functional/verification/test_pbyteseq.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index c26613c7b..4ae8b6cf3 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -548,6 +548,7 @@ function __seq_ref_to_seq_int(sr: Seq[Ref]): Seq[Int] ensures sr == Seq() ==> result == Seq() ensures forall r: Ref :: {__seq_ref_to_seq_int(Seq(r))} issubtype(typeof(r), int()) ==> __seq_ref_to_seq_int(Seq(r)) == Seq(int___unbox__(r)) ensures forall sr1: Seq[Ref], sr2: Seq[Ref] :: {__seq_ref_to_seq_int(sr1 ++ sr2)} __seq_ref_to_seq_int(sr1 ++ sr2) == __seq_ref_to_seq_int(sr1) ++ __seq_ref_to_seq_int(sr2) + ensures forall i: Int :: {(i in result)} (i in result) ==> (exists j: Int :: 0 <= j && j < |sr| && issubtype(typeof(sr[j]), int()) && i == int___unbox__(sr[j]) && (sr[j] in sr)) decreases _ diff --git a/tests/functional/verification/test_pbyteseq.py b/tests/functional/verification/test_pbyteseq.py index 460bc3e98..1a040b390 100644 --- a/tests/functional/verification/test_pbyteseq.py +++ b/tests/functional/verification/test_pbyteseq.py @@ -87,4 +87,15 @@ def test_list_interop(b_array: bytearray) -> None: byteseq_direct = ToByteSeq(b_array) byteseq = ToByteSeq(l) - assert byteseq_direct == byteseq \ No newline at end of file + assert byteseq_direct == byteseq + +def test_list_interop2(b_list: List[int]) -> None: + Requires(list_pred(b_list)) + Requires(Forall(b_list, lambda el: 0 <= el and el < 256)) + + byteseq_direct = ToByteSeq(b_list) + + b_array = bytearray(b_list) + byteseq = ToByteSeq(b_array) + + assert byteseq_direct == byteseq From ccd2655c6f0211a160b2830b77c8180a6722c43f Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 17 Feb 2026 14:41:30 +0100 Subject: [PATCH 102/126] Rename IntEnum __unbox__ to __int__ and register as viper function --- src/nagini_translation/translators/call.py | 5 ---- src/nagini_translation/translators/common.py | 2 +- src/nagini_translation/translators/program.py | 27 +++++++++++++++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index c5ebb57fc..1ac2a1764 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -142,11 +142,6 @@ def _translate_int(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: assert len(node.args) == 1 stmt, target = self.translate_expr(node.args[0], ctx) arg_type = self.get_type(node.args[0], ctx) - # hasattr check as workaround for Union types, which are not properly covered - if hasattr(arg_type, "enum") and arg_type.enum and arg_type.enum_type == INT_TYPE: - unboxed = self.to_int(target, ctx, arg_type) - boxed = self.to_ref(unboxed, ctx) - return stmt, boxed str_stmt, str_val = self.get_func_or_method_call(arg_type, '__int__', [target], [None], node, ctx) return stmt + str_stmt, str_val diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 185dee5e3..81ee556fb 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -208,7 +208,7 @@ def to_int(self, e: Expr, ctx: Context, return e.args().head() result = e if python_type and python_type.python_class.enum and python_type.python_class.enum_type == INT_TYPE: - unbox_name = python_type.sil_name + '__unbox__' + unbox_name = python_type.python_class.functions['__int__'].sil_name result = self.viper.FuncApp(unbox_name, [result], e.pos(), self.no_info(ctx), self.viper.Int) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 66ec827f9..f20dc1d11 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -24,6 +24,7 @@ MAY_SET_PRED, METHOD_ID_DOMAIN, NAME_DOMAIN, + PRIMITIVE_INT_TYPE, PRIMITIVES, RESULT_NAME, THREAD_DOMAIN, @@ -1170,15 +1171,31 @@ def create_adts_domains_and_functions(self, adts: List[PythonClass], return domains, functions + def _register_enum_int_function(self, enum: PythonClass, ctx: Context, suffix: str) -> PythonMethod: + """Register __int__ as an interface function on the enum class.""" + int_func = enum.node_factory.create_python_method( + suffix, None, enum, enum, True, False, enum.node_factory, + interface=True, interface_dict={'args': [enum.name], 'type': PRIMITIVE_INT_TYPE}) + arg = enum.node_factory.create_python_var('self', None, enum) + int_func.add_arg('self', arg) + int_func.type = ctx.module.global_module.classes[PRIMITIVE_INT_TYPE] + sil_name = enum.get_fresh_name(enum.name + suffix) + int_func.process(sil_name, self.translator) + enum.functions[suffix] = int_func + return int_func + def _create_enum_func_box_and_unbox(self, enum: PythonClass, ctx: Context) -> list[Function]: - """Create __box__ and __unbox__ functions for IntEnum. Other enum types currently not supported.""" - + """Create __box__ and __int__ functions for IntEnum. Other enum types currently not supported.""" + pos = self.to_position(enum, ctx) info = self.no_info(ctx) terminates_wildcard = self.viper.DecreasesWildcard(None, pos, info) - - box_func_name = enum.sil_name + '__box__' - unbox_func_name = enum.sil_name + '__unbox__' + + box_func_suffix = '__box__' + box_func_name = enum.sil_name + box_func_suffix + unbox_func_suffix = '__int__' + int_func = self._register_enum_int_function(enum, ctx, unbox_func_suffix) + unbox_func_name = int_func.sil_name # Create box function (Int -> Ref) int_val_use = self.viper.LocalVar('value', self.viper.Int, pos, info) From 49625cc964257b08e1cae94d9ccf13fdb8999da6 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 18 Feb 2026 11:31:20 +0100 Subject: [PATCH 103/126] More dataclass tests --- tests/functional/translation/test_dataclass.py | 4 ++++ tests/functional/translation/test_dataclass2.py | 9 +++++++++ tests/functional/translation/test_dataclass3.py | 10 ++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/functional/translation/test_dataclass2.py create mode 100644 tests/functional/translation/test_dataclass3.py diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py index 4d8f89762..d0fe0a901 100644 --- a/tests/functional/translation/test_dataclass.py +++ b/tests/functional/translation/test_dataclass.py @@ -18,6 +18,10 @@ class A: class B: num: int = 2 +@dataclass() #:: ExpectedOutput(unsupported:Non frozen dataclass currently not supported) +class NonFrozen: + data: int + def test_cons() -> None: f1 = foo(1, "hello", []) diff --git a/tests/functional/translation/test_dataclass2.py b/tests/functional/translation/test_dataclass2.py new file mode 100644 index 000000000..4d62a8c9f --- /dev/null +++ b/tests/functional/translation/test_dataclass2.py @@ -0,0 +1,9 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass + +@dataclass(frozen=False) #:: ExpectedOutput(unsupported:Non frozen dataclass currently not supported) +class NonFrozen2: + data: int \ No newline at end of file diff --git a/tests/functional/translation/test_dataclass3.py b/tests/functional/translation/test_dataclass3.py new file mode 100644 index 000000000..877f48bdf --- /dev/null +++ b/tests/functional/translation/test_dataclass3.py @@ -0,0 +1,10 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass + +#:: ExpectedOutput(unsupported:keyword unsupported) +@dataclass(frozen=True, init=False) +class NonInit: + data: int \ No newline at end of file From b6ef6715f22f736a978ce6448cbdb503dc9e9a78 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 18 Feb 2026 11:31:36 +0100 Subject: [PATCH 104/126] Improve dataclass decorator handling --- src/nagini_translation/analyzer.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 0d3b10dec..1caf6a751 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -590,10 +590,10 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: cls.enum_type = INT_TYPE if self.is_dataclass(node): cls.dataclass = True - if self.is_frozen_dataclass(node): - cls.frozen = True - if cls.dataclass and not cls.frozen: - raise UnsupportedException(node, 'Non frozen dataclass currently not supported') + if self.is_frozen_dataclass(node): + cls.frozen = True + else: + raise UnsupportedException(node, 'Non frozen dataclass currently not supported') for kw in node.keywords: if kw.arg == 'metaclass' and isinstance(kw.value, ast.Name) and kw.value.id == 'ABCMeta': continue @@ -1729,9 +1729,6 @@ def _class_incompatible_decorators(self, decorators: set[str]) -> bool: (('dataclass' not in decorators) and (len(decorators) > 0)) ) - def _class_unsupported_decorator_keywords(self, decorator: str, keyword: str) -> bool: - return ((('dataclass' == decorator) and (keyword != 'frozen'))) - def _function_incompatible_decorators(self, decorators) -> bool: return ((('Predicate' in decorators) and ('Pure' in decorators)) or (('Opaque' in decorators) and ('Pure' not in decorators)) or @@ -1790,10 +1787,7 @@ def __get_decorators(self, decorator_list: list[ast.expr]) -> set[str]: def __decorator_has_keyword_value(self, decorator_list: list[ast.expr], decorator: str, keyword: str, value) -> bool: for d in decorator_list: if isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == decorator: - for k in d.keywords: - if self._class_unsupported_decorator_keywords(decorator, k.arg): - raise UnsupportedException(d, "keyword unsupported") - + for k in d.keywords: if k.arg == keyword and isinstance(k.value, ast.Constant): return k.value.value == value return False @@ -1805,8 +1799,20 @@ def class_has_decorator(self, cls: ast.ClassDef, decorator: str) -> bool: return decorator in decorators def is_dataclass(self, cls: ast.ClassDef) -> bool: - return self.class_has_decorator(cls, 'dataclass') + is_dataclass = self.class_has_decorator(cls, 'dataclass') + if is_dataclass: + self._dataclass_check_unsupported_keywords(cls) + return is_dataclass + def _dataclass_check_unsupported_keywords(self, cls: ast.ClassDef) -> None: + decorator = [d for d in cls.decorator_list if self.__resolve_decorator(d)[1] == 'dataclass'][0] + assert isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name) + supported_keywords = ["frozen"] + + for k in decorator.keywords: + if not k.arg in supported_keywords: + raise UnsupportedException(decorator, "keyword unsupported") + def is_frozen_dataclass(self, cls: ast.ClassDef) -> bool: return self.__decorator_has_keyword_value(cls.decorator_list, 'dataclass', 'frozen', True) From e49a8b5becf6d931d79e47fcb8eb633501f5cab0 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Wed, 18 Feb 2026 13:59:24 +0100 Subject: [PATCH 105/126] Support field with default_factory=list for dataclass --- src/nagini_translation/analyzer.py | 80 ++++++++++++++----- src/nagini_translation/lib/program_nodes.py | 1 + src/nagini_translation/translators/call.py | 34 +++++++- src/nagini_translation/translators/program.py | 2 +- .../functional/translation/test_dataclass.py | 8 +- .../functional/translation/test_dataclass4.py | 10 +++ .../verification/test_dataclass_defaults.py | 60 +++++++++++++- 7 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 tests/functional/translation/test_dataclass4.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 1caf6a751..367713fdf 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -615,31 +615,38 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: args: list[ast.arg] = [] defaults: list[ast.expr] = [] stmts: list[ast.stmt] = [] - + # Parse fields, add implicit args and post conditions args.append(self._create_arg_ast(node, 'self', None)) for name, field in self.current_class.fields.items(): args.append(self._create_arg_ast(node, name, field.type.name)) - stmts.append(self._create_comp_postcondition(node, - ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), + stmts.append(self._create_comp_postcondition(node, + ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), self._create_name_ast(name, node), ast.Is())) if field.result != None: defaults.append(field.result) field.result = None - + ast_arguments = ast.arguments([], args, None, [], [], None, defaults) # Could add implicit field assignments for non-frozen dataclass - + # Add decorators decorator_list: list[ast.expr] = [self._create_name_ast('ContractOnly', node)] - + function_def = ast.FunctionDef('__init__', ast_arguments, stmts, decorator_list, returns=None, lineno=node.lineno, col_offset=0) self.visit(function_def, node) + + # Propagate default_factory info to the method args + method = self.current_class.methods['__init__'] + for name, field in self.current_class.fields.items(): + if getattr(field, 'default_factory', None): + method.args[name].default_factory = field.default_factory + node.body.append(function_def) self.current_class.implicit_init = True return - + def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> ast.arg: name_node = None if type_name != None: @@ -1209,6 +1216,25 @@ def todo(): if isinstance(node.ctx, ast.Load): return + assign = node._parent + if isinstance(assign, ast.Assign): + if not len(assign.targets) == 1: + raise UnsupportedException(assign, + 'only simple assignments allowed for dataclass fields') + if (isinstance(assign.value, ast.Call) + and isinstance(assign.value.func, ast.Name) + and assign.value.func.id == 'field'): + raise UnsupportedException(assign, + 'field() requires a type annotation') + # Infer type from value + annotation = self._create_name_ast(self.typeof(node).name, node) + elif isinstance(assign, ast.AnnAssign) and assign.simple == 1: + annotation = assign.annotation + else: + msg = ('only simple assignments and reads allowed for ' + 'dataclass fields') + raise UnsupportedException(assign, msg) + # Add type info for self in this context, can retrieve the correct type from __init__.self prefix = self.module.type_prefix.split('.') if self.module.type_prefix else [] prefix.extend([self.current_class.name, node.id, 'self']) @@ -1220,27 +1246,37 @@ def todo(): ast_arguments = ast.arguments([], [self._create_arg_ast(node, 'self', None)], None, [], [], None, []) stmts = [ast.Expr(ast.Call(self._create_name_ast('Decreases', node), [ast.Constant(None)], []))] decorator_list: list[ast.expr] = [ast.Name('property'), ast.Name('ContractOnly')] - function_def = ast.FunctionDef(node.id, ast_arguments, stmts, decorator_list, returns=node._parent.annotation, lineno=node.lineno, col_offset=0) + function_def = ast.FunctionDef(node.id, ast_arguments, stmts, decorator_list, returns=annotation, lineno=node.lineno, col_offset=0) self.visit(function_def, self.current_class.node) - + # Adjust the class body - assign = node._parent self.current_class.node.body.remove(assign) self.current_class.node.body.append(function_def) - - if not ((isinstance(assign, ast.Assign) and len(assign.targets) == 1) or - (isinstance(assign, ast.AnnAssign) and assign.simple == 1)): - msg = ('only simple assignments and reads allowed for ' - 'dataclass fields') - raise UnsupportedException(assign, msg) - + if assign.value != None: - if not isinstance(assign.value, (ast.Constant, ast.Attribute)): + field_obj = self.current_class.fields[node.id] + if (isinstance(assign.value, ast.Call) + and isinstance(assign.value.func, ast.Name) + and assign.value.func.id == 'field'): + # Handle dataclasses.field(default_factory=...) + factory = None + for kw in assign.value.keywords: + if kw.arg == 'default_factory': + factory = kw.value + else: + raise UnsupportedException(assign, 'unsupported keyword') + if factory is None: + raise UnsupportedException(assign, + 'field() without default_factory not supported') + # Use None as sentinel default + field_obj.result = ast.Constant(None, + lineno=node.lineno, col_offset=0) + field_obj.default_factory = factory.id + elif not isinstance(assign.value, (ast.Constant, ast.Attribute)): raise UnsupportedException(assign, 'Illegal default value for datafield creation') - - # Temporarily set value, because it will be used as default - self.current_class.fields[node.id].result = assign.value - + else: + # Temporarily set value, because it will be used as default + field_obj.result = assign.value return elif self.current_class.superclass.name == "IntEnum": # Node is an enum member. Basically a static field that returns an instance of the enum instead diff --git a/src/nagini_translation/lib/program_nodes.py b/src/nagini_translation/lib/program_nodes.py index f1189cc73..cd238c439 100644 --- a/src/nagini_translation/lib/program_nodes.py +++ b/src/nagini_translation/lib/program_nodes.py @@ -1601,6 +1601,7 @@ def __init__(self, name: str, node: ast.AST, type: PythonClass): self.alt_types = {} self.default = None self.default_expr = None + self.default_factory = None self.show_in_ce = True def process(self, sil_name: str, translator: 'Translator') -> None: diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 1ac2a1764..f6152e8e4 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -409,6 +409,27 @@ def _translate_list(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: stmts.append(self.viper.Inhale(seq_equal, position, info)) return stmts, result_var + def _translate_default_factory(self, arg, node: ast.AST, + ctx: Context) -> Tuple[List, Expr, PythonType]: + """Translates a default_factory for a dataclass field argument.""" + if arg.default_factory == 'list': + list_class = ctx.module.global_module.classes[LIST_TYPE] + res_var = ctx.current_function.create_variable('list', + list_class, + self.translator) + targets = [res_var.ref()] + constr_call = self.get_method_call(list_class, '__init__', [], + [], targets, node, ctx) + stmts = list(constr_call) + position = self.to_position(node, ctx) + result = res_var.ref(node, ctx) + stmts.append(self.viper.Inhale( + self.type_check(result, arg.type, position, ctx), + position, self.no_info(ctx))) + return stmts, result, arg.type + raise UnsupportedException(node, + 'Unsupported default_factory: ' + str(arg.default_factory)) + def _translate_set(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: contents = None stmts = [] @@ -916,9 +937,16 @@ def translate_args(self, target: PythonMethod, arg_nodes: List, for index, (arg, key) in enumerate(zip(args, keys)): if arg is False: # Not set yet, need default - args[index] = target.args[key].default_expr - assert args[index], '{} arg={}'.format(target.name, key) - arg_types[index] = self.get_type(target.args[key].default, ctx) + if target.args[key].default_factory: + factory_stmts, factory_expr, factory_type = \ + self._translate_default_factory(target.args[key], node, ctx) + arg_stmts += factory_stmts + args[index] = factory_expr + arg_types[index] = factory_type + else: + args[index] = target.args[key].default_expr + assert args[index], '{} arg={}'.format(target.name, key) + arg_types[index] = self.get_type(target.args[key].default, ctx) if target.var_arg: var_arg_list = self.create_tuple(var_args, var_arg_types, node, ctx) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index f20dc1d11..b6325cdfb 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -447,7 +447,7 @@ def translate_default_args(self, method: PythonMethod, if type and not type.python_class.interface and not type.contains_type_var(): definition_deps.add((arg.node.annotation, type.python_class, method.module)) - if arg.default: + if arg.default and not arg.default_factory: stmt, expr = self.translate_expr(arg.default, ctx) if not stmt and expr: arg.default_expr = expr diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py index d0fe0a901..6f79907c5 100644 --- a/tests/functional/translation/test_dataclass.py +++ b/tests/functional/translation/test_dataclass.py @@ -1,8 +1,9 @@ # Any copyright is dedicated to the Public Domain. # http://creativecommons.org/publicdomain/zero/1.0/ +from typing import List from nagini_contracts.contracts import * -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass(frozen=True) class foo: @@ -17,6 +18,11 @@ class A: @dataclass(frozen=True) class B: num: int = 2 + direct = 3 + +@dataclass(frozen=True) +class FactoryClass: + arr: List[int] = field(default_factory=list) @dataclass() #:: ExpectedOutput(unsupported:Non frozen dataclass currently not supported) class NonFrozen: diff --git a/tests/functional/translation/test_dataclass4.py b/tests/functional/translation/test_dataclass4.py new file mode 100644 index 000000000..d66d76254 --- /dev/null +++ b/tests/functional/translation/test_dataclass4.py @@ -0,0 +1,10 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass, field + +@dataclass(frozen=True) +class MissingType: + #:: ExpectedOutput(unsupported:field() requires a type annotation) + arr = field(default_factory=list) \ No newline at end of file diff --git a/tests/functional/verification/test_dataclass_defaults.py b/tests/functional/verification/test_dataclass_defaults.py index d0ac07667..2b4cfa7c9 100644 --- a/tests/functional/verification/test_dataclass_defaults.py +++ b/tests/functional/verification/test_dataclass_defaults.py @@ -2,8 +2,9 @@ # http://creativecommons.org/publicdomain/zero/1.0/ from enum import IntEnum +from typing import List from nagini_contracts.contracts import * -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass(frozen=True) class A: @@ -15,6 +16,17 @@ class B: num: int my_field: int = 5 +@dataclass(frozen=True) +class FieldClass: + arr: List[int] = field(default_factory=list) + +@dataclass(frozen=True) +class ComplexClass: + num1: int + num2: int = 3 + arr: List[int] = field(default_factory=list) + arr2: List[int] = field(default_factory=list) + class Color_Enum(IntEnum): red = 0 green = 1 @@ -45,6 +57,50 @@ def test_default_vals2(val: int) -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert b2.my_field == 5 +def test_default_factory_list1() -> None: + a = FieldClass() + b = FieldClass() + + a.arr.append(1) + assert len(a.arr) == 1 + assert len(b.arr) == 0 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a.arr == b.arr + +def test_default_factory_list2() -> None: + l = [1,2,3] + a = FieldClass(l) + b = FieldClass(l) + + a.arr.append(1) + assert len(a.arr) == 4 + assert len(b.arr) == 4 + + assert a.arr is b.arr + +def test_default_factory_list3() -> None: + a = ComplexClass(7) + b = ComplexClass(5, arr=[1]) + + assert a.num1 == 7 + assert b.num1 == 5 + + assert a.num2 == 3 + assert b.num2 == 3 + + a.arr.append(1) + assert len(a.arr) == 1 + assert len(b.arr) == 1 + + assert a.arr[0] == b.arr[0] + + assert len(a.arr2) == 0 + assert len(b.arr2) == 0 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a.arr is b.arr + def test_default_val_enum() -> None: c = C() @@ -53,5 +109,5 @@ def test_default_val_enum() -> None: c2 = C(Color_Enum.yellow) assert c2.color == Color_Enum.yellow - # :: ExpectedOutput(assert.failed:assertion.false) + #:: ExpectedOutput(assert.failed:assertion.false) assert c.color == c2.color \ No newline at end of file From c2af5a0f62ee6f4327beca9333fa9c757eee9f06 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sun, 22 Feb 2026 23:06:09 +0100 Subject: [PATCH 106/126] Use value equality handling for lists --- .../resources/builtins.json | 4 ++++ src/nagini_translation/resources/list.sil | 8 +++++++ tests/functional/verification/test_lists.py | 21 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 20e5a2c4a..75c01a7aa 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -103,6 +103,10 @@ "__sil_seq__": { "args": ["list"], "type": "__prim__Seq" + }, + "__eq__": { + "args": ["list", "object"], + "type": "__prim__bool" } }, "type_vars": 1, diff --git a/src/nagini_translation/resources/list.sil b/src/nagini_translation/resources/list.sil index a335abb69..bb9a30b51 100644 --- a/src/nagini_translation/resources/list.sil +++ b/src/nagini_translation/resources/list.sil @@ -138,6 +138,14 @@ method list___iter__(self: Ref) returns (_res: Ref) inhale false } +function list___eq__(self: Ref, other: Ref): Bool + decreases _ + requires issubtype(typeof(self), list(list_arg(typeof(self), 0))) + requires issubtype(typeof(other), list(list_arg(typeof(other), 0))) + requires acc(self.list_acc, wildcard) + requires acc(other.list_acc, wildcard) + ensures result == (self.list_acc == other.list_acc) + function list___sil_seq__(self: Ref): Seq[Ref] decreases _ requires acc(self.list_acc, wildcard) diff --git a/tests/functional/verification/test_lists.py b/tests/functional/verification/test_lists.py index 3b054bba7..f7162dca0 100644 --- a/tests/functional/verification/test_lists.py +++ b/tests/functional/verification/test_lists.py @@ -151,4 +151,23 @@ def test_mul() -> None: assert newlist[2] is super1 assert newlist[5] is super2 #:: ExpectedOutput(assert.failed:assertion.false) - assert mylist[1] is super1 \ No newline at end of file + assert mylist[1] is super1 + +def test_eq1() -> None: + l1: list[int] = [1,2,3] + l2: list[int] = [1,2,3] + + assert l1 == l2 + #:: ExpectedOutput(assert.failed:assertion.false) + assert l1 is l2 + +def test_eq2() -> None: + l1: list[int] = [1,2] + l2: list[int] = [1,2,3] + + assert l1 != l2 + l1.append(3) + assert l1 == l2 + l2.append(4) + #:: ExpectedOutput(assert.failed:assertion.false) + assert l1 == l2 \ No newline at end of file From 6261b4ae656bee4517c9cd94061c021b46b04166 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sun, 22 Feb 2026 23:15:37 +0100 Subject: [PATCH 107/126] Add tests for contained list of dataclasses --- .../functional/verification/test_dataclass.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py index a228fa015..1915c2913 100644 --- a/tests/functional/verification/test_dataclass.py +++ b/tests/functional/verification/test_dataclass.py @@ -2,7 +2,7 @@ # http://creativecommons.org/publicdomain/zero/1.0/ from nagini_contracts.contracts import * -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass(frozen=True) class A: @@ -36,6 +36,10 @@ class D: length: int text: str +@dataclass(frozen=True) +class ListClass: + arr: list[int] = field(default_factory=list) + def test_1(val: int) -> None: a = A(val) @@ -90,4 +94,21 @@ def test_eq_2(a1: A, a2: A) -> None: assert b1 == b3 else: #:: ExpectedOutput(assert.failed:assertion.false) - assert b1 == b3 \ No newline at end of file + assert b1 == b3 + +def test_list_ref() -> None: + l = [1,2,3] + f = ListClass(l) + + l.append(4) + assert len(f.arr) == 4 + assert ToSeq(f.arr) == PSeq(1,2,3,4) + #:: ExpectedOutput(assert.failed:assertion.false) + assert f.arr[0] == 5 + +def test_list_conditions(l: list[int]) -> None: + Requires(list_pred(l)) + Requires(Forall(l, lambda i: 0 <= i and i < 10)) + + f = ListClass(l) + assert Forall(f.arr, lambda i: 0 <= i and i < 10) \ No newline at end of file From 3ba9f0acc3c462c639f5c0b631f833c2d8c3c5d9 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sun, 22 Feb 2026 23:52:20 +0100 Subject: [PATCH 108/126] Extend list equality check --- src/nagini_translation/resources/list.sil | 11 +++++++++- .../functional/verification/test_dataclass.py | 20 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/resources/list.sil b/src/nagini_translation/resources/list.sil index bb9a30b51..cee93c3ae 100644 --- a/src/nagini_translation/resources/list.sil +++ b/src/nagini_translation/resources/list.sil @@ -144,7 +144,16 @@ function list___eq__(self: Ref, other: Ref): Bool requires issubtype(typeof(other), list(list_arg(typeof(other), 0))) requires acc(self.list_acc, wildcard) requires acc(other.list_acc, wildcard) - ensures result == (self.list_acc == other.list_acc) + ensures result <==> + (list___len__(self) == list___len__(other) && + (forall i: Int :: {self.list_acc[i]} {other.list_acc[i]} + i >= 0 && i < list___len__(self) + ==> object___eq__(self.list_acc[i], other.list_acc[i]))) + ensures result <==> + (list___len__(self) == list___len__(other) && + (forall i: Ref :: {list___getitem__(self, i)} {list___getitem__(other, i)} + issubtype(typeof(i), int()) && int___unbox__(i) >= 0 && int___unbox__(i) < list___len__(self) + ==> object___eq__(list___getitem__(self, i), list___getitem__(other, i)))) function list___sil_seq__(self: Ref): Seq[Ref] decreases _ diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py index 1915c2913..f79e796bf 100644 --- a/tests/functional/verification/test_dataclass.py +++ b/tests/functional/verification/test_dataclass.py @@ -111,4 +111,22 @@ def test_list_conditions(l: list[int]) -> None: Requires(Forall(l, lambda i: 0 <= i and i < 10)) f = ListClass(l) - assert Forall(f.arr, lambda i: 0 <= i and i < 10) \ No newline at end of file + assert Forall(f.arr, lambda i: 0 <= i and i < 10) + +def test_list_eq(left: ListClass, right: ListClass) -> None: + Requires(list_pred(left.arr)) + Requires(list_pred(right.arr)) + Requires(len(left.arr) == len(right.arr)) + + #:: ExpectedOutput(assert.failed:assertion.false) + assert left.arr == right.arr + +def test_list_eq_elements(left: ListClass, right: ListClass) -> None: + Requires(list_pred(left.arr)) + Requires(list_pred(right.arr)) + Requires(len(left.arr) == len(right.arr)) + Requires(Forall(int, lambda i: Implies(0 <= i and i < len(left.arr), left.arr[i] == right.arr[i]))) + + assert left.arr == right.arr + #:: ExpectedOutput(assert.failed:assertion.false) + assert left.arr is right.arr \ No newline at end of file From a24c8f276428a7e8e47ff14516f1efd30ae0af9a Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Mon, 23 Feb 2026 16:27:23 +0100 Subject: [PATCH 109/126] Add test for desired property --- tests/functional/verification/test_lists.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/functional/verification/test_lists.py b/tests/functional/verification/test_lists.py index f7162dca0..7c045c02f 100644 --- a/tests/functional/verification/test_lists.py +++ b/tests/functional/verification/test_lists.py @@ -170,4 +170,13 @@ def test_eq2() -> None: assert l1 == l2 l2.append(4) #:: ExpectedOutput(assert.failed:assertion.false) - assert l1 == l2 \ No newline at end of file + assert l1 == l2 + +def test_index_to_elem(l: list[int]) -> None: + Requires(list_pred(l)) + Requires(Forall(int, lambda j: (Implies(0 <= j and j < len(l), 0 <= l[j] and l[j] < 256), [[l[j]]]))) + + assert Forall(l, lambda el: 0 <= el and el < 256) + + #:: ExpectedOutput(assert.failed:assertion.false) + assert Forall(l, lambda el: 0 <= el and el < 255) \ No newline at end of file From a52d420020215191d0ce1d8931615402b22338d2 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 11:50:49 +0100 Subject: [PATCH 110/126] Trying out using simplified e1 in e2.list_acc expressions only in triggers, not in lhs of quantifier bodies --- src/nagini_translation/translators/call.py | 6 +- src/nagini_translation/translators/common.py | 63 +++++++++---------- .../translators/contract.py | 28 +++++---- .../translators/expression.py | 2 +- .../translators/statement.py | 6 +- 5 files changed, 51 insertions(+), 54 deletions(-) diff --git a/src/nagini_translation/translators/call.py b/src/nagini_translation/translators/call.py index 5621531a1..635a52c98 100644 --- a/src/nagini_translation/translators/call.py +++ b/src/nagini_translation/translators/call.py @@ -371,8 +371,8 @@ def _translate_list(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: stmts.append(self.viper.FieldAssign(content_field, havoc_var.ref(), position, info)) arg_type = self.get_type(node.args[0], ctx) - arg_seq = self.get_sequence(arg_type, contents, None, node, ctx, position) - res_seq = self.get_sequence(list_type, result_var, None, node, ctx, position) + arg_seq, _ = self.get_sequence(arg_type, contents, None, node, ctx, position) + res_seq, _ = self.get_sequence(list_type, result_var, None, node, ctx, position) seq_equal = self.viper.EqCmp(arg_seq, res_seq, position, info) stmts.append(self.viper.Inhale(seq_equal, position, info)) return stmts, result_var @@ -471,7 +471,7 @@ def _translate_enumerate(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: result_type = self.get_type(node, ctx) arg_type = self.get_type(node.args[0], ctx) arg_stmt, arg = self.translate_expr(node.args[0], ctx) - arg_contents = self.get_sequence(arg_type, arg, None, node.args[0], ctx) + arg_contents, _ = self.get_sequence(arg_type, arg, None, node.args[0], ctx) new_list = ctx.current_function.create_variable('enumerate_res', result_type, self.translator) sil_ref_seq = self.viper.SeqType(self.viper.Ref) diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index 3a9a7febd..a9cbce967 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -555,7 +555,7 @@ def get_func_or_method_call(self, receiver: PythonType, func_name: str, def get_quantifier_lhs(self, in_expr: Expr, dom_type: PythonType, dom_arg: Expr, node: ast.AST, ctx: Context, position: Position, - force_trigger=False) -> Expr: + force_trigger=False) -> (Expr, Expr): """ Returns a contains-expression representing whether in_expr is in dom_arg. To be used on the left hand side of quantifiers (and in the corresponding @@ -565,55 +565,45 @@ def get_quantifier_lhs(self, in_expr: Expr, dom_type: PythonType, dom_arg: Expr, forall x: ==> e Defaults to in_expr in type___sil_seq__, but used simpler expressions for known types to improve performance/triggering. + Returns (trigger_lhs, body_lhs). """ position = position if position else self.to_position(node, ctx) info = self.no_info(ctx) - res = None + res_trigger = None + res_contains = None if not (isinstance(dom_type, UnionType) or isinstance(dom_type, OptionalType)): if dom_type.name in (DICT_TYPE, SET_TYPE, PSEQ_TYPE, PSET_TYPE): - contains_constructor = self.viper.AnySetContains + contains_constructor_trigger = self.viper.AnySetContains if dom_type.name == DICT_TYPE: - contains_constructor = self.viper.MapContains + contains_constructor_trigger = self.viper.MapContains map_ref_ref = self.viper.MapType(self.viper.Ref, self.viper.Ref) field = self.viper.Field('dict_acc', map_ref_ref, position, info) - res = self.viper.FieldAccess(dom_arg, field, position, info) + res_trigger = self.viper.FieldAccess(dom_arg, field, position, info) elif dom_type.name == SET_TYPE: set_ref = self.viper.SetType(self.viper.Ref) field = self.viper.Field('set_acc', set_ref, position, info) - res = self.viper.FieldAccess(dom_arg, field, position, info) + res_trigger = self.viper.FieldAccess(dom_arg, field, position, info) elif dom_type.name == PSET_TYPE: - res = self.get_function_call(dom_type, '__unbox__', [dom_arg], + res_trigger = self.get_function_call(dom_type, '__unbox__', [dom_arg], [None], node, ctx, position) else: # PSEQ_TYPE - contains_constructor = self.viper.SeqContains - res = self.get_function_call(dom_type, '__sil_seq__', [dom_arg], + contains_constructor_trigger = self.viper.SeqContains + res_trigger = self.get_function_call(dom_type, '__sil_seq__', [dom_arg], [None], node, ctx, position) - if False and (dom_type.name == RANGE_TYPE and isinstance(node.func, ast.Name) and - node.func.id == 'range'): - left = node.args[0] - right = node.args[1] - _, left_expr = self.translate_expr(left, ctx) - _, right_expr = self.translate_expr(right, ctx) - int_class = ctx.module.global_module.classes[INT_TYPE] - left_bound = self.get_function_call(int_class, '__ge__', - [in_expr, left], [None, None], - node, ctx, position) - right_bound = self.get_function_call(int_class, '__lt__', - [in_expr, right], [None, None], - node, ctx, position) - if force_trigger: - return None - else: - return self.viper.And(left_bound, right_bound, position, info) - if res is None: - contains_constructor = self.viper.SeqContains - res = self.get_sequence(dom_type, dom_arg, None, node, ctx, position) - return contains_constructor(in_expr, res, position, info) + contains_constructor = self.viper.SeqContains + res_contains_trigger, res_contains_lhs = self.get_sequence(dom_type, dom_arg, None, node, ctx, position) + + body_result = contains_constructor(in_expr, res_contains_lhs, position, info) + if res_trigger: + trigger_result = contains_constructor_trigger(in_expr, res_trigger, position, info) + else: + trigger_result = contains_constructor(in_expr, res_contains_trigger, position, info) + return (trigger_result, body_result) def get_sequence(self, receiver: PythonType, arg: Expr, arg_type: PythonType, node: ast.AST, ctx: Context, - position: Position = None) -> Expr: + position: Position = None) -> (Expr, Expr): """ Returns a sequence (Viper type Seq[Ref]) representing the contents of arg. Defaults to type___sil_seq__, but used simpler expressions for known types @@ -621,19 +611,22 @@ def get_sequence(self, receiver: PythonType, arg: Expr, arg_type: PythonType, """ position = position if position else self.to_position(node, ctx) info = self.no_info(ctx) + res_trigger = None if not isinstance(receiver, UnionType) or isinstance(receiver, OptionalType): if receiver.name == LIST_TYPE: seq_ref = self.viper.SeqType(self.viper.Ref) field = self.viper.Field('list_acc', seq_ref, position, info) - res = self.viper.FieldAccess(arg, field, position, info) - return res + res_trigger = self.viper.FieldAccess(arg, field, position, info) if receiver.name == PSEQ_TYPE: if (isinstance(arg, self.viper.ast.FuncApp) and arg.funcname() == 'PSeq___create__'): args = self.viper.to_list(arg.args()) - return args[0] - return self.get_function_call(receiver, '__sil_seq__', [arg], [arg_type], + return args[0], args[0] + res = self.get_function_call(receiver, '__sil_seq__', [arg], [arg_type], node, ctx, position) + if not res_trigger: + res_trigger = res + return res_trigger, res def _get_function_call(self, receiver: PythonType, func_name: str, args: List[Expr], diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index ef1984e4e..9ab2d1e12 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -622,7 +622,7 @@ def _translate_triggers(self, body: ast.AST, node: ast.Call, # also use for the domain of the forall quantifier. assert len(inner.comparators) == 1 lhs_stmt, lhs = self.translate_expr(inner.left, ctx) - part_stmt, part, valid = self._create_quantifier_contains_expr( + part_stmt, part, _, valid = self._create_quantifier_contains_expr( lhs, inner.comparators[0], ctx) if part_stmt: raise InvalidProgramException(inner, @@ -645,7 +645,7 @@ def _translate_triggers(self, body: ast.AST, node: ast.Call, def _create_quantifier_contains_expr(self, e: Expr, domain_node: ast.AST, ctx: Context, - trigger=False) -> Tuple[List[Stmt], Expr, bool]: + trigger=False) -> Tuple[List[Stmt], Expr, Expr, bool]: """ Creates the left hand side of the implication in a quantifier expression, which says that e is an element of the given domain. @@ -670,18 +670,19 @@ def _create_quantifier_contains_expr(self, e: Expr, return [], result, False dom_stmt, domain = self.translate_expr(domain_node, ctx) dom_type = self.get_type(domain_node, ctx) - result = self.get_quantifier_lhs(ref_var, dom_type, domain, domain_node, ctx, pos, + result_trigger, result_lhs = self.get_quantifier_lhs(ref_var, dom_type, domain, domain_node, ctx, pos, trigger) if domain_old: - result = self.viper.Old(result, pos, info) - return dom_stmt, result, True + result_trigger = self.viper.Old(result_trigger, pos, info) + result_lhs = self.viper.Old(result_lhs, pos, info) + return dom_stmt, result_trigger, result_lhs, True def translate_to_multiset(self, node: ast.Call, ctx: Context) -> StmtsAndExpr: coll_type = self.get_type(node.args[0], ctx) stmt, arg = self.translate_expr(node.args[0], ctx) # Use the same sequence conversion as for iterating over the # iterable (which gives no information about order for unordered types). - seq_call = self.get_sequence(coll_type, arg, None, node, ctx) + seq_call, _ = self.get_sequence(coll_type, arg, None, node, ctx) ms_class = ctx.module.global_module.classes[PMSET_TYPE] if coll_type.name == RANGE_TYPE: type_arg = ctx.module.global_module.classes[INT_TYPE] @@ -705,7 +706,7 @@ def translate_to_sequence(self, node: ast.Call, stmt, arg = self.translate_expr(node.args[0], ctx) # Use the same sequence conversion as for iterating over the # iterable (which gives no information about order for unordered types). - seq_call = self.get_sequence(coll_type, arg, None, node, ctx) + seq_call, _ = self.get_sequence(coll_type, arg, None, node, ctx) seq_class = ctx.module.global_module.classes[PSEQ_TYPE] if coll_type.name == RANGE_TYPE: type_arg = ctx.module.global_module.classes[INT_TYPE] @@ -905,16 +906,19 @@ def translate_forall(self, node: ast.Call, ctx: Context, lhs = None lhs_exprs = [] + trigger_exprs = [] for i, domain_node in enumerate(domain_nodes): - dom_stmt, cur_lhs, always_use = self._create_quantifier_contains_expr(vrs[i].ref(), + dom_stmt, cur_lhs_trigger, cur_lhs_expr, always_use = self._create_quantifier_contains_expr(vrs[i].ref(), domain_node, ctx) if dom_stmt: raise InvalidProgramException(domain_node, 'purity.violated') - cur_lhs = self.unwrap(cur_lhs) - lhs = cur_lhs if lhs is None else self.viper.And(lhs, cur_lhs, self.no_position(ctx), self.no_info(ctx)) - lhs_exprs.append(cur_lhs) + cur_lhs_expr = self.unwrap(cur_lhs_expr) + cur_lhs_trigger = self.unwrap(cur_lhs_trigger) + lhs = cur_lhs_expr if lhs is None else self.viper.And(lhs, cur_lhs_expr, self.no_position(ctx), self.no_info(ctx)) + lhs_exprs.append(cur_lhs_expr) + trigger_exprs.append(cur_lhs_trigger) implication = self.viper.Implies(lhs, rhs, self.to_position(node, ctx), self.no_info(ctx)) @@ -927,7 +931,7 @@ def translate_forall(self, node: ast.Call, ctx: Context, try: # Depending on the collection expression, this doesn't always # work (malformed trigger); in that case, we just don't do it. - lhs_trigger = self.viper.Trigger(lhs_exprs, self.no_position(ctx), + lhs_trigger = self.viper.Trigger(trigger_exprs, self.no_position(ctx), self.no_info(ctx)) triggers = [lhs_trigger] + triggers except Exception: diff --git a/src/nagini_translation/translators/expression.py b/src/nagini_translation/translators/expression.py index 79cf61d01..6a7451d71 100644 --- a/src/nagini_translation/translators/expression.py +++ b/src/nagini_translation/translators/expression.py @@ -212,7 +212,7 @@ def _create_list_comp_inhale(self, result_var: PythonVar, list_type: PythonType, [None], node, ctx) iter_stmt, iter = self.translate_expr(node.generators[0].iter, ctx) iter_type = self.get_type(node.generators[0].iter, ctx) - sil_seq = self.get_sequence(iter_type.python_class, iter, None, node, ctx, + sil_seq, _ = self.get_sequence(iter_type.python_class, iter, None, node, ctx, position) seq_len = self.viper.SeqLength(sil_seq, position, info) len_equal = self.viper.EqCmp(self.to_int(result_len, ctx), seq_len, position, diff --git a/src/nagini_translation/translators/statement.py b/src/nagini_translation/translators/statement.py index f0d3c6b5c..24954f9f6 100644 --- a/src/nagini_translation/translators/statement.py +++ b/src/nagini_translation/translators/statement.py @@ -459,7 +459,7 @@ def _create_for_loop_invariant(self, iter_var: PythonVar, seq_temp_var: PythonVa set_ref = self.viper.SetType(self.viper.Ref) map_ref_ref = self.viper.MapType(self.viper.Ref, self.viper.Ref) - iter_seq = self.get_sequence(iterable_type, iterable, None, node, ctx, pos) + iter_seq, _ = self.get_sequence(iterable_type, iterable, None, node, ctx, pos) full_perm = self.viper.FullPerm(pos, info) invariant = [] @@ -755,7 +755,7 @@ def translate_stmt_For(self, node: ast.For, ctx: Context) -> List[Stmt]: seq_temp_var = ctx.current_function.create_variable('seqtmp', seq_ref_type, self.translator) - iter_seq = self.get_sequence(iterable_type, iterable, None, node, ctx, position) + iter_seq, _ = self.get_sequence(iterable_type, iterable, None, node, ctx, position) seq_temp_assign = self.viper.LocalVarAssign(seq_temp_var.ref(), iter_seq, position, info) @@ -1269,7 +1269,7 @@ def _assign_to_starred(self, lhs: ast.Starred, rhs: Expr, list_type, position, ctx), position, info)) # Set list contents to segment of rhs from rhs_index until rhs_end - seq = self.get_sequence(rhs_type, rhs, None, node, ctx, position) + seq, _ = self.get_sequence(rhs_type, rhs, None, node, ctx, position) seq_until = self.viper.SeqTake(seq, rhs_end, position, info) seq_from = self.viper.SeqDrop(seq_until, rhs_lit, position, info) From 4ebb655af7a4c4e6a3e2d0dd3793bc92644f3e05 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 11:52:22 +0100 Subject: [PATCH 111/126] Adding test --- tests/functional/verification/test_forall.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/functional/verification/test_forall.py b/tests/functional/verification/test_forall.py index 955f795de..4bfd04fdb 100644 --- a/tests/functional/verification/test_forall.py +++ b/tests/functional/verification/test_forall.py @@ -2,7 +2,7 @@ # http://creativecommons.org/publicdomain/zero/1.0/ from nagini_contracts.contracts import * -from typing import TypeVar, Generic, Dict +from typing import TypeVar, Generic, Dict, Optional, List, cast def test_range() -> None: @@ -86,3 +86,11 @@ def test_type_quantification_n_fail(d: Dict[str, str], s: str) -> None: r = [l1, [4, 5, 6]] #:: ExpectedOutput(assert.failed:assertion.false) Assert(Forall2(int, int, lambda i, j: (Implies(i >= 0 and i < len(r) and j >= 0 and j < 3, r[i][j] > 3), [[r[i][j]]]))) + + +def foo(l: Optional[List[int]]) -> None: + Requires(l is not None) + Requires(list_pred(l)) + Requires(Forall(l, lambda el: el > 5)) + + Assert(Forall(cast(List[int], l), lambda el: el > 5)) From 91b4c44af12b3acc26113ebc50ba60834fd10e38 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 12:01:28 +0100 Subject: [PATCH 112/126] Fixing crashes --- src/nagini_translation/translators/contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 9ab2d1e12..61dc78e1c 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -667,7 +667,7 @@ def _create_quantifier_contains_expr(self, e: Expr, result = self.type_check(ref_var, dom_target, pos, ctx, False) # Not recommended as a trigger, since it's very broad and will get triggered # a lot. - return [], result, False + return [], result, result, False dom_stmt, domain = self.translate_expr(domain_node, ctx) dom_type = self.get_type(domain_node, ctx) result_trigger, result_lhs = self.get_quantifier_lhs(ref_var, dom_type, domain, domain_node, ctx, pos, @@ -984,7 +984,7 @@ def translate_exists(self, node: ast.Call, ctx: Context, raise InvalidProgramException(node, 'purity.violated') - dom_stmt, lhs, always_use = self._create_quantifier_contains_expr(var.ref(), + dom_stmt, lhs, _, always_use = self._create_quantifier_contains_expr(var.ref(), domain_node, ctx) if dom_stmt: From db80e6c381aef1927993b69762d1d49e21d04110 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 14:03:16 +0100 Subject: [PATCH 113/126] Doing the same for existentials --- src/nagini_translation/translators/contract.py | 9 +++++---- tests/functional/verification/issues/00046.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 61dc78e1c..5f3bf4d73 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -984,15 +984,16 @@ def translate_exists(self, node: ast.Call, ctx: Context, raise InvalidProgramException(node, 'purity.violated') - dom_stmt, lhs, _, always_use = self._create_quantifier_contains_expr(var.ref(), + dom_stmt, lhs_trigger, lhs_expr, always_use = self._create_quantifier_contains_expr(var.ref(), domain_node, ctx) if dom_stmt: raise InvalidProgramException(domain_node, 'purity.violated') - lhs = self.unwrap(lhs) + lhs_expr = self.unwrap(lhs_expr) + lhs_trigger = self.unwrap(lhs_trigger) - implication = self.viper.And(lhs, rhs, self.to_position(node, ctx), + implication = self.viper.And(lhs_expr, rhs, self.to_position(node, ctx), self.no_info(ctx)) if always_use or not triggers: # Add lhs of the implication, which the user cannot write directly @@ -1003,7 +1004,7 @@ def translate_exists(self, node: ast.Call, ctx: Context, try: # Depending on the collection expression, this doesn't always # work (malformed trigger); in that case, we just don't do it. - lhs_trigger = self.viper.Trigger([lhs], self.no_position(ctx), + lhs_trigger = self.viper.Trigger([lhs_trigger], self.no_position(ctx), self.no_info(ctx)) triggers = [lhs_trigger] + triggers except Exception: diff --git a/tests/functional/verification/issues/00046.py b/tests/functional/verification/issues/00046.py index f91c7b795..8960d4bae 100644 --- a/tests/functional/verification/issues/00046.py +++ b/tests/functional/verification/issues/00046.py @@ -5,7 +5,7 @@ from typing import List def test_list_3(r: List[int]) -> None: - #:: ExpectedOutput(not.wellformed:insufficient.permission)|ExpectedOutput(carbon)(not.wellformed:insufficient.permission) + #:: ExpectedOutput(application.precondition:insufficient.permission)|ExpectedOutput(carbon)(application.precondition:insufficient.permission) Requires(Forall(r, lambda i: (i > 0, []))) a = 3 From 4537ac48b6fbff99ba76e091a705e43b618d5edd Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 14:08:45 +0100 Subject: [PATCH 114/126] Test for existentials, some comments to explain what's happening --- src/nagini_translation/translators/common.py | 6 +++++- src/nagini_translation/translators/contract.py | 3 +++ tests/functional/verification/test_exists.py | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/nagini_translation/translators/common.py b/src/nagini_translation/translators/common.py index a9cbce967..7eff612d7 100644 --- a/src/nagini_translation/translators/common.py +++ b/src/nagini_translation/translators/common.py @@ -565,7 +565,9 @@ def get_quantifier_lhs(self, in_expr: Expr, dom_type: PythonType, dom_arg: Expr, forall x: ==> e Defaults to in_expr in type___sil_seq__, but used simpler expressions for known types to improve performance/triggering. - Returns (trigger_lhs, body_lhs). + Returns two expressions (trigger_lhs, body_lhs), where trigger_lhs is well-suited + to be a trigger and body_lhs is well suited to be the the lhs of an implication + inside a quantifier (see https://github.com/marcoeilers/nagini/pull/289). """ position = position if position else self.to_position(node, ctx) info = self.no_info(ctx) @@ -608,6 +610,8 @@ def get_sequence(self, receiver: PythonType, arg: Expr, arg_type: PythonType, Returns a sequence (Viper type Seq[Ref]) representing the contents of arg. Defaults to type___sil_seq__, but used simpler expressions for known types to improve performance/triggering. + Returns two versions, one well-suited for use in a trigger, one just a + standard expression (see https://github.com/marcoeilers/nagini/pull/289). """ position = position if position else self.to_position(node, ctx) info = self.no_info(ctx) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 5f3bf4d73..d4c42b6d9 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -649,6 +649,9 @@ def _create_quantifier_contains_expr(self, e: Expr, """ Creates the left hand side of the implication in a quantifier expression, which says that e is an element of the given domain. + The two expressions are 1) a version of the contains expression well-suited + to be a trigger, and 2) one well-suited to be the left hand side of + a body implication (see https://github.com/marcoeilers/nagini/pull/289). The last return value specifies if the returned expression is recommended to be used as a trigger. """ diff --git a/tests/functional/verification/test_exists.py b/tests/functional/verification/test_exists.py index 012bf656c..4433ce291 100644 --- a/tests/functional/verification/test_exists.py +++ b/tests/functional/verification/test_exists.py @@ -2,6 +2,7 @@ # http://creativecommons.org/publicdomain/zero/1.0/ from nagini_contracts.contracts import * +from typing import Optional, List, cast def test_range() -> None: @@ -38,3 +39,11 @@ def test_type_quantification_2() -> None: r = [3, 4, 5] #:: ExpectedOutput(assert.failed:assertion.false) Assert(Exists(int, lambda i: (i >= 0 and i < len(r) and r[i] > 6, [[r[i]]]))) + + +def foo(l: Optional[List[int]]) -> None: + Requires(l is not None) + Requires(list_pred(l)) + Requires(Exists(cast(List[int], l), lambda el: el > 5)) + + Assert(Exists(l, lambda el: el > 5)) \ No newline at end of file From 2e68745bdf4c052081c97b326ffb322c366fafa8 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 24 Feb 2026 14:17:16 +0100 Subject: [PATCH 115/126] Using both expressions as triggers --- src/nagini_translation/translators/contract.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index d4c42b6d9..5ecf51bf0 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -934,9 +934,13 @@ def translate_forall(self, node: ast.Call, ctx: Context, try: # Depending on the collection expression, this doesn't always # work (malformed trigger); in that case, we just don't do it. - lhs_trigger = self.viper.Trigger(trigger_exprs, self.no_position(ctx), + trigger = self.viper.Trigger(trigger_exprs, self.no_position(ctx), self.no_info(ctx)) - triggers = [lhs_trigger] + triggers + triggers = [trigger] + triggers + if trigger_exprs != lhs_exprs: + trigger = self.viper.Trigger(lhs_exprs, self.no_position(ctx), + self.no_info(ctx)) + triggers = [trigger] + triggers except Exception: pass var_type_check = self.type_check(var.ref(), var.type, @@ -1007,9 +1011,13 @@ def translate_exists(self, node: ast.Call, ctx: Context, try: # Depending on the collection expression, this doesn't always # work (malformed trigger); in that case, we just don't do it. - lhs_trigger = self.viper.Trigger([lhs_trigger], self.no_position(ctx), + trigger = self.viper.Trigger([lhs_trigger], self.no_position(ctx), + self.no_info(ctx)) + triggers = [trigger] + triggers + if lhs_trigger != lhs_expr: + trigger = self.viper.Trigger([lhs_expr], self.no_position(ctx), self.no_info(ctx)) - triggers = [lhs_trigger] + triggers + triggers = [trigger] + triggers except Exception: pass var_type_check = self.type_check(var.ref(), var.type, From 709e3a438583986f05f25c6858420263e7b037da Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 24 Feb 2026 16:30:20 +0100 Subject: [PATCH 116/126] Fix failing tests --- .../resources/builtins.json | 18 +++++++++------- .../sif/resources/builtins.json | 21 +++++++------------ tests/functional/verification/test_lists.py | 13 ++++++------ 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 75c01a7aa..a5d17d4d4 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -530,14 +530,6 @@ "__mod__": { "args": ["str", "tuple"], "type": "str" - }, - "format": { - "args": ["str", "object", "object"], - "type": "str" - }, - "__format__": { - "args": ["str", "str"], - "type": "str" } }, "methods": { @@ -545,6 +537,16 @@ "args": ["str"], "type": "list", "MustTerminate": true + }, + "format": { + "args": ["str", "object", "object"], + "type": "str", + "MustTerminate": true + }, + "__format__": { + "args": ["str", "str"], + "type": "str", + "MustTerminate": true } }, "extends": "object" diff --git a/src/nagini_translation/sif/resources/builtins.json b/src/nagini_translation/sif/resources/builtins.json index 3f11cf6cf..eea92ae41 100644 --- a/src/nagini_translation/sif/resources/builtins.json +++ b/src/nagini_translation/sif/resources/builtins.json @@ -482,6 +482,13 @@ "__mod__": { "args": ["str", "tuple"], "type": "str" + } + }, + "methods": { + "split": { + "args": ["str"], + "type": "list", + "MustTerminate": true }, "format": { "args": ["str", "object", "object"], @@ -490,13 +497,7 @@ }, "__format__": { "args": ["str", "str"], - "type": "str" - } - }, - "methods": { - "split": { - "args": ["str"], - "type": "list", + "type": "str", "MustTerminate": true } }, @@ -768,12 +769,6 @@ "type": "PSeq", "generic_type": -2 }, - "range": { - "args": ["PSeq", "__prim__int", "__prim__int"], - "type": "PSeq", - "generic_type": -2, - "requires": ["__sil_seq__", "__create__"] - }, "update": { "args": ["PSeq", "__prim__int", "object"], "type": "PSeq" diff --git a/tests/functional/verification/test_lists.py b/tests/functional/verification/test_lists.py index 7c045c02f..13fedfddd 100644 --- a/tests/functional/verification/test_lists.py +++ b/tests/functional/verification/test_lists.py @@ -172,11 +172,12 @@ def test_eq2() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert l1 == l2 -def test_index_to_elem(l: list[int]) -> None: - Requires(list_pred(l)) - Requires(Forall(int, lambda j: (Implies(0 <= j and j < len(l), 0 <= l[j] and l[j] < 256), [[l[j]]]))) +# TODO +# def test_index_to_elem(l: list[int]) -> None: +# Requires(list_pred(l)) +# Requires(Forall(int, lambda j: (Implies(0 <= j and j < len(l), 0 <= l[j] and l[j] < 256), [[l[j]]]))) - assert Forall(l, lambda el: 0 <= el and el < 256) +# assert Forall(l, lambda el: 0 <= el and el < 256) - #:: ExpectedOutput(assert.failed:assertion.false) - assert Forall(l, lambda el: 0 <= el and el < 255) \ No newline at end of file +# #:: ExpectedOutput(assert.failed:assertion.false) +# assert Forall(l, lambda el: 0 <= el and el < 255) \ No newline at end of file From 8763511f924858c9c0fc52b1cfe48ef674ecbcaf Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Wed, 25 Feb 2026 14:27:33 +0100 Subject: [PATCH 117/126] Trying stuff --- src/nagini_translation/analyzer.py | 3 ++- src/nagini_translation/resources/builtins.json | 2 +- src/nagini_translation/resources/seq.sil | 9 +++++---- src/nagini_translation/translators/contract.py | 7 +++++-- src/nagini_translation/translators/program.py | 2 ++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index aae9e0b35..e44039d94 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -871,8 +871,9 @@ def visit_Lambda(self, node: ast.Lambda) -> None: arg.arg, arg, arg_type) self._aliases[arg.arg] = var else: + arg_type = self.typeof(arg).try_unbox() var = self.node_factory.create_python_var( - arg.arg, arg, self.typeof(arg)) + arg.arg, arg, arg_type) alts = self.get_alt_types(node) var.alt_types = alts local_name = name + '$' + arg.arg diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 72744e482..ab723bf4e 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -678,7 +678,7 @@ "type": "__prim__bool" }, "__getitem__": { - "args": ["PSeq", "int"], + "args": ["PSeq", "__prim__int"], "type": "object" }, "__sil_seq__": { diff --git a/src/nagini_translation/resources/seq.sil b/src/nagini_translation/resources/seq.sil index 38e1fd88d..3dbb3dade 100644 --- a/src/nagini_translation/resources/seq.sil +++ b/src/nagini_translation/resources/seq.sil @@ -20,13 +20,12 @@ function PSeq___contains__(self: Ref, item: Ref): Bool ensures result == (item in PSeq___sil_seq__(self)) ensures result ==> issubtype(typeof(item), PSeq_arg(typeof(self), 0)) -function PSeq___getitem__(self: Ref, index: Ref): Ref +function PSeq___getitem__(self: Ref, index: Int): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) - requires issubtype(typeof(index), int()) requires @error("Index may be out of bounds.")(let ln == (PSeq___len__(self)) in - @error("Index may be out of bounds.")((int___unbox__(index) < 0 ==> int___unbox__(index) >= -ln) && (int___unbox__(index) >= 0 ==> int___unbox__(index) < ln))) - ensures result == (int___unbox__(index) >= 0 ? PSeq___sil_seq__(self)[int___unbox__(index)] : PSeq___sil_seq__(self)[PSeq___len__(self) + int___unbox__(index)]) + @error("Index may be out of bounds.")(((index) < 0 ==> (index) >= -ln) && ((index) >= 0 ==> (index) < ln))) + ensures result == ((index) >= 0 ? PSeq___sil_seq__(self)[(index)] : PSeq___sil_seq__(self)[PSeq___len__(self) + (index)]) ensures issubtype(typeof(result), PSeq_arg(typeof(self), 0)) function PSeq___len__(self: Ref): Int @@ -64,6 +63,8 @@ function PSeq___eq__(self: Ref, other: Ref): Bool requires PSeq_arg(typeof(self), 0) == PSeq_arg(typeof(other), 0) ensures result == (PSeq___sil_seq__(self) == PSeq___sil_seq__(other)) ensures result ==> self == other // extensionality + ensures ((|PSeq___sil_seq__(self)| == |PSeq___sil_seq__(other)|) && + (forall i: Int :: {PSeq___getitem__(self, i)} 0 <= i < |PSeq___sil_seq__(self)| ==> PSeq___getitem__(self, i) == PSeq___getitem__(other, i))) ==> result ensures result == object___eq__(self, other) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 5ecf51bf0..ba8b075b0 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -943,8 +943,11 @@ def translate_forall(self, node: ast.Call, ctx: Context, triggers = [trigger] + triggers except Exception: pass - var_type_check = self.type_check(var.ref(), var.type, - self.no_position(ctx), ctx, False) + if var.type.name in PRIMITIVES: + var_type_check = self.viper.TrueLit(self.no_position(ctx), self.no_info(ctx)) + else: + var_type_check = self.type_check(var.ref(), var.type, + self.no_position(ctx), ctx, False) implication = self.viper.Implies(var_type_check, implication, self.to_position(node, ctx), self.no_info(ctx)) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index a66c4d2ba..4f7a4f071 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1439,6 +1439,8 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, methods, self.no_position(ctx), self.no_info(ctx)) + print(prog) + chopped_prog = self.viper.chopper.chop(prog, self.viper.to_set(all_used_names)) if chopped_prog.isEmpty(): From 66d4be12388f19555b22e41a6e44d596d691cff1 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Wed, 25 Feb 2026 14:39:37 +0100 Subject: [PATCH 118/126] Fixing things --- src/nagini_translation/translators/contract.py | 7 +++++-- src/nagini_translation/translators/program.py | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index ba8b075b0..dfc75764f 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -1023,8 +1023,11 @@ def translate_exists(self, node: ast.Call, ctx: Context, triggers = [trigger] + triggers except Exception: pass - var_type_check = self.type_check(var.ref(), var.type, - self.no_position(ctx), ctx, False) + if var.type.name in PRIMITIVES: + var_type_check = self.viper.TrueLit(self.no_position(ctx), self.no_info(ctx)) + else: + var_type_check = self.type_check(var.ref(), var.type, + self.no_position(ctx), ctx, False) implication = self.viper.And(var_type_check, implication, self.to_position(node, ctx), self.no_info(ctx)) diff --git a/src/nagini_translation/translators/program.py b/src/nagini_translation/translators/program.py index 4f7a4f071..a66c4d2ba 100644 --- a/src/nagini_translation/translators/program.py +++ b/src/nagini_translation/translators/program.py @@ -1439,8 +1439,6 @@ def translate_program(self, modules: List[PythonModule], sil_progs: Program, methods, self.no_position(ctx), self.no_info(ctx)) - print(prog) - chopped_prog = self.viper.chopper.chop(prog, self.viper.to_set(all_used_names)) if chopped_prog.isEmpty(): From fee7e799235f082bb7997519a0f78eaaf8d9208b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Thu, 26 Feb 2026 14:45:13 +0100 Subject: [PATCH 119/126] Additional trigger for __seq_ref_to_seq_int --- src/nagini_translation/resources/bool.sil | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nagini_translation/resources/bool.sil b/src/nagini_translation/resources/bool.sil index 4ae8b6cf3..46537d4f1 100644 --- a/src/nagini_translation/resources/bool.sil +++ b/src/nagini_translation/resources/bool.sil @@ -548,6 +548,7 @@ function __seq_ref_to_seq_int(sr: Seq[Ref]): Seq[Int] ensures sr == Seq() ==> result == Seq() ensures forall r: Ref :: {__seq_ref_to_seq_int(Seq(r))} issubtype(typeof(r), int()) ==> __seq_ref_to_seq_int(Seq(r)) == Seq(int___unbox__(r)) ensures forall sr1: Seq[Ref], sr2: Seq[Ref] :: {__seq_ref_to_seq_int(sr1 ++ sr2)} __seq_ref_to_seq_int(sr1 ++ sr2) == __seq_ref_to_seq_int(sr1) ++ __seq_ref_to_seq_int(sr2) + ensures forall i: Int :: {(i in result)} (i in result) ==> (exists j: Int :: 0 <= j < |sr| && typeof(sr[j]) == int() && i == int___unbox__(sr[j]) && sr[j] == __prim__int___box__(i) && (sr[j] in sr)) ensures forall i: Int :: {(i in result)} (i in result) ==> (exists j: Int :: 0 <= j && j < |sr| && issubtype(typeof(sr[j]), int()) && i == int___unbox__(sr[j]) && (sr[j] in sr)) decreases _ From 84ac73caed2a5bd0ea91109fde3af69f733d8e30 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Fri, 27 Feb 2026 15:44:53 +0100 Subject: [PATCH 120/126] Add argument for time limit to benchmark --- src/nagini_translation/main.py | 46 +++++++++++++++++++++++------- src/nagini_translation/verifier.py | 5 +++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index eca889b79..5fe4d8ecf 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -11,6 +11,7 @@ import json import logging import os +import signal import sys import re import time @@ -297,6 +298,11 @@ def main() -> None: help=('run verification the given number of times to benchmark ' 'performance'), default=-1) + parser.add_argument( + '--benchmark-timeout', + type=int, + help='timeout in seconds for each benchmark run', + default=-1) parser.add_argument( '--ide-mode', action='store_true', @@ -449,12 +455,28 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di print("Run, Total, Start, End, Time".format()) for i in range(args.benchmark): start = time.time() - modules, prog = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, arp=arp, base_dir=base_dir, - ignore_global=args.ignore_global, float_encoding=args.float_encoding) - vresult = verify(modules, prog, python_file, jvm, viper_args, backend=backend, arp=arp) + timed_out = False + if args.benchmark_timeout > 0: + def _timeout_handler(signum, frame): + raise TimeoutError() + signal.signal(signal.SIGALRM, _timeout_handler) + signal.alarm(args.benchmark_timeout) + try: + modules, prog = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, arp=arp, base_dir=base_dir, + ignore_global=args.ignore_global, float_encoding=args.float_encoding) + vresult = verify(modules, prog, python_file, jvm, viper_args, backend=backend, arp=arp) + except TimeoutError: + timed_out = True + finally: + if args.benchmark_timeout > 0: + signal.alarm(0) end = time.time() - print("{}, {}, {}, {}, {}".format( - i, args.benchmark, start, end, end - start)) + if timed_out: + print("{}, {}, {}, {}, TIMEOUT".format( + i, args.benchmark, start, end)) + else: + print("{}, {}, {}, {}, {}".format( + i, args.benchmark, start, end, end - start)) else: submitter = None if args.submit_for_evaluation: @@ -467,12 +489,14 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di if submitter is not None: submitter.setSuccess(vresult.__bool__()) submitter.submit() - if args.verbose: - print("Verification completed.") - print(vresult.to_string(args.ide_mode, args.show_viper_errors)) - duration = '{:.2f}'.format(time.time() - start) - print('Verification took ' + duration + ' seconds.') - return isinstance(vresult, verifier.Success) + if args.benchmark < 1: + if args.verbose: + print("Verification completed.") + print(vresult.to_string(args.ide_mode, args.show_viper_errors)) + duration = '{:.2f}'.format(time.time() - start) + print('Verification took ' + duration + ' seconds.') + return isinstance(vresult, verifier.Success) + return True except (TypeException, InvalidProgramException, UnsupportedException) as e: print("Translation failed") if isinstance(e, (InvalidProgramException, UnsupportedException)): diff --git a/src/nagini_translation/verifier.py b/src/nagini_translation/verifier.py index bfc995239..5342c4baa 100644 --- a/src/nagini_translation/verifier.py +++ b/src/nagini_translation/verifier.py @@ -148,7 +148,10 @@ def verify(self, modules, prog: 'silver.ast.Program', arp=False, sif=False) -> V def __del__(self): if hasattr(self, 'silicon') and self.silicon: - self.silicon.stop() + try: + self.silicon.stop() + except Exception: + pass class Carbon: From a305167e7c665e46e1ffb148d5072d5e4cd2a991 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 28 Feb 2026 11:59:13 +0100 Subject: [PATCH 121/126] Use threads for benchmark to reliably stop them --- src/nagini_translation/main.py | 49 ++++++++++++++++++------------ src/nagini_translation/verifier.py | 14 +++++++-- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 5fe4d8ecf..26fde8ee6 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -11,8 +11,8 @@ import json import logging import os -import signal import sys +import threading import re import time import traceback @@ -52,7 +52,7 @@ ViperVerifier ) from nagini_translation import verifier -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Union TYPE_ERROR_PATTERN = r"^(?P.*):(?P\d+): error: (?P.*)$" @@ -196,6 +196,11 @@ def collect_modules(analyzer: Analyzer, path: str) -> None: for task in analyzer.deferred_tasks: task() +def get_verifier(path: str, jvm: JVM, viper_args: List[str], backend=ViperVerifier.silicon, counterexample=False) -> Union[Silicon, Carbon]: + if backend == ViperVerifier.silicon: + return Silicon(jvm, path, viper_args, counterexample) + elif backend == ViperVerifier.carbon: + return Carbon(jvm, path, viper_args) def verify(modules, prog: 'viper.silver.ast.Program', path: str, jvm: JVM, viper_args: List[str], backend=ViperVerifier.silicon, arp=False, counterexample=False, sif=False) -> VerificationResult: @@ -203,10 +208,7 @@ def verify(modules, prog: 'viper.silver.ast.Program', path: str, jvm: JVM, viper Verifies the given Viper program """ try: - if backend == ViperVerifier.silicon: - verifier = Silicon(jvm, path, viper_args, counterexample) - elif backend == ViperVerifier.carbon: - verifier = Carbon(jvm, path, viper_args) + verifier = get_verifier(path, jvm, viper_args, backend, counterexample) vresult = verifier.verify(modules, prog, arp=arp, sif=sif) return vresult except JException as je: @@ -456,20 +458,28 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di for i in range(args.benchmark): start = time.time() timed_out = False - if args.benchmark_timeout > 0: - def _timeout_handler(signum, frame): - raise TimeoutError() - signal.signal(signal.SIGALRM, _timeout_handler) - signal.alarm(args.benchmark_timeout) - try: - modules, prog = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, arp=arp, base_dir=base_dir, - ignore_global=args.ignore_global, float_encoding=args.float_encoding) - vresult = verify(modules, prog, python_file, jvm, viper_args, backend=backend, arp=arp) - except TimeoutError: + + verifier_ref = [None] + def _run_iteration(holder=verifier_ref): + try: + modules_local, prog_local = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, + arp=arp, base_dir=base_dir, ignore_global=args.ignore_global, float_encoding=args.float_encoding) + verifier = get_verifier(python_file, jvm, viper_args, backend) + holder[0] = verifier + verifier.verify(modules_local, prog_local, arp=arp) + except Exception: + pass + + thread = threading.Thread(target=_run_iteration, daemon=True) + thread.start() + timeout = args.benchmark_timeout if args.benchmark_timeout > 0 else None + thread.join(timeout=timeout) + if thread.is_alive(): timed_out = True - finally: - if args.benchmark_timeout > 0: - signal.alarm(0) + verifier = verifier_ref[0] + if verifier is not None: + verifier.stop() + thread.join(timeout=10) end = time.time() if timed_out: print("{}, {}, {}, {}, TIMEOUT".format( @@ -489,7 +499,6 @@ def _timeout_handler(signum, frame): if submitter is not None: submitter.setSuccess(vresult.__bool__()) submitter.submit() - if args.benchmark < 1: if args.verbose: print("Verification completed.") print(vresult.to_string(args.ide_mode, args.show_viper_errors)) diff --git a/src/nagini_translation/verifier.py b/src/nagini_translation/verifier.py index 5342c4baa..6580a8ebd 100644 --- a/src/nagini_translation/verifier.py +++ b/src/nagini_translation/verifier.py @@ -146,13 +146,16 @@ def verify(self, modules, prog: 'silver.ast.Program', arp=False, sif=False) -> V else: return Success() - def __del__(self): + def stop(self): if hasattr(self, 'silicon') and self.silicon: try: self.silicon.stop() except Exception: pass + def __del__(self): + self.stop() + class Carbon: """ @@ -196,4 +199,11 @@ def verify(self, modules, prog: 'silver.ast.Program', arp=False, sif=False) -> V errors += [it.next()] return Failure(errors, self.jvm, modules, sif) else: - return Success() \ No newline at end of file + return Success() + + def stop(self): + if hasattr(self, 'carbon') and self.carbon: + try: + self.carbon.stop() + except Exception: + pass \ No newline at end of file From a09873134b8f910c1c9c331e9d3cd96abf4a754b Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 28 Feb 2026 13:01:22 +0100 Subject: [PATCH 122/126] Accumulate benchmark results --- src/nagini_translation/main.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 26fde8ee6..26d45f970 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -454,19 +454,23 @@ def translate_and_verify(python_file, jvm, args, print=print, arp=False, base_di raise ValueError('Unknown verifier specified: ' + args.verifier) viper_args = [] if args.viper_arg is None else args.viper_arg.split(",") if args.benchmark >= 1: - print("Run, Total, Start, End, Time".format()) + print("Run, Total, Start, End, Time, Result") + n_success = 0 + n_failure = 0 + n_timeout = 0 for i in range(args.benchmark): start = time.time() timed_out = False verifier_ref = [None] - def _run_iteration(holder=verifier_ref): + result_ref = [None] + def _run_iteration(vholder=verifier_ref, rholder=result_ref): try: modules_local, prog_local = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, arp=arp, base_dir=base_dir, ignore_global=args.ignore_global, float_encoding=args.float_encoding) verifier = get_verifier(python_file, jvm, viper_args, backend) - holder[0] = verifier - verifier.verify(modules_local, prog_local, arp=arp) + vholder[0] = verifier + rholder[0] = verifier.verify(modules_local, prog_local, arp=arp) except Exception: pass @@ -482,11 +486,20 @@ def _run_iteration(holder=verifier_ref): thread.join(timeout=10) end = time.time() if timed_out: - print("{}, {}, {}, {}, TIMEOUT".format( + n_timeout += 1 + print("{}, {}, {}, {}, TIMEOUT, TIMEOUT".format( i, args.benchmark, start, end)) else: - print("{}, {}, {}, {}, {}".format( - i, args.benchmark, start, end, end - start)) + vresult = result_ref[0] + result_str = "SUCCESS" if isinstance(vresult, verifier.Success) else "FAILURE" + if isinstance(vresult, verifier.Success): + n_success += 1 + else: + n_failure += 1 + print("{}, {}, {}, {}, {}, {}".format( + i, args.benchmark, start, end, end - start, result_str)) + print("Results: {} success, {} failure, {} timeout out of {} runs".format( + n_success, n_failure, n_timeout, args.benchmark)) else: submitter = None if args.submit_for_evaluation: From 186039546c41c0e019b0262de0d2aee0f731bc5f Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 28 Feb 2026 17:45:03 +0100 Subject: [PATCH 123/126] Fix name overwrite in benchmark (cherry picked from commit 23a47621d1d13859226becaf16aa4b824d4ff201) --- src/nagini_translation/main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/nagini_translation/main.py b/src/nagini_translation/main.py index 26d45f970..b3b11508d 100755 --- a/src/nagini_translation/main.py +++ b/src/nagini_translation/main.py @@ -468,9 +468,9 @@ def _run_iteration(vholder=verifier_ref, rholder=result_ref): try: modules_local, prog_local = translate(python_file, jvm, args.int_bitops_size, selected=selected, sif=args.sif, arp=arp, base_dir=base_dir, ignore_global=args.ignore_global, float_encoding=args.float_encoding) - verifier = get_verifier(python_file, jvm, viper_args, backend) - vholder[0] = verifier - rholder[0] = verifier.verify(modules_local, prog_local, arp=arp) + ver = get_verifier(python_file, jvm, viper_args, backend) + vholder[0] = ver + rholder[0] = ver.verify(modules_local, prog_local, arp=arp) except Exception: pass @@ -478,13 +478,15 @@ def _run_iteration(vholder=verifier_ref, rholder=result_ref): thread.start() timeout = args.benchmark_timeout if args.benchmark_timeout > 0 else None thread.join(timeout=timeout) + if thread.is_alive(): timed_out = True - verifier = verifier_ref[0] - if verifier is not None: - verifier.stop() + ver = verifier_ref[0] + if ver is not None: + ver.stop() thread.join(timeout=10) end = time.time() + if timed_out: n_timeout += 1 print("{}, {}, {}, {}, TIMEOUT, TIMEOUT".format( From e6b837c5faa4d1abda6e1083a30fc0c1740fcc53 Mon Sep 17 00:00:00 2001 From: marcoeilers Date: Tue, 3 Mar 2026 15:34:14 +0100 Subject: [PATCH 124/126] Fixing let expression crash --- src/nagini_translation/translators/contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index dfc75764f..0169c9c99 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -850,7 +850,7 @@ def translate_let(self, node: ast.Call, ctx: Context, arg = lambda_.args.args[0] var = ctx.actual_function.get_variable(lambda_prefix + arg.arg) - exp_stmt, exp_val = self.translate_expr(node.args[0], ctx) + exp_stmt, exp_val = self.translate_expr(node.args[0], ctx, target_type=var.decl.typ()) ctx.set_alias(arg.arg, var, None) From c697778119cb448752d3018e9808ef211caac489 Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Sat, 7 Mar 2026 11:02:39 +0100 Subject: [PATCH 125/126] Add support for non-frozen dataclasses Failing tests are due to list triggers --- src/nagini_translation/analyzer.py | 48 ++++--- .../functional/translation/test_dataclass.py | 6 +- .../functional/translation/test_dataclass2.py | 9 -- .../translation/test_dataclass_defaults.py | 6 +- ..._dataclass4.py => test_dataclass_field.py} | 0 ...ataclass3.py => test_dataclass_no_init.py} | 0 .../functional/verification/test_dataclass.py | 55 +++----- .../verification/test_dataclass_defaults.py | 4 +- .../verification/test_dataclass_frozen.py | 132 ++++++++++++++++++ 9 files changed, 193 insertions(+), 67 deletions(-) delete mode 100644 tests/functional/translation/test_dataclass2.py rename tests/functional/translation/{test_dataclass4.py => test_dataclass_field.py} (100%) rename tests/functional/translation/{test_dataclass3.py => test_dataclass_no_init.py} (100%) create mode 100644 tests/functional/verification/test_dataclass_frozen.py diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index b90aeb251..5a7ae8635 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -592,8 +592,6 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: cls.dataclass = True if self.is_frozen_dataclass(node): cls.frozen = True - else: - raise UnsupportedException(node, 'Non frozen dataclass currently not supported') for kw in node.keywords: if kw.arg == 'metaclass' and isinstance(kw.value, ast.Name) and kw.value.id == 'ABCMeta': continue @@ -614,16 +612,19 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: args: list[ast.arg] = [] defaults: list[ast.expr] = [] - stmts: list[ast.stmt] = [] + postconditions: list[ast.stmt] = [] # Parse fields, add implicit args and post conditions args.append(self._create_arg_ast(node, 'self', None)) for name, field in self.current_class.fields.items(): args.append(self._create_arg_ast(node, name, field.type.name)) - stmts.append(self._create_comp_postcondition(node, + self_attr = ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0) + if not self.current_class.frozen: + postconditions.append(self._create_acc_postcondition(node, self_attr)) + postconditions.append(self._create_comp_postcondition(node, ast.Attribute(self._create_name_ast('self', node), name, ast.Load(), lineno=node.lineno, col_offset=0), self._create_name_ast(name, node), ast.Is())) - if field.result != None: + if getattr(field, 'result', None) is not None: defaults.append(field.result) field.result = None @@ -634,6 +635,7 @@ def _add_dataclass_init_method(self, node: ast.ClassDef) -> None: # Add decorators decorator_list: list[ast.expr] = [self._create_name_ast('ContractOnly', node)] + stmts = postconditions function_def = ast.FunctionDef('__init__', ast_arguments, stmts, decorator_list, returns=None, lineno=node.lineno, col_offset=0) self.visit(function_def, node) @@ -654,13 +656,15 @@ def _create_arg_ast(self, node, arg: str, type_name: Optional[str] = None) -> as return ast.arg(arg, name_node, lineno=node.lineno, col_offset=0) def _create_comp_postcondition(self, node, left: ast.expr, right: ast.expr, op: ast.cmpop) -> ast.stmt: - compare = ast.Compare( - left, - ops=[op], - comparators=[right], - lineno=node.lineno, col_offset=0) + compare = ast.Compare(left, ops=[op], comparators=[right], + lineno=node.lineno, col_offset=0) return ast.Expr(ast.Call(self._create_name_ast('Ensures', node), [compare], [], lineno=node.lineno, col_offset=0)) + def _create_acc_postcondition(self, node, attr: ast.expr) -> ast.stmt: + acc_call = ast.Call(self._create_name_ast('Acc', node), [attr], [], + lineno=node.lineno, col_offset=0) + return ast.Expr(ast.Call(self._create_name_ast('Ensures', node), [acc_call], [], lineno=node.lineno, col_offset=0)) + def _create_name_ast(self, id: str, node) -> ast.Name: return ast.Name(id, ast.Load(), lineno=node.lineno, col_offset=0) @@ -1212,8 +1216,8 @@ def todo(): self.track_access(node, var) self.deferred_tasks.append(todo) return - elif self.current_class.dataclass and self.current_class.frozen: - # Node is a field of a frozen dataclass + elif self.current_class.dataclass: + # Node is a field of a dataclass if isinstance(node.ctx, ast.Load): return @@ -1243,16 +1247,20 @@ def todo(): self_type, _ = self.module.get_type([self.current_class.name, '__init__'], 'self') self.module.types.all_types[context] = self_type - # Create a property for this field - ast_arguments = ast.arguments([], [self._create_arg_ast(node, 'self', None)], None, [], [], None, []) - stmts = [ast.Expr(ast.Call(self._create_name_ast('Decreases', node), [ast.Constant(None)], []))] - decorator_list: list[ast.expr] = [ast.Name('property'), ast.Name('ContractOnly')] - function_def = ast.FunctionDef(node.id, ast_arguments, stmts, decorator_list, returns=annotation, lineno=node.lineno, col_offset=0) - self.visit(function_def, self.current_class.node) + # Create a property for frozen fields, or a normal field for non-frozen + if self.current_class.frozen: + ast_arguments = ast.arguments([], [self._create_arg_ast(node, 'self', None)], None, [], [], None, []) + stmts = [ast.Expr(ast.Call(self._create_name_ast('Decreases', node), [ast.Constant(None)], []))] + decorator_list: list[ast.expr] = [ast.Name('property'), ast.Name('ContractOnly')] + function_def = ast.FunctionDef(node.id, ast_arguments, stmts, decorator_list, returns=annotation, lineno=node.lineno, col_offset=0) + self.visit(function_def, self.current_class.node) + else: + self.current_class.add_field(node.id, node, self.typeof(node)) # Adjust the class body self.current_class.node.body.remove(assign) - self.current_class.node.body.append(function_def) + if self.current_class.frozen: + self.current_class.node.body.append(function_def) if assign.value != None: field_obj = self.current_class.fields[node.id] @@ -1843,6 +1851,8 @@ def is_dataclass(self, cls: ast.ClassDef) -> bool: def _dataclass_check_unsupported_keywords(self, cls: ast.ClassDef) -> None: decorator = [d for d in cls.decorator_list if self.__resolve_decorator(d)[1] == 'dataclass'][0] + if isinstance(decorator, ast.Name): + return assert isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name) supported_keywords = ["frozen"] diff --git a/tests/functional/translation/test_dataclass.py b/tests/functional/translation/test_dataclass.py index 6f79907c5..48cf3650e 100644 --- a/tests/functional/translation/test_dataclass.py +++ b/tests/functional/translation/test_dataclass.py @@ -20,12 +20,12 @@ class B: num: int = 2 direct = 3 -@dataclass(frozen=True) +@dataclass class FactoryClass: arr: List[int] = field(default_factory=list) -@dataclass() #:: ExpectedOutput(unsupported:Non frozen dataclass currently not supported) -class NonFrozen: +@dataclass(frozen=False) +class NonFrozenKeyword: data: int def test_cons() -> None: diff --git a/tests/functional/translation/test_dataclass2.py b/tests/functional/translation/test_dataclass2.py deleted file mode 100644 index 4d62a8c9f..000000000 --- a/tests/functional/translation/test_dataclass2.py +++ /dev/null @@ -1,9 +0,0 @@ -# Any copyright is dedicated to the Public Domain. -# http://creativecommons.org/publicdomain/zero/1.0/ - -from nagini_contracts.contracts import * -from dataclasses import dataclass - -@dataclass(frozen=False) #:: ExpectedOutput(unsupported:Non frozen dataclass currently not supported) -class NonFrozen2: - data: int \ No newline at end of file diff --git a/tests/functional/translation/test_dataclass_defaults.py b/tests/functional/translation/test_dataclass_defaults.py index 88332fc98..a689c653e 100644 --- a/tests/functional/translation/test_dataclass_defaults.py +++ b/tests/functional/translation/test_dataclass_defaults.py @@ -11,6 +11,10 @@ class Color_Enum(IntEnum): blue = 2 yellow = 3 -@dataclass(frozen=True) +@dataclass class MyClass(): + color: Color_Enum = Color_Enum.red + +@dataclass(frozen=True) +class MyClassF(): color: Color_Enum = Color_Enum.red \ No newline at end of file diff --git a/tests/functional/translation/test_dataclass4.py b/tests/functional/translation/test_dataclass_field.py similarity index 100% rename from tests/functional/translation/test_dataclass4.py rename to tests/functional/translation/test_dataclass_field.py diff --git a/tests/functional/translation/test_dataclass3.py b/tests/functional/translation/test_dataclass_no_init.py similarity index 100% rename from tests/functional/translation/test_dataclass3.py rename to tests/functional/translation/test_dataclass_no_init.py diff --git a/tests/functional/verification/test_dataclass.py b/tests/functional/verification/test_dataclass.py index f79e796bf..f404a3a5e 100644 --- a/tests/functional/verification/test_dataclass.py +++ b/tests/functional/verification/test_dataclass.py @@ -1,42 +1,35 @@ # Any copyright is dedicated to the Public Domain. # http://creativecommons.org/publicdomain/zero/1.0/ +from typing import cast + from nagini_contracts.contracts import * from dataclasses import dataclass, field -@dataclass(frozen=True) +@dataclass class A: data: int @Pure def __eq__(self, other: object) -> bool: + Requires(Rd(self.data)) + Requires(Implies(isinstance(other, A), Rd(cast(A, other).data))) if not isinstance(other, A): return False return self.data == other.data -@dataclass(frozen=True) -class B: - field: A - - @Pure - def __eq__(self, other: object) -> bool: - if not isinstance(other, B): - return False - - return self.field == other.field - -@dataclass(frozen=True) +@dataclass class C: fields: list[A] -@dataclass(frozen=True) +@dataclass class D: value: int length: int text: str -@dataclass(frozen=True) +@dataclass class ListClass: arr: list[int] = field(default_factory=list) @@ -45,8 +38,9 @@ def test_1(val: int) -> None: assert a.data == val + a.data = 3 #:: ExpectedOutput(assert.failed:assertion.false) - assert a.data == 2 + assert a.data == val def test_2() -> None: a1 = A(0) @@ -64,6 +58,14 @@ def test_2() -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert c.fields[1].data == c.fields[2].data +def test_3() -> None: + c = C([A(0)]) + + assert c.fields[0].data == 0 + + c.fields = [] + assert len(c.fields) == 0 + def test_named_param(val: int, length: int) -> None: d = D(length=length, value=val, text="") @@ -83,19 +85,6 @@ def test_eq_1(val: int) -> None: #:: ExpectedOutput(assert.failed:assertion.false) assert a1 == a3 -def test_eq_2(a1: A, a2: A) -> None: - b1 = B(a1) - b2 = B(a1) - b3 = B(a2) - - assert b1 == b2 - - if a1 == a2: - assert b1 == b3 - else: - #:: ExpectedOutput(assert.failed:assertion.false) - assert b1 == b3 - def test_list_ref() -> None: l = [1,2,3] f = ListClass(l) @@ -114,16 +103,16 @@ def test_list_conditions(l: list[int]) -> None: assert Forall(f.arr, lambda i: 0 <= i and i < 10) def test_list_eq(left: ListClass, right: ListClass) -> None: - Requires(list_pred(left.arr)) - Requires(list_pred(right.arr)) + Requires(Acc(left.arr) and list_pred(left.arr)) + Requires(Acc(right.arr) and list_pred(right.arr)) Requires(len(left.arr) == len(right.arr)) #:: ExpectedOutput(assert.failed:assertion.false) assert left.arr == right.arr def test_list_eq_elements(left: ListClass, right: ListClass) -> None: - Requires(list_pred(left.arr)) - Requires(list_pred(right.arr)) + Requires(Acc(left.arr) and list_pred(left.arr)) + Requires(Acc(right.arr) and list_pred(right.arr)) Requires(len(left.arr) == len(right.arr)) Requires(Forall(int, lambda i: Implies(0 <= i and i < len(left.arr), left.arr[i] == right.arr[i]))) diff --git a/tests/functional/verification/test_dataclass_defaults.py b/tests/functional/verification/test_dataclass_defaults.py index 2b4cfa7c9..8733d97e8 100644 --- a/tests/functional/verification/test_dataclass_defaults.py +++ b/tests/functional/verification/test_dataclass_defaults.py @@ -6,7 +6,7 @@ from nagini_contracts.contracts import * from dataclasses import dataclass, field -@dataclass(frozen=True) +@dataclass class A: num: int = 2 num2: int = 10 @@ -33,7 +33,7 @@ class Color_Enum(IntEnum): blue = 2 yellow = 3 -@dataclass(frozen=True) +@dataclass class C: color: Color_Enum = Color_Enum.green diff --git a/tests/functional/verification/test_dataclass_frozen.py b/tests/functional/verification/test_dataclass_frozen.py new file mode 100644 index 000000000..f79e796bf --- /dev/null +++ b/tests/functional/verification/test_dataclass_frozen.py @@ -0,0 +1,132 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from nagini_contracts.contracts import * +from dataclasses import dataclass, field + +@dataclass(frozen=True) +class A: + data: int + + @Pure + def __eq__(self, other: object) -> bool: + if not isinstance(other, A): + return False + + return self.data == other.data + +@dataclass(frozen=True) +class B: + field: A + + @Pure + def __eq__(self, other: object) -> bool: + if not isinstance(other, B): + return False + + return self.field == other.field + +@dataclass(frozen=True) +class C: + fields: list[A] + +@dataclass(frozen=True) +class D: + value: int + length: int + text: str + +@dataclass(frozen=True) +class ListClass: + arr: list[int] = field(default_factory=list) + +def test_1(val: int) -> None: + a = A(val) + + assert a.data == val + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a.data == 2 + +def test_2() -> None: + a1 = A(0) + a2 = A(3) + a3 = A(42) + c = C([a1, a2, a3]) + + assert len(c.fields) == 3 + assert c.fields[0].data == 0 + + c.fields.append(A(20)) + assert len(c.fields) == 4 + assert c.fields[3].data == 20 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert c.fields[1].data == c.fields[2].data + +def test_named_param(val: int, length: int) -> None: + d = D(length=length, value=val, text="") + + assert d.value == val + assert d.text == "" + + #:: ExpectedOutput(assert.failed:assertion.false) + assert d.length == 2 + +def test_eq_1(val: int) -> None: + a1 = A(val) + a2 = A(val) + a3 = A(0) + + assert a1 == a2 + + #:: ExpectedOutput(assert.failed:assertion.false) + assert a1 == a3 + +def test_eq_2(a1: A, a2: A) -> None: + b1 = B(a1) + b2 = B(a1) + b3 = B(a2) + + assert b1 == b2 + + if a1 == a2: + assert b1 == b3 + else: + #:: ExpectedOutput(assert.failed:assertion.false) + assert b1 == b3 + +def test_list_ref() -> None: + l = [1,2,3] + f = ListClass(l) + + l.append(4) + assert len(f.arr) == 4 + assert ToSeq(f.arr) == PSeq(1,2,3,4) + #:: ExpectedOutput(assert.failed:assertion.false) + assert f.arr[0] == 5 + +def test_list_conditions(l: list[int]) -> None: + Requires(list_pred(l)) + Requires(Forall(l, lambda i: 0 <= i and i < 10)) + + f = ListClass(l) + assert Forall(f.arr, lambda i: 0 <= i and i < 10) + +def test_list_eq(left: ListClass, right: ListClass) -> None: + Requires(list_pred(left.arr)) + Requires(list_pred(right.arr)) + Requires(len(left.arr) == len(right.arr)) + + #:: ExpectedOutput(assert.failed:assertion.false) + assert left.arr == right.arr + +def test_list_eq_elements(left: ListClass, right: ListClass) -> None: + Requires(list_pred(left.arr)) + Requires(list_pred(right.arr)) + Requires(len(left.arr) == len(right.arr)) + Requires(Forall(int, lambda i: Implies(0 <= i and i < len(left.arr), left.arr[i] == right.arr[i]))) + + assert left.arr == right.arr + #:: ExpectedOutput(assert.failed:assertion.false) + assert left.arr is right.arr \ No newline at end of file From ea43a7ec466a1f39e57f25c60cb8ab87be11ddba Mon Sep 17 00:00:00 2001 From: Luca Schafroth Date: Tue, 17 Mar 2026 15:43:11 +0100 Subject: [PATCH 126/126] Revert "Fixing things" This reverts commit 66d4be12388f19555b22e41a6e44d596d691cff1. Revert "Trying stuff" This reverts commit 8763511f924858c9c0fc52b1cfe48ef674ecbcaf. --- src/nagini_translation/analyzer.py | 3 +-- src/nagini_translation/resources/builtins.json | 2 +- src/nagini_translation/resources/seq.sil | 9 ++++----- src/nagini_translation/translators/contract.py | 14 ++++---------- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/nagini_translation/analyzer.py b/src/nagini_translation/analyzer.py index 5a7ae8635..06b7c5563 100644 --- a/src/nagini_translation/analyzer.py +++ b/src/nagini_translation/analyzer.py @@ -946,9 +946,8 @@ def visit_Lambda(self, node: ast.Lambda) -> None: arg.arg, arg, arg_type) self._aliases[arg.arg] = var else: - arg_type = self.typeof(arg).try_unbox() var = self.node_factory.create_python_var( - arg.arg, arg, arg_type) + arg.arg, arg, self.typeof(arg)) alts = self.get_alt_types(node) var.alt_types = alts local_name = name + '$' + arg.arg diff --git a/src/nagini_translation/resources/builtins.json b/src/nagini_translation/resources/builtins.json index 85eb11fcd..a5d17d4d4 100644 --- a/src/nagini_translation/resources/builtins.json +++ b/src/nagini_translation/resources/builtins.json @@ -824,7 +824,7 @@ "type": "__prim__bool" }, "__getitem__": { - "args": ["PSeq", "__prim__int"], + "args": ["PSeq", "int"], "type": "object" }, "__sil_seq__": { diff --git a/src/nagini_translation/resources/seq.sil b/src/nagini_translation/resources/seq.sil index 97fcfd0c4..f494849bf 100644 --- a/src/nagini_translation/resources/seq.sil +++ b/src/nagini_translation/resources/seq.sil @@ -20,12 +20,13 @@ function PSeq___contains__(self: Ref, item: Ref): Bool ensures result == (item in PSeq___sil_seq__(self)) ensures result ==> issubtype(typeof(item), PSeq_arg(typeof(self), 0)) -function PSeq___getitem__(self: Ref, index: Int): Ref +function PSeq___getitem__(self: Ref, index: Ref): Ref decreases _ requires issubtype(typeof(self), PSeq(PSeq_arg(typeof(self), 0))) + requires issubtype(typeof(index), int()) requires @error("Index may be out of bounds.")(let ln == (PSeq___len__(self)) in - @error("Index may be out of bounds.")(((index) < 0 ==> (index) >= -ln) && ((index) >= 0 ==> (index) < ln))) - ensures result == ((index) >= 0 ? PSeq___sil_seq__(self)[(index)] : PSeq___sil_seq__(self)[PSeq___len__(self) + (index)]) + @error("Index may be out of bounds.")((int___unbox__(index) < 0 ==> int___unbox__(index) >= -ln) && (int___unbox__(index) >= 0 ==> int___unbox__(index) < ln))) + ensures result == (int___unbox__(index) >= 0 ? PSeq___sil_seq__(self)[int___unbox__(index)] : PSeq___sil_seq__(self)[PSeq___len__(self) + int___unbox__(index)]) ensures issubtype(typeof(result), PSeq_arg(typeof(self), 0)) function PSeq___len__(self: Ref): Int @@ -65,8 +66,6 @@ function PSeq___eq__(self: Ref, other: Ref): Bool requires PSeq_arg(typeof(self), 0) == PSeq_arg(typeof(other), 0) ensures result == (PSeq___sil_seq__(self) == PSeq___sil_seq__(other)) ensures result ==> self == other // extensionality - ensures ((|PSeq___sil_seq__(self)| == |PSeq___sil_seq__(other)|) && - (forall i: Int :: {PSeq___getitem__(self, i)} 0 <= i < |PSeq___sil_seq__(self)| ==> PSeq___getitem__(self, i) == PSeq___getitem__(other, i))) ==> result ensures result == object___eq__(self, other) diff --git a/src/nagini_translation/translators/contract.py b/src/nagini_translation/translators/contract.py index 3a3f2b62c..32f92bfcf 100644 --- a/src/nagini_translation/translators/contract.py +++ b/src/nagini_translation/translators/contract.py @@ -990,11 +990,8 @@ def translate_forall(self, node: ast.Call, ctx: Context, triggers = [trigger] + triggers except Exception: pass - if var.type.name in PRIMITIVES: - var_type_check = self.viper.TrueLit(self.no_position(ctx), self.no_info(ctx)) - else: - var_type_check = self.type_check(var.ref(), var.type, - self.no_position(ctx), ctx, False) + var_type_check = self.type_check(var.ref(), var.type, + self.no_position(ctx), ctx, False) implication = self.viper.Implies(var_type_check, implication, self.to_position(node, ctx), self.no_info(ctx)) @@ -1070,11 +1067,8 @@ def translate_exists(self, node: ast.Call, ctx: Context, triggers = [trigger] + triggers except Exception: pass - if var.type.name in PRIMITIVES: - var_type_check = self.viper.TrueLit(self.no_position(ctx), self.no_info(ctx)) - else: - var_type_check = self.type_check(var.ref(), var.type, - self.no_position(ctx), ctx, False) + var_type_check = self.type_check(var.ref(), var.type, + self.no_position(ctx), ctx, False) implication = self.viper.And(var_type_check, implication, self.to_position(node, ctx), self.no_info(ctx))