use std::collections::{BTreeMap, HashMap};
use rucc_rules::{Error, Term, TermKind, parse_terms};
const BUILTIN: [(&str, &str); 32] = [
("=", "="),
("and", "and"),
("or", "or"),
("not", "not"),
("<", "bvslt"),
("<=", "bvsle"),
(">", "bvsgt"),
(">=", "bvsge"),
("bvslt", "bvslt"),
("bvsle", "bvsle"),
("bvsgt", "bvsgt"),
("bvsge", "bvsge"),
("bvult", "bvult"),
("bvule", "bvule"),
("bvugt", "bvugt"),
("bvuge", "bvuge"),
("bvadd", "bvadd"),
("bvsub", "bvsub"),
("bvmul", "bvmul"),
("bvneg", "bvneg"),
("bvnot", "bvnot"),
("bvand", "bvand"),
("bvor", "bvor"),
("bvxor", "bvxor"),
("bvshl", "bvshl"),
("bvlshr", "bvlshr"),
("bvashr", "bvashr"),
("bvsdiv", "bvsdiv"),
("bvudiv", "bvudiv"),
("bvsrem", "bvsrem"),
("bvurem", "bvurem"),
("ite", "ite"),
];
const LOGICAL: [&str; 4] = ["and", "or", "not", "ite"];
const FLOAT: [(&str, usize); 4] = [("fp.add", 2), ("fp.sub", 2), ("fp.mul", 2), ("fp.div", 2)];
const FLOAT_TEST: [(&str, usize); 6] =
[("fp.eq", 2), ("fp.lt", 2), ("fp.leq", 2), ("fp.gt", 2), ("fp.geq", 2), ("fp.isNaN", 1)];
const ROUNDING: &str = "RNE";
const TOWARDS_ZERO: &str = "RTZ";
const FORMATS: [(u32, u32, u32); 4] = [(16, 5, 11), (32, 8, 24), (64, 11, 53), (128, 15, 113)];
const REINTERPRET: [&str; 2] = ["float_from_bits", "bits_from_float"];
const CROSSING: [&str; 3] = ["float_from_float", "float_from_signed", "signed_from_float"];
const CONVERSION: [&str; 3] = ["sign_extend", "zero_extend", "extract"];
const MEMORY: [&str; 3] = ["mem", "select", "store"];
const CONCAT: &str = "concat";
pub const ADDRESS_WIDTH: u32 = 64;
pub const BYTE_WIDTH: u32 = 8;
pub const MEMORY_CONST: &str = "mem";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sort {
Bits(u32),
Float(u32),
Memory,
}
impl Sort {
#[must_use]
pub fn bits(self) -> Option<u32> {
match self {
Sort::Bits(width) => Some(width),
Sort::Float(_) | Sort::Memory => None,
}
}
#[must_use]
pub fn write(self, widths: &Widths) -> String {
match self {
Sort::Bits(width) => format!("(_ BitVec {width})"),
Sort::Float(width) => format!("Float{width}"),
Sort::Memory => {
format!("(Array (_ BitVec {}) (_ BitVec {}))", widths.address(), widths.byte())
}
}
}
pub(crate) fn describe(self) -> String {
match self {
Sort::Bits(width) => format!("{width} bits wide"),
Sort::Float(width) => format!("{width} bits of float"),
Sort::Memory => "the whole of memory".to_owned(),
}
}
}
pub const DEFAULT_WIDTH: u32 = 64;
#[derive(Debug, Clone, Default)]
pub struct Widths {
natural: u32,
asked: u32,
at: BTreeMap<String, Sort>,
}
impl Widths {
#[must_use]
pub fn of(pattern: &Term) -> Widths {
Widths::at(pattern, rule_width(pattern))
}
#[must_use]
pub fn at(pattern: &Term, asked: u32) -> Widths {
let natural = rule_width(pattern);
let mut widths = Widths { natural, asked, at: BTreeMap::new() };
widths.bind(pattern, Sort::Bits(asked));
widths
}
#[must_use]
pub fn width(&self) -> u32 {
self.asked
}
#[must_use]
pub fn natural(&self) -> u32 {
self.natural
}
pub fn names(&self) -> impl Iterator<Item = (&str, Sort)> {
self.at
.iter()
.filter(|(_, sort)| **sort != Sort::Memory)
.map(|(name, sort)| (name.as_str(), *sort))
}
#[must_use]
pub fn with(&self, name: &str, sort: Sort) -> Widths {
let mut out = self.clone();
out.at.insert(name.to_owned(), sort);
out
}
#[must_use]
pub fn address(&self) -> u32 {
self.scale(ADDRESS_WIDTH)
}
#[must_use]
pub fn byte(&self) -> u32 {
self.scale(BYTE_WIDTH)
}
fn of_name(&self, name: &str) -> Option<Sort> {
self.at.get(name).copied()
}
fn sort_of(&self, head: &str) -> Option<Sort> {
match declared(head)? {
Sort::Bits(width) => Some(Sort::Bits(self.scale(width))),
other => Some(other),
}
}
fn suffix(&self, head: &str) -> Option<u32> {
self.sort_of(head).and_then(Sort::bits)
}
fn scale(&self, width: u32) -> u32 {
if self.asked == self.natural || self.natural == 0 {
return width;
}
self.index(width).max(1)
}
fn index(&self, position: u32) -> u32 {
if self.asked == self.natural || self.natural == 0 {
return position;
}
let scaled = u64::from(position) * u64::from(self.asked) / u64::from(self.natural);
u32::try_from(scaled).unwrap_or(position)
}
fn bind(&mut self, term: &Term, context: Sort) {
match &term.kind {
TermKind::Var(name) => {
self.at.insert(name.clone(), context);
}
TermKind::Int(_) => {}
TermKind::App { head, args } => {
let inner = self.sort_of(head).unwrap_or(context);
for arg in args {
self.bind(arg, inner);
}
}
}
}
}
#[must_use]
pub fn rule_width(pattern: &Term) -> u32 {
let TermKind::App { head, .. } = &pattern.kind else {
return DEFAULT_WIDTH;
};
match declared(head) {
Some(Sort::Bits(width) | Sort::Float(width)) => width,
Some(Sort::Memory) | None => DEFAULT_WIDTH,
}
}
fn declared(head: &str) -> Option<Sort> {
let (_, suffix) = head.rsplit_once('.')?;
let number = |kind: char| suffix.strip_prefix(kind).and_then(|bits| bits.parse::<u32>().ok());
if let Some(bits) = number('i') {
return Some(Sort::Bits(bits));
}
let bits = number('f')?;
format_of(bits).map(|_| Sort::Float(bits))
}
fn format_of(width: u32) -> Option<(u32, u32)> {
FORMATS
.iter()
.find(|(bits, _, _)| *bits == width)
.map(|(_, exponent, significand)| (*exponent, *significand))
}
#[derive(Debug, Clone)]
struct Meaning {
params: Vec<String>,
body: Term,
}
#[derive(Debug, Default)]
pub struct Model {
heads: HashMap<String, Meaning>,
}
impl Model {
pub fn read(path: &str, text: &str) -> Result<Model, Vec<Error>> {
let terms = parse_terms(path, text)?;
let mut model = Model::default();
let mut errors = Vec::new();
for term in terms {
let TermKind::App { head, args } = &term.kind else {
errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
continue;
};
if head != "semantics" || args.len() != 2 {
errors.push(fail(path, &term, "expected a `(semantics ...)` form".to_owned()));
continue;
}
let TermKind::App { head: name, args: params } = &args[0].kind else {
errors.push(fail(path, &args[0], "expected a head and its parameters".to_owned()));
continue;
};
let mut names = Vec::new();
for param in params {
match ¶m.kind {
TermKind::Var(name) => names.push(name.clone()),
_ => errors.push(fail(path, param, "a parameter has to be a name".to_owned())),
}
}
if known(name) {
let said = format!("`{name}` is something the solver already knows");
errors.push(fail(path, &args[0], said));
continue;
}
let meaning = Meaning { params: names, body: args[1].clone() };
if model.heads.insert(name.clone(), meaning).is_some() {
let said = format!("`{name}` is given a meaning twice");
errors.push(fail(path, &args[0], said));
}
}
if errors.is_empty() { Ok(model) } else { Err(errors) }
}
pub fn write(&self, path: &str, term: &Term, widths: &Widths) -> Result<(String, Sort), Error> {
self.write_at(path, term, widths.width(), widths, &HashMap::new())
}
#[must_use]
pub fn touches_memory(&self, term: &Term) -> bool {
match &term.kind {
TermKind::Var(_) | TermKind::Int(_) => false,
TermKind::App { head, args } => {
if MEMORY.contains(&head.as_str()) {
return true;
}
if args.iter().any(|arg| self.touches_memory(arg)) {
return true;
}
self.heads.get(head).is_some_and(|meaning| self.touches_memory(&meaning.body))
}
}
}
#[must_use]
pub fn touches_floats(&self, term: &Term) -> bool {
match &term.kind {
TermKind::Var(_) | TermKind::Int(_) => false,
TermKind::App { head, args } => {
if float_op(head).is_some() || float_test(head).is_some() {
return true;
}
if matches!(declared(head), Some(Sort::Float(_))) {
return true;
}
if REINTERPRET.contains(&head.as_str()) || CROSSING.contains(&head.as_str()) {
return true;
}
if args.iter().any(|arg| self.touches_floats(arg)) {
return true;
}
self.heads.get(head).is_some_and(|meaning| self.touches_floats(&meaning.body))
}
}
}
fn write_at(
&self,
path: &str,
term: &Term,
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
match &term.kind {
TermKind::Var(name) => match bound.get(name.as_str()) {
Some((already, sort)) => Ok((already.clone(), *sort)),
None => Ok((name.clone(), widths.of_name(name).unwrap_or(Sort::Bits(context)))),
},
TermKind::Int(value) => Ok((literal(*value, context), Sort::Bits(context))),
TermKind::App { head, args } => {
if CONVERSION.contains(&head.as_str()) {
return self.convert(path, term, head, args, context, widths, bound);
}
if MEMORY.contains(&head.as_str()) {
return self.reach(path, term, head, args, context, widths, bound);
}
if head == CONCAT {
return self.join(path, term, args, context, widths, bound);
}
if let Some(name) = builtin(head) {
return self.combine(path, term, head, name, args, context, widths, bound);
}
if let Some(takes) = float_op(head) {
return self.rounded(path, term, head, takes, args, context, widths, bound);
}
if let Some(takes) = float_test(head) {
return self.asking(path, term, head, takes, args, context, widths, bound);
}
if REINTERPRET.contains(&head.as_str()) {
return self.reinterpret(path, term, head, args, widths, bound);
}
if CROSSING.contains(&head.as_str()) {
return self.crossing(path, term, head, args, widths, bound);
}
let own = widths.suffix(head).unwrap_or(context);
let mut written = Vec::with_capacity(args.len());
for arg in args {
written.push(self.write_at(path, arg, own, widths, bound)?);
}
let Some(meaning) = self.heads.get(head) else {
let said = format!("nothing in the model says what `{head}` means");
return Err(fail(path, term, said));
};
if meaning.params.len() != written.len() {
let said = format!(
"`{head}` means something with {} arguments and this gives it {}",
meaning.params.len(),
written.len()
);
return Err(fail(path, term, said));
}
let inner: HashMap<&str, (String, Sort)> =
meaning.params.iter().map(String::as_str).zip(written).collect();
let (text, sort) = self.write_at(path, &meaning.body, own, widths, &inner)?;
if let Some(said) = widths.sort_of(head).filter(|_| sort != Sort::Memory) {
let agrees = match (said, sort) {
(Sort::Bits(a), Sort::Bits(b)) | (Sort::Float(a), Sort::Float(b)) => a == b,
_ => false,
};
if !agrees {
let told = match (said, sort) {
(Sort::Bits(said), Sort::Bits(width)) => format!(
"`{head}` is written for {said} bits and means something {width} \
bits wide"
),
_ => format!(
"`{head}` is written for something {} and means something {}",
said.describe(),
sort.describe()
),
};
return Err(fail(path, term, told));
}
}
Ok((text, sort))
}
}
}
#[allow(clippy::too_many_arguments)]
fn combine(
&self,
path: &str,
term: &Term,
head: &str,
name: &str,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
let beside = if LOGICAL.contains(&head) {
context
} else {
self.beside(path, args, context, widths, bound)?
};
let mut written = Vec::with_capacity(args.len());
for arg in args {
let at = if matches!(arg.kind, TermKind::Int(_)) { beside } else { context };
written.push(self.write_at(path, arg, at, widths, bound)?);
}
let Some((_, first)) = written.first() else {
return Err(fail(path, term, format!("`{head}` needs arguments")));
};
let first = *first;
if !LOGICAL.contains(&head) {
if name.starts_with("bv") && !matches!(first, Sort::Bits(_)) {
let said = format!("`{head}` works on bitvectors and this is {}", first.describe());
return Err(fail(path, term, said));
}
if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
let said = format!(
"`{head}` is given something {} and something {}, and those are not the \
same kind of thing",
first.describe(),
other.describe()
);
return Err(fail(path, term, said));
}
}
let sort = if head == "ite" && written.len() > 1 { written[1].1 } else { first };
let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
Ok((format!("({name} {})", texts.join(" ")), sort))
}
#[allow(clippy::too_many_arguments)]
fn rounded(
&self,
path: &str,
term: &Term,
head: &str,
takes: usize,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() != takes {
let said = format!("`{head}` takes {takes} arguments and this gives it {}", args.len());
return Err(fail(path, term, said));
}
let mut written = Vec::with_capacity(args.len());
for arg in args {
written.push(self.write_at(path, arg, context, widths, bound)?);
}
let first = written[0].1;
if !matches!(first, Sort::Float(_)) {
let said = format!("`{head}` works on floats and this is {}", first.describe());
return Err(fail(path, &args[0], said));
}
if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
let said = format!(
"`{head}` is given something {} and something {}, and those are not the same \
kind of thing",
first.describe(),
other.describe()
);
return Err(fail(path, term, said));
}
let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
Ok((format!("({head} {ROUNDING} {})", texts.join(" ")), first))
}
#[allow(clippy::too_many_arguments)]
fn asking(
&self,
path: &str,
term: &Term,
head: &str,
takes: usize,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() != takes {
let said = format!("`{head}` takes {takes} arguments and this gives it {}", args.len());
return Err(fail(path, term, said));
}
let mut written = Vec::with_capacity(args.len());
for arg in args {
written.push(self.write_at(path, arg, context, widths, bound)?);
}
let first = written[0].1;
if !matches!(first, Sort::Float(_)) {
let said = format!("`{head}` asks about floats and this is {}", first.describe());
return Err(fail(path, &args[0], said));
}
if let Some((_, other)) = written.iter().find(|(_, sort)| *sort != first) {
let said = format!(
"`{head}` is given something {} and something {}, and a comparison is between two \
of one format",
first.describe(),
other.describe()
);
return Err(fail(path, term, said));
}
let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
Ok((format!("({head} {})", texts.join(" ")), first))
}
fn reinterpret(
&self,
path: &str,
term: &Term,
head: &str,
args: &[Term],
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() != 2 {
let said =
format!("`{head}` takes a format and a value, and this gives it {}", args.len());
return Err(fail(path, term, said));
}
let width = number(path, head, &args[0])?;
let Some((exponent, significand)) = format_of(width) else {
let said = format!("`{head}` is written at {width} bits, which is not a float format");
return Err(fail(path, term, said));
};
let into_float = head == "float_from_bits";
let (text, sort) = self.write_at(path, &args[1], width, widths, bound)?;
let wanted = if into_float { Sort::Bits(width) } else { Sort::Float(width) };
if sort != wanted {
let said = format!(
"`{head}` takes something {} and this is {}",
wanted.describe(),
sort.describe()
);
return Err(fail(path, &args[1], said));
}
if into_float {
let said = format!("((_ to_fp {exponent} {significand}) {text})");
return Ok((said, Sort::Float(width)));
}
Ok((format!("(fp.to_ieee_bv {text})"), Sort::Bits(width)))
}
fn crossing(
&self,
path: &str,
term: &Term,
head: &str,
args: &[Term],
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() != 3 {
let said = format!(
"`{head}` takes the width it comes from, the width it goes to and a value, and \
this gives it {}",
args.len()
);
return Err(fail(path, term, said));
}
let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
let from_float = head != "float_from_signed";
let into_float = head != "signed_from_float";
let float_format = |width: u32| {
format_of(width).ok_or_else(|| {
let said = format!("`{head}` is written at {width} bits, which is not a format");
fail(path, term, said)
})
};
let from = if from_float { first } else { widths.scale(first) };
let to = if into_float { second } else { widths.scale(second) };
let wanted = if from_float {
float_format(from)?;
Sort::Float(from)
} else {
Sort::Bits(from)
};
let (text, sort) = self.write_at(path, &args[2], from, widths, bound)?;
if sort != wanted {
let said = format!(
"`{head}` takes something {} and this is {}",
wanted.describe(),
sort.describe()
);
return Err(fail(path, &args[2], said));
}
if into_float {
let (exponent, significand) = float_format(to)?;
let said = format!("((_ to_fp {exponent} {significand}) {ROUNDING} {text})");
return Ok((said, Sort::Float(to)));
}
Ok((format!("((_ fp.to_sbv {to}) {TOWARDS_ZERO} {text})"), Sort::Bits(to)))
}
fn beside(
&self,
path: &str,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<u32, Error> {
if !args.iter().any(|arg| matches!(arg.kind, TermKind::Int(_))) {
return Ok(context);
}
let Some(sized) = args.iter().find(|arg| !matches!(arg.kind, TermKind::Int(_))) else {
return Ok(context);
};
let (_, sort) = self.write_at(path, sized, context, widths, bound)?;
Ok(sort.bits().unwrap_or(context))
}
#[allow(clippy::too_many_arguments)]
fn reach(
&self,
path: &str,
term: &Term,
head: &str,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if head == "mem" {
if !args.is_empty() {
let said = "`mem` is the memory a rule starts from and takes nothing".to_owned();
return Err(fail(path, term, said));
}
return Ok((MEMORY_CONST.to_owned(), Sort::Memory));
}
let wanted = if head == "select" { 2 } else { 3 };
if args.len() != wanted {
let said =
format!("`{head}` takes {wanted} arguments and this gives it {}", args.len());
return Err(fail(path, term, said));
}
let mut written = Vec::with_capacity(args.len());
for arg in args {
let at = if matches!(arg.kind, TermKind::Int(_)) { widths.address() } else { context };
written.push(self.write_at(path, arg, at, widths, bound)?);
}
let expected = [Sort::Memory, Sort::Bits(widths.address()), Sort::Bits(widths.byte())];
for (at, (_, got)) in written.iter().enumerate() {
if *got != expected[at] {
let said = format!(
"`{head}` takes something {} in position {at} and this is {}",
expected[at].describe(),
got.describe()
);
return Err(fail(path, term, said));
}
}
let texts: Vec<&str> = written.iter().map(|(text, _)| text.as_str()).collect();
let sort = if head == "select" { Sort::Bits(widths.byte()) } else { Sort::Memory };
Ok((format!("({head} {})", texts.join(" ")), sort))
}
fn join(
&self,
path: &str,
term: &Term,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() < 2 {
let said = format!("`concat` puts two or more things together and this gives it {}", {
args.len()
});
return Err(fail(path, term, said));
}
let mut total = 0;
let mut texts = Vec::with_capacity(args.len());
for arg in args {
let (text, sort) = self.write_at(path, arg, context, widths, bound)?;
let Some(width) = sort.bits() else {
let said = "`concat` puts bitvectors together and this is a memory".to_owned();
return Err(fail(path, arg, said));
};
total += width;
texts.push(text);
}
Ok((format!("(concat {})", texts.join(" ")), Sort::Bits(total)))
}
#[allow(clippy::too_many_arguments)]
fn convert(
&self,
path: &str,
term: &Term,
head: &str,
args: &[Term],
context: u32,
widths: &Widths,
bound: &HashMap<&str, (String, Sort)>,
) -> Result<(String, Sort), Error> {
if args.len() != 3 {
let said = format!("`{head}` takes two numbers and a value, and this gives it {}", {
args.len()
});
return Err(fail(path, term, said));
}
let (first, second) = (number(path, head, &args[0])?, number(path, head, &args[1])?);
if head == "extract" {
let (high, low) = (first, second);
if high < low {
let said = format!("`extract` takes bits {high} down to {low}, which is none");
return Err(fail(path, term, said));
}
let width = widths.scale(high - low + 1);
let bottom = widths.index(low);
let top = bottom + width - 1;
let (text, sort) = self.write_at(path, &args[2], context, widths, bound)?;
let of = bits(path, head, &args[2], sort)?;
if top >= of {
let said = format!(
"`extract` takes bits {top} down to {bottom} of something {of} bits wide"
);
return Err(fail(path, term, said));
}
return Ok((format!("((_ extract {top} {bottom}) {text})"), Sort::Bits(width)));
}
let (from, to) = (widths.scale(first), widths.scale(second));
if to < from {
let said = format!("`{head}` goes from {from} bits to {to}, which is narrower");
return Err(fail(path, term, said));
}
let (text, sort) = self.write_at(path, &args[2], from, widths, bound)?;
let of = bits(path, head, &args[2], sort)?;
if of != from {
let said =
format!("`{head}` goes from {from} bits and is given something {of} bits wide");
return Err(fail(path, term, said));
}
if to == from {
return Ok((text, Sort::Bits(to)));
}
Ok((format!("((_ {head} {}) {text})", to - from), Sort::Bits(to)))
}
}
fn builtin(head: &str) -> Option<&'static str> {
BUILTIN.iter().find(|(name, _)| *name == head).map(|(_, smt)| *smt)
}
fn float_op(head: &str) -> Option<usize> {
FLOAT.iter().find(|(name, _)| *name == head).map(|(_, takes)| *takes)
}
fn float_test(head: &str) -> Option<usize> {
FLOAT_TEST.iter().find(|(name, _)| *name == head).map(|(_, takes)| *takes)
}
fn known(head: &str) -> bool {
builtin(head).is_some()
|| float_op(head).is_some()
|| float_test(head).is_some()
|| REINTERPRET.contains(&head)
|| CROSSING.contains(&head)
|| CONVERSION.contains(&head)
|| MEMORY.contains(&head)
|| head == CONCAT
}
fn bits(path: &str, head: &str, term: &Term, sort: Sort) -> Result<u32, Error> {
sort.bits().ok_or_else(|| {
let said = format!("`{head}` works on bitvectors and this is {}", sort.describe());
fail(path, term, said)
})
}
fn number(path: &str, head: &str, term: &Term) -> Result<u32, Error> {
match &term.kind {
TermKind::Int(value) => u32::try_from(*value).map_err(|_| {
let said = format!("`{head}` is given {value} where it needs a number of bits");
fail(path, term, said)
}),
_ => {
let said = format!("`{head}` says which widths it goes between, in numbers");
Err(fail(path, term, said))
}
}
}
fn literal(value: i128, width: u32) -> String {
let wrapped =
if width >= 128 { value as u128 } else { (value as u128) & ((1u128 << width) - 1) };
format!("(_ bv{wrapped} {width})")
}
fn fail(path: &str, term: &Term, message: String) -> Error {
Error { path: path.to_owned(), line: term.line, column: term.column, message }
}