1.6
¤
Arrays
Array types represent maps from an interval [0,end) of a type "domain" to a type "range". An array type thing.t can be created like this:
instance thing : array(domain.t,range.t)
The implementation of this type requires that domain.t be interpreted as 'int'.
module array(domain,range) = {
type t
action empty returns (a:t)
action create(s:domain,y:range) returns (a:t)
action set(a:t,x:domain,y:range) returns (a:t)
action get(a:t,x:domain) returns (y:range)
action size(a:t) returns (s:domain)
action resize(a:t,s:domain,v:range) returns (a:t)
action append(a:t,v:range) returns (a:t)
¤
Representation
Function "end" gives the end value of an array while value(a,x) gives the value that x maps to in a.
function end(A:t) : domain
function value(A:t,X:domain) : range
¤
Specification
Notice that get and set have the precondition that x is in [0,end).
object spec = {
property end(X) >= 0
after empty {
assert end(a) = 0
}
before create {
assert 0 <= s
}
after create {
assert end(a) = s & value(a,X) = y
}
before set {
assert 0 <= x & x < end(a)
}
after set {
assert end(a) = end(old a);
assert value(a,X) = y if X = x else value(old a,X)
}
before get {
assert 0 <= x & x < end(a)
}
after get {
assert value(a,x) = y
}
after size {
assert s = end(a)
}
after resize {
assert end(a) = s;
assert 0 <= X & X < end(old a) -> value(a,X) = value(old a,X);
assert end(old a) <= X & X < s -> value(a,X) = v
}
after append {
assert end(a) > end(old a) & ~(end(old a) < X & X < end(a));
assert 0 <= X & X < end(old a) -> value(a,X) = value(old a,X);
assert value(a,end(old a)) = v
}
}
object impl = {
<<< member
class t_ : std::vector<range> {};
interpret t -> <<< t_ >>>
interpret t -> <<< std::vector<`range`> >>>
definition value(a:t,i:domain) = <<< (0 <= `i` && `i` < `a`.size()) ? `a`[`i`] : val >>>
definition end(a:t) = <<< `a`.size() >>>
implement create {
<<<
`a`.resize(`s`);
for (unsigned i = 0; i < `s`; i++)
`a`[i] = y;
>>>
}
implement empty {
<<<
>>>
}
implement set {
<<<
if (0 <= `x` && `x` < (`domain`)`a`.size())
`a`[`x`] = `y`;
>>>
}
implement get {
<<<
if (0 <= `x` && `x` < (`domain`)`a`.size())
`y` = `a`[`x`];
>>>
}
implement size {
<<<
`s` = (`domain`) `a`.size();
>>>
}
implement resize {
<<<
unsigned __old_size = `a`.size();
`a`.resize(`s`);
for (unsigned i = __old_size; i < (unsigned)`s`; i++)
`a`[i] = v;
>>>
}
implement append {
<<<
`a`.push_back(`v`);
>>>
}
<<< impl
std::ostream &operator <<(std::ostream &s, const `t` &a) {
s << '[';
for (unsigned i = 0; i < a.size(); i++) {
if (i != 0)
s << ',';
s << a[i];
}
s << ']';
return s;
}
template <>
`t` _arg<`t`>(std::vector<ivy_value> &args, unsigned idx, long long bound) {
ivy_value &arg = args[idx];
if (arg.atom.size())
throw out_of_bounds(idx);
`t` a;
a.resize(arg.fields.size());
for (unsigned i = 0; i < a.size(); i++) {
a[i] = _arg<`range`>(arg.fields,i,0);
}
return a;
}
template <>
void __deser<`t`>(ivy_deser &inp, `t` &res) {
inp.open_list();
while(inp.open_list_elem()) {
res.resize(res.size()+1);
__deser(inp,res.back());
inp.close_list_elem();
}
inp.close_list();
}
template <>
void __ser<`t`>(ivy_ser &res, const `t` &inp) {
int sz = inp.size();
res.open_list(sz);
for (unsigned i = 0; i < (unsigned)sz; i++) {
res.open_list_elem();
__ser(res,inp[i]);
res.close_list_elem();
}
res.close_list();
}
template <>
z3::expr __to_solver(gen& g, const z3::expr& z3val, `t`& val) {
z3::expr z3end = g.apply("`end`",z3val);
z3::expr __ret = z3end == g.int_to_z3(z3end.get_sort(),val.size());
unsigned __sz = val.size();
for (unsigned __i = 0; __i < __sz; ++__i)
__ret = __ret && __to_solver(g,g.apply("`value`",z3val,g.int_to_z3(g.sort("`domain`"),__i)),val[__i]);
return __ret;
}
template <>
void __from_solver<`t`>( gen &g, const z3::expr &v,`t` &res){
`domain` __end;
__from_solver(g,g.apply("`end`",v),__end);
unsigned __sz = (unsigned) __end;
res.resize(__sz);
for (unsigned __i = 0; __i < __sz; ++__i)
__from_solver(g,g.apply("`value`",v,g.int_to_z3(g.sort("`domain`"),__i)),res[__i]);
}
template <>
void __randomize<`t`>( gen &g, const z3::expr &v, const std::string &sort_name){
unsigned __sz = rand() % 4;
z3::expr val_expr = g.int_to_z3(g.sort("`domain`"),__sz);
z3::expr pred = g.apply("`end`",v) == val_expr;
g.add_alit(pred);
for (unsigned __i = 0; __i < __sz; ++__i)
__randomize<`range`>(g,g.apply("`value`",v,g.int_to_z3(g.sort("`domain`"),__i)),"`range`");
}
>>>
}
trusted isolate iso = spec,impl
attribute test = impl
}
¤
Option types
An option is either empty or has value or type range_t.
module option(range_t) = {
type domain_t
instantiate array(domain_t,range_t)
derived is_empty(X:t) = (end(X) = 0)
derived contents(X:t) = value(X,0)
action just(inp:range_t) returns(out:t) = {
out := create(1,inp);
}
interpret domain_t -> bv[1]
}
¤
Null number types
A Null number type is one that has a special value representing "not a number".
The "not a number" value is encoded as the constant null_value.
module null_type(range_t,null_value) = {
type t = struct {
is_null : bool,
contents : range_t
}
<<< encode `t`
std::ostream &operator <<(std::ostream &s, const `t` &a) {
if (a.is_null)
s << "`null_value`";
else
s << a.contents;
return s;
}
template <>
`t` _arg<`t`>(std::vector<ivy_value> &args, unsigned idx, long long bound) {
ivy_value &arg = args[idx];
if (arg.fields.size())
throw out_of_bounds(idx);
`t` a;
a.is_null = (arg.atom == "`null_value`");
a.contents = _arg<`range_t`>(args,idx,bound);
return a;
}
template <>
void __ser<`t`>(ivy_ser &res, const `t` &t){
if (t.is_null)
res.set((long long)strtoull("`null_value`",NULL,0));
else
__ser(res,t.contents);
}
template <>
void __deser<`t`>(ivy_deser &inp, `t` &res){
long long val;
inp.get(val);
res.is_null = (val == (long long)strtoull("`null_value`",NULL,0));
std::vector<ivy_value> args;
args.resize(1);
std::ostringstream sts;
sts << val;
args[0].atom = sts.str();
res.contents = _arg<`range_t`>(args,0,0);
}
>>>
}
¤
Paths
A path is similar to an array but differs in the way it is serialized. That is, a path with elements e_0, e_1, ... is serialized as a string of the form "e_0/e_1/...". To prevent misinterpretation, a separator character '/' is replaced by '\/' and '\' by '\'. Also, string quatation marks '"' are elided from e_0, e_1 ... A further difference between paths and strings is that paths are extensional -- if two paths have the same sequence of elements, they are equal. This means that to stay decidable, the index type of paths must be finite.
A path type thing.t can be created like this:
instance thing : path(domain.t,range.t)
The implementation of this type requires that domain.t be interpreted as an integer type.
module path(domain,range) = {
type t
action empty returns (a:t)
action create(s:domain,y:range) returns (a:t)
action set(a:t,x:domain,y:range) returns (a:t)
action get(a:t,x:domain) returns (y:range)
action size(a:t) returns (s:domain)
action resize(a:t,s:domain,v:range) returns (a:t)
action append(a:t,v:range) returns (a:t)
¤
Representation
Function "end" gives the end value of an array while value(a,x) gives the value that x maps to in a.
function end(A:t) : domain
function value(A:t,X:domain) : range
¤
Specification
Notice that get and set have the precondition that x is in [0,end).
property [extensionality]
forall X,Y. (end(X) = end(Y))
& (forall I. (I < end(X) -> value(X,I) = value(Y,I)))
-> X = Y
object spec = {
property end(X) >= 0
after empty {
assert end(a) = 0
}
before create {
assert 0 <= s
}
after create {
assert end(a) = s & value(a,X) = y
}
before set {
assert 0 <= x & x < end(a)
}
after set {
assert value(a,X) = y if X = x else value(old a,X)
}
before get {
assert 0 <= x & x < end(a)
}
after get {
assert value(a,x) = y
}
after size {
assert s = end(a)
}
after resize {
assert end(a) = s;
assert 0 <= X & X < end(old a) -> value(a,X) = value(old a,X);
assert end(old a) <= X & X < s -> value(a,X) = v
}
after append {
assert end(a) > end(old a) & ~(end(old a) < X & X < end(a));
assert 0 <= X & X < end(old a) -> value(a,X) = value(old a,X);
assert value(a,end(old a)) = v
}
}
object impl = {
<<< member
class t_ : std::vector<range> {};
interpret t -> <<< t_ >>>
interpret t -> <<< std::vector<`range`> >>>
definition value(a:t,i:domain) = <<< (0 <= `i` && `i` < `a`.size()) ? `a`[`i`] : val >>>
definition end(a:t) = <<< `a`.size() >>>
implement create {
<<<
`a`.resize(`s`);
for (unsigned i = 0; i < `s`; i++)
`a`[i] = y;
>>>
}
implement empty {
<<<
>>>
}
implement set {
<<<
if (0 <= `x` && `x` < (`domain`)`a`.size())
`a`[`x`] = `y`;
>>>
}
implement get {
<<<
if (0 <= `x` && `x` < (`domain`)`a`.size())
`y` = `a`[`x`];
>>>
}
implement size {
<<<
`s` = (`domain`) `a`.size();
>>>
}
implement resize {
<<<
unsigned __old_size = `a`.size();
`a`.resize(`s`);
for (unsigned i = __old_size; i < (unsigned)`s`; i++)
`a`[i] = v;
>>>
}
implement append {
<<<
`a`.push_back(`v`);
>>>
}
<<< impl
std::ostream &operator <<(std::ostream &s, const `t` &a) {
s << '\"';
for (unsigned i = 0; i < a.size(); i++) {
if (i != 0)
s << '/';
std::ostringstream os;
os << a[i];
std::string st = os.str();
for (unsigned j = 0; j < st.size(); j++) {
char c = st[j];
if (c == '/')
s << "\\/";
else if (c == '\\')
s << "\\\\";
else if (c != '\"')
s << c;
}
}
s << '\"';
return s;
}
template <>
`t` _arg<`t`>(std::vector<ivy_value> &args, unsigned idx, long long bound) {
ivy_value &arg = args[idx];
if (arg.fields.size())
throw out_of_bounds(idx);
`t` a;
ivy_value tmp;
tmp.fields.resize(1);
std::string str = arg.atom;
for (unsigned i = 0; i < str.size(); i++) {
char c = str[i];
if (c == '\\' && i+1 < str.size())
tmp.fields.back().atom.push_back(str[++i]);
else if (c == '/')
tmp.fields.resize(tmp.fields.size()+1);
else if (c != '\"')
tmp.fields.back().atom.push_back(c);
}
a.resize(tmp.fields.size());
for (unsigned i = 0; i < a.size(); i++) {
a[i] = _arg<`range`>(tmp.fields,i,0);
}
return a;
}
template <>
void __deser<`t`>(ivy_deser &inp, `t` &res) {
inp.open_list();
while(inp.open_list_elem()) {
res.resize(res.size()+1);
__deser(inp,res.back());
inp.close_list_elem();
}
inp.close_list();
}
template <>
void __ser<`t`>(ivy_ser &res, const `t` &inp) {
int sz = inp.size();
res.open_list(sz);
for (unsigned i = 0; i < (unsigned)sz; i++) {
res.open_list_elem();
__ser(res,inp[i]);
res.close_list_elem();
}
res.close_list();
}
template <>
z3::expr __to_solver(gen& g, const z3::expr& z3val, `t`& val) {
z3::expr z3end = g.apply("`end`",z3val);
z3::expr cond = z3end == g.int_to_z3(z3end.get_sort(),val.size());
for (unsigned i = 0; i < val.size(); i++)
cond = cond && __to_solver(g,g.apply("`value`",z3val,g.int_to_z3(g.sort("`domain`"),i)),val[i]);
return cond;
}
template <>
void __from_solver<`t`>( gen &g, const z3::expr &v,`t` &res){
`domain` __end;
__from_solver(g,g.apply("`end`",v),__end);
unsigned __sz = (unsigned) __end;
res.resize(__sz);
for (unsigned __i = 0; __i < __sz; ++__i)
__from_solver(g,g.apply("`value`",v,g.int_to_z3(g.sort("`domain`"),__i)),res[__i]);
}
template <>
void __randomize<`t`>( gen &g, const z3::expr &v, const std::string &sort_name){
unsigned __sz = rand() % 4;
z3::expr val_expr = g.int_to_z3(g.sort("`domain`"),__sz);
z3::expr pred = g.apply("`end`",v) == val_expr;
g.add_alit(pred);
for (unsigned __i = 0; __i < __sz; ++__i)
__randomize<`range`>(g,g.apply("`value`",v,g.int_to_z3(g.sort("`domain`"),__i)),"`range`");
}
>>>
}
trusted isolate iso = spec,impl
}
¤
Maps.
This module provides a relational model of a function from a type dom to a type rng. Initially, everything is mapped to the element zero.
The key invariant of a map is that it is a function, i.e., for every X there exists a Y such that map(X,Y). The module does not state this invariant, however, since the quantifier alternation could destroy stratification. Instead it provides a "lemma" that allows the user to instantiate Y for particular values of X.
Parameters:
dom The domain type rng The range type zero The initial value for every domain element
module map(dom,rng,zero) = {
relation map(X:dom,Y:rng)
action set_(x:dom,y:rng)
action get(x:dom) returns (y:rng)
action lemma(x:dom)
object spec = {
init map(X,Y) <-> Y = zero
implement set_(x:dom,y:rng) {
call lemma(x);
map(x,Y) := Y = y
}
implement get(x:dom) returns (y:rng) {
assume map(x,y)
}
implement lemma(x:dom) {
assume exists Y. map(x,Y)
}
}
conjecture map(K,L) & map(K,M) -> L = M
}
¤
Partial functions
This module represents a mutable partial function type dom to type rng. The state consists of relation map that stores the function as a set of tuples, and an auxiliary set pre that gives the pre-image.
¤
module partial_function(dom,rng) = {
relation map(X:dom,Y:rng)
relation pre(X:dom)
action remap(x:dom,y:rng)
action remove(x:dom)
action get(x:dom) returns (y:rng)
action present(x:dom) returns (r:bool)
action lemma(x:dom)
object spec = {
init ~map(X,Y)
implement remap {
call lemma(x);
map(x,Y) := Y = y;
pre(x) := true
}
implement remove {
call lemma(x);
map(x,Y) := false;
pre(x) := false
}
after get {
assume pre(x) -> map(x,y)
}
after present {
assume r = pre(x)
}
implement lemma(x:dom) {
assume pre(x) -> exists Y. map(x,Y)
}
}
conjecture map(K,L) & map(K,M) -> L = M
}
¤
Witness
This is similar to partial_function, but has no domain. Useful for storing a witness to an existential quantifier without creating a function cycle.
¤
module witness(rng) = {
action set(v:rng)
action get returns (v:rng)
action is_empty returns (r:bool)
action clear
relation is(Y:rng)
relation present
object spec = {
after init {
is(Y) := false;
present := false
}
after set {
is(Y) := Y = v;
present := true
}
after get {
assert present -> is(v)
}
after clear {
is(Y) := false;
present := false
}
after is_empty {
assert r <-> ~present
}
}
object impl = {
relation full
individual val : rng
after init {
full := false
}
implement set {
val := v;
full := true
}
implement get {
v := val
}
implement clear {
full := false
}
implement is_empty {
r := ~full
}
conjecture (present <-> full) & (full -> is(val))
}
isolate iso = this
attribute test = impl
}
module functional(rel) = {
property [totality] exists Y. rel(X,Y)
property [injectivity] rel(X,Y) & rel(X,Z) -> Y = Z
}
module inverse_functional(rel) = {
property [totality] exists Y. rel(Y,X)
property [injectivity] rel(Y,X) & rel(Z,X) -> Y = Z
}
module set_wrapper(key) = {
object s = {}
<<< header
>>>
<<< member
std::set<`key`> `s`;
>>>
<<< init
`s`.insert(0);
>>>
implement insert(x:key) {
<<<
`s`.insert(`x`);
>>>
}
implement erase(lo:key,hi:key) {
<<<
`s`.erase(`s`.lower_bound(`lo`),`s`.upper_bound(`hi`));
>>>
}
implement get_glb(k:key) returns (res:key) {
<<<
`res` = *(--`s`.upper_bound(`k`));
>>>
}
}
¤
Ordered set representation
This is intended to be implemented using the STL set template.
Ordered sets assume the element type has a total non-strict order, with a least element 0. They provide insertion of elements (in log time), deletion of ranges (in n log(n) time) and finding greatest lower bounds (log n).
For help with proofs, this module also provides an auxiliary map "succ" that gives the successor of every element in the set. The "successor" of the maximal element in the set is 0.
module ordered_set(key) = {
action insert(nkey:key)
action erase(lo:key,hi:key)
action get_glb(k:key) returns (res:key)
relation s(K:key)
instantiate succ : map(key,key,0) # ghost
object spec = {
init s(K) <-> K = 0
before insert {
s(nkey) := true;
local v:key {
if some lub:key. ~(lub <= nkey) & s(lub) minimizing lub {
v := lub
}
else {
v := 0
};
call succ.set_(nkey,v)
};
if some glb:key. ~(nkey <= glb) & s(glb) maximizing glb {
call succ.lemma(glb); # instantiate succ(glb)
call succ.set_(glb,nkey)
}
}
before erase {
s(K) := s(K) & ~(lo <= K & K <= hi)
}
after get_glb {
if some glb:key. glb <= k & s(glb) maximizing glb {
call succ.lemma(glb); # instantiate succ(glb)
assume res = glb
}
}
}
conjecture s(K) & succ.map(K,L) & L ~= 0 -> ~(L <= K) & s(L)
conjecture s(K) & succ.map(K,L) & ~(M <= K) & (L = 0 | ~(L <= M)) -> ~s(M)
instance impl : set_wrapper(key)
}
module map_wrapper(key,value) = {
object s = {}
<<< header