Skip to content

Commit 7c38a6f

Browse files
rahxephon89claude
andcommitted
[move prover] ghost carrier and iterator validity for intrinsic maps
Intrinsic map types may declare ghost fields; such maps are represented by a per-instance carrier datatype wrapping the raw table, giving the ghosts constructor arguments to live in. Structural mutations havoc the ghosts; value writes through borrows preserve them. Ghost-less maps keep their raw table representation byte-identically. This enables per-object iterator validity as plain Move-level specs: a ghost brand on the map plus a ghost stamp on the iterator, with validity a spec fun over the two — no pragmas, AST ops, or loop instrumentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4fe6e03 commit 7c38a6f

17 files changed

Lines changed: 1500 additions & 193 deletions

third_party/move/move-model/src/builder/module_builder.rs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::{
2929
},
3030
pragmas::{
3131
is_pragma_valid_for_block, is_property_valid_for_condition, CONDITION_DEACTIVATED_PROP,
32-
CONDITION_EXPORT_PROP, CONDITION_INJECTED_PROP, INTRINSIC_PRAGMA,
32+
CONDITION_EXPORT_PROP, CONDITION_INJECTED_PROP, INTRINSIC_PRAGMA, INTRINSIC_TYPE_MAP,
3333
},
3434
symbol::{Symbol, SymbolPool},
3535
ty::{
@@ -4310,6 +4310,11 @@ impl ModuleBuilder<'_, '_> {
43104310
);
43114311
return;
43124312
}
4313+
// Ghost fields on intrinsic map types are read-only from spec
4314+
// blocks; that check lives in spec instrumentation, where the
4315+
// intrinsics annotation is reliably complete for same-module
4316+
// declarations too (the builder's spec tables are not yet
4317+
// populated when inline code specs are translated here).
43134318
// Bitwise operators on the RHS produce bitvector-typed Boogie
43144319
// expressions, but ghost fields are declared as unbounded integer
43154320
// in Boogie (they are model-only and don't participate in
@@ -5324,18 +5329,37 @@ impl ModuleBuilder<'_, '_> {
53245329
// New struct in this module
53255330
let spec = self.struct_specs.remove(&name.symbol).unwrap_or_default();
53265331
// Intrinsic types have no generated Boogie datatype to carry
5327-
// ghost constructor arguments; reject ghosts on them. This is
5328-
// the earliest point where the intrinsic pragma is reliably
5329-
// known (spec blocks are fully analyzed).
5330-
if !entry.ghost_fields.is_empty()
5331-
&& spec
5332-
.properties
5333-
.contains_key(&self.parent.env.symbol_pool().make(INTRINSIC_PRAGMA))
5334-
{
5335-
for f in entry.ghost_fields.values() {
5336-
self.parent
5337-
.env
5338-
.error(&f.loc, "ghost fields are not supported on intrinsic types");
5332+
// ghost constructor arguments; reject ghosts on them — except
5333+
// intrinsic MAP types, whose backend representation gains a
5334+
// carrier datatype when ghosts are declared (used for iterator
5335+
// validity). This is the earliest point where the intrinsic
5336+
// pragma is reliably known (spec blocks are fully analyzed).
5337+
if !entry.ghost_fields.is_empty() {
5338+
let pool = self.parent.env.symbol_pool();
5339+
let is_intrinsic_map = matches!(
5340+
spec.properties.get(&pool.make(INTRINSIC_PRAGMA)),
5341+
Some(PropertyValue::Symbol(s))
5342+
if pool.string(*s).as_str() == INTRINSIC_TYPE_MAP
5343+
);
5344+
if !is_intrinsic_map && spec.properties.contains_key(&pool.make(INTRINSIC_PRAGMA)) {
5345+
for f in entry.ghost_fields.values() {
5346+
self.parent
5347+
.env
5348+
.error(&f.loc, "ghost fields are not supported on intrinsic types");
5349+
}
5350+
} else if is_intrinsic_map {
5351+
// The carrier datatype is declared once per map type with the
5352+
// ghost argument types baked in; map ghosts are restricted to
5353+
// `num` (sufficient for identity/version state, and avoids
5354+
// per-instance ghost typing in the carrier).
5355+
for f in entry.ghost_fields.values() {
5356+
if !matches!(f.ty, Type::Primitive(crate::ty::PrimitiveType::Num)) {
5357+
self.parent.env.error(
5358+
&f.loc,
5359+
"ghost fields on intrinsic map types must have type `num`",
5360+
);
5361+
}
5362+
}
53395363
}
53405364
}
53415365
let mut field_data: BTreeMap<FieldId, FieldData> = BTreeMap::new();

third_party/move/move-prover/boogie-backend/src/boogie_helpers.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ pub fn boogie_module_name(env: &ModuleEnv<'_>) -> String {
5959
/// Return boogie name of given structure.
6060
pub fn boogie_struct_name(struct_env: &StructEnv<'_>, inst: &[Type], bv_flag: bool) -> String {
6161
if struct_env.is_intrinsic_of(INTRINSIC_TYPE_MAP) {
62+
if struct_env.get_ghost_fields().next().is_some() {
63+
// Intrinsic maps that declare ghost fields use a per-instance
64+
// carrier datatype wrapping the table, so the ghosts have
65+
// constructor arguments to live in. The name must agree with the
66+
// suffix convention (`boogie_type_suffix_for_struct`), including
67+
// the bv twin of the value type, so twin instances reference
68+
// their own carrier consistently.
69+
return format!(
70+
"${}_{}{}",
71+
boogie_module_name(&struct_env.module_env),
72+
struct_env.get_name().display(struct_env.symbol_pool()),
73+
boogie_inst_suffix(struct_env.module_env.env, inst, &[false, bv_flag])
74+
);
75+
}
6276
// Map to the theory type representation, which is `Table int V`. The key
6377
// is encoded as an integer to avoid extensionality problems, and to support
6478
// $Mutation paths, which are sequences of ints.

third_party/move/move-prover/boogie-backend/src/bytecode_translator.rs

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ use move_model::{
4646
QualifiedInstId, StructEnv, StructId,
4747
},
4848
pragmas::{
49-
ADDITION_OVERFLOW_UNCHECKED_PRAGMA, SEED_PRAGMA, TIMEOUT_PRAGMA,
49+
ADDITION_OVERFLOW_UNCHECKED_PRAGMA, INTRINSIC_TYPE_MAP, SEED_PRAGMA, TIMEOUT_PRAGMA,
5050
VERIFY_DURATION_ESTIMATE_PRAGMA,
5151
},
5252
symbol::Symbol,
@@ -7306,13 +7306,19 @@ impl FunctionTranslator<'_> {
73067306
if matches!(edge, BorrowEdge::Invoke) {
73077307
emitln!(writer, "call $t{} := $HavocMutation($t{});", idx, idx);
73087308
} else {
7309+
// Type of the value behind the destination reference, for
7310+
// ghost-carrier detection along the write-back chain.
7311+
let root_ty = self
7312+
.inst(self.get_local_type(*idx).skip_reference())
7313+
.clone();
73097314
let update = if let BorrowEdge::Hyper(edges) = edge {
73107315
self.translate_write_back_update(
73117316
&mut || dst_value.clone(),
73127317
&get_path_index,
73137318
src_value,
73147319
edges,
73157320
0,
7321+
&root_ty,
73167322
)
73177323
} else {
73187324
self.translate_write_back_update(
@@ -7321,6 +7327,7 @@ impl FunctionTranslator<'_> {
73217327
src_value,
73227328
&[edge.to_owned()],
73237329
0,
7330+
&root_ty,
73247331
)
73257332
};
73267333
emitln!(
@@ -7358,21 +7365,31 @@ impl FunctionTranslator<'_> {
73587365
None
73597366
}
73607367

7368+
/// `dest_ty` is the type of the value denoted by `mk_dest()` at this edge
7369+
/// position (threaded from the borrow root), used to detect intrinsic-map
7370+
/// ghost carriers on `Index(Table)` edges. `Type::Error` when unknown
7371+
/// (custom index edges), which never matches a carrier.
73617372
fn translate_write_back_update(
73627373
&self,
73637374
mk_dest: &mut dyn FnMut() -> String,
73647375
get_path_index: &dyn Fn(usize) -> String,
73657376
src: String,
73667377
edges: &[BorrowEdge],
73677378
at: usize,
7379+
dest_ty: &Type,
73687380
) -> String {
73697381
if at >= edges.len() {
73707382
src
73717383
} else {
73727384
match &edges[at] {
7373-
BorrowEdge::Direct => {
7374-
self.translate_write_back_update(mk_dest, get_path_index, src, edges, at + 1)
7375-
},
7385+
BorrowEdge::Direct => self.translate_write_back_update(
7386+
mk_dest,
7387+
get_path_index,
7388+
src,
7389+
edges,
7390+
at + 1,
7391+
dest_ty,
7392+
),
73767393
BorrowEdge::Field(memory, variant, offset) => {
73777394
let memory = memory.to_owned().instantiate(self.type_inst);
73787395
let struct_env = &self.parent.env.get_struct_qid(memory.to_qualified_id());
@@ -7384,6 +7401,7 @@ impl FunctionTranslator<'_> {
73847401
*offset,
73857402
)
73867403
};
7404+
let field_ty = field_env.get_type().instantiate(&memory.inst);
73877405
let field_sel = boogie_field_sel(&field_env);
73887406
let new_dest = format!("{}->{}", (*mk_dest)(), field_sel);
73897407
let mut new_dest_needed = false;
@@ -7396,6 +7414,7 @@ impl FunctionTranslator<'_> {
73967414
src,
73977415
edges,
73987416
at + 1,
7417+
&field_ty,
73997418
);
74007419
let update_fun = if variant.is_none() {
74017420
boogie_field_update(&field_env, &memory.inst)
@@ -7441,14 +7460,54 @@ impl FunctionTranslator<'_> {
74417460
self.get_borrow_native_aggregate_names(name).unwrap()
74427461
},
74437462
};
7463+
let env = self.parent.env;
7464+
// A ghost-carrier map (an intrinsic map declaring ghost
7465+
// fields) wraps its table: content is read through `->$t`,
7466+
// and the update rebuilds the carrier PRESERVING the ghost
7467+
// arguments — a write through a borrowed value entry is not
7468+
// a structural mutation and must not disturb e.g. the
7469+
// iterator-validity brand.
7470+
let carrier = if matches!(index_edge_kind, IndexEdgeKind::Table) {
7471+
if let Type::Struct(mid, sid, targs) = dest_ty.skip_reference() {
7472+
let struct_env = env.get_struct(mid.qualified(*sid));
7473+
let ghost_sels: Vec<String> = struct_env
7474+
.get_ghost_fields()
7475+
.map(|f| boogie_field_sel(&f))
7476+
.collect();
7477+
if struct_env.is_intrinsic_of(INTRINSIC_TYPE_MAP)
7478+
&& !ghost_sels.is_empty()
7479+
{
7480+
Some((boogie_struct_name(&struct_env, targs, false), ghost_sels))
7481+
} else {
7482+
None
7483+
}
7484+
} else {
7485+
None
7486+
}
7487+
} else {
7488+
None
7489+
};
7490+
// The element/value type for the recursion step.
7491+
let elem_ty = match (index_edge_kind, dest_ty.skip_reference()) {
7492+
(IndexEdgeKind::Vector, Type::Vector(elem)) => (**elem).clone(),
7493+
(IndexEdgeKind::Table, Type::Struct(_, _, targs)) if targs.len() >= 2 => {
7494+
targs[1].clone()
7495+
},
7496+
_ => Type::Error,
7497+
};
74447498

74457499
// Compute the offset into the path where to retrieve the index.
74467500
let offset = edges[0..at]
74477501
.iter()
74487502
.filter(|e| !matches!(e, BorrowEdge::Direct))
74497503
.count();
74507504
let index = (*get_path_index)(offset);
7451-
let new_dest = format!("{}({}, {})", read_aggregate, (*mk_dest)(), index);
7505+
let content = |dest: String| match &carrier {
7506+
Some(_) => format!("{}->$t", dest),
7507+
None => dest,
7508+
};
7509+
let new_dest =
7510+
format!("{}({}, {})", read_aggregate, content((*mk_dest)()), index);
74527511
let mut new_dest_needed = false;
74537512
// Recursively perform write backs for next edges
74547513
let new_src = self.translate_write_back_update(
@@ -7460,25 +7519,32 @@ impl FunctionTranslator<'_> {
74607519
src,
74617520
edges,
74627521
at + 1,
7522+
&elem_ty,
74637523
);
7524+
let mk_update = |dest: String, new_src: &str| match &carrier {
7525+
Some((carrier_name, ghost_sels)) => {
7526+
let ghosts = ghost_sels
7527+
.iter()
7528+
.map(|sel| format!(", {}->{}", dest, sel))
7529+
.join("");
7530+
format!(
7531+
"{}({}({}->$t, {}, {}){})",
7532+
carrier_name, update_aggregate, dest, index, new_src, ghosts
7533+
)
7534+
},
7535+
None => {
7536+
format!("{}({}, {}, {})", update_aggregate, dest, index, new_src)
7537+
},
7538+
};
74647539
if new_dest_needed {
74657540
format!(
7466-
"(var $$sel{} := {}; {}({}, {}, {}))",
7541+
"(var $$sel{} := {}; {})",
74677542
at,
74687543
new_dest,
7469-
update_aggregate,
7470-
(*mk_dest)(),
7471-
index,
7472-
new_src
7544+
mk_update((*mk_dest)(), &new_src)
74737545
)
74747546
} else {
7475-
format!(
7476-
"{}({}, {}, {})",
7477-
update_aggregate,
7478-
(*mk_dest)(),
7479-
index,
7480-
new_src
7481-
)
7547+
mk_update((*mk_dest)(), &new_src)
74827548
}
74837549
},
74847550
BorrowEdge::Hyper(_) | BorrowEdge::Invoke => unreachable!("unexpected borrow edge"),

third_party/move/move-prover/boogie-backend/src/lib.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
#![forbid(unsafe_code)]
77

88
use crate::{
9-
boogie_helpers::{boogie_module_name, boogie_num_type_base, boogie_type, boogie_type_suffix},
9+
boogie_helpers::{
10+
boogie_field_sel, boogie_module_name, boogie_num_type_base, boogie_type, boogie_type_suffix,
11+
},
1012
bytecode_translator::has_native_equality,
1113
options::{BoogieOptions, VectorTheory},
1214
};
@@ -105,6 +107,12 @@ struct BvInfo {
105107
max: String,
106108
}
107109

110+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
111+
struct GhostArg {
112+
sel: String,
113+
ty: String,
114+
}
115+
108116
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
109117
struct MapImpl {
110118
struct_name: String,
@@ -132,6 +140,20 @@ struct MapImpl {
132140
iter_ptr_prefix: String,
133141
iter_variant: String,
134142
iter_key_sel: String,
143+
// Ghost carrier: an intrinsic map that declares ghost fields is
144+
// represented as a per-instance datatype wrapping the table, so the
145+
// ghosts have constructor arguments to live in. `struct_base` plus the
146+
// instance suffix is the carrier datatype name (agreeing with
147+
// `boogie_struct_name`); `ghost_args` are the ghost selectors and their
148+
// (type-parameter-free) Boogie types.
149+
has_ghost_carrier: bool,
150+
struct_base: String,
151+
ghost_args: Vec<GhostArg>,
152+
gb_args: String,
153+
gb_decls: String,
154+
gb_havoc: String,
155+
ghost_preserve_args: String,
156+
ghost_zero_args: String,
135157
fun_get: String,
136158
fun_borrow_front: String,
137159
fun_borrow_back: String,
@@ -555,7 +577,7 @@ impl MapImpl {
555577
ty_args: &BTreeSet<(Type, Type)>,
556578
bv_flag: bool,
557579
) -> Self {
558-
let insts = ty_args
580+
let insts: Vec<(TypeInfo, TypeInfo)> = ty_args
559581
.iter()
560582
.map(|(kty, vty)| {
561583
(
@@ -580,6 +602,38 @@ impl MapImpl {
580602
env,
581603
decl.get_fun_triple(env, INTRINSIC_FUN_MAP_ITER_BORROW_MUT),
582604
);
605+
let ghost_args: Vec<GhostArg> = struct_env
606+
.get_ghost_fields()
607+
.map(|f| GhostArg {
608+
sel: boogie_field_sel(&f),
609+
ty: boogie_type(env, &f.get_type(), false),
610+
})
611+
.collect();
612+
let has_ghost_carrier = !ghost_args.is_empty();
613+
let struct_base = struct_name.clone();
614+
// Rebuild plumbing for mutating templates: fresh (havoced) ghost
615+
// values per rebuild site, shared between the rebuilt value and any
616+
// post-state spec-function application describing it.
617+
let (gb_args, gb_decls, gb_havoc) = if has_ghost_carrier {
618+
let idxs = 0..ghost_args.len();
619+
(
620+
idxs.clone().map(|i| format!(", $gb{}", i)).join(""),
621+
idxs.clone()
622+
.map(|i| format!("\n var $gb{}: int;", i))
623+
.join(""),
624+
idxs.map(|i| format!("havoc $gb{}; ", i)).join(""),
625+
)
626+
} else {
627+
(String::new(), String::new(), String::new())
628+
};
629+
// Pure spec functions cannot havoc: map-returning spec funs preserve
630+
// the input's ghost args (whole-map equalities are ghost-excluding,
631+
// so the choice is immaterial) or use zeros when there is no input.
632+
let ghost_preserve_args = ghost_args
633+
.iter()
634+
.map(|g| format!(", t->{}", g.sel))
635+
.join("");
636+
let ghost_zero_args = ghost_args.iter().map(|_| ", 0".to_string()).join("");
583637

584638
MapImpl {
585639
struct_name,
@@ -646,6 +700,14 @@ impl MapImpl {
646700
iter_ptr_prefix: iter_parts.0,
647701
iter_variant: iter_parts.1,
648702
iter_key_sel: iter_parts.2,
703+
has_ghost_carrier,
704+
struct_base,
705+
ghost_args,
706+
gb_args,
707+
gb_decls,
708+
gb_havoc,
709+
ghost_preserve_args,
710+
ghost_zero_args,
649711
fun_get: Self::triple_opt_to_name(env, decl.get_fun_triple(env, INTRINSIC_FUN_MAP_GET)),
650712
fun_borrow_front: Self::triple_opt_to_name(
651713
env,

0 commit comments

Comments
 (0)