use super::array;
use super::bdd::BddBuildError;
use super::dist::{Dist, DistError, SELECTOR_BASE};
use super::func::{self, Func, FuncError};
use super::func_order;
use super::guard;
use super::ir::{Assignment, ConstraintEvalError, Expr, Ty, eval, mask};
use super::joint;
use super::order;
use super::priority::{self, Soft};
use super::randc::RandcState;
use super::real_domain::{self, FloatKind, RealError, RealProblem};
use super::reduce::{self, BlockItems};
use super::solve::{Engine, Group, Measure, Plan, SolveError, Weighted};
use crate::collections;
use crate::netlist::ClassTemplateId;
use crate::objects::{self, FieldSlot, Window};
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Home {
Instance,
Static(u32),
Object(u64),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgHandle {
pub slot: FieldSlot,
pub home: Home,
pub prop_index: Option<usize>,
pub bit: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgVar {
pub ty: Ty,
pub slot: FieldSlot,
pub home: Home,
pub prop_index: Option<usize>,
pub domain_conjunct: Option<usize>,
pub randc: bool,
pub window: Option<Window>,
}
impl ProgVar {
fn randc_key(&self) -> usize {
randc_key(self.slot, self.window)
}
}
fn randc_key(slot: FieldSlot, window: Option<Window>) -> usize {
match window {
None => slot.offset,
Some(w) => slot.offset | ((w.bit as usize + 1) << 32),
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgState {
pub ty: Ty,
pub slot: FieldSlot,
pub window: Option<Window>,
pub home: Home,
pub via: Vec<FieldSlot>,
pub root_bit: u32,
#[serde(default)]
pub class_of: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Program {
pub vars: Vec<ProgVar>,
pub states: Vec<ProgState>,
pub conjuncts: Vec<Expr>,
pub before: Vec<(u32, u32)>,
pub dists: Vec<Dist>,
pub real_vars: Vec<ProgRealVar>,
pub real: RealProblem,
pub arrays: Vec<ProgArray>,
pub funcs: Vec<Func>,
pub softs: Vec<Soft>,
pub rand_handles: Vec<ProgHandle>,
pub blocks: Vec<ProgBlock>,
pub n_disables: usize,
pub disable_keys: Vec<super::priority::SoftKey>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgBlock {
pub home: Home,
pub ordinal: u32,
pub conjuncts: std::ops::Range<usize>,
pub items: BlockItems,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgArray {
pub elem_ty: Ty,
pub elem_words: usize,
pub slot: FieldSlot,
pub home: Home,
pub rand: bool,
pub prop_index: Option<usize>,
pub elem_domain: Option<Vec<BigUint>>,
pub assoc: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ProgRealVar {
pub kind: FloatKind,
pub slot: FieldSlot,
pub home: Home,
pub prop_index: Option<usize>,
pub window: Option<Window>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u64)]
pub enum Outcome {
Unsatisfiable = 0,
Solved = 1,
DeadHandle = 2,
NoProgram = 3,
StateUnknown = 4,
TooLarge = 5,
Failed = 6,
StaticUnavailable = 7,
BadDistWeight = 8,
GuardError = 9,
NullHandle = 10,
RealDomain = 11,
ArraySizeTooLarge = 12,
ArrayIndexOutOfRange = 13,
ArraySizeSearch = 14,
FunctionFailed = 15,
ImplicitOrderCycle = 16,
FunctionSearch = 17,
GlobalUnsupported = 18,
SatTimeout = 19,
SatUnsupported = 20,
}
impl Outcome {
pub fn code(self) -> u64 {
self as u64
}
pub fn from_code(code: u64) -> Option<Outcome> {
[
Outcome::Unsatisfiable,
Outcome::Solved,
Outcome::DeadHandle,
Outcome::NoProgram,
Outcome::StateUnknown,
Outcome::TooLarge,
Outcome::Failed,
Outcome::StaticUnavailable,
Outcome::BadDistWeight,
Outcome::GuardError,
Outcome::NullHandle,
Outcome::RealDomain,
Outcome::ArraySizeTooLarge,
Outcome::ArrayIndexOutOfRange,
Outcome::ArraySizeSearch,
Outcome::FunctionFailed,
Outcome::ImplicitOrderCycle,
Outcome::FunctionSearch,
Outcome::GlobalUnsupported,
Outcome::SatTimeout,
Outcome::SatUnsupported,
]
.into_iter()
.find(|o| o.code() == code)
}
pub fn error_message(self) -> Option<&'static str> {
match self {
Outcome::Unsatisfiable | Outcome::Solved => None,
Outcome::DeadHandle => Some("randomize() on a null or deleted object"),
Outcome::NoProgram => Some(
"randomize(): the constraints of this object's class could not be compiled \
(see the class's constraint blocks)",
),
Outcome::StateUnknown => Some(
"randomize(): a constraint reads a variable with an X or Z bit; a 4-state \
value in a constraint is an error (IEEE 1800-2023 18.3)",
),
Outcome::TooLarge => Some(
"randomize(): the constraint problem is too large for both the BDD solver \
(node budget exceeded) and its SAT fallback (clause limit exceeded)",
),
Outcome::SatTimeout => Some(
"randomize(): the constraint problem is too large for the BDD solver and \
the SAT fallback did not find a solution within its time limit",
),
Outcome::SatUnsupported => Some(
"randomize(): the constraint problem is too large for the BDD solver and \
uses a `dist` over an expression with more than 512 feasible assignments, a `dist` \
over a variable wider than 32 bits or with more than 1024 feasible values per \
range, or a `randc` variable wider than 10 bits, which the SAT fallback does \
not support",
),
Outcome::Failed => Some("randomize(): internal solver failure on a malformed program"),
Outcome::StaticUnavailable => Some(
"randomize(): the object's class declares a static property used by its \
constraints or `rand`/`randc` variables that this call site cannot reach \
(it is declared in a class derived from the handle's static type); call \
randomize() through a handle of that class",
),
Outcome::BadDistWeight => Some(
"randomize(): a `dist` weight is negative, or a weight or range bound \
divides by zero (IEEE 1800-2023 18.5.3)",
),
Outcome::GuardError => Some(
"randomize(): the guard of an implication or `if` constraint evaluated to \
ERROR (a null object handle, or an X/Z value, read where no operand \
decides it; IEEE 1800-2023 18.5.12)",
),
Outcome::NullHandle => Some(
"randomize(): a constraint reads a member through a null or deleted object \
handle (IEEE 1800-2023 18.5.12)",
),
Outcome::RealDomain => Some(
"randomize(): a constraint on a real variable evaluates to NaN or divides by \
zero, so it has no well-defined set of solutions",
),
Outcome::ArraySizeTooLarge => Some(
"randomize(): the constraints on the size of a dynamic array or queue can \
only be met by a size above 65536 elements, which is not supported",
),
Outcome::ArrayIndexOutOfRange => Some(
"randomize(): a constraint reads an array element outside the array's \
bounds (guard it with an `if` on the size, IEEE 1800-2023 18.5.7.1)",
),
Outcome::ArraySizeSearch => Some(
"randomize(): no array size tried satisfied both the size constraints and \
the element constraints (the bounded search over sizes gave up)",
),
Outcome::FunctionFailed => Some(
"randomize(): a function called from a constraint failed (division by zero, \
a runaway loop or recursion that is too deep)",
),
Outcome::ImplicitOrderCycle => Some(
"randomize(): the random arguments of function calls in the constraints \
impose a circular solve order (IEEE 1800-2023 18.5.11)",
),
Outcome::FunctionSearch => Some(
"randomize(): no draw satisfied a constraint that compares the results of \
function calls over several random variables (the bounded search gave up)",
),
Outcome::GlobalUnsupported => Some(
"randomize(): the object tree being randomized together (a `rand` object \
member, IEEE 1800-2023 18.5.8) has `real` random variables, or a `static` \
random variable that two objects of the tree share, which the joint solver \
does not combine yet",
),
}
}
}
type ProgramTable = HashMap<ClassTemplateId, Arc<Result<Program, String>>>;
fn table() -> &'static Mutex<ProgramTable> {
static T: OnceLock<Mutex<ProgramTable>> = OnceLock::new();
T.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn register_program(template: ClassTemplateId, program: Result<Program, String>) {
table()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(template, Arc::new(program));
}
pub fn program(template: ClassTemplateId) -> Option<Arc<Result<Program, String>>> {
table()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&template)
.cloned()
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Snapshot {
programs: Vec<(u32, Result<Program, String>)>,
specs: Vec<(u32, Spec)>,
}
pub fn snapshot(templates: &[ClassTemplateId]) -> Vec<u8> {
let mut ids: Vec<u32> = templates.iter().map(|t| t.0).collect();
ids.sort_unstable();
ids.dedup();
let programs: Vec<(u32, Result<Program, String>)> = ids
.into_iter()
.filter_map(|id| program(ClassTemplateId(id)).map(|p| (id, Result::clone(&p))))
.collect();
let specs: Vec<(u32, Spec)> = all_specs()
.into_iter()
.map(|(id, s)| (id, Spec::clone(&s)))
.collect();
if programs.is_empty() && specs.is_empty() {
return Vec::new();
}
postcard::to_allocvec(&Snapshot { programs, specs })
.expect("a constraint program is plain data and always serializes")
}
pub fn preload(bytes: &[u8]) -> Result<(), String> {
if bytes.is_empty() {
return Ok(());
}
let snap: Snapshot =
postcard::from_bytes(bytes).map_err(|e| format!("randomize section: {e}"))?;
for (id, p) in snap.programs {
register_program(ClassTemplateId(id), p);
}
for (id, s) in snap.specs {
register_spec_at(id, s);
}
Ok(())
}
pub(super) fn unpack(words: &[u64], width: u32) -> (BigUint, BigUint) {
let mut n = BigUint::zero();
for (i, w) in words.iter().enumerate() {
n |= BigUint::from(*w) << (64 * i);
}
let m = mask(width);
(&n & &m, (n >> width) & m)
}
fn pack(val: &BigUint, words: usize) -> Option<Vec<u64>> {
let mut d = val.to_u64_digits();
if d.len() > words {
return None;
}
d.resize(words, 0);
Some(d)
}
fn read(
h: u64,
slot: FieldSlot,
window: Option<Window>,
ty: Ty,
) -> Result<Option<BigUint>, Outcome> {
let (val, unk) = read_unknown(h, slot, window, ty.width)?;
Ok(if unk.is_zero() { Some(val) } else { None })
}
fn read_unknown(
h: u64,
slot: FieldSlot,
window: Option<Window>,
width: u32,
) -> Result<(BigUint, BigUint), Outcome> {
match window {
None => {
let mut buf = vec![0u64; slot.words];
if !objects::field_get(h, slot, &mut buf) {
return Err(Outcome::DeadHandle);
}
Ok(unpack(&buf, width))
}
Some(w) => {
let n = (w.width as usize).div_ceil(64);
let (mut val, mut unk) = (vec![0u64; n], vec![0u64; n]);
if !objects::field_get_window(h, slot, w, &mut val, &mut unk) {
return Err(Outcome::DeadHandle);
}
let m = mask(width);
let to_big = |ws: &[u64]| {
ws.iter().enumerate().fold(BigUint::zero(), |a, (i, x)| {
a | (BigUint::from(*x) << (64 * i))
})
};
Ok((to_big(&val) & &m, to_big(&unk) & &m))
}
}
}
enum Reading {
Value(Option<BigUint>),
NullHandle,
}
fn read_state(owner: u64, s: &ProgState) -> Result<Reading, Outcome> {
let (mut obj, mut slot) = (owner, s.slot);
for (i, hop) in s.via.iter().enumerate() {
let mut buf = vec![0u64; slot.words];
let bit = if i == 0 { s.root_bit } else { 0 };
if !objects::field_get(obj, slot, &mut buf) {
return if i == 0 {
Err(Outcome::DeadHandle)
} else {
Ok(Reading::NullHandle)
};
}
let w = (bit / 64) as usize;
let half = slot.words / 2;
let (Some(&v), Some(&u)) = (buf.get(w), buf.get(half + w)) else {
return Err(Outcome::Failed);
};
let (val, unk) = (BigUint::from(v), BigUint::from(u));
if !unk.is_zero() {
return Ok(Reading::Value(None));
}
match val.to_u64() {
Some(0) => return Ok(Reading::NullHandle),
Some(next) => obj = next,
None => return Err(Outcome::Failed),
}
slot = *hop;
}
if s.class_of {
let (val, unk) = match read_unknown(obj, slot, None, 64) {
Ok(r) => r,
Err(_) if s.via.is_empty() => return Err(Outcome::DeadHandle),
Err(_) => return Ok(Reading::NullHandle),
};
if !unk.is_zero() {
return Ok(Reading::Value(None));
}
return match val.to_u64() {
Some(0) => Ok(Reading::NullHandle),
Some(h) => Ok(match objects::dynamic_template(h) {
Some(t) => Reading::Value(Some(BigUint::from(t.0))),
None => Reading::NullHandle,
}),
None => Err(Outcome::Failed),
};
}
match read_unknown(obj, slot, s.window, s.ty.width) {
Ok((val, unk)) => Ok(Reading::Value(if unk.is_zero() { Some(val) } else { None })),
Err(_) if s.via.is_empty() => Err(Outcome::DeadHandle),
Err(_) => Ok(Reading::NullHandle),
}
}
pub(super) fn owner(home: Home, h: u64, statics: &[(u32, u64)]) -> Result<u64, Outcome> {
match home {
Home::Instance => Ok(h),
Home::Object(o) => Ok(o),
Home::Static(id) => statics
.iter()
.find(|(i, s)| *i == id && *s != 0)
.map(|&(_, s)| s)
.ok_or(Outcome::StaticUnavailable),
}
}
fn unreadable_outcome(id: u32, null_state: &[bool]) -> Outcome {
if null_state.get(id as usize).copied().unwrap_or(false) {
Outcome::NullHandle
} else {
Outcome::StateUnknown
}
}
fn outcome_of(e: SolveError, null_state: &[bool]) -> Outcome {
match e {
SolveError::Unsatisfiable => Outcome::Unsatisfiable,
SolveError::Build(BddBuildError::StateUnknown(id)) => unreadable_outcome(id, null_state),
SolveError::Build(BddBuildError::TooLarge { .. } | BddBuildError::CnfTooLarge { .. }) => {
Outcome::TooLarge
}
SolveError::SatTimeout => Outcome::SatTimeout,
SolveError::SatUnsupported { .. } => Outcome::SatUnsupported,
SolveError::Build(_)
| SolveError::BadOrder { .. }
| SolveError::BadPlan { .. }
| SolveError::SatModelRejected
| SolveError::SatSolverFailure { .. } => Outcome::Failed,
}
}
fn outcome_of_dist(e: DistError, null_state: &[bool]) -> Outcome {
match e {
DistError::Eval(ConstraintEvalError::StateUnknown(id)) => {
unreadable_outcome(id, null_state)
}
DistError::Eval(ConstraintEvalError::DivideByZero) | DistError::NegativeWeight => {
Outcome::BadDistWeight
}
DistError::Eval(
ConstraintEvalError::UnboundVar(_)
| ConstraintEvalError::UnboundState(_)
| ConstraintEvalError::Unexpanded,
) => Outcome::Failed,
}
}
fn outcome_of_real(e: RealError, null_state: &[bool]) -> Outcome {
match e {
RealError::Unsatisfiable => Outcome::Unsatisfiable,
RealError::GuardError => Outcome::GuardError,
RealError::StateUnreadable(id) => unreadable_outcome(id, null_state),
RealError::NonFinite(_) => Outcome::RealDomain,
RealError::BadWeight => Outcome::BadDistWeight,
RealError::Shape(_) => Outcome::Failed,
}
}
struct Write {
owner: u64,
slot: FieldSlot,
window: Option<Window>,
words: Vec<u64>,
}
impl Write {
fn new(owner: u64, slot: FieldSlot, window: Option<Window>, val: &BigUint) -> Option<Write> {
let n = match window {
None => slot.words,
Some(w) => (w.width as usize).div_ceil(64),
};
Some(Write {
owner,
slot,
window,
words: pack(val, n)?,
})
}
fn apply(&self) -> bool {
match self.window {
None => objects::field_set(self.owner, self.slot, &self.words),
Some(w) => objects::field_set_window(self.owner, self.slot, w, &self.words),
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_real(
program: &Program,
h: u64,
rand_mode: &[bool],
statics: &[(u32, u64)],
states: &[Option<BigUint>],
null_state: &[bool],
ints: Option<(&[Option<u32>], &[BigUint])>,
seed: &mut u64,
) -> Result<Vec<Write>, Outcome> {
if program.real_vars.is_empty() {
return Ok(Vec::new());
}
if program.real_vars.len() != program.real.kinds.len() {
return Err(Outcome::Failed);
}
let mut owners = Vec::with_capacity(program.real_vars.len());
let mut known = Vec::with_capacity(program.real_vars.len());
for v in &program.real_vars {
let o = owner(v.home, h, statics)?;
let active = match v.prop_index {
Some(i) => rand_mode.get(i).copied().ok_or(Outcome::Failed)?,
None => true,
};
known.push(if active {
None
} else {
match read(o, v.slot, v.window, Ty::unsigned(v.kind.width()))? {
Some(bits) => Some(v.kind.from_bits(&bits)),
None => return Err(Outcome::StateUnknown),
}
});
owners.push(o);
}
let (lowered, ext);
let (problem, states) = match ints {
Some((var_new, values)) if program.real.reads_int_vars() => {
let mut e = states.to_vec();
let mut ix = Vec::with_capacity(var_new.len());
let mut inactive = program.states.len();
for d in var_new {
match d {
Some(d) => {
ix.push(e.len() as u32);
e.push(Some(
values.get(*d as usize).ok_or(Outcome::Failed)?.clone(),
));
}
None => {
ix.push(inactive as u32);
inactive += 1;
}
}
}
lowered = program
.real
.with_int_vars(|v, t| Expr::State(ix[v as usize], t));
ext = e;
(&lowered, &ext[..])
}
_ => (&program.real, states),
};
let solved = real_domain::solve(problem, &known, states, seed)
.map_err(|e| outcome_of_real(e, null_state))?;
let mut writes = Vec::new();
for ((v, o), x) in program.real_vars.iter().zip(owners).zip(solved) {
let Some(x) = x else { continue };
match Write::new(o, v.slot, v.window, &BigUint::from(v.kind.to_bits(x))) {
Some(w) => writes.push(w),
None => return Err(Outcome::Failed),
}
}
Ok(writes)
}
struct Call<'a> {
program: &'a Program,
h: u64,
rand_mode: &'a [bool],
statics: &'a [(u32, u64)],
var_owner: Vec<u64>,
var_new: Vec<Option<u32>>,
var_tys: Vec<Ty>,
active_ids: Vec<usize>,
arrays: Vec<array::ArrayState>,
array_owner: Vec<u64>,
states: Vec<Option<BigUint>>,
null_state: Vec<bool>,
real_unsat: std::cell::Cell<bool>,
}
struct Solution {
values: Vec<BigUint>,
randc_keys: Vec<(u64, usize)>,
randc_work: Vec<RandcState>,
real_writes: Vec<Write>,
}
fn read_array(a: &ProgArray, owner: u64, active: bool) -> Result<array::ArrayState, Outcome> {
let mut buf = vec![0u64; a.slot.words];
if !objects::field_get(owner, a.slot, &mut buf) {
return Err(Outcome::DeadHandle);
}
let (handle, unk) = unpack(&buf, 64);
if !unk.is_zero() {
return Err(Outcome::Failed);
}
let handle = handle.to_u64().ok_or(Outcome::Failed)?;
let cur = collections::elements(handle)
.iter()
.map(|words| {
let (val, unk) = unpack(words, a.elem_ty.width);
if unk.is_zero() { Some(val) } else { None }
})
.collect();
Ok(array::ArrayState {
elem_ty: a.elem_ty,
active,
cur,
domain: a.elem_domain.clone(),
})
}
fn outcome_of_func(e: FuncError, null_state: &[bool]) -> Outcome {
match e {
FuncError::Eval(ConstraintEvalError::StateUnknown(id)) => {
unreadable_outcome(id, null_state)
}
FuncError::Eval(ConstraintEvalError::DivideByZero)
| FuncError::TooDeep { .. }
| FuncError::StepLimit { .. } => Outcome::FunctionFailed,
FuncError::Eval(
ConstraintEvalError::UnboundVar(_)
| ConstraintEvalError::UnboundState(_)
| ConstraintEvalError::Unexpanded,
)
| FuncError::Malformed(_) => Outcome::Failed,
}
}
fn outcome_of_expand(e: array::ExpandError, null_state: &[bool]) -> Outcome {
match e {
array::ExpandError::IndexOutOfRange { .. }
| array::ExpandError::LeafIndexOutOfRange { .. } => Outcome::ArrayIndexOutOfRange,
array::ExpandError::GuardError => Outcome::GuardError,
array::ExpandError::StateUnknown(id) => unreadable_outcome(id, null_state),
array::ExpandError::Func(e) => outcome_of_func(e, null_state),
array::ExpandError::NonConstantIndex
| array::ExpandError::DistRandomBound
| array::ExpandError::Malformed(_) => Outcome::Failed,
}
}
fn outcome_of_size(e: array::SizeError, null_state: &[bool]) -> Outcome {
match e {
array::SizeError::Unsatisfiable => Outcome::Unsatisfiable,
array::SizeError::TooLarge => Outcome::ArraySizeTooLarge,
array::SizeError::GuardError => Outcome::GuardError,
array::SizeError::Solve(e) => outcome_of(e, null_state),
}
}
const MAX_CHECK_TRIES: usize = 4096;
fn checks_hold(
checks: &[Expr],
drawn: &[BigUint],
funcs: &[Func],
states: &[Option<BigUint>],
null_state: &[bool],
) -> Result<bool, Outcome> {
let asg = Assignment {
vars: drawn.to_vec(),
states: states.to_vec(),
};
for c in checks {
let c = c.substitute_vars(&mut |v, t| match drawn.get(v as usize) {
Some(val) => Expr::Const(t, val.clone()),
None => Expr::Var(v, t),
});
let c = func::fold_ground_calls(&c, funcs, states)
.map_err(|e| outcome_of_func(e, null_state))?;
let v = eval(&c, &asg).map_err(|e| outcome_of_func(FuncError::Eval(e), null_state))?;
if v.is_zero() {
return Ok(false);
}
}
Ok(true)
}
fn solve_expanded(
call: &Call<'_>,
x: &array::Expanded,
seed: &mut u64,
) -> Result<Solution, Outcome> {
let joint = call.program.real.reads_int_vars();
let tries = if joint { REAL_JOINT_TRIES } else { 1 };
for _ in 1..tries {
call.real_unsat.set(false);
match solve_expanded_once(call, x, seed) {
Err(Outcome::Unsatisfiable) if call.real_unsat.get() => {}
r => return r,
}
}
solve_expanded_once(call, x, seed)
}
const REAL_JOINT_TRIES: usize = 4096;
fn solve_expanded_once(
call: &Call<'_>,
x: &array::Expanded,
seed: &mut u64,
) -> Result<Solution, Outcome> {
let program = call.program;
let n_active = call.var_tys.len();
let n_real = n_active + x.elem_vars.len();
let mut states = call.states.clone();
states.extend(x.extra_states.iter().cloned());
let mut null_state = call.null_state.clone();
null_state.resize(states.len(), false);
let mut var_tys = call.var_tys.clone();
var_tys.extend(x.elem_tys.iter().copied());
let conjuncts: Vec<Expr> = x
.conjuncts
.iter()
.map(|c| {
c.substitute_vars(&mut |v, t| {
if v >= SELECTOR_BASE {
Expr::Var(n_real as u32 + (v - SELECTOR_BASE), t)
} else {
Expr::Var(v, t)
}
})
})
.collect();
let assignment = Assignment {
vars: Vec::new(),
states: states.clone(),
};
let has_calls = conjuncts.iter().any(func::has_call);
let stage_of: Vec<u32> = if has_calls {
func_order::stages(n_real, &conjuncts).map_err(|_| Outcome::ImplicitOrderCycle)?
} else {
vec![0; n_real]
};
let n_stages = stage_of.iter().max().map_or(1, |m| *m as usize + 1);
let mut conj_stage: Vec<usize> = Vec::with_capacity(conjuncts.len());
let n_prog_dists = program.dists.len();
let all_dists: Vec<&Dist> = program.dists.iter().chain(&x.extra_dists).collect();
let mut dist_stage: Vec<usize> = vec![0; all_dists.len()];
for c in &conjuncts {
let mut s = 0usize;
c.for_each_var(&mut |v, _| {
if let Some(&st) = stage_of.get(v as usize) {
s = s.max(st as usize);
}
});
c.for_each_var(&mut |v, _| {
if let Some(j) = (v as usize).checked_sub(n_real)
&& let Some(d) = dist_stage.get_mut(j)
{
*d = s;
}
});
conj_stage.push(s);
}
let mut edges: Vec<(u32, u32)> = Vec::with_capacity(program.before.len());
for &(a, b) in &program.before {
let (Some(&na), Some(&nb)) = (call.var_new.get(a as usize), call.var_new.get(b as usize))
else {
return Err(Outcome::Failed);
};
if let (Some(na), Some(nb)) = (na, nb) {
edges.push((na, nb));
}
}
let mut randc_dense: Vec<u32> = Vec::new();
let mut randc_keys: Vec<(u64, usize)> = Vec::new();
let mut randc_work: Vec<RandcState> = Vec::new();
for (dense, &i) in call.active_ids.iter().enumerate() {
let v = &program.vars[i];
if !v.randc {
continue;
}
let Some(st) = objects::randc_state(call.var_owner[i], v.randc_key()) else {
return Err(Outcome::DeadHandle);
};
randc_dense.push(dense as u32);
randc_keys.push((call.var_owner[i], v.randc_key()));
randc_work.push(st);
}
let mut dist_weights = Vec::with_capacity(all_dists.len());
for d in &all_dists {
if d.subject.is_some() {
dist_weights.push(Vec::new());
} else {
match d.weights(&states) {
Ok(w) => dist_weights.push(w),
Err(e) => return Err(outcome_of_dist(e, &null_state)),
}
}
var_tys.push(d.selector_ty());
}
let mut values: Vec<BigUint> = vec![BigUint::zero(); n_real];
let mut known: Vec<Option<BigUint>> = vec![None; n_real];
for stage in 0..n_stages {
let in_stage = |v: u32| {
stage_of
.get(v as usize)
.is_some_and(|s| *s as usize == stage)
};
let mut plain: Vec<Expr> = Vec::new();
let mut checks: Vec<Expr> = Vec::new();
for (c, _) in conjuncts
.iter()
.zip(&conj_stage)
.filter(|(_, s)| **s == stage)
{
let mut c = c.clone();
if has_calls {
if stage > 0 {
c = c.substitute_vars(&mut |v, t| match known.get(v as usize) {
Some(Some(val)) => Expr::Const(t, val.clone()),
_ => Expr::Var(v, t),
});
}
c = func::fold_ground_calls(&c, &program.funcs, &states)
.map_err(|e| outcome_of_func(e, &null_state))?;
}
if func::has_call(&c) {
checks.push(c);
continue;
}
match guard::resolve(&c, &assignment) {
Ok(r) => plain.push(r),
Err(_) => return Err(Outcome::GuardError),
}
}
let stage_edges: Vec<(u32, u32)> = edges
.iter()
.copied()
.filter(|&(a, b)| in_stage(a) && in_stage(b))
.collect();
let Ok(groups) = order::groups(n_real, &stage_edges) else {
return Err(Outcome::Failed);
};
let stage_randc: Vec<usize> = (0..randc_dense.len())
.filter(|&k| in_stage(randc_dense[k]))
.collect();
let stage_randc_dense: Vec<u32> = stage_randc.iter().map(|&k| randc_dense[k]).collect();
let mut plan_groups: Vec<Group> = groups
.into_iter()
.map(|g| Group {
vars: g
.into_iter()
.filter(|v| in_stage(*v) && !randc_dense.contains(v))
.collect(),
weighted: Vec::new(),
})
.collect();
let stage_dists: Vec<usize> = (0..all_dists.len())
.filter(|&j| dist_stage[j] == stage && all_dists[j].subject.is_none())
.collect();
if !stage_dists.is_empty() && plan_groups.is_empty() {
plan_groups.push(Group::default());
}
for &j in &stage_dists {
let d = all_dists[j];
let dense = |v: u32| {
if j >= n_prog_dists {
Some(v)
} else {
call.var_new.get(v as usize).copied().flatten()
}
};
let subject: Vec<u32> = d
.vars
.iter()
.filter_map(|&v| dense(v))
.filter(|&v| in_stage(v))
.collect();
let gi = plan_groups
.iter()
.position(|g| g.vars.iter().any(|v| subject.contains(v)))
.unwrap_or(plan_groups.len() - 1);
let measure = match (
d.plain_var.and_then(dense).filter(|&v| in_stage(v)),
subject.is_empty(),
) {
(Some(s), _) => Measure::Distinct(s),
(None, false) => Measure::Models,
(None, true) => Measure::Flat,
};
plan_groups[gi].weighted.push(Weighted {
var: (n_real + j) as u32,
weights: dist_weights[j].clone(),
measure,
});
}
let plan = Plan {
groups: plan_groups
.into_iter()
.filter(|g| !g.vars.is_empty() || !g.weighted.is_empty())
.collect(),
randc: stage_randc_dense,
};
let solver =
Engine::new(&var_tys, &states, &plain).map_err(|e| outcome_of(e, &null_state))?;
let mut tries = 0;
let drawn = loop {
let mut work: Vec<RandcState> =
stage_randc.iter().map(|&k| randc_work[k].clone()).collect();
let drawn = solver
.sample_plan(&plan, &mut work, seed)
.map_err(|e| outcome_of(e, &null_state))?;
if checks_hold(&checks, &drawn, &program.funcs, &states, &null_state)? {
for (&k, st) in stage_randc.iter().zip(work) {
randc_work[k] = st;
}
break drawn;
}
tries += 1;
if tries >= MAX_CHECK_TRIES {
return Err(Outcome::FunctionSearch);
}
};
for v in 0..n_real {
if in_stage(v as u32) {
values[v] = drawn[v].clone();
known[v] = Some(drawn[v].clone());
}
}
}
let real_writes = run_real(
program,
call.h,
call.rand_mode,
call.statics,
&states,
&null_state,
Some((&call.var_new, &values)),
seed,
)
.inspect_err(|o| call.real_unsat.set(*o == Outcome::Unsatisfiable))?;
Ok(Solution {
values,
randc_keys,
randc_work,
real_writes,
})
}
type Solved = (Solution, Vec<(u32, usize)>, Vec<usize>);
fn solve_all(call: &Call<'_>, conjuncts: &[Expr], seed: &mut u64) -> Result<Solved, Outcome> {
let n_active = call.var_tys.len() as u32;
let mut blocked: Vec<Vec<usize>> = Vec::new();
loop {
let chosen = array::solve_sizes(
conjuncts,
&call.arrays,
&call.var_tys,
&call.states,
&blocked,
seed,
)
.map_err(|e| outcome_of_size(e, &call.null_state))?;
let x = array::expand_with(
conjuncts,
&call.arrays,
&chosen.sizes,
n_active,
&call.states,
&call.program.funcs,
&call.program.dists,
)
.map_err(|e| outcome_of_expand(e, &call.null_state))?;
match solve_expanded(call, &x, seed) {
Ok(s) => return Ok((s, x.elem_vars, chosen.sizes)),
Err(Outcome::Unsatisfiable) if !chosen.sized.is_empty() => {
blocked.push(chosen.tuple);
if blocked.len() >= array::MAX_SIZE_RETRIES {
return Err(Outcome::ArraySizeSearch);
}
}
Err(o) => return Err(o),
}
}
}
fn run(program: &Program, h: u64, rand_mode: &[bool], statics: &[(u32, u64)]) -> Outcome {
let mut off: Vec<&ProgBlock> = Vec::new();
for b in &program.blocks {
let Ok(o) = owner(b.home, h, statics) else {
continue;
};
match objects::constraint_mode_of(o, b.ordinal) {
Some(true) => {}
Some(false) => off.push(b),
None => return Outcome::DeadHandle,
}
}
if off.is_empty() {
return run_blocks_on(program, h, rand_mode, statics);
}
match reduce::without_blocks(program, &off) {
Ok(p) => run_blocks_on(&p, h, rand_mode, statics),
Err(o) => o,
}
}
fn run_blocks_on(program: &Program, h: u64, rand_mode: &[bool], statics: &[(u32, u64)]) -> Outcome {
let Some(seed0) = objects::prng_seed(h) else {
return Outcome::DeadHandle;
};
let mut var_owner = Vec::with_capacity(program.vars.len());
for v in &program.vars {
match owner(v.home, h, statics) {
Ok(o) => var_owner.push(o),
Err(o) => return o,
}
}
let mut state_owner = Vec::with_capacity(program.states.len());
for s in &program.states {
match owner(s.home, h, statics) {
Ok(o) => state_owner.push(o),
Err(o) => return o,
}
}
let mut array_owner = Vec::with_capacity(program.arrays.len());
for a in &program.arrays {
match owner(a.home, h, statics) {
Ok(o) => array_owner.push(o),
Err(o) => return o,
}
}
let active = |v: &ProgVar| match v.prop_index {
Some(i) => rand_mode.get(i).copied(),
None => Some(true),
};
let mut var_new: Vec<Option<u32>> = Vec::with_capacity(program.vars.len());
let mut var_tys: Vec<Ty> = Vec::new();
let mut active_ids: Vec<usize> = Vec::new();
let mut inactive_state: Vec<Option<u32>> = Vec::with_capacity(program.vars.len());
let mut inactive_ids: Vec<usize> = Vec::new();
for (i, v) in program.vars.iter().enumerate() {
match active(v) {
None => return Outcome::Failed,
Some(true) => {
var_new.push(Some(var_tys.len() as u32));
inactive_state.push(None);
var_tys.push(v.ty);
active_ids.push(i);
}
Some(false) => {
var_new.push(None);
inactive_state.push(Some((program.states.len() + inactive_ids.len()) as u32));
inactive_ids.push(i);
}
}
}
let mut states: Vec<Option<BigUint>> = Vec::with_capacity(program.states.len());
let mut null_state: Vec<bool> = Vec::with_capacity(program.states.len());
for (s, &o) in program.states.iter().zip(&state_owner) {
match read_state(o, s) {
Ok(Reading::Value(v)) => {
states.push(v);
null_state.push(false);
}
Ok(Reading::NullHandle) => {
states.push(None);
null_state.push(true);
}
Err(o) => return o,
}
}
for &i in &inactive_ids {
let v = &program.vars[i];
match read(var_owner[i], v.slot, v.window, v.ty) {
Ok(x) => states.push(x),
Err(o) => return o,
}
null_state.push(false);
}
let mut arrays = Vec::with_capacity(program.arrays.len());
for (a, &o) in program.arrays.iter().zip(&array_owner) {
let on = a.rand
&& match a.prop_index {
Some(i) => match rand_mode.get(i) {
Some(&b) => b,
None => return Outcome::Failed,
},
None => true,
};
match read_array(a, o, on) {
Ok(s) => arrays.push(s),
Err(o) => return o,
}
}
let mut dropped = vec![false; program.conjuncts.len()];
for &i in &inactive_ids {
if let Some(d) = program.vars[i].domain_conjunct {
match dropped.get_mut(d) {
Some(slot) => *slot = true,
None => return Outcome::Failed,
}
}
}
let call = Call {
program,
h,
rand_mode,
statics,
var_owner,
var_new,
var_tys,
active_ids,
arrays,
array_owner,
states,
null_state,
real_unsat: std::cell::Cell::new(false),
};
let has_softs = !program.softs.is_empty();
let prepare = |discard: &[bool]| -> Result<Vec<Expr>, Outcome> {
let mut out = Vec::with_capacity(program.conjuncts.len());
for (i, c) in program.conjuncts.iter().enumerate() {
if dropped[i] {
continue;
}
let resolved;
let c = if has_softs {
resolved = priority::resolve_softs(c, discard).map_err(|_| Outcome::Failed)?;
&resolved
} else {
c
};
let c = c.substitute_vars(&mut |v, t| {
if v >= SELECTOR_BASE {
return Expr::Var(v, t);
}
match (call.var_new[v as usize], inactive_state[v as usize]) {
(Some(n), _) => Expr::Var(n, t),
(None, Some(s)) => Expr::State(s, t),
(None, None) => unreachable!("every variable is active or inactive"),
}
});
out.push(if program.funcs.is_empty() {
c
} else {
func::fold_ground_calls(&c, &program.funcs, &call.states)
.map_err(|e| outcome_of_func(e, &call.null_state))?
});
}
Ok(out)
};
let mut seed = seed0;
let solved = if has_softs {
priority::solve_relaxing(
&program.softs,
&mut seed,
|discard, seed| solve_all(&call, &prepare(discard)?, seed),
|o| matches!(o, Outcome::Unsatisfiable | Outcome::ArraySizeSearch),
)
} else {
prepare(&[]).and_then(|c| solve_all(&call, &c, &mut seed))
};
let (solution, elem_vars, sizes) = match solved {
Ok(s) => s,
Err(o) => return o,
};
let mut writes = solution.real_writes;
writes.reserve(call.active_ids.len());
for (k, &i) in call.active_ids.iter().enumerate() {
let v = &program.vars[i];
match Write::new(call.var_owner[i], v.slot, v.window, &solution.values[k]) {
Some(w) => writes.push(w),
None => return Outcome::Failed,
}
}
let mut new_arrays: Vec<(usize, Vec<u64>)> = Vec::new();
for (ai, a) in call.arrays.iter().enumerate() {
if !a.active {
continue;
}
let ew = program.arrays[ai].elem_words;
let mut flat = Vec::with_capacity(sizes[ai] * ew);
for (e, &(arr, _)) in elem_vars.iter().enumerate() {
if arr as usize != ai {
continue;
}
match pack(&solution.values[call.active_ids.len() + e], ew) {
Some(w) => flat.extend(w),
None => return Outcome::Failed,
}
}
new_arrays.push((ai, flat));
}
for w in writes {
if !w.apply() {
return Outcome::DeadHandle;
}
}
for (ai, flat) in new_arrays {
let pa = &program.arrays[ai];
let o = call.array_owner[ai];
let mut old = vec![0u64; pa.slot.words];
if !objects::field_get(o, pa.slot, &mut old) {
return Outcome::DeadHandle;
}
let new = if pa.assoc {
let (old_h, _) = unpack(&old, 64);
match collections::map_with_values(old_h.to_u64().unwrap_or(0), pa.elem_words, &flat) {
Some(h) => h,
None => return Outcome::Failed,
}
} else {
let shape = collections::Shape {
elem_words: pa.elem_words,
assoc: false,
key: collections::KeyKind::Int {
width: 32,
signed: false,
},
obj: false,
};
collections::from_words(&shape, &flat)
};
let Some(w) = pack(&BigUint::from(new), pa.slot.words) else {
collections::release(new);
return Outcome::Failed;
};
if !objects::field_set(o, pa.slot, &w) {
collections::release(new);
return Outcome::DeadHandle;
}
let (old, _) = unpack(&old, 64);
collections::release(old.to_u64().unwrap_or(0));
}
for ((o, offset), st) in solution.randc_keys.into_iter().zip(solution.randc_work) {
if !objects::set_randc_states(o, vec![(offset, st)]) {
return Outcome::DeadHandle;
}
}
objects::set_prng_seed(h, seed);
Outcome::Solved
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Capture {
pub state: u32,
pub width: u32,
}
impl Capture {
pub fn words(&self) -> usize {
(2 * self.width as usize).div_ceil(64).max(1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Target {
pub home: Home,
pub slot: FieldSlot,
pub window: Option<Window>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Spec {
pub frag: Program,
pub captures: Vec<Capture>,
pub only: Option<Vec<Target>>,
}
impl Spec {
pub fn capture_words(&self) -> usize {
self.captures.iter().map(Capture::words).sum()
}
pub fn scope_words(&self) -> usize {
let ints = self.frag.vars.iter().map(|v| v.slot.offset + v.slot.words);
let reals = self
.frag
.real_vars
.iter()
.map(|v| v.slot.offset + v.slot.words);
let arrays = self
.frag
.arrays
.iter()
.map(|v| v.slot.offset + v.slot.words);
ints.chain(reals).chain(arrays).max().unwrap_or(0)
}
}
type SpecTable = HashMap<u32, Arc<Spec>>;
fn specs() -> &'static Mutex<SpecTable> {
static T: OnceLock<Mutex<SpecTable>> = OnceLock::new();
T.get_or_init(|| Mutex::new(HashMap::new()))
}
static NEXT_SPEC: AtomicU32 = AtomicU32::new(1);
pub fn register_spec(spec: Spec) -> u32 {
let id = NEXT_SPEC.fetch_add(1, Ordering::Relaxed);
specs()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, Arc::new(spec));
id
}
pub(super) fn register_spec_at(id: u32, spec: Spec) {
NEXT_SPEC.fetch_max(id + 1, Ordering::Relaxed);
specs()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, Arc::new(spec));
}
pub(super) fn all_specs() -> Vec<(u32, Arc<Spec>)> {
let mut v: Vec<_> = specs()
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.map(|(&id, s)| (id, Arc::clone(s)))
.collect();
v.sort_by_key(|(id, _)| *id);
v
}
pub fn spec(id: u32) -> Option<Arc<Spec>> {
specs()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&id)
.cloned()
}
pub fn spec_count() -> usize {
specs().lock().unwrap_or_else(|e| e.into_inner()).len()
}
static OBJECT_SITES: AtomicU32 = AtomicU32::new(0);
pub fn note_object_site() {
OBJECT_SITES.fetch_add(1, Ordering::Relaxed);
}
pub fn object_site_count() -> u32 {
OBJECT_SITES.load(Ordering::Relaxed)
}
pub fn spec_capture_words(id: u32) -> usize {
spec(id).map_or(0, |s| s.capture_words())
}
fn capture_consts(spec: &Spec, words: &[u64]) -> Result<Vec<Option<BigUint>>, Outcome> {
let mut consts: Vec<Option<BigUint>> = vec![None; spec.frag.states.len()];
let mut at = 0;
for c in &spec.captures {
let n = c.words();
let chunk = words.get(at..at + n).ok_or(Outcome::Failed)?;
at += n;
let (val, unk) = unpack(chunk, c.width);
if !unk.is_zero() {
return Err(Outcome::StateUnknown);
}
*consts.get_mut(c.state as usize).ok_or(Outcome::Failed)? = Some(val);
}
Ok(consts)
}
pub fn set_all_constraint_modes(h: u64, on: bool) -> Outcome {
let Some(template) = objects::dynamic_template(h) else {
return Outcome::DeadHandle;
};
let Some(entry) = program(template) else {
return Outcome::NoProgram;
};
let Ok(prog) = entry.as_ref() else {
return Outcome::NoProgram;
};
for b in prog.blocks.iter().filter(|b| b.home == Home::Instance) {
if !objects::set_constraint_mode_of(h, b.ordinal, on) {
return Outcome::DeadHandle;
}
}
Outcome::Solved
}
pub fn randomize(h: u64, statics: &[(u32, u64)]) -> Outcome {
randomize_spec(h, statics, 0, &[])
}
pub fn randomize_spec(h: u64, statics: &[(u32, u64)], spec_id: u32, captures: &[u64]) -> Outcome {
let Some(template) = objects::dynamic_template(h) else {
return Outcome::DeadHandle;
};
let Some(base) = program(template) else {
return Outcome::NoProgram;
};
let Ok(base_prog) = base.as_ref() else {
return Outcome::NoProgram;
};
let Some(meta) = objects::rand_meta(h) else {
return Outcome::DeadHandle;
};
let mut rand_mode = meta.rand_mode;
let entry = if spec_id == 0 {
base.clone()
} else {
let Some(spec) = spec(spec_id) else {
return Outcome::Failed;
};
let consts = match capture_consts(&spec, captures) {
Ok(c) => c,
Err(o) => return o,
};
let mut p = match joint::overlay(base_prog, &spec.frag, &consts) {
Ok(p) => p,
Err(o) => return o,
};
if let Some(only) = &spec.only
&& let Err(o) = joint::restrict(&mut p, &mut rand_mode, only)
{
return o;
}
Arc::new(Ok(p))
};
let Ok(prog) = entry.as_ref() else {
return Outcome::NoProgram;
};
let entry = match joint::apply_leaf_modes(prog, &mut rand_mode, &meta.leaf_off, &meta.leaf_on) {
Some(p) => Arc::new(Ok(p)),
None => entry,
};
let Ok(prog) = entry.as_ref() else {
return Outcome::NoProgram;
};
let entry = match joint::apply_static_modes(prog, &mut rand_mode, statics) {
Some(p) => Arc::new(Ok(p)),
None => entry,
};
let Ok(prog) = entry.as_ref() else {
return Outcome::NoProgram;
};
let closure = match joint::closure(h, &entry, &rand_mode, statics) {
Ok(c) => c,
Err(o) => return o,
};
if closure.members.len() == 1 {
match joint::self_alias(&closure, prog, statics) {
Ok(joint::SelfAlias::None) => return run(prog, h, &rand_mode, statics),
Ok(joint::SelfAlias::Integral) => {}
Err(o) => return o,
}
}
match joint::merge(&closure, statics) {
Ok(m) => run(&m.program, h, &m.rand_mode, statics),
Err(o) => o,
}
}
const SCOPE_TEMPLATE: ClassTemplateId = ClassTemplateId(u32::MAX - 1);
pub fn randomize_scope(spec_id: u32, captures: &[u64], out: &mut [u64], seed: u64) -> Outcome {
let Some(spec) = spec(spec_id) else {
return Outcome::Failed;
};
if out.len() != spec.scope_words() || captures.len() != spec.capture_words() {
return Outcome::Failed;
}
let consts = match capture_consts(&spec, captures) {
Ok(c) => c,
Err(o) => return o,
};
let prog = match joint::overlay(&Program::default(), &spec.frag, &consts) {
Ok(p) => p,
Err(o) => return o,
};
let h = objects::alloc(SCOPE_TEMPLATE, out.len());
objects::set_prng_seed(h, seed);
let slot = FieldSlot {
offset: 0,
words: out.len(),
};
if !out.is_empty() && !objects::field_set(h, slot, out) {
objects::release(h);
return Outcome::Failed;
}
let outcome = run(&prog, h, &[], &[]);
if outcome == Outcome::Solved && !out.is_empty() && !objects::field_get(h, slot, out) {
objects::release(h);
return Outcome::Failed;
}
objects::release(h);
outcome
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraints::ir::BinOp;
use crate::objects::{RandKind, RandProp, alloc, register_rand_props, release};
pub(super) fn u(w: u32) -> Ty {
Ty::unsigned(w)
}
pub(super) fn var(i: u32, t: Ty) -> Expr {
Expr::Var(i, t)
}
pub(super) fn k(t: Ty, v: u64) -> Expr {
Expr::constant(t, v)
}
pub(super) fn bin(op: BinOp, a: Expr, b: Expr, t: Ty) -> Expr {
Expr::Binary(op, Box::new(a), Box::new(b), t)
}
fn randomize(h: u64) -> Outcome {
super::randomize(h, &[])
}
pub(super) fn setup(id: u32, n: usize, conjuncts: Vec<Expr>) -> (ClassTemplateId, u64) {
setup_with_softs(id, n, conjuncts, Vec::new())
}
fn setup_with_softs(
id: u32,
n: usize,
conjuncts: Vec<Expr>,
softs: Vec<Soft>,
) -> (ClassTemplateId, u64) {
let t = ClassTemplateId(id);
let vars = (0..n)
.map(|i| ProgVar {
ty: u(8),
slot: FieldSlot {
offset: i,
words: 1,
},
home: Home::Instance,
prop_index: Some(i),
domain_conjunct: None,
randc: false,
window: None,
})
.collect();
register_rand_props(
t,
(0..n)
.map(|i| RandProp {
slot: FieldSlot {
offset: i,
words: 1,
},
kind: RandKind::Rand,
})
.collect(),
);
register_program(
t,
Ok(Program {
vars,
states: vec![ProgState {
class_of: false,
root_bit: 0,
ty: u(8),
slot: FieldSlot {
offset: n,
words: 1,
},
window: None,
home: Home::Instance,
via: Vec::new(),
}],
conjuncts,
before: Vec::new(),
dists: Vec::new(),
softs,
..Program::default()
}),
);
let h = alloc(t, n + 1);
objects::set_prng_seed(h, 0x1234_5678);
(t, h)
}
fn soft_on_x0(seq: u32) -> Soft {
Soft {
priority: priority::Priority {
object: 0,
level: 0,
seq,
},
dists: Vec::new(),
keys: vec![priority::SoftKey::Var(0)],
disabled: false,
disabled_by: Vec::new(),
}
}
#[test]
fn soft_constraints_hold_unless_a_hard_or_higher_soft_conflicts() {
let eq = |v: u64| bin(BinOp::Eq, var(0, u(8)), k(u(8), v), Ty::BIT);
let c = vec![priority::wrap_soft(0, eq(5)), priority::wrap_soft(1, eq(6))];
let (_, h) = setup_with_softs(0x7000_0f01, 1, c, vec![soft_on_x0(0), soft_on_x0(1)]);
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 6);
}
release(h);
let c = vec![
bin(BinOp::Ge, var(0, u(8)), k(u(8), 1), Ty::BIT),
bin(BinOp::Le, var(0, u(8)), k(u(8), 3), Ty::BIT),
priority::wrap_soft(0, eq(5)),
priority::wrap_soft(1, eq(6)),
];
let (_, h) = setup_with_softs(0x7000_0f02, 1, c, vec![soft_on_x0(0), soft_on_x0(1)]);
let mut seen = std::collections::BTreeSet::new();
for _ in 0..100 {
assert_eq!(randomize(h), Outcome::Solved);
seen.insert(get(h, 0));
}
assert_eq!(seen.len(), 3, "{seen:?}");
release(h);
let c = vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 5), Ty::BIT),
bin(BinOp::Lt, var(0, u(8)), k(u(8), 3), Ty::BIT),
priority::wrap_soft(0, eq(4)),
];
let (_, h) = setup_with_softs(0x7000_0f03, 1, c, vec![soft_on_x0(0)]);
set(h, 0, 0x42);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert_eq!(get(h, 0), 0x42, "a failed randomize writes nothing");
release(h);
}
pub(super) fn get(h: u64, off: usize) -> u64 {
let mut b = [0u64];
assert!(objects::field_get(
h,
FieldSlot {
offset: off,
words: 1
},
&mut b
));
b[0]
}
fn set(h: u64, off: usize, v: u64) {
assert!(objects::field_set(
h,
FieldSlot {
offset: off,
words: 1
},
&[v]
));
}
#[test]
fn solves_writes_two_state_values_and_advances_the_seed() {
let c = vec![
bin(BinOp::Ge, var(0, u(8)), k(u(8), 10), Ty::BIT),
bin(BinOp::Le, var(0, u(8)), k(u(8), 12), Ty::BIT),
];
let (_, h) = setup(0x7000_0001, 1, c);
let before = objects::prng_seed(h);
let mut seen = std::collections::BTreeSet::new();
for _ in 0..200 {
assert_eq!(randomize(h), Outcome::Solved);
let v = get(h, 0);
assert!((10..=12).contains(&v), "{v}");
seen.insert(v);
}
assert_eq!(seen.len(), 3, "all three legal values are reachable");
assert_ne!(objects::prng_seed(h), before);
release(h);
}
#[test]
fn failure_writes_nothing_and_consumes_no_draws() {
let c = vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 5), Ty::BIT),
bin(BinOp::Lt, var(0, u(8)), k(u(8), 3), Ty::BIT),
];
let (_, h) = setup(0x7000_0002, 2, c);
set(h, 0, 0x77);
set(h, 1, 0x88);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert_eq!((get(h, 0), get(h, 1)), (0x77, 0x88), "values unchanged");
assert_eq!(objects::prng_seed(h), seed, "stream not perturbed");
release(h);
}
#[test]
fn determinism_by_seed() {
let (_, a) = setup(0x7000_0003, 2, vec![]);
let b = alloc(ClassTemplateId(0x7000_0003), 3);
objects::set_prng_seed(b, 0x1234_5678);
for _ in 0..5 {
assert_eq!(randomize(a), Outcome::Solved);
assert_eq!(randomize(b), Outcome::Solved);
assert_eq!((get(a, 0), get(a, 1)), (get(b, 0), get(b, 1)));
}
objects::set_prng_seed(b, 99);
let mut diff = false;
for _ in 0..8 {
randomize(a);
randomize(b);
diff |= get(a, 0) != get(b, 0);
}
assert!(diff, "different seeds give different streams");
release(a);
release(b);
}
#[test]
fn rand_mode_off_variable_is_state_not_solved() {
let c = vec![bin(
BinOp::Eq,
bin(BinOp::Add, var(0, u(8)), var(1, u(8)), u(8)),
k(u(8), 100),
Ty::BIT,
)];
let (_, h) = setup(0x7000_0004, 2, c);
set(h, 0, 30);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!((get(h, 0), get(h, 1)), (30, 70));
}
let c2 = vec![bin(BinOp::Lt, var(0, u(8)), k(u(8), 10), Ty::BIT)];
let (_, h2) = setup(0x7000_0005, 1, c2);
set(h2, 0, 200);
let mut m = objects::rand_meta(h2).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h2, m);
assert_eq!(randomize(h2), Outcome::Unsatisfiable, "acts as a checker");
assert_eq!(get(h2, 0), 200);
release(h);
release(h2);
}
#[test]
fn all_variables_inactive_checks_the_constraints() {
let c = vec![bin(BinOp::Eq, var(0, u(8)), k(u(8), 5), Ty::BIT)];
let (_, h) = setup(0x7000_0006, 1, c);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
set(h, 0, 5);
assert_eq!(randomize(h), Outcome::Solved);
set(h, 0, 6);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
release(h);
}
fn windowed(id: u32, randc_hi: bool) -> (ClassTemplateId, u64) {
let t = ClassTemplateId(id);
let slot = FieldSlot {
offset: 0,
words: 1,
};
register_rand_props(
t,
vec![RandProp {
slot,
kind: RandKind::Rand,
}],
);
let leaf = |bit| {
Some(Window {
bit,
width: 8,
field_width: 24,
})
};
let c = vec![
bin(BinOp::Ge, var(0, u(8)), k(u(8), 10), Ty::BIT),
bin(BinOp::Le, var(0, u(8)), k(u(8), 12), Ty::BIT),
bin(
BinOp::Eq,
var(1, u(8)),
bin(BinOp::Add, var(0, u(8)), k(u(8), 1), u(8)),
Ty::BIT,
),
];
let mk = |bit, randc| ProgVar {
ty: u(8),
slot,
home: Home::Instance,
prop_index: Some(0),
domain_conjunct: None,
randc,
window: leaf(bit),
};
register_program(
t,
Ok(Program {
vars: vec![mk(0, false), mk(16, randc_hi)],
conjuncts: c,
..Program::default()
}),
);
let h = alloc(t, 2);
set(h, 0, (0x5au64 << 8) | (0xffu64 << (24 + 8)));
set(h, 1, 0xdead);
(t, h)
}
#[test]
fn leaf_windows_of_one_property_are_solved_and_written_in_place() {
let (_, h) = windowed(0x7000_0f10, false);
let mut seen = std::collections::BTreeSet::new();
for _ in 0..60 {
assert_eq!(randomize(h), Outcome::Solved);
let w = get(h, 0);
let (lo, mid, hi) = (w & 0xff, (w >> 8) & 0xff, (w >> 16) & 0xff);
assert!((10..=12).contains(&lo), "{lo}");
assert_eq!(hi, lo + 1);
assert_eq!(mid, 0x5a, "the neighbouring leaf keeps its value");
assert_eq!(w >> 24, 0xff << 8, "the neighbour's unknown bits survive");
assert_eq!(get(h, 1), 0xdead, "the next property is untouched");
seen.insert(lo);
}
assert_eq!(seen.len(), 3, "all three legal values are reachable");
release(h);
}
#[test]
fn an_inactive_leaf_is_read_through_its_window() {
let (_, h) = windowed(0x7000_0f11, false);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
set(h, 0, 11 | (0x5 << 8) | (12 << 16));
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 11 | (0x5 << 8) | (12 << 16));
set(h, 0, 11 | (13 << 16));
assert_eq!(randomize(h), Outcome::Unsatisfiable);
set(h, 0, 11 | (12 << 16) | (1u64 << (24 + 16)));
assert_eq!(randomize(h), Outcome::StateUnknown);
set(h, 0, 11 | (12 << 16) | (1u64 << (24 + 8)));
assert_eq!(randomize(h), Outcome::Solved);
release(h);
}
#[test]
fn randc_cycles_of_two_leaves_of_one_property_are_independent() {
let (_, h) = windowed(0x7000_0f12, true);
assert_eq!(randomize(h), Outcome::Solved);
let hi_key = randc_key(
FieldSlot {
offset: 0,
words: 1,
},
Some(Window {
bit: 16,
width: 8,
field_width: 24,
}),
);
assert_ne!(hi_key, 0, "a leaf's key differs from its property's");
assert!(objects::randc_state(h, hi_key).unwrap().live);
assert!(!objects::randc_state(h, 0).unwrap().live);
let mut cycle = vec![get(h, 0) >> 16 & 0xff];
for _ in 0..2 {
assert_eq!(randomize(h), Outcome::Solved);
cycle.push(get(h, 0) >> 16 & 0xff);
}
cycle.sort_unstable();
assert_eq!(cycle, vec![11, 12, 13]);
release(h);
}
#[test]
fn enum_domain_dropped_only_while_inactive() {
let dom = bin(
BinOp::LogOr,
bin(BinOp::Eq, var(0, u(8)), k(u(8), 1), Ty::BIT),
bin(BinOp::Eq, var(0, u(8)), k(u(8), 3), Ty::BIT),
Ty::BIT,
);
let (t, h) = setup(0x7000_0007, 1, vec![dom]);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.vars[0].domain_conjunct = Some(0);
register_program(t, Ok(p));
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert!(matches!(get(h, 0), 1 | 3));
}
set(h, 0, 2);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 2);
release(h);
}
#[test]
fn x_state_is_a_loud_error_and_changes_nothing() {
let c = vec![bin(BinOp::Eq, var(0, u(8)), Expr::State(0, u(8)), Ty::BIT)];
let (_, h) = setup(0x7000_0008, 1, c);
set(h, 0, 9);
set(h, 1, 1 << 8);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::StateUnknown);
assert_eq!(get(h, 0), 9);
assert_eq!(objects::prng_seed(h), seed);
set(h, 1, 0x42);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 0x42);
release(h);
}
#[test]
fn unreferenced_x_state_is_not_an_error() {
let (_, h) = setup(0x7000_0009, 1, vec![]);
set(h, 1, 1 << 8);
assert_eq!(randomize(h), Outcome::Solved);
release(h);
}
#[test]
fn dead_handle_missing_program_and_failed_program() {
assert_eq!(randomize(0), Outcome::DeadHandle);
let t = ClassTemplateId(0x7000_000a);
let h = alloc(t, 1);
assert_eq!(randomize(h), Outcome::NoProgram, "nothing registered");
register_program(t, Err("unsupported thing".into()));
assert_eq!(randomize(h), Outcome::NoProgram, "build failure");
assert_eq!(
program(t).unwrap().as_ref().clone(),
Err("unsupported thing".to_string())
);
release(h);
assert_eq!(randomize(h), Outcome::DeadHandle, "stale handle");
}
#[test]
fn four_state_field_round_trip_packs_value_low_unknown_high() {
let (v, x) = unpack(&[0xde_adbe_ef12, 0], 40);
assert_eq!(v, BigUint::from(0xde_adbe_ef12u64));
assert!(x.is_zero());
let (_, x) = unpack(&[0, 1 << 8], 40);
assert!(!x.is_zero(), "bit 72 of the packed value is unknown bit 32");
assert_eq!(pack(&BigUint::from(7u8), 2), Some(vec![7, 0]));
assert_eq!(pack(&(BigUint::from(1u8) << 70), 1), None);
}
fn skewed(id: u32, before: Vec<(u32, u32)>) -> (ClassTemplateId, u64) {
let c = vec![
bin(BinOp::Lt, var(0, u(8)), k(u(8), 2), Ty::BIT),
bin(
BinOp::LogImpl,
bin(BinOp::Eq, var(0, u(8)), k(u(8), 1), Ty::BIT),
bin(BinOp::Eq, var(1, u(8)), k(u(8), 0), Ty::BIT),
Ty::BIT,
),
];
let (t, h) = setup(id, 2, c);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.before = before;
register_program(t, Ok(p));
(t, h)
}
fn count_x0_is_one(h: u64, n: u32) -> u32 {
let mut ones = 0;
for _ in 0..n {
assert_eq!(randomize(h), Outcome::Solved);
ones += u32::from(get(h, 0) == 1);
}
ones
}
#[test]
fn solve_before_orders_the_solve_but_not_the_solution_space() {
let (_, h) = skewed(0x7000_0020, vec![]);
let ones = count_x0_is_one(h, 3000);
assert!(ones < 40, "unordered: {ones}/3000");
let (_, h) = skewed(0x7000_0021, vec![(0, 1)]);
let ones = count_x0_is_one(h, 3000);
assert!((1350..1650).contains(&ones), "x0 before x1: {ones}/3000");
let (_, h) = skewed(0x7000_0022, vec![(1, 0)]);
let ones = count_x0_is_one(h, 3000);
assert!(ones < 40, "x1 before x0: {ones}/3000");
}
#[test]
fn an_edge_through_an_inactive_variable_is_ignored() {
let (_, h) = skewed(0x7000_0023, vec![(0, 1)]);
set(h, 0, 1);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(
(get(h, 0), get(h, 1)),
(1, 0),
"x0 is state, x1 forced to 0"
);
}
}
#[test]
fn a_circular_or_out_of_range_order_is_a_failed_program_not_a_guess() {
let (_, h) = skewed(0x7000_0024, vec![(0, 1), (1, 0)]);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::Failed);
assert_eq!(objects::prng_seed(h), seed);
let (_, h) = skewed(0x7000_0025, vec![(0, 9)]);
assert_eq!(randomize(h), Outcome::Failed);
}
#[test]
fn outcome_codes_round_trip_and_every_error_has_a_message() {
for c in 0..=20u64 {
let o = Outcome::from_code(c).unwrap();
assert_eq!(o.code(), c);
assert_eq!(o.error_message().is_some(), c >= 2, "{o:?}");
}
assert_eq!(Outcome::from_code(21), None);
}
#[test]
fn every_sat_fallback_failure_has_its_own_outcome() {
use crate::constraints::bdd::BddBuildError;
let none: &[bool] = &[];
for (e, want) in [
(
SolveError::Build(BddBuildError::TooLarge {
budget: 1,
during: "x",
}),
Outcome::TooLarge,
),
(
SolveError::Build(BddBuildError::CnfTooLarge {
limit: 1,
during: "x",
}),
Outcome::TooLarge,
),
(SolveError::SatTimeout, Outcome::SatTimeout),
(
SolveError::SatUnsupported { what: "x" },
Outcome::SatUnsupported,
),
(SolveError::SatModelRejected, Outcome::Failed),
(SolveError::SatSolverFailure { what: "x" }, Outcome::Failed),
] {
assert_eq!(outcome_of(e.clone(), none), want, "{e}");
}
assert!(
Outcome::TooLarge
.error_message()
.unwrap()
.contains("SAT fallback")
);
assert!(
Outcome::SatTimeout
.error_message()
.unwrap()
.contains("time limit")
);
assert!(
Outcome::SatUnsupported
.error_message()
.unwrap()
.contains("does not support")
);
}
fn dist_program(id: u32) -> (ClassTemplateId, u64) {
use crate::constraints::dist::{Dist, Item, SELECTOR_BASE, Span};
let x = var(0, u(8));
let item = |v: u64, w: Expr| Item {
member: bin(BinOp::Eq, x.clone(), k(u(8), v), Ty::BIT),
ty: u(8),
span: Span::Point(k(u(8), v)),
weight: w,
per_range: false,
};
let d = Dist {
subject_ty: u(8),
plain_var: Some(0),
vars: vec![0],
items: vec![
item(
0,
Expr::Resize(Box::new(Expr::State(0, u(8))), Ty::signed(8)),
),
item(1, k(Ty::signed(8), 1)),
],
default: None,
subject: None,
};
let (t, h) = setup(id, 1, vec![d.conjunct(SELECTOR_BASE)]);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.dists = vec![d];
register_program(t, Ok(p));
(t, h)
}
#[test]
fn a_dist_weight_is_read_from_the_state_at_each_call() {
let (_, h) = dist_program(0x7000_0040);
set(h, 1, 3);
let mut zeros = 0;
for _ in 0..2000 {
assert_eq!(randomize(h), Outcome::Solved);
let x = get(h, 0);
assert!(x <= 1, "x = {x}");
zeros += u32::from(x == 0);
}
assert!(
(1400..1600).contains(&zeros),
"x == 0: {zeros}/2000 (want 1500)"
);
set(h, 1, 0);
for _ in 0..50 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 1);
}
}
#[test]
fn a_negative_dist_weight_fails_the_call_and_changes_nothing() {
let (_, h) = dist_program(0x7000_0041);
set(h, 1, 3);
assert_eq!(randomize(h), Outcome::Solved);
let (before, seed) = (get(h, 0), objects::prng_seed(h));
set(h, 1, 200); assert_eq!(randomize(h), Outcome::BadDistWeight);
assert_eq!((get(h, 0), objects::prng_seed(h)), (before, seed));
set(h, 1, 1 << 8);
assert_eq!(randomize(h), Outcome::StateUnknown);
}
#[test]
fn a_dist_on_an_inactive_variable_is_a_membership_test_on_its_value() {
let (_, h) = dist_program(0x7000_0042);
set(h, 1, 3);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
set(h, 0, 1);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 1);
set(h, 0, 7);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
set(h, 0, 0);
set(h, 1, 0);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
}
#[test]
fn a_dist_default_spreads_its_weight_over_the_values_no_item_names() {
use crate::constraints::dist::{DefaultWeight, Dist, Item, SELECTOR_BASE, Span};
let x = var(0, u(8));
let d = Dist {
subject_ty: u(8),
plain_var: Some(0),
vars: vec![0],
items: vec![Item {
member: bin(BinOp::Le, x.clone(), k(u(8), 3), Ty::BIT),
ty: u(8),
span: Span::Range {
lo: Some(k(u(8), 0)),
hi: Some(k(u(8), 3)),
},
weight: k(u(32), 8),
per_range: true,
}],
default: Some(DefaultWeight {
weight: k(u(32), 8),
per_range: true,
}),
subject: None,
};
let (t, h) = setup(0x7000_0043, 1, vec![d.conjunct(SELECTOR_BASE)]);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.dists = vec![d];
register_program(t, Ok(p));
let (mut low, mut distinct) = (0u32, std::collections::BTreeSet::new());
for _ in 0..4000 {
assert_eq!(randomize(h), Outcome::Solved);
let v = get(h, 0);
if v <= 3 {
low += 1;
} else {
distinct.insert(v);
}
}
assert!(
(1850..2150).contains(&low),
"x <= 3: {low}/4000 (want 2000)"
);
assert!(
distinct.len() > 100,
"the default values are spread: {}",
distinct.len()
);
}
fn randc_program(id: u32, limit: u64) -> (ClassTemplateId, u64) {
let c = vec![
bin(BinOp::Lt, var(0, u(8)), k(u(8), limit), Ty::BIT),
bin(BinOp::Ge, var(0, u(8)), Expr::State(0, u(8)), Ty::BIT),
bin(BinOp::Gt, var(1, u(8)), var(0, u(8)), Ty::BIT),
];
let (t, h) = setup(id, 2, c);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.vars[0].randc = true;
register_program(t, Ok(p));
(t, h)
}
fn draw_cycle(h: u64, n: usize) -> Vec<u64> {
(0..n)
.map(|_| {
assert_eq!(randomize(h), Outcome::Solved);
get(h, 0)
})
.collect()
}
#[test]
fn randc_visits_every_legal_value_once_per_cycle() {
let (_, h) = randc_program(0x7000_0030, 6);
let mut orders = std::collections::BTreeSet::new();
for _ in 0..40 {
let mut cyc = draw_cycle(h, 6);
orders.insert(cyc.clone());
cyc.sort_unstable();
assert_eq!(cyc, vec![0, 1, 2, 3, 4, 5]);
}
assert!(orders.len() > 10, "cycles are shuffled, not a fixed order");
release(h);
}
#[test]
fn randc_state_is_committed_only_when_the_call_succeeds() {
let (_, h) = randc_program(0x7000_0031, 4);
let first = draw_cycle(h, 2);
let saved = objects::randc_state(h, 0).unwrap();
set(h, 2, 200);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert_eq!(objects::randc_state(h, 0).unwrap(), saved);
assert_eq!(objects::prng_seed(h), seed);
set(h, 2, 0);
let rest = draw_cycle(h, 2);
let mut all = [first, rest].concat();
all.sort_unstable();
assert_eq!(all, vec![0, 1, 2, 3], "the interrupted cycle completes");
release(h);
}
#[test]
fn randc_constraint_change_starts_a_new_cycle() {
let (_, h) = randc_program(0x7000_0032, 8);
draw_cycle(h, 3);
set(h, 2, 6); let next = draw_cycle(h, 2);
let mut sorted = next.clone();
sorted.sort_unstable();
assert_eq!(sorted, vec![6, 7], "{next:?}: a fresh cycle of the new set");
release(h);
}
#[test]
fn randc_interacts_with_rand_without_failing() {
let (_, h) = randc_program(0x7000_0033, 6);
for _ in 0..10 {
let mut cyc = Vec::new();
for _ in 0..6 {
assert_eq!(randomize(h), Outcome::Solved);
assert!(get(h, 1) > get(h, 0));
cyc.push(get(h, 0));
}
cyc.sort_unstable();
assert_eq!(cyc, vec![0, 1, 2, 3, 4, 5]);
}
release(h);
}
#[test]
fn an_inactive_randc_variable_is_state_and_keeps_its_cycle() {
let (_, h) = randc_program(0x7000_0034, 4);
draw_cycle(h, 2);
let saved = objects::randc_state(h, 0).unwrap();
set(h, 0, 2);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
for _ in 0..5 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 2, "not redrawn");
assert!(get(h, 1) > 2);
}
assert_eq!(objects::randc_state(h, 0).unwrap(), saved);
release(h);
}
fn static_randc(id: u32) -> (u64, u64, u64) {
let (t, a) = randc_program(id, 4);
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.vars[0].home = Home::Static(7);
p.vars[0].prop_index = None;
register_program(t, Ok(p));
let b = alloc(t, 3);
objects::set_prng_seed(b, 0x9999);
let st = alloc(ClassTemplateId(id + 0x100), 1);
(a, b, st)
}
#[test]
fn a_static_randc_cycles_across_all_instances() {
let (a, b, st) = static_randc(0x7000_0035);
let pairs = [(7u32, st)];
for _ in 0..10 {
let mut cyc = Vec::new();
for i in 0..4 {
let h = if i % 2 == 0 { a } else { b };
assert_eq!(super::randomize(h, &pairs), Outcome::Solved);
let mut w = [0u64];
assert!(objects::field_get(
st,
FieldSlot {
offset: 0,
words: 1
},
&mut w
));
cyc.push(w[0]);
assert_eq!(get(h, 0), 0, "the instance's own word is untouched");
}
cyc.sort_unstable();
assert_eq!(cyc, vec![0, 1, 2, 3]);
}
assert_eq!(
objects::randc_state(a, 0).unwrap(),
RandcState::default(),
"the cycle lives in the static object, not the instance"
);
assert_ne!(objects::randc_state(st, 0).unwrap(), RandcState::default());
release(a);
release(b);
release(st);
}
#[test]
fn instance_randc_cycles_are_per_object() {
let (_, a) = randc_program(0x7000_0036, 4);
let b = alloc(ClassTemplateId(0x7000_0036), 3);
objects::set_prng_seed(b, 0x4242);
for _ in 0..6 {
let mut ca = Vec::new();
let mut cb = Vec::new();
for _ in 0..4 {
assert_eq!(randomize(a), Outcome::Solved);
ca.push(get(a, 0));
assert_eq!(randomize(b), Outcome::Solved);
cb.push(get(b, 0));
}
ca.sort_unstable();
cb.sort_unstable();
assert_eq!((ca, cb), (vec![0, 1, 2, 3], vec![0, 1, 2, 3]));
}
release(a);
release(b);
}
#[test]
fn a_missing_static_object_is_a_loud_error_that_changes_nothing() {
let (a, _, st) = static_randc(0x7000_0037);
let seed = objects::prng_seed(a);
assert_eq!(super::randomize(a, &[]), Outcome::StaticUnavailable);
assert_eq!(super::randomize(a, &[(8, st)]), Outcome::StaticUnavailable);
assert_eq!(objects::prng_seed(a), seed);
assert_eq!(super::randomize(a, &[(7, st)]), Outcome::Solved);
release(a);
release(st);
}
const HANDLE_A: usize = 2;
const HANDLE_B: usize = 4;
fn handle_slot(offset: usize) -> FieldSlot {
FieldSlot { offset, words: 2 }
}
fn d_object(id: u32, x: u64) -> u64 {
let d = alloc(ClassTemplateId(id), 1);
set(d, 0, x);
d
}
fn set_handle(h: u64, offset: usize, target: u64) {
assert!(objects::field_set(h, handle_slot(offset), &[target, 0]));
}
fn guard_class(id: u32, conjuncts: Vec<Expr>, a_x: Option<u64>, b_x: Option<u64>) -> u64 {
let t = ClassTemplateId(id);
let rand_slot = |i: usize| FieldSlot {
offset: i,
words: 1,
};
register_rand_props(
t,
(0..2)
.map(|i| RandProp {
slot: rand_slot(i),
kind: RandKind::Rand,
})
.collect(),
);
let state = |root: usize| ProgState {
class_of: false,
root_bit: 0,
ty: u(8),
slot: handle_slot(root),
window: None,
home: Home::Instance,
via: vec![rand_slot(0)],
};
register_program(
t,
Ok(Program {
vars: (0..2)
.map(|i| ProgVar {
ty: u(8),
slot: rand_slot(i),
home: Home::Instance,
prop_index: Some(i),
domain_conjunct: None,
randc: false,
window: None,
})
.collect(),
states: vec![state(HANDLE_A), state(HANDLE_B)],
conjuncts,
before: Vec::new(),
dists: Vec::new(),
..Program::default()
}),
);
let h = alloc(t, 6);
objects::set_prng_seed(h, 0x5eed_0018);
if let Some(x) = a_x {
set_handle(h, HANDLE_A, d_object(id + 0x100, x));
}
if let Some(x) = b_x {
set_handle(h, HANDLE_B, d_object(id + 0x200, x));
}
h
}
fn x() -> Expr {
var(0, u(8))
}
fn y() -> Expr {
var(1, u(8))
}
fn a_x() -> Expr {
Expr::State(0, u(8))
}
fn b_x() -> Expr {
Expr::State(1, u(8))
}
fn lt_xy() -> Expr {
bin(BinOp::Lt, x(), y(), Ty::BIT)
}
fn a_gt_b() -> Expr {
bin(BinOp::Gt, a_x(), b_x(), Ty::BIT)
}
fn a_is_5() -> Expr {
bin(BinOp::Eq, a_x(), k(u(8), 5), Ty::BIT)
}
fn or(l: Expr, r: Expr) -> Expr {
bin(BinOp::LogOr, l, r, Ty::BIT)
}
fn and(l: Expr, r: Expr) -> Expr {
bin(BinOp::LogAnd, l, r, Ty::BIT)
}
fn guard_sum(pred: Expr) -> Vec<Expr> {
vec![Expr::Guard(
Box::new(pred),
Box::new(bin(
BinOp::Eq,
bin(BinOp::Add, x(), y(), u(8)),
k(u(8), 10),
Ty::BIT,
)),
Box::new(k(Ty::BIT, 1)),
Ty::BIT,
)]
}
fn sum_draws(h: u64) -> (u32, u32, bool) {
let (mut sums, mut lts, mut implied) = (0, 0, true);
for _ in 0..400 {
assert_eq!(randomize(h), Outcome::Solved);
let (x, y) = (get(h, 0) as u8, get(h, 1) as u8);
let sum = x.wrapping_add(y) == 10;
sums += u32::from(sum);
lts += u32::from(x < y);
implied &= x >= y || sum;
}
(sums, lts, implied)
}
fn assert_body_unconditional(h: u64) {
let (sums, _, _) = sum_draws(h);
assert_eq!(sums, 400, "an unconditional `x + y == 10`");
}
fn assert_body_conditional(h: u64) {
let (sums, lts, implied) = sum_draws(h);
assert!(implied, "(x < y) -> (x + y == 10) always holds");
assert!(lts < 400 && sums < 400, "not unconditional: {sums}/{lts}");
}
fn assert_eliminated(h: u64) {
let (sums, lts, _) = sum_draws(h);
assert!(sums < 20, "x + y == 10 is a 1/256 accident: {sums}");
assert!((120..280).contains(<s), "x and y are free: {lts}/400");
}
fn assert_error(h: u64) {
let (seed, x0) = (objects::prng_seed(h), get(h, 0));
assert_eq!(randomize(h), Outcome::GuardError);
assert_eq!((objects::prng_seed(h), get(h, 0)), (seed, x0), "no change");
}
#[test]
fn lrm_example_1_or_guard() {
let c = || guard_sum(or(or(lt_xy(), a_gt_b()), a_is_5()));
assert_body_unconditional(guard_class(0x7100_0001, c(), Some(5), None));
assert_error(guard_class(0x7100_0002, c(), None, Some(1)));
assert_body_conditional(guard_class(0x7100_0003, c(), Some(10), Some(20)));
}
#[test]
fn lrm_example_2_and_guard() {
let c = || guard_sum(and(and(lt_xy(), a_gt_b()), a_is_5()));
assert_eliminated(guard_class(0x7100_0011, c(), Some(6), None));
assert_error(guard_class(0x7100_0012, c(), None, Some(1)));
assert_body_conditional(guard_class(0x7100_0013, c(), Some(5), Some(2)));
}
#[test]
fn lrm_example_3_nested_guard() {
let c = || guard_sum(and(lt_xy(), or(a_gt_b(), a_is_5())));
assert_body_conditional(guard_class(0x7100_0021, c(), Some(5), None));
assert_error(guard_class(0x7100_0022, c(), Some(8), None));
assert_error(guard_class(0x7100_0023, c(), None, Some(1)));
assert_body_conditional(guard_class(0x7100_0024, c(), Some(5), Some(2)));
}
#[test]
fn a_false_guard_selects_the_else_arm_and_never_examines_the_other() {
let inner = Expr::Guard(
Box::new(bin(BinOp::Eq, b_x(), k(u(8), 1), Ty::BIT)),
Box::new(bin(BinOp::Eq, x(), k(u(8), 9), Ty::BIT)),
Box::new(k(Ty::BIT, 1)),
Ty::BIT,
);
let c = vec![Expr::Guard(
Box::new(a_is_5()),
Box::new(inner),
Box::new(bin(BinOp::Eq, y(), k(u(8), 3), Ty::BIT)),
Ty::BIT,
)];
let h = guard_class(0x7100_0031, c.clone(), Some(6), None);
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 1), 3);
}
assert_error(guard_class(0x7100_0032, c.clone(), Some(5), None));
let h = guard_class(0x7100_0033, c, Some(5), Some(1));
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 9);
}
#[test]
fn a_null_handle_outside_a_guard_fails_the_call_with_its_own_outcome() {
let c = vec![bin(BinOp::Eq, x(), a_x(), Ty::BIT)];
let h = guard_class(0x7100_0041, c.clone(), None, None);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::NullHandle);
assert_eq!(objects::prng_seed(h), seed);
let h = guard_class(0x7100_0042, c.clone(), Some(77), None);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 77);
let h = guard_class(0x7100_0043, c.clone(), Some(77), None);
let mut buf = [0u64; 2];
assert!(objects::field_get(h, handle_slot(HANDLE_A), &mut buf));
release(buf[0]);
assert_eq!(randomize(h), Outcome::NullHandle);
let h = guard_class(0x7100_0044, c, Some(77), None);
assert!(objects::field_set(h, handle_slot(HANDLE_A), &[1, 1]));
assert_eq!(randomize(h), Outcome::StateUnknown);
}
#[test]
fn an_unknown_value_in_a_guard_is_an_error() {
let h = guard_class(0x7100_0051, guard_sum(a_is_5()), Some(5), None);
let mut buf = [0u64; 2];
assert!(objects::field_get(h, handle_slot(HANDLE_A), &mut buf));
assert!(objects::field_set(
buf[0],
FieldSlot {
offset: 0,
words: 1
},
&[5 | (1 << 8)]
));
assert_error(h);
}
#[test]
fn a_two_hop_handle_chain_reads_the_far_member_and_fails_on_a_null_link() {
let node = |id: u32, v: u64| {
let o = alloc(ClassTemplateId(id), 3);
set(o, 2, v);
o
};
let t = ClassTemplateId(0x7100_0061);
register_rand_props(
t,
vec![RandProp {
slot: FieldSlot {
offset: 0,
words: 1,
},
kind: RandKind::Rand,
}],
);
let root = |offset| ProgState {
class_of: false,
root_bit: 0,
ty: u(8),
slot: handle_slot(offset),
window: None,
home: Home::Instance,
via: vec![
handle_slot(0),
FieldSlot {
offset: 2,
words: 1,
},
],
};
register_program(
t,
Ok(Program {
vars: vec![ProgVar {
ty: u(8),
slot: FieldSlot {
offset: 0,
words: 1,
},
home: Home::Instance,
prop_index: Some(0),
domain_conjunct: None,
randc: false,
window: None,
}],
states: vec![root(1)],
conjuncts: vec![bin(BinOp::Eq, x(), Expr::State(0, u(8)), Ty::BIT)],
before: Vec::new(),
dists: Vec::new(),
..Program::default()
}),
);
let h = alloc(t, 3);
objects::set_prng_seed(h, 1);
let (n0, n1) = (node(0x7100_0062, 1), node(0x7100_0063, 42));
set_handle(h, 1, n0);
set_handle(n0, 0, n1);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(h, 0), 42);
set_handle(n0, 0, 0);
assert_eq!(randomize(h), Outcome::NullHandle);
}
use crate::constraints::real_domain::{
CmpOp, RealCond, RealDist, RealDistItem, RealExpr, RealSpan,
};
fn rslot(offset: usize) -> FieldSlot {
FieldSlot { offset, words: 2 }
}
fn rk(x: f64) -> RealExpr {
RealExpr::Const(real_domain::Real::new(x), FloatKind::F64)
}
fn rcmp(op: CmpOp, a: RealExpr, b: RealExpr) -> RealCond {
RealCond::Cmp(op, a, b)
}
fn rget(h: u64, off: usize) -> f64 {
let mut b = [0u64; 2];
assert!(objects::field_get(h, rslot(off), &mut b));
assert_eq!(b[1], 0, "a solved real has no unknown bits");
f64::from_bits(b[0])
}
fn rset_bits(h: u64, off: usize, bits: u64, unk: u64) {
assert!(objects::field_set(h, rslot(off), &[bits, unk]));
}
fn setup_real(id: u32, real: RealProblem, conjuncts: Vec<Expr>) -> u64 {
let t = ClassTemplateId(id);
register_rand_props(
t,
vec![
RandProp {
slot: rslot(0),
kind: RandKind::Rand,
},
RandProp {
slot: FieldSlot {
offset: 2,
words: 1,
},
kind: RandKind::Rand,
},
],
);
register_program(
t,
Ok(Program {
vars: vec![ProgVar {
ty: u(8),
slot: FieldSlot {
offset: 2,
words: 1,
},
home: Home::Instance,
prop_index: Some(1),
domain_conjunct: None,
randc: false,
window: None,
}],
states: vec![ProgState {
class_of: false,
root_bit: 0,
ty: u(64),
slot: rslot(3),
window: None,
home: Home::Instance,
via: Vec::new(),
}],
conjuncts,
real_vars: vec![ProgRealVar {
kind: FloatKind::F64,
slot: rslot(0),
home: Home::Instance,
prop_index: Some(0),
window: None,
}],
real,
..Program::default()
}),
);
let h = alloc(t, 5);
objects::set_prng_seed(h, 0x5eed_0c09);
h
}
fn unit_interval() -> RealProblem {
RealProblem {
kinds: vec![FloatKind::F64],
conds: vec![
rcmp(CmpOp::Ge, RealExpr::Var(0), rk(1.0)),
rcmp(CmpOp::Lt, RealExpr::Var(0), rk(2.0)),
],
..RealProblem::default()
}
}
#[test]
fn real_variable_is_solved_with_the_integral_ones_and_the_seed_advances() {
let c = vec![bin(BinOp::Eq, var(0, u(8)), k(u(8), 7), Ty::BIT)];
let h = setup_real(0x7000_0c01, unit_interval(), c);
let before = objects::prng_seed(h);
let mut bins = [0u32; 4];
for _ in 0..400 {
assert_eq!(randomize(h), Outcome::Solved);
let r = rget(h, 0);
assert!((1.0..2.0).contains(&r), "{r}");
assert_eq!(get(h, 2), 7);
bins[((r - 1.0) * 4.0) as usize] += 1;
}
assert!(
bins.iter().all(|&n| n > 60),
"uniform over the interval: {bins:?}"
);
assert_ne!(objects::prng_seed(h), before);
release(h);
}
#[test]
fn unsatisfiable_real_constraint_writes_nothing_and_keeps_the_seed() {
let mut p = unit_interval();
p.conds.push(rcmp(CmpOp::Gt, RealExpr::Var(0), rk(5.0)));
let h = setup_real(0x7000_0c02, p, Vec::new());
rset_bits(h, 0, 3.25f64.to_bits(), 0);
set(h, 2, 99);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert_eq!(rget(h, 0), 3.25);
assert_eq!(get(h, 2), 99, "the integral variable is untouched too");
assert_eq!(objects::prng_seed(h), seed);
release(h);
}
#[test]
fn inactive_real_variable_is_a_constant_and_is_not_written() {
let h = setup_real(0x7000_0c03, unit_interval(), Vec::new());
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
rset_bits(h, 0, 1.5f64.to_bits(), 0);
for _ in 0..10 {
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(rget(h, 0), 1.5);
}
rset_bits(h, 0, 2.0f64.to_bits(), 0);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert_eq!(rget(h, 0), 2.0);
rset_bits(h, 0, 0, 1);
assert_eq!(randomize(h), Outcome::StateUnknown);
release(h);
}
#[test]
fn real_state_variable_bounds_the_solution_and_division_by_zero_is_a_loud_error() {
let p = RealProblem {
kinds: vec![FloatKind::F64],
conds: vec![
rcmp(CmpOp::Ge, RealExpr::Var(0), rk(0.0)),
rcmp(
CmpOp::Lt,
RealExpr::Var(0),
RealExpr::State(0, FloatKind::F64),
),
],
..RealProblem::default()
};
let h = setup_real(0x7000_0c04, p, Vec::new());
rset_bits(h, 3, 0.5f64.to_bits(), 0);
for _ in 0..50 {
assert_eq!(randomize(h), Outcome::Solved);
assert!((0.0..0.5).contains(&rget(h, 0)));
}
rset_bits(h, 3, f64::NAN.to_bits(), 0);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
rset_bits(h, 3, 0, 1);
assert_eq!(randomize(h), Outcome::StateUnknown);
release(h);
let p = RealProblem {
kinds: vec![FloatKind::F64],
conds: vec![rcmp(
CmpOp::Lt,
RealExpr::Div(
Box::new(RealExpr::Var(0)),
Box::new(RealExpr::State(0, FloatKind::F64)),
),
rk(1.0),
)],
..RealProblem::default()
};
let h = setup_real(0x7000_0c08, p, Vec::new());
rset_bits(h, 3, 2.0f64.to_bits(), 0);
assert_eq!(randomize(h), Outcome::Solved);
assert!(rget(h, 0) < 2.0);
rset_bits(h, 3, 0.0f64.to_bits(), 0);
assert_eq!(randomize(h), Outcome::RealDomain);
release(h);
}
#[test]
fn real_dist_follows_its_weights() {
let p = RealProblem {
kinds: vec![FloatKind::F64],
dists: vec![RealDist {
var: 0,
items: vec![
RealDistItem {
span: RealSpan::Point(rk(1.0)),
weight: k(u(32), 3),
},
RealDistItem {
span: RealSpan::Range {
lo: Some(rk(10.0)),
hi: Some(rk(20.0)),
},
weight: k(u(32), 1),
},
],
default: None,
}],
..RealProblem::default()
};
let h = setup_real(0x7000_0c05, p, Vec::new());
let (mut point, mut range) = (0u32, 0u32);
for _ in 0..2000 {
assert_eq!(randomize(h), Outcome::Solved);
let r = rget(h, 0);
if r == 1.0 {
point += 1;
} else {
assert!((10.0..=20.0).contains(&r), "{r}");
range += 1;
}
}
assert!((1400..=1600).contains(&point), "{point} / {range}");
release(h);
}
#[test]
fn the_integral_solution_is_drawn_before_the_real_one() {
let c = vec![bin(BinOp::Ge, var(0, u(8)), k(u(8), 3), Ty::BIT)];
let (_, plain) = setup(0x7000_0c06, 1, c.clone());
let h = setup_real(0x7000_0c07, unit_interval(), c);
objects::set_prng_seed(h, objects::prng_seed(plain).unwrap());
let seed = objects::prng_seed(plain).unwrap();
assert_eq!(randomize(plain), Outcome::Solved);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(get(plain, 0), get(h, 2));
assert_ne!(
objects::prng_seed(h),
objects::prng_seed(plain),
"real draws follow"
);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
rset_bits(h, 0, 1.5f64.to_bits(), 0);
objects::set_prng_seed(h, seed);
assert_eq!(randomize(h), Outcome::Solved);
assert_eq!(objects::prng_seed(h), objects::prng_seed(plain));
release(plain);
release(h);
}
}
#[cfg(test)]
mod array_tests {
use super::*;
use crate::collections::{KeyKind, Shape};
use crate::constraints::ir::{ArrayNode, BinOp, LoopId, ReduceOp};
use crate::objects::{RandKind, RandProp, alloc, register_rand_props, release};
fn u8t() -> Ty {
Ty::unsigned(8)
}
fn s32() -> Ty {
Ty::signed(32)
}
fn bin(op: BinOp, a: Expr, b: Expr, t: Ty) -> Expr {
Expr::Binary(op, Box::new(a), Box::new(b), t)
}
fn k(t: Ty, v: u64) -> Expr {
Expr::constant(t, v)
}
fn node(n: ArrayNode) -> Expr {
Expr::Array(n)
}
fn lp(id: LoopId) -> Expr {
node(ArrayNode::Loop(id, s32()))
}
fn elem(a: u32, i: Expr) -> Expr {
node(ArrayNode::Elem {
array: a,
index: Box::new(i),
ty: u8t(),
})
}
fn shape() -> Shape {
Shape {
elem_words: 1,
assoc: false,
key: KeyKind::Int {
width: 32,
signed: false,
},
obj: false,
}
}
fn slot(o: usize) -> FieldSlot {
FieldSlot {
offset: o,
words: 1,
}
}
fn contents(h: u64) -> Vec<u64> {
let mut b = [0u64];
assert!(objects::field_get(h, slot(0), &mut b));
collections::elements(b[0])
.into_iter()
.map(|e| e[0])
.collect()
}
fn setup(id: u32, conjuncts: Vec<Expr>, initial: &[u64]) -> u64 {
let t = ClassTemplateId(id);
register_rand_props(
t,
vec![
RandProp {
slot: slot(0),
kind: RandKind::Rand,
},
RandProp {
slot: slot(1),
kind: RandKind::Rand,
},
],
);
register_program(
t,
Ok(Program {
vars: vec![ProgVar {
ty: u8t(),
slot: slot(1),
home: Home::Instance,
prop_index: Some(1),
domain_conjunct: None,
randc: false,
window: None,
}],
conjuncts,
arrays: vec![ProgArray {
elem_ty: u8t(),
elem_words: 1,
slot: slot(0),
home: Home::Instance,
rand: true,
prop_index: Some(0),
elem_domain: None,
assoc: false,
}],
..Program::default()
}),
);
let h = alloc(t, 2);
objects::set_prng_seed(h, 0xA11A_5EED);
let c = collections::from_words(&shape(), initial);
assert!(objects::field_set(h, slot(0), &[c]));
h
}
#[test]
fn a_size_constraint_resizes_and_randomizes_every_element() {
let size_eq = bin(
BinOp::Eq,
node(ArrayNode::Size(0)),
Expr::Resize(Box::new(Expr::Var(0, u8t())), s32()),
Ty::BIT,
);
let lo = bin(BinOp::Ge, Expr::Var(0, u8t()), k(u8t(), 2), Ty::BIT);
let hi = bin(BinOp::Le, Expr::Var(0, u8t()), k(u8t(), 4), Ty::BIT);
let each = node(ArrayNode::Foreach {
array: 0,
var: 0,
body: Box::new(bin(
BinOp::Gt,
Expr::Resize(Box::new(elem(0, lp(0))), s32()),
lp(0),
Ty::BIT,
)),
});
let h = setup(0x7a00_0001, vec![size_eq, lo, hi, each], &[9]);
let mut sizes = std::collections::BTreeSet::new();
for _ in 0..60 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
let a = contents(h);
let mut len = [0u64];
assert!(objects::field_get(h, slot(1), &mut len));
assert_eq!(a.len() as u64, len[0], "size == len");
assert!((2..=4).contains(&a.len()));
for (i, v) in a.iter().enumerate() {
assert!(*v > i as u64, "element {i} = {v}");
}
sizes.insert(a.len());
}
assert_eq!(sizes.len(), 3, "every size 2..=4 is reached: {sizes:?}");
release(h);
}
#[test]
fn an_unsized_array_keeps_its_length_but_gets_new_elements() {
let each = node(ArrayNode::Foreach {
array: 0,
var: 0,
body: Box::new(bin(BinOp::Ge, elem(0, lp(0)), k(u8t(), 200), Ty::BIT)),
});
let h = setup(0x7a00_0002, vec![each], &[1, 2, 3]);
assert_eq!(randomize(h, &[]), Outcome::Solved);
let a = contents(h);
assert_eq!(a.len(), 3);
assert!(a.iter().all(|v| *v >= 200), "{a:?}");
release(h);
}
#[test]
fn an_inactive_array_is_state_and_unchanged() {
let sum = node(ArrayNode::Reduce {
op: ReduceOp::Sum,
array: 0,
var: 0,
index_var: 1,
body: Box::new(Expr::Resize(
Box::new(node(ArrayNode::Loop(0, u8t()))),
s32(),
)),
});
let c = bin(
BinOp::Eq,
Expr::Resize(Box::new(Expr::Var(0, u8t())), s32()),
sum,
Ty::BIT,
);
let h = setup(0x7a00_0003, vec![c], &[3, 4, 5]);
let mut m = objects::rand_meta(h).unwrap();
m.rand_mode[0] = false;
objects::set_rand_meta(h, m);
assert_eq!(randomize(h, &[]), Outcome::Solved);
assert_eq!(contents(h), vec![3, 4, 5]);
let mut len = [0u64];
assert!(objects::field_get(h, slot(1), &mut len));
assert_eq!(len[0], 12);
release(h);
}
#[test]
fn an_out_of_range_element_fails_loudly_and_changes_nothing() {
let c = bin(BinOp::Eq, elem(0, k(s32(), 7)), k(u8t(), 1), Ty::BIT);
let h = setup(0x7a00_0004, vec![c], &[1, 2]);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h, &[]), Outcome::ArrayIndexOutOfRange);
assert_eq!(contents(h), vec![1, 2]);
assert_eq!(objects::prng_seed(h), seed);
release(h);
}
#[test]
fn a_size_that_fails_the_element_constraints_is_retried() {
let lo = bin(BinOp::Ge, node(ArrayNode::Size(0)), k(s32(), 1), Ty::BIT);
let hi = bin(BinOp::Le, node(ArrayNode::Size(0)), k(s32(), 3), Ty::BIT);
let guarded = Expr::Guard(
Box::new(bin(
BinOp::Gt,
node(ArrayNode::Size(0)),
k(s32(), 2),
Ty::BIT,
)),
Box::new(bin(BinOp::Eq, elem(0, k(s32(), 2)), k(u8t(), 5), Ty::BIT)),
Box::new(k(Ty::BIT, 0)),
Ty::BIT,
);
let h = setup(0x7a00_0005, vec![lo, hi, guarded], &[]);
for _ in 0..20 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
let a = contents(h);
assert_eq!(a.len(), 3);
assert_eq!(a[2], 5);
}
release(h);
}
#[test]
fn an_impossible_size_is_unsatisfiable_not_an_error() {
let c = bin(BinOp::Lt, node(ArrayNode::Size(0)), k(s32(), 0), Ty::BIT);
let h = setup(0x7a00_0006, vec![c], &[1]);
assert_eq!(randomize(h, &[]), Outcome::Unsatisfiable);
assert_eq!(contents(h), vec![1]);
release(h);
}
#[test]
fn a_rand_associative_array_keeps_its_keys_and_redraws_its_values() {
use crate::collections::Key;
let t = ClassTemplateId(0x7a00_0007);
register_rand_props(
t,
vec![RandProp {
slot: slot(0),
kind: RandKind::Rand,
}],
);
register_program(
t,
Ok(Program {
arrays: vec![ProgArray {
elem_ty: u8t(),
elem_words: 1,
slot: slot(0),
home: Home::Instance,
rand: true,
prop_index: Some(0),
elem_domain: None,
assoc: true,
}],
..Program::default()
}),
);
let h = alloc(t, 1);
objects::set_prng_seed(h, 0xA550_0001);
let src = collections::frozen_map(
vec![
(Key::Int(7), vec![1]),
(Key::Int(3), vec![1]),
(Key::Int(40), vec![1]),
],
Some(vec![9]),
);
let c = collections::clone(src);
assert!(objects::field_set(h, slot(0), &[c]));
let mut seen = std::collections::BTreeSet::new();
for _ in 0..30 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
let mut b = [0u64];
assert!(objects::field_get(h, slot(0), &mut b));
assert_eq!(
collections::keys(b[0]),
vec![Key::Int(3), Key::Int(7), Key::Int(40)],
"the keys never change"
);
assert_eq!(collections::size(b[0]), 3);
let mut dflt = [0u64];
assert!(collections::lookup(b[0], &Key::Int(1000), &mut dflt));
assert_eq!(dflt[0], 9, "the default survives");
for e in collections::elements(b[0]) {
seen.insert(e[0]);
}
}
assert!(seen.len() > 20, "the values are redrawn: {seen:?}");
release(h);
}
}
#[cfg(test)]
mod mode_tests {
use super::tests::{bin, get, k, setup, u, var};
use super::*;
use crate::constraints::ir::BinOp;
use crate::objects::{alloc, register_rand_props, release};
fn slot(offset: usize) -> FieldSlot {
FieldSlot { offset, words: 1 }
}
fn randomize(h: u64) -> Outcome {
super::randomize(h, &[])
}
fn with_blocks(t: ClassTemplateId, blocks: Vec<ProgBlock>) {
let mut p = program(t).unwrap().as_ref().clone().unwrap();
p.blocks = blocks;
register_program(t, Ok(p));
}
fn block(home: Home, ordinal: u32, conjuncts: std::ops::Range<usize>) -> ProgBlock {
ProgBlock {
home,
ordinal,
conjuncts,
items: BlockItems::default(),
}
}
#[test]
fn a_constraint_block_switched_off_is_left_out_of_the_solve() {
let c = vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 200), Ty::BIT),
bin(BinOp::Lt, var(0, u(8)), k(u(8), 100), Ty::BIT),
];
let (t, h) = setup(0x7c00_0001, 1, c);
with_blocks(
t,
vec![
block(Home::Instance, 0, 0..1),
block(Home::Instance, 1, 1..2),
],
);
assert_eq!(randomize(h), Outcome::Unsatisfiable);
assert!(objects::set_constraint_mode_of(h, 0, false));
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert!(get(h, 0) < 100, "block 1 still holds");
}
assert!(objects::set_constraint_mode_of(h, 0, true));
assert!(objects::set_constraint_mode_of(h, 1, false));
for _ in 0..20 {
assert_eq!(randomize(h), Outcome::Solved);
assert!(get(h, 0) > 200, "block 0 holds again, block 1 is off");
}
assert!(objects::set_constraint_mode_of(h, 1, true));
assert_eq!(randomize(h), Outcome::Unsatisfiable);
release(h);
}
#[test]
fn constraint_mode_is_per_object() {
let c = vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 200), Ty::BIT),
bin(BinOp::Lt, var(0, u(8)), k(u(8), 100), Ty::BIT),
];
let (t, a) = setup(0x7c00_0002, 1, c);
with_blocks(
t,
vec![
block(Home::Instance, 0, 0..1),
block(Home::Instance, 1, 1..2),
],
);
let b = alloc(t, 2);
objects::set_prng_seed(b, 7);
assert!(objects::set_constraint_mode_of(a, 0, false));
assert_eq!(randomize(a), Outcome::Solved);
assert_eq!(randomize(b), Outcome::Unsatisfiable);
release(a);
release(b);
}
#[test]
fn a_static_blocks_mode_is_shared_by_every_instance() {
let c = vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 200), Ty::BIT),
bin(BinOp::Lt, var(0, u(8)), k(u(8), 100), Ty::BIT),
];
let (t, a) = setup(0x7c00_0004, 1, c);
with_blocks(
t,
vec![
block(Home::Static(9), 0, 0..1),
block(Home::Instance, 1, 1..2),
],
);
let b = alloc(t, 2);
objects::set_prng_seed(b, 11);
let st = alloc(ClassTemplateId(0x7c00_0104), 1);
let pairs = [(9u32, st)];
assert_eq!(super::randomize(a, &pairs), Outcome::Unsatisfiable);
assert!(objects::set_constraint_mode_of(st, 0, false));
assert_eq!(super::randomize(a, &pairs), Outcome::Solved);
assert_eq!(super::randomize(b, &pairs), Outcome::Solved);
assert_eq!(super::randomize(a, &[]), Outcome::Unsatisfiable);
release(a);
release(b);
release(st);
}
#[test]
fn a_static_rand_mode_is_shared_and_makes_the_variable_state() {
let t = ClassTemplateId(0x7c00_0005);
register_rand_props(t, Vec::new());
register_program(
t,
Ok(Program {
vars: vec![ProgVar {
ty: u(8),
slot: slot(0),
home: Home::Static(5),
prop_index: None,
domain_conjunct: None,
randc: false,
window: None,
}],
conjuncts: vec![bin(BinOp::Gt, var(0, u(8)), k(u(8), 200), Ty::BIT)],
..Program::default()
}),
);
let a = alloc(t, 1);
objects::set_prng_seed(a, 3);
let st = alloc(ClassTemplateId(0x7c00_0105), 1);
let pairs = [(5u32, st)];
assert!(objects::field_set(st, slot(0), &[7]));
assert_eq!(super::randomize(a, &pairs), Outcome::Solved);
assert!(get(st, 0) > 200);
assert!(objects::field_set(st, slot(0), &[7]));
assert!(objects::set_static_rand_mode(st, 0, false));
assert_eq!(super::randomize(a, &pairs), Outcome::Unsatisfiable);
assert_eq!(get(st, 0), 7);
assert!(objects::set_static_rand_mode(st, 0, true));
assert_eq!(super::randomize(a, &pairs), Outcome::Solved);
release(a);
release(st);
}
}
#[cfg(test)]
mod func_tests {
use super::*;
use crate::constraints::func::{Stmt, tests::count_ones};
use crate::constraints::ir::{ArrayNode, BinOp};
use crate::objects::{RandKind, RandProp, alloc, register_rand_props, release};
fn u(w: u32) -> Ty {
Ty::unsigned(w)
}
fn s32() -> Ty {
Ty::signed(32)
}
fn var(i: u32, t: Ty) -> Expr {
Expr::Var(i, t)
}
fn k(t: Ty, v: u64) -> Expr {
Expr::constant(t, v)
}
fn bin(op: BinOp, a: Expr, b: Expr, t: Ty) -> Expr {
Expr::Binary(op, Box::new(a), Box::new(b), t)
}
fn widen(x: Expr, t: Ty) -> Expr {
Expr::Resize(Box::new(x), t)
}
fn call(func: u32, args: Vec<Expr>, ty: Ty) -> Expr {
Expr::Array(ArrayNode::Call { func, args, ty })
}
fn slot(o: usize) -> FieldSlot {
FieldSlot {
offset: o,
words: 1,
}
}
fn twice() -> Func {
Func {
name: "twice".into(),
slots: vec![u(8), u(8)],
n_params: 1,
ret: 1,
body: vec![Stmt::Assign(
1,
bin(BinOp::Add, var(0, u(8)), var(0, u(8)), u(8)),
)],
}
}
fn zero() -> Func {
Func {
name: "zero".into(),
slots: vec![u(8), u(8)],
n_params: 1,
ret: 1,
body: vec![Stmt::Assign(1, k(u(8), 0))],
}
}
fn setup(id: u32, n: usize, funcs: Vec<Func>, conjuncts: Vec<Expr>) -> u64 {
let t = ClassTemplateId(id);
register_rand_props(
t,
(0..n)
.map(|i| RandProp {
slot: slot(i),
kind: RandKind::Rand,
})
.collect(),
);
register_program(
t,
Ok(Program {
vars: (0..n)
.map(|i| ProgVar {
ty: u(8),
slot: slot(i),
home: Home::Instance,
prop_index: Some(i),
domain_conjunct: None,
randc: false,
window: None,
})
.collect(),
conjuncts,
funcs,
..Program::default()
}),
);
let h = alloc(t, n);
objects::set_prng_seed(h, 0xF00D_0001);
h
}
fn get(h: u64, off: usize) -> u64 {
let mut b = [0u64];
assert!(objects::field_get(h, slot(off), &mut b));
b[0]
}
#[test]
fn a_return_value_follows_its_argument_which_is_solved_first() {
let c = bin(
BinOp::Eq,
widen(var(1, u(8)), s32()),
call(0, vec![widen(var(0, u(8)), u(10))], s32()),
Ty::BIT,
);
let h = setup(0x7a00_0008, 2, vec![count_ones()], vec![c]);
let mut by_len = [0u32; 9];
for _ in 0..400 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
let (v, len) = (get(h, 0), get(h, 1));
assert_eq!(len, u64::from(v.count_ones()), "len == count_ones({v})");
by_len[len as usize] += 1;
}
assert!(by_len[4] > 60, "len 4 is the likeliest: {by_len:?}");
assert!(by_len[0] < 10, "len 0 needs v == 0: {by_len:?}");
release(h);
}
#[test]
fn the_argument_is_drawn_uniformly_before_the_variable_it_bounds() {
let le = bin(
BinOp::Le,
widen(var(0, u(8)), u(8)),
call(0, vec![var(1, u(8))], u(8)),
Ty::BIT,
);
let y = var(1, u(8));
let inside = [2u64, 4, 8]
.into_iter()
.map(|v| bin(BinOp::Eq, y.clone(), k(u(8), v), Ty::BIT))
.reduce(|a, b| bin(BinOp::LogOr, a, b, Ty::BIT))
.unwrap();
let h = setup(0x7a00_0009, 2, vec![twice()], vec![le, inside]);
let mut hits = [0u32; 3];
for _ in 0..300 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
let (x, y) = (get(h, 0), get(h, 1));
assert!(x <= 2 * y, "x={x} y={y}");
hits[match y {
2 => 0,
4 => 1,
8 => 2,
other => panic!("y = {other}"),
}] += 1;
}
assert!(
hits.iter().all(|&n| n > 75),
"each y about a third of the time: {hits:?}"
);
release(h);
}
#[test]
fn a_circular_implicit_order_is_a_loud_outcome() {
let cs = vec![
bin(
BinOp::Eq,
var(0, u(8)),
call(0, vec![var(1, u(8))], u(8)),
Ty::BIT,
),
bin(
BinOp::Eq,
var(1, u(8)),
call(0, vec![var(0, u(8))], u(8)),
Ty::BIT,
),
];
let h = setup(0x7a00_000a, 2, vec![twice()], cs);
let seed = objects::prng_seed(h);
assert_eq!(randomize(h, &[]), Outcome::ImplicitOrderCycle);
assert_eq!(objects::prng_seed(h), seed, "a failed call leaves the seed");
release(h);
}
#[test]
fn an_unsatisfiable_check_is_function_search_not_a_wrong_value() {
let c = bin(
BinOp::Eq,
call(0, vec![var(0, u(8))], u(8)),
k(u(8), 1),
Ty::BIT,
);
let h = setup(0x7a00_000b, 1, vec![zero()], vec![c]);
assert_eq!(randomize(h, &[]), Outcome::FunctionSearch);
release(h);
}
#[test]
fn a_satisfiable_check_is_found_by_sampling() {
let c = bin(
BinOp::Eq,
call(0, vec![var(0, u(8))], u(8)),
k(u(8), 6),
Ty::BIT,
);
let h = setup(0x7a00_000c, 1, vec![twice()], vec![c]);
for _ in 0..10 {
assert_eq!(randomize(h, &[]), Outcome::Solved);
assert!(matches!(get(h, 0), 3 | 131));
}
release(h);
}
}
#[cfg(test)]
mod scope_tests {
use super::tests::{bin, k, u, var};
use super::*;
use crate::constraints::ir::BinOp;
fn scope_var(offset: usize, width: u32) -> ProgVar {
ProgVar {
ty: u(width),
slot: FieldSlot {
offset,
words: (2 * width as usize).div_ceil(64).max(1),
},
home: Home::Instance,
prop_index: None,
domain_conjunct: None,
randc: false,
window: None,
}
}
fn captured_state(width: u32) -> ProgState {
ProgState {
class_of: false,
root_bit: 0,
ty: u(width),
slot: FieldSlot {
offset: 0,
words: 1,
},
home: Home::Instance,
via: Vec::new(),
window: None,
}
}
fn register(vars: Vec<ProgVar>, states: Vec<ProgState>, conjuncts: Vec<Expr>) -> u32 {
let captures = states
.iter()
.enumerate()
.map(|(i, s)| Capture {
state: i as u32,
width: s.ty.width,
})
.collect();
register_spec(Spec {
frag: Program {
vars,
states,
conjuncts,
..Program::default()
},
captures,
only: None,
})
}
fn lt(a: Expr, b: Expr) -> Expr {
bin(BinOp::Lt, a, b, Ty::BIT)
}
#[test]
fn a_scope_randomize_solves_without_any_object_and_returns_the_values() {
let id = register(
vec![scope_var(0, 8), scope_var(1, 8)],
Vec::new(),
vec![
lt(var(0, u(8)), var(1, u(8))),
bin(BinOp::Le, var(1, u(8)), k(u(8), 10), Ty::BIT),
],
);
let mut distinct = std::collections::BTreeSet::new();
for seed in 1..60u64 {
let mut out = [0xffu64; 2];
assert_eq!(randomize_scope(id, &[], &mut out, seed), Outcome::Solved);
assert!(out[0] < out[1] && out[1] <= 10, "{out:?}");
distinct.insert((out[0], out[1]));
}
assert!(
distinct.len() > 10,
"the draw follows the seed: {distinct:?}"
);
}
#[test]
fn the_same_seed_draws_the_same_values() {
let id = register(
vec![scope_var(0, 16)],
Vec::new(),
vec![lt(var(0, u(16)), k(u(16), 1000))],
);
let (mut a, mut b) = ([0u64], [0u64]);
assert_eq!(randomize_scope(id, &[], &mut a, 77), Outcome::Solved);
assert_eq!(randomize_scope(id, &[], &mut b, 77), Outcome::Solved);
assert_eq!(a, b);
}
#[test]
fn captured_locals_are_constants_of_the_solve() {
let id = register(
vec![scope_var(0, 8)],
vec![captured_state(8)],
vec![bin(
BinOp::Eq,
var(0, u(8)),
bin(BinOp::Add, Expr::State(0, u(8)), k(u(8), 1), u(8)),
Ty::BIT,
)],
);
for s in [0u64, 41, 200] {
let mut out = [0u64];
assert_eq!(randomize_scope(id, &[s], &mut out, 5), Outcome::Solved);
assert_eq!(out[0], s + 1);
}
}
#[test]
fn an_unsatisfiable_scope_randomize_writes_nothing() {
let id = register(
vec![scope_var(0, 8)],
Vec::new(),
vec![
bin(BinOp::Gt, var(0, u(8)), k(u(8), 5), Ty::BIT),
lt(var(0, u(8)), k(u(8), 3)),
],
);
let mut out = [0x77u64];
assert_eq!(
randomize_scope(id, &[], &mut out, 1),
Outcome::Unsatisfiable
);
assert_eq!(out, [0x77], "a failed randomize changes nothing (18.6.1)");
}
#[test]
fn with_no_variable_the_call_is_a_checker() {
let id = register(
Vec::new(),
vec![captured_state(8), captured_state(8)],
vec![lt(Expr::State(0, u(8)), Expr::State(1, u(8)))],
);
let mut none: [u64; 0] = [];
assert_eq!(randomize_scope(id, &[3, 9], &mut none, 1), Outcome::Solved);
assert_eq!(
randomize_scope(id, &[9, 3], &mut none, 1),
Outcome::Unsatisfiable
);
assert_eq!(
randomize_scope(id, &[4, 4], &mut none, 1),
Outcome::Unsatisfiable
);
}
#[test]
fn an_unknown_bit_in_a_captured_local_is_an_error_not_a_value() {
let id = register(
Vec::new(),
vec![captured_state(8)],
vec![lt(Expr::State(0, u(8)), k(u(8), 9))],
);
let mut none: [u64; 0] = [];
let x_bit = 1u64 << 8;
assert_eq!(
randomize_scope(id, &[x_bit], &mut none, 1),
Outcome::StateUnknown
);
}
#[test]
fn a_wide_variable_spans_words_and_keeps_its_pinned_bits() {
let wide = scope_var(0, 70);
assert_eq!(wide.slot.words, 3);
let top = bin(
BinOp::Eq,
Expr::Slice(Box::new(var(0, u(70))), 64, u(6)),
k(u(6), 0x2a),
Ty::BIT,
);
let id = register(vec![wide], Vec::new(), vec![top]);
let mut seen_low = std::collections::BTreeSet::new();
for seed in 1..20u64 {
let mut out = [0u64; 3];
assert_eq!(randomize_scope(id, &[], &mut out, seed), Outcome::Solved);
assert_eq!(out[1] & 0x3f, 0x2a, "{out:x?}");
assert_eq!(out[2], 0, "no unknown bits: {out:x?}");
seen_low.insert(out[0]);
}
assert!(seen_low.len() > 10, "the other bits are random");
}
#[test]
fn a_call_that_does_not_match_its_spec_fails_loudly() {
let id = register(
vec![scope_var(0, 8)],
vec![captured_state(8)],
vec![lt(var(0, u(8)), Expr::State(0, u(8)))],
);
let spec = spec(id).expect("registered");
assert_eq!((spec.scope_words(), spec.capture_words()), (1, 1));
let mut one = [0u64];
let mut two = [0u64; 2];
assert_eq!(randomize_scope(id, &[], &mut one, 1), Outcome::Failed);
assert_eq!(randomize_scope(id, &[5], &mut two, 1), Outcome::Failed);
assert_eq!(randomize_scope(0, &[], &mut one, 1), Outcome::Failed);
assert_eq!(randomize_scope(u32::MAX, &[], &mut one, 1), Outcome::Failed);
}
}