The various syntactic constructs of MiniRust (types, functions, ...) come with well-formedness requirements: certain invariants need to be satisfied for this to be considered a well-formed program.
The idea is that for well-formed programs, the step
function will never panic.
Those requirements are defined in this file.
We also define the idea of a "value being well-formed at a type".
decode
will only ever return well-formed values, and encode
will never panic on a well-formed value.
Note that check_wf
functions for testing well-formedness return Result<()>
to pass information in case an error occured.
We use the following helper function to convert Boolean checks into this form.
fn ensure_wf(b: bool, msg: &str) -> Result<()> {
if !b { throw_ill_formed!("{}", msg); }
ret(())
}
impl IntType {
fn check_wf(self) -> Result<()> {
// In particular, this checks that the size is at least one byte.
ensure_wf(self.size.bytes().is_power_of_two(), "IntType: size is not power of two")
}
}
impl SizeStrategy {
fn check_wf<T: Target>(self) -> Result<()> {
match self {
SizeStrategy::Sized(size) => { ensure_wf(T::valid_size(size), "SizeStrategy: size not valid")?; }
SizeStrategy::Slice(size) => { ensure_wf(T::valid_size(size), "SizeStrategy: element size not valid")?; }
};
ret(())
}
fn check_aligned_to(self, align: Align) -> Result<()> {
match self {
SizeStrategy::Sized(size) => {
ensure_wf(size.bytes() % align.bytes() == 0, "check_aligned_to: size not a multiple of alignment")?;
}
SizeStrategy::Slice(size) => {
ensure_wf(size.bytes() % align.bytes() == 0, "check_aligned_to: element size not a multiple of alignment")?;
}
};
ret(())
}
}
impl PointeeInfo {
fn check_wf<T: Target>(self) -> Result<()> {
// We do *not* require that size is a multiple of align!
// TODO(UnsizedTypes): Ensure any future alignment strategy matches the size strategy.
self.size.check_wf::<T>()?;
ret(())
}
}
impl PtrType {
fn check_wf<T: Target>(self) -> Result<()> {
match self {
PtrType::Ref { pointee, .. } | PtrType::Box { pointee } => {
pointee.check_wf::<T>()?;
}
PtrType::Raw { .. } | PtrType::FnPtr => ()
}
ret(())
}
}
impl Type {
fn check_wf<T: Target>(self) -> Result<()> {
use Type::*;
match self {
Int(int_type) => {
int_type.check_wf()?;
}
Bool => (),
Ptr(ptr_type) => {
ptr_type.check_wf::<T>()?;
}
Tuple { mut fields, size, align: _ } => {
// The fields must not overlap.
// We check fields in the order of their (absolute) offsets.
fields.sort_by_key(|(offset, _ty)| offset);
let mut last_end = Size::ZERO;
for (offset, ty) in fields {
// Recursively check the field type.
ty.check_wf::<T>()?;
// Ensure it is a sized type.
ensure_wf(ty.size::<T>().is_sized(), "Type::Tuple: unsized field type")?;
// And ensure it fits after the one we previously checked.
ensure_wf(offset >= last_end, "Type::Tuple: overlapping fields")?;
last_end = offset + ty.size::<T>().expect_sized("ensured to be sized above");
}
// And they must all fit into the size.
// The size is in turn checked to be valid for `M`, and hence all offsets are valid, too.
ensure_wf(size >= last_end, "Type::Tuple: size of fields is bigger than total size")?;
}
Array { elem, count } => {
ensure_wf(count >= 0, "Type::Array: negative amount of elements")?;
ensure_wf(elem.size::<T>().is_sized(), "Type::Array: unsized element type")?;
elem.check_wf::<T>()?;
}
Slice { elem } => {
ensure_wf(elem.size::<T>().is_sized(), "Type::Slice: unsized element type")?;
elem.check_wf::<T>()?;
}
Union { fields, size, chunks, align: _ } => {
// The fields may overlap, but they must all fit the size.
for (offset, ty) in fields {
ty.check_wf::<T>()?;
ensure_wf(ty.size::<T>().is_sized(), "Type::Union: unsized field type")?;
ensure_wf(
size >= offset + ty.size::<T>().expect_sized("ensured to be sized above"),
"Type::Union: field size does not fit union",
)?;
// This field may overlap with gaps between the chunks. That's perfectly normal
// when there is padding inside the field.
// FIXME: should we check that all the non-padding bytes of the field are in some chunk?
// But then we'd have to add a definition of "used (non-padding) bytes" in the spec, and then
// we may as well remove 'chunks' entirely and just compute the set of used bytes for
// encoding/decoding...
}
// The chunks must be sorted in their offsets and disjoint.
// FIXME: should we relax this and allow arbitrary chunk order?
let mut last_end = Size::ZERO;
for (offset, size) in chunks {
ensure_wf(
offset >= last_end,
"Type::Union: chunks are not stored in ascending order",
)?;
last_end = offset + size;
}
// And they must all fit into the size.
ensure_wf(size >= last_end, "Type::Union: chunks do not fit union")?;
}
Enum { variants, size, discriminator, discriminant_ty, .. } => {
// All the variants need to be well-formed and be the size of the enum so
// we don't have to handle different sizes in the memory representation.
// Also their alignment may not be larger than the total enum alignment and
// all the values written by the tagger must fit into the variant.
for (discriminant, variant) in variants {
ensure_wf(
discriminant_ty.can_represent(discriminant),
"Type::Enum: invalid value for discriminant"
)?;
variant.ty.check_wf::<T>()?;
ensure_wf(
SizeStrategy::Sized(size) == variant.ty.size::<T>(),
"Type::Enum: variant size is not the same as enum size"
)?;
ensure_wf(
variant.ty.align::<T>().bytes() <= self.align::<T>().bytes(),
"Type::Enum: invalid align requirement"
)?;
for (offset, (value_type, value)) in variant.tagger {
value_type.check_wf()?;
ensure_wf(value_type.can_represent(value), "Type::Enum: invalid tagger value")?;
ensure_wf(offset + value_type.size <= size, "Type::Enum tagger type size too big for enum")?;
}
// FIXME: check that the values written by the tagger do not overlap.
}
// check that all variants reached by the discriminator are valid,
// that it never performs out-of-bounds accesses and all discriminant values
// can be represented by the discriminant type.
discriminator.check_wf::<T>(size, variants)?;
}
}
// Now that we know the type is well-formed,
// we are allowed to call the size function to check that the size is valid and a multiple of the alignment.
let size = self.size::<T>();
size.check_wf::<T>()?;
size.check_aligned_to(self.align::<T>())?;
ret(())
}
}
impl Discriminator {
fn check_wf<T: Target>(self, size: Size, variants: Map<Int, Variant>) -> Result<()> {
match self {
Discriminator::Known(discriminant) => ensure_wf(variants.get(discriminant).is_some(), "Discriminator: invalid discriminant"),
Discriminator::Invalid => ret(()),
Discriminator::Branch { offset, value_type, fallback, children } => {
// Ensure that the value we branch on is stored in bounds and that all children all valid.
value_type.check_wf()?;
ensure_wf(offset + value_type.size <= size, "Discriminator: branch offset exceeds size")?;
fallback.check_wf::<T>(size, variants)?;
for (idx, ((start, end), discriminator)) in children.into_iter().enumerate() {
ensure_wf(value_type.can_represent(start), "Discriminator: invalid branch start bound")?;
// Since the end is exclusive we only need to represent the number before the end.
ensure_wf(value_type.can_represent(end - Int::ONE), "Discriminator: invalid branch end bound")?;
ensure_wf(start < end, "Discriminator: invalid bound values")?;
// Ensure that the ranges don't overlap.
ensure_wf(children.keys().enumerate().all(|(other_idx, (other_start, other_end))|
other_end <= start || other_start >= end || idx == other_idx), "Discriminator: branch ranges overlap")?;
discriminator.check_wf::<T>(size, variants)?;
}
ret(())
}
}
}
}
impl Constant {
/// Check that the constant has the expected type.
/// Assumes that `ty` has already been checked.
fn check_wf<T: Target>(self, ty: Type, prog: Program) -> Result<()> {
// For now, we only support integer and boolean literals and pointers.
// TODO: add more.
match (self, ty) {
(Constant::Int(i), Type::Int(int_type)) => {
ensure_wf(int_type.can_represent(i), "Constant::Int: invalid int value")?;
}
(Constant::Bool(_), Type::Bool) => (),
(Constant::GlobalPointer(relocation), Type::Ptr(_)) => {
relocation.check_wf(prog.globals)?;
}
(Constant::FnPointer(fn_name), Type::Ptr(ptr_ty)) => {
ensure_wf(matches!(ptr_ty, PtrType::FnPtr), "Constant::FnPointer: non function pointer type")?;
ensure_wf(prog.functions.contains_key(fn_name), "Constant::FnPointer: invalid function name")?;
}
(Constant::PointerWithoutProvenance(addr), Type::Ptr(_)) => {
ensure_wf(
addr.in_bounds(Signedness::Unsigned, T::PTR_SIZE),
"Constant::PointerWithoutProvenance: pointer out-of-bounds"
)?;
}
_ => throw_ill_formed!("Constant: value does not match type"),
}
ret(())
}
}
impl ValueExpr {
#[allow(unused_braces)]
fn check_wf<T: Target>(self, locals: Map<LocalName, Type>, prog: Program) -> Result<Type> {
use ValueExpr::*;
ret(match self {
Constant(value, ty) => {
ty.check_wf::<T>()?;
value.check_wf::<T>(ty, prog)?;
ty
}
Tuple(exprs, t) => {
t.check_wf::<T>()?;
match t {
Type::Tuple { fields, .. } => {
ensure_wf(exprs.len() == fields.len(), "ValueExpr::Tuple: invalid number of tuple fields")?;
for (e, (_offset, ty)) in exprs.zip(fields) {
let checked = e.check_wf::<T>(locals, prog)?;
ensure_wf(checked == ty, "ValueExpr::Tuple: invalid tuple field type")?;
}
},
Type::Array { elem, count } => {
ensure_wf(exprs.len() == count, "ValueExpr::Tuple: invalid number of array elements")?;
for e in exprs {
let checked = e.check_wf::<T>(locals, prog)?;
ensure_wf(checked == elem, "ValueExpr::Tuple: invalid array element type")?;
}
},
_ => throw_ill_formed!("ValueExpr::Tuple: expression does not match type"),
}
t
}
Union { field, expr, union_ty } => {
union_ty.check_wf::<T>()?;
let Type::Union { fields, .. } = union_ty else {
throw_ill_formed!("ValueExpr::Union: invalid type")
};
ensure_wf(field < fields.len(), "ValueExpr::Union: invalid field length")?;
let (_offset, ty) = fields[field];
let checked = expr.check_wf::<T>(locals, prog)?;
ensure_wf(checked == ty, "ValueExpr::Union: invalid field type")?;
union_ty
}
Variant { discriminant, data, enum_ty } => {
let Type::Enum { variants, .. } = enum_ty else {
throw_ill_formed!("ValueExpr::Variant: invalid type")
};
enum_ty.check_wf::<T>()?;
let Some(variant) = variants.get(discriminant) else {
throw_ill_formed!("ValueExpr::Variant: invalid discriminant");
};
let checked = data.check_wf::<T>(locals, prog)?;
ensure_wf(checked == variant.ty, "ValueExpr::Variant: invalid type")?;
enum_ty
}
GetDiscriminant { place } => {
let Type::Enum { discriminant_ty, .. } = place.check_wf::<T>(locals, prog)? else {
throw_ill_formed!("ValueExpr::GetDiscriminant: invalid type");
};
Type::Int(discriminant_ty)
}
Load { source } => {
let val_ty = source.check_wf::<T>(locals, prog)?;
ensure_wf(val_ty.size::<T>().is_sized(), "ValueExpr::Load: unsized value type")?;
val_ty
}
AddrOf { target, ptr_ty } => {
ptr_ty.check_wf::<T>()?;
let target_ty = target.check_wf::<T>(locals, prog)?;
ensure_wf(target_ty.meta_kind() == ptr_ty.meta_kind(), "ValueExpr::AddrOf: mismatched metadata kind")?;
// No check of how the alignment changes here -- that is purely a runtime constraint.
Type::Ptr(ptr_ty)
}
UnOp { operator, operand } => {
use lang::UnOp::*;
let operand = operand.check_wf::<T>(locals, prog)?;
match operator {
Int(int_op) => {
let Type::Int(int_ty) = operand else {
throw_ill_formed!("UnOp::Int: invalid operand");
};
let ret_ty = match int_op {
IntUnOp::CountOnes => IntType { signed: Unsigned, size: Size::from_bytes(4).unwrap() },
_ => int_ty,
};
Type::Int(ret_ty)
}
Cast(cast_op) => {
use lang::CastOp::*;
match cast_op {
IntToInt(int_ty) => {
ensure_wf(matches!(operand, Type::Int(_)), "Cast::IntToInt: invalid operand")?;
Type::Int(int_ty)
}
Transmute(new_ty) => {
ensure_wf(operand.size::<T>().is_sized(), "Cast::Transmute: unsized source type")?;
ensure_wf(new_ty.size::<T>().is_sized(), "Cast::Transmute: unsized target type")?;
new_ty
}
}
}
GetThinPointer => {
ensure_wf(matches!(operand, Type::Ptr(_)), "UnOp::GetThinPointer: invalid operand: not a pointer")?;
Type::Ptr(PtrType::Raw { meta_kind: PointerMetaKind::None })
}
GetMetadata => {
let Type::Ptr(ptr_ty) = operand else {
throw_ill_formed!("UnOp::GetMetadata: invalid operand: not a pointer");
};
// If the pointer does not have metadata, this will still be well-formed but return the unit type.
ptr_ty.meta_kind().ty::<T>().unwrap_or_else(unit_type)
}
SizeOfVal => {
// TODO(UnsizedTypes): Support raw pointers as well.
ensure_wf(
matches!(operand, Type::Ptr(PtrType::Ref { .. })),
"UnOp::SizeOfVal: invalid operand: not a reference"
)?;
Type::Int(IntType { signed: Unsigned, size: T::PTR_SIZE })
}
}
}
BinOp { operator, left, right } => {
use lang::BinOp::*;
let left = left.check_wf::<T>(locals, prog)?;
let right = right.check_wf::<T>(locals, prog)?;
match operator {
Int(int_op) => {
let Type::Int(left) = left else {
throw_ill_formed!("BinOp::Int: invalid left type");
};
let Type::Int(right) = right else {
throw_ill_formed!("BinOp::Int: invalid right type");
};
use IntBinOp::*;
// Shift operators allow unequal left and right type
if !matches!(int_op, Shl | Shr | ShlUnchecked | ShrUnchecked) {
ensure_wf(left == right, "BinOp:Int: right and left type are not equal")?;
}
Type::Int(left)
}
IntWithOverflow(_int_op) => {
let Type::Int(int_ty) = left else {
throw_ill_formed!("BinOp::IntWithOverflow: invalid left type");
};
ensure_wf(right == Type::Int(int_ty), "BinOp::IntWithOverflow: invalid right type")?;
int_ty.with_overflow::<T>()
}
Rel(rel_op) => {
ensure_wf(matches!(left, Type::Int(_) | Type::Bool | Type::Ptr(_)), "BinOp::Rel: invalid left type")?;
ensure_wf(right == left, "BinOp::Rel: invalid right type")?;
if let Type::Ptr(ptr_ty) = left {
// TODO(UnsizedTypes): add support for this
ensure_wf(ptr_ty.meta_kind() == PointerMetaKind::None, "BinOp::Rel: cannot compare wide pointers (yet)")?;
}
match rel_op {
RelOp::Cmp => Type::Int(IntType::I8),
_ => Type::Bool,
}
}
PtrOffset { inbounds: _ } => {
let Type::Ptr(left_ptr_ty) = left else {
throw_ill_formed!("BinOp::PtrOffset: invalid left type: not a pointer");
};
if left_ptr_ty.meta_kind() != PointerMetaKind::None {
throw_ill_formed!("BinOp::PtrOffset: invalid left type: unsized pointee");
}
ensure_wf(matches!(right, Type::Int(_)), "BinOp::PtrOffset: invalid right type")?;
left
}
PtrOffsetFrom { inbounds: _, nonneg: _ } => {
let Type::Ptr(left_ptr_ty) = left else {
throw_ill_formed!("BinOp::PtrOffsetFrom: invalid left type: not a pointer");
};
if left_ptr_ty.meta_kind() != PointerMetaKind::None {
throw_ill_formed!("BinOp::PtrOffsetFrom: invalid left type: unsized pointee");
}
let Type::Ptr(right_ptr_ty) = right else {
throw_ill_formed!("BinOp::PtrOffsetFrom: invalid right type: not a pointer");
};
if right_ptr_ty.meta_kind() != PointerMetaKind::None {
throw_ill_formed!("BinOp::PtrOffsetFrom: invalid right type: unsized pointee");
}
let isize_int = IntType { signed: Signed, size: T::PTR_SIZE };
Type::Int(isize_int)
}
ConstructWidePointer(ptr_ty) => {
let Type::Ptr(thin_ptr_ty) = left else {
throw_ill_formed!("BinOp::ConstructWidePointer: invalid left type: not a pointer");
};
if thin_ptr_ty.meta_kind() != PointerMetaKind::None {
throw_ill_formed!("BinOp::ConstructWidePointer: invalid left type: not a thin pointer");
}
// A thin pointer can also be the target type, with unit metadata.
let meta_ty = ptr_ty.meta_kind().ty::<T>().unwrap_or_else(unit_type);
if right != meta_ty {
throw_ill_formed!("BinOp::ConstructWidePointer: invalid right type: not metadata of target");
}
Type::Ptr(ptr_ty)
}
}
}
})
}
}
impl PlaceExpr {
fn check_wf<T: Target>(self, locals: Map<LocalName, Type>, prog: Program) -> Result<Type> {
use PlaceExpr::*;
ret(match self {
Local(name) => {
match locals.get(name) {
None => throw_ill_formed!("PlaceExpr::Local: unknown local name"),
Some(local) => local,
}
},
Deref { operand, ty } => {
ty.check_wf::<T>()?;
let op_ty = operand.check_wf::<T>(locals, prog)?;
let Type::Ptr(op_ptr_ty) = op_ty else {
throw_ill_formed!("PlaceExpr::Deref: invalid operand type");
};
ensure_wf(op_ptr_ty.meta_kind() == ty.meta_kind(), "PlaceExpr::Deref: metadata kind of operand and type don't match")?;
// No check of how the alignment changes here -- that is purely a runtime constraint.
ty
}
Field { root, field } => {
let root = root.check_wf::<T>(locals, prog)?;
let (_offset, field_ty) = match root {
Type::Tuple { fields, .. } | Type::Union { fields, .. } => {
match fields.get(field) {
None => throw_ill_formed!("PlaceExpr::Field: invalid field"),
Some(field) => field,
}
}
_ => throw_ill_formed!("PlaceExpr::Field: expression does not match type"),
};
field_ty
}
Index { root, index } => {
let root = root.check_wf::<T>(locals, prog)?;
let index = index.check_wf::<T>(locals, prog)?;
ensure_wf(matches!(index, Type::Int(_)), "PlaceExpr::Index: invalid index type")?;
match root {
Type::Array { elem, .. } | Type::Slice { elem } => elem,
_ => throw_ill_formed!("PlaceExpr::Index: expression type is not indexable"),
}
}
Downcast { root, discriminant } => {
let root = root.check_wf::<T>(locals, prog)?;
match root {
// A valid downcast points to an existing variant.
Type::Enum { variants, .. } => {
let Some(variant) = variants.get(discriminant) else {
throw_ill_formed!("PlaceExpr::Downcast: invalid discriminant");
};
variant.ty
}
_ => throw_ill_formed!("PlaceExpr::Downcast: invalid root type"),
}
}
})
}
}
impl ArgumentExpr {
fn check_wf<T: Target>(self, locals: Map<LocalName, Type>, prog: Program) -> Result<Type> {
ret(match self {
ArgumentExpr::ByValue(value) => value.check_wf::<T>(locals, prog)?,
ArgumentExpr::InPlace(place) => place.check_wf::<T>(locals, prog)?
})
}
}
impl Statement {
fn check_wf<T: Target>(
self,
func: Function,
prog: Program,
) -> Result<()> {
use Statement::*;
match self {
Assign { destination, source } => {
let left = destination.check_wf::<T>(func.locals, prog)?;
let right = source.check_wf::<T>(func.locals, prog)?;
ensure_wf(left == right, "Statement::Assign: destination and source type differ")?;
assert!(right.size::<T>().is_sized(), "ValueExpr always return sized types");
}
PlaceMention(place) => {
place.check_wf::<T>(func.locals, prog)?;
}
SetDiscriminant { destination, value } => {
let Type::Enum { variants, .. } = destination.check_wf::<T>(func.locals, prog)? else {
throw_ill_formed!("Statement::SetDiscriminant: invalid type");
};
// We don't ensure that we can actually represent the discriminant.
// The well-formedness checks for the type just ensure that every discriminant
// reached by the discriminator is valid, however there we don't require that every
// variant is represented. Setting such an unrepresented discriminant would probably
// result in an invalid value as either the discriminator returns
// `Discriminator::Invalid` or another variant.
// This is fine as SetDiscriminant does not guarantee that the enum is a valid value.
if variants.get(value) == None {
throw_ill_formed!("Statement::SetDiscriminant: invalid discriminant write")
}
}
Validate { place, fn_entry: _ } => {
let ty = place.check_wf::<T>(func.locals, prog)?;
ensure_wf(ty.size::<T>().is_sized(), "Statement::Validate: unsized place")?;
}
Deinit { place } => {
let ty = place.check_wf::<T>(func.locals, prog)?;
ensure_wf(ty.size::<T>().is_sized(), "Statement::Deinit: unsized place")?;
}
StorageLive(local) => {
ensure_wf(func.locals.contains_key(local), "Statement::StorageLive: invalid local variable")?;
}
StorageDead(local) => {
ensure_wf(func.locals.contains_key(local), "Statement::StorageDead: invalid local variable")?;
if local == func.ret || func.args.any(|arg_name| local == arg_name) {
throw_ill_formed!("Statement::StorageDead: trying to mark argument or return local as dead");
}
}
}
ret(())
}
}
/// Predicate to indicate if integer bin-op can be used for atomic fetch operations.
/// Needed for atomic fetch operations.
///
/// We limit the binops that are allowed to be atomic based on current LLVM and Rust API exposures.
fn is_atomic_binop(op: IntBinOp) -> bool {
use IntBinOp as B;
match op {
B::Add | B::Sub => true,
_ => false
}
}
impl Terminator {
fn check_wf<T: Target>(
self,
func: Function,
prog: Program,
) -> Result<()> {
use Terminator::*;
match self {
Goto(block_name) => {
ensure_wf(func.blocks.contains_key(block_name), "Terminator::Goto: next block does not exist")?;
}
Switch { value, cases, fallback } => {
let ty = value.check_wf::<T>(func.locals, prog)?;
let Type::Int(switch_ty) = ty else {
// We only switch on integers.
// This is in contrast to Rust MIR where switch can work on `char`s and booleans as well.
// However since those are trivial casts we chose to only accept integers.
throw_ill_formed!("Terminator::Switch: switch is not Int")
};
// Ensure the switch cases are all valid.
for (case, block) in cases.iter() {
ensure_wf(switch_ty.can_represent(case), "Terminator::Switch: value does not fit in switch type")?;
ensure_wf(func.blocks.contains_key(block), "Terminator::Switch: next block does not exist")?;
}
// we can also reach the fallback block.
ensure_wf(func.blocks.contains_key(fallback), "Terminator::Switch: fallback block does not exist")?;
}
Unreachable => {}
Intrinsic { intrinsic, arguments, ret, next_block } => {
// Return and argument expressions must all typecheck with some type.
let ret_ty = ret.check_wf::<T>(func.locals, prog)?;
ensure_wf(ret_ty.size::<T>().is_sized(), "Terminator::Intrinsic: unsized return type")?;
for arg in arguments {
let arg_ty = arg.check_wf::<T>(func.locals, prog)?;
ensure_wf(arg_ty.size::<T>().is_sized(), "Terminator::Intrinsic: unsized argument type")?;
}
// Currently only AtomicFetchAndOp has special well-formedness requirements.
match intrinsic {
IntrinsicOp::AtomicFetchAndOp(op) => {
if !is_atomic_binop(op) {
throw_ill_formed!("IntrinsicOp::AtomicFetchAndOp: non atomic op");
}
}
_ => {}
}
if let Some(next_block) = next_block {
ensure_wf(func.blocks.contains_key(next_block), "Terminator::Call: next block does not exist")?;
}
}
Call { callee, calling_convention: _, arguments, ret, next_block } => {
let ty = callee.check_wf::<T>(func.locals, prog)?;
ensure_wf(matches!(ty, Type::Ptr(PtrType::FnPtr)), "Terminator::Call: invalid type")?;
// Return and argument expressions must all typecheck with some sized type.
let ret_ty = ret.check_wf::<T>(func.locals, prog)?;
ensure_wf(ret_ty.size::<T>().is_sized(), "Terminator::Call: unsized return type")?;
for arg in arguments {
let arg_ty = arg.check_wf::<T>(func.locals, prog)?;
ensure_wf(arg_ty.size::<T>().is_sized(), "Terminator::Call: unsized argument type")?;
}
if let Some(next_block) = next_block {
ensure_wf(func.blocks.contains_key(next_block), "Terminator::Call: next block does not exist")?;
}
}
Return => {}
}
ret(())
}
}
impl Function {
fn check_wf<T: Target>(self, prog: Program) -> Result<()> {
// Ensure all locals have a valid type.
for ty in self.locals.values() {
ensure_wf(ty.size::<T>().is_sized(), "Function: unsized local variable")?;
ty.check_wf::<T>()?;
}
// Compute initially live locals: arguments and return values.
// They must all exist and be distinct.
let mut start_live: Set<LocalName> = Set::new();
for arg in self.args {
ensure_wf(self.locals.contains_key(arg), "Function: argument local does not exist")?;
if start_live.try_insert(arg).is_err() {
throw_ill_formed!("Function: two arguments refer to the same local");
};
}
ensure_wf(self.locals.contains_key(self.ret), "Function: return local does not exist")?;
if start_live.try_insert(self.ret).is_err() {
throw_ill_formed!("Function: return local is also used for an argument");
};
// Check all basic blocks.
for block in self.blocks.values() {
for statement in block.statements {
statement.check_wf::<T>(self, prog)?;
}
block.terminator.check_wf::<T>(self, prog)?;
}
ret(())
}
}
impl Relocation {
// Checks whether the relocation is within bounds.
fn check_wf(self, globals: Map<GlobalName, Global>) -> Result<()> {
// The global we are pointing to needs to exist.
let Some(global) = globals.get(self.name) else {
throw_ill_formed!("Relocation: invalid global name");
};
let size = Size::from_bytes(global.bytes.len()).unwrap();
// And the offset needs to be in-bounds of its size.
ensure_wf(self.offset <= size, "Relocation: offset out-of-bounds")?;
ret(())
}
}
impl Program {
fn check_wf<T: Target>(self) -> Result<()> {
// Check all the functions.
for function in self.functions.values() {
function.check_wf::<T>(self)?;
}
// Ensure the start function exists, has the right ABI, takes no arguments, and returns a 1-ZST.
let Some(start) = self.functions.get(self.start) else {
throw_ill_formed!("Program: start function does not exist");
};
ensure_wf(start.calling_convention == CallingConvention::C, "Program: start function has invalid calling convention")?;
let ret_size = start.locals[start.ret].size::<T>();
let ret_align = start.locals[start.ret].align::<T>();
ensure_wf(
ret_size == SizeStrategy::Sized(Size::ZERO) && ret_align == Align::ONE,
"Program: start function return local has invalid layout"
)?;
ensure_wf(start.args.is_empty(), "Program: start function has arguments")?;
// Check globals.
for (_name, global) in self.globals {
let size = Size::from_bytes(global.bytes.len()).unwrap();
for (offset, relocation) in global.relocations {
// A relocation fills `PTR_SIZE` many bytes starting at the offset, those need to fit into the size.
ensure_wf(offset + T::PTR_SIZE <= size, "Program: invalid global pointer value")?;
relocation.check_wf(self.globals)?;
}
}
ret(())
}
}
impl<M: Memory> Value<M> {
/// We assume `ty` is itself well-formed.
fn check_wf(self, ty: Type) -> Result<()> {
match (self, ty) {
(Value::Int(i), Type::Int(ity)) => {
ensure_wf(ity.can_represent(i), "Value::Int: invalid integer value")?;
}
(Value::Bool(_), Type::Bool) => {},
(Value::Ptr(ptr), Type::Ptr(ptr_ty)) => {
ensure_wf(ptr_ty.meta_kind().matches(ptr.metadata), "Value::Ptr: invalid metadata")?;
ensure_wf(ptr_ty.addr_valid(ptr.thin_pointer.addr), "Value::Ptr: invalid pointer address")?;
ensure_wf(ptr.thin_pointer.addr.in_bounds(Unsigned, M::T::PTR_SIZE), "Value::Ptr: pointer out-of-bounds")?;
}
(Value::Tuple(vals), Type::Tuple { fields, .. }) => {
ensure_wf(vals.len() == fields.len(), "Value::Tuple: invalid number of fields")?;
for (val, (_, ty)) in vals.zip(fields) {
val.check_wf(ty)?;
}
}
(Value::Tuple(vals), Type::Array { elem, count }) => {
ensure_wf(vals.len() == count, "Value::Tuple: invalid number of elements")?;
for val in vals {
val.check_wf(elem)?;
}
}
(Value::Union(chunk_data), Type::Union { chunks, .. }) => {
ensure_wf(chunk_data.len() == chunks.len(), "Value::Union: invalid chunk size")?;
for (data, (_, size)) in chunk_data.zip(chunks) {
ensure_wf(data.len() == size.bytes(), "Value::Union: invalid chunk data")?;
}
}
(Value::Variant { discriminant, data }, Type::Enum { variants, .. }) => {
let Some(variant) = variants.get(discriminant) else {
throw_ill_formed!("Value::Variant: invalid discrimant type");
};
data.check_wf(variant.ty)?;
}
(_, Type::Slice { .. }) => throw_ill_formed!("Value: slices cannot be represented as values"),
_ => throw_ill_formed!("Value: value does not match type")
}
ret(())
}
}