This defines the evaluation of place and value expressions.
One design decision I made here is that eval_value
and eval_place
return both a Value
/Place
and its type.
Separately, well-formedness defines check_wf
functions that return a Type
.
This adds some redundancy (we basically have two definitions of what the type of an expression is).
The separate check_wf
enforces structurally that the type information is determined entirely statically.
The type propagated during evaluation means we only do a single recursive traversal, and we avoid losing track of which type a given value has (which would be a problem since a value without a type is fairly meaningless).
This section defines the following function:
impl<M: Memory> Machine<M> {
/// Evaluate a value expression to a value. The result value will always be well-formed for the given type.
#[specr::argmatch(val)]
fn eval_value(&mut self, val: ValueExpr) -> Result<(Value<M>, Type)> { .. }
}
One key property of value (and place) expression evaluation is that it is reorderable and removable. However, they are not deterministic due to int-to-pointer casts.
impl<M: Memory> Machine<M> {
/// converts `Constant` to their `Value` counterpart.
fn eval_constant(&mut self, constant: Constant) -> Result<Value<M>> {
ret(match constant {
Constant::Int(i) => Value::Int(i),
Constant::Bool(b) => Value::Bool(b),
Constant::GlobalPointer(relocation) => {
let ptr = self.global_ptrs[relocation.name].wrapping_offset::<M::T>(relocation.offset.bytes());
Value::Ptr(ptr.widen(None))
},
Constant::FnPointer(fn_name) => {
Value::Ptr(ThinPointer {
addr: self.fn_addrs[fn_name],
provenance: None,
}.widen(None))
},
Constant::PointerWithoutProvenance(addr) => {
Value::Ptr(ThinPointer {
addr,
provenance: None,
}.widen(None))
}
})
}
fn eval_value(&mut self, ValueExpr::Constant(constant, ty): ValueExpr) -> Result<(Value<M>, Type)> {
ret((self.eval_constant(constant)?, ty))
}
}
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::Tuple(exprs, ty): ValueExpr) -> Result<(Value<M>, Type)> {
let vals = exprs.try_map(|e| self.eval_value(e))?.map(|e| e.0);
ret((Value::Tuple(vals), ty))
}
}
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::Union { field, expr, union_ty } : ValueExpr) -> Result<(Value<M>, Type)> {
let Type::Union { fields, size, .. } = union_ty else { panic!("ValueExpr::Union requires union type") };
let (offset, expr_ty) = fields[field];
let mut data = list![AbstractByte::Uninit; size.bytes()];
let (val, _) = self.eval_value(expr)?;
data.write_subslice_at_index(offset.bytes(), expr_ty.encode::<M>(val));
ret((union_ty.decode(data).unwrap(), union_ty))
}
}
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::Variant { enum_ty, discriminant, data } : ValueExpr) -> Result<(Value<M>, Type)> {
ret((Value::Variant { discriminant, data: self.eval_value(data)?.0 }, enum_ty))
}
}
Read the discriminant of an Enum. The well-formedness checks already ensured that the type is an enum.
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::GetDiscriminant { place } : ValueExpr) -> Result<(Value<M>, Type)> {
// Get the place of the enum and its information.
let (place, ty) = self.eval_place(place)?;
let Type::Enum { discriminator, discriminant_ty, .. } = ty else {
panic!("ValueExpr::GetDiscriminant requires enum type");
};
if !place.aligned {
throw_ub!("Getting the discriminant of a place based on a misaligned pointer.");
}
// We don't require the variant to be valid,
// we are only interested in the bytes that the discriminator actually touches.
let accessor = |idx: Offset, size: Size| {
let ptr = self.ptr_offset_inbounds(place.ptr.thin_pointer, idx.bytes())?;
// We have ensured that the place is aligned, so no alignment requirement here.
self.mem.load(ptr, size, Align::ONE, Atomicity::None)
};
let Some(discriminant) = decode_discriminant::<M>(accessor, discriminator)? else {
throw_ub!("ValueExpr::GetDiscriminant encountered invalid discriminant.");
};
ret((Value::Int(discriminant), Type::Int(discriminant_ty)))
}
}
This loads a value from a place (often called "place-to-value coercion").
impl<M: Memory> ConcurrentMemory<M> {
fn place_load(&mut self, place: Place<M>, ty: Type) -> Result<Value<M>> {
if !place.aligned {
throw_ub!("loading from a place based on a misaligned pointer");
}
// Alignment was already checked.
// `ty` is ensured to be sized by WF of callers: Loads, Validates and InPlace arguments.
ret(self.typed_load(place.ptr.thin_pointer, ty, Align::ONE, Atomicity::None)?)
}
}
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::Load { source }: ValueExpr) -> Result<(Value<M>, Type)> {
let (place, ty) = self.eval_place(source)?;
// WF ensures all load expressions are sized.
let v = self.mem.place_load(place, ty)?;
ret((v, ty))
}
}
The &
operators simply converts a place to the pointer it denotes.
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::AddrOf { target, ptr_ty }: ValueExpr) -> Result<(Value<M>, Type)> {
let (place, _ty) = self.eval_place(target)?;
// Make sure the new pointer has a valid address.
// Remember that places are basically raw pointers so this is not guaranteed!
if !ptr_ty.addr_valid(place.ptr.thin_pointer.addr) {
throw_ub!("taking the address of an invalid (null, misaligned, or uninhabited) place");
}
// Let the aliasing model know. (Will also check dereferenceability if appropriate.)
let ptr = self.mutate_cur_frame(|frame, mem| {
mem.retag_ptr(&mut frame.extra, place.ptr, ptr_ty, /* fn_entry */ false)
})?;
if !ptr_ty.meta_kind().matches(ptr.metadata) {
// The metadata kind is checked in WF.
panic!("AddrOf generated an incompatible pointer type for the metadata");
}
ret((Value::Ptr(ptr), Type::Ptr(ptr_ty)))
}
}
The functions eval_un_op
and eval_bin_op
are defined in a separate file.
impl<M: Memory> Machine<M> {
fn eval_value(&mut self, ValueExpr::UnOp { operator, operand }: ValueExpr) -> Result<(Value<M>, Type)> {
use lang::UnOp::*;
let operand = self.eval_value(operand)?;
ret(self.eval_un_op(operator, operand)?)
}
fn eval_value(&mut self, ValueExpr::BinOp { operator, left, right }: ValueExpr) -> Result<(Value<M>, Type)> {
use lang::BinOp::*;
let left = self.eval_value(left)?;
let right = self.eval_value(right)?;
ret(self.eval_bin_op(operator, left, right)?)
}
}
Place expressions evaluate to places. For now, that is just a pointer (but this might have to change). Place evaluation ensures that this pointer is always dereferenceable (for the type of the place expression).
impl<M: Memory> Machine<M> {
/// Evaluate a place expression to a place.
///
/// Like a raw pointer, the result can be misaligned or null!
#[specr::argmatch(place)]
fn eval_place(&mut self, place: PlaceExpr) -> Result<(Place<M>, Type)> { .. }
}
One key property of place (and value) expression evaluation is that it is reorderable and removable.
The place for a local is directly given by the stack frame.
impl<M: Memory> Machine<M> {
fn eval_place(&mut self, PlaceExpr::Local(name): PlaceExpr) -> Result<(Place<M>, Type)> {
let ty = self.cur_frame().func.locals[name];
let Some(ptr) = self.cur_frame().locals.get(name) else {
throw_ub!("access to a dead local");
};
ret((Place { ptr: ptr.widen(None), aligned: true }, ty))
}
}
The *
operator turns a value of pointer type into a place.
It also ensures that the pointer is dereferenceable.
impl<M: Memory> Machine<M> {
fn eval_place(&mut self, PlaceExpr::Deref { operand, ty }: PlaceExpr) -> Result<(Place<M>, Type)> {
let (Value::Ptr(ptr), Type::Ptr(ptr_type)) = self.eval_value(operand)? else {
panic!("dereferencing a non-pointer")
};
// We know the pointer is valid for its type, but make sure safe pointers are also dereferenceable.
// (We don't do a full retag here, this is not considered creating a new pointer.)
if let Some(layout) = ptr_type.safe_pointee() {
assert!(layout.align.is_aligned(ptr.thin_pointer.addr)); // this was already checked when the value got created
self.mem.dereferenceable(ptr.thin_pointer, layout.size.compute(ptr.metadata))?;
}
// Check whether this pointer is sufficiently aligned.
// Don't error immediately though! Unaligned places can still be turned into raw pointers.
// However, they cannot be loaded from.
let aligned = ty.align::<M::T>().is_aligned(ptr.thin_pointer.addr);
ret((Place { ptr, aligned }, ty))
}
}
impl<M: Memory> Machine<M> {
fn eval_place(&mut self, PlaceExpr::Field { root, field }: PlaceExpr) -> Result<(Place<M>, Type)> {
let (root, ty) = self.eval_place(root)?;
let (offset, field_ty) = match ty {
Type::Tuple { fields, .. } => fields[field],
Type::Union { fields, .. } => fields[field],
_ => panic!("field projection on non-projectable type"),
};
assert!(offset <= ty.size::<M::T>().compute(root.ptr.metadata));
let ptr = self.ptr_offset_inbounds(root.ptr.thin_pointer, offset.bytes())?;
// TODO(UnsizedTypes): Field projections to the last field should retain the metadata.
ret((Place { ptr: ptr.widen(None), ..root }, field_ty))
}
fn eval_place(&mut self, PlaceExpr::Index { root, index }: PlaceExpr) -> Result<(Place<M>, Type)> {
let (root, ty) = self.eval_place(root)?;
let (Value::Int(index), _) = self.eval_value(index)? else {
panic!("non-integer operand for array index")
};
let (elem_ty, count) = match ty {
Type::Array { elem, count } => (elem, count),
Type::Slice { elem } => {
let Some(PointerMeta::ElementCount(count)) = root.ptr.metadata else {
panic!("eval_place should always return a ptr which matches meta for the SizeStrategy of ty");
};
(elem, count)
}
_ => panic!("index projection on non-indexable type"),
};
if index < 0 || index >= count {
throw_ub!("access to out-of-bounds index");
}
let elem_size = elem_ty.size::<M::T>().expect_sized("WF ensures array & slice elements are sized");
let offset = index * elem_size;
assert!(
offset <= ty.size::<M::T>().compute(root.ptr.metadata),
"sanity check: the indexed offset should not be outside what the type allows."
);
let ptr = self.ptr_offset_inbounds(root.ptr.thin_pointer, offset.bytes())?;
ret((Place { ptr: ptr.widen(None), ..root }, elem_ty))
}
fn eval_place(&mut self, PlaceExpr::Downcast { root, discriminant }: PlaceExpr) -> Result<(Place<M>, Type)> {
let (root, ty) = self.eval_place(root)?;
// We only need to downcast the enum type into the variant data type
// since all the enum data must have the same size with offset 0 (invariant).
let var_ty = match ty {
Type::Enum { variants, .. } => variants[discriminant].ty,
_ => panic!("enum downcast on non-enum"),
};
ret((root, var_ty))
}
}