Skip to main content

bitwright/
ext.rs

1//! Extension operations: host-defined, total, multi-output operators.
2//!
3//! A host whose instruction set has operations bitwright does not model as a composition of its
4//! own (a flags bundle, a funnel shift, a vendor-specific count) registers them as [`ExtOp`]s.
5//! An extension call takes one to three arguments and has one to eight outputs; each output is a
6//! node of its own, hash-consed by the operation, the output and the arguments, so its value is a
7//! function of its arguments like any other node. Evaluation, facts, sampled verification and
8//! SMT-LIB export go through the operation's own methods; to the rules and the passes an
9//! extension node is an opaque atom (its arguments are still simplified, unless the operation
10//! asks otherwise).
11//!
12//! Operations are collected into an immutable [`Registry`] before a context uses it
13//! ([`Context::with_registry`](crate::Context::with_registry)). Registering one runs a contract
14//! self-test: declared widths, determinism, known-bits soundness against `eval`, and
15//! `expand ≡ eval`, at a battery of widths.
16//!
17//! An operation that must not be merged with another call on the same arguments (an
18//! occurrence), takes a fresh symbol as one of its arguments.
19//!
20//! ```
21//! use std::sync::Arc;
22//! use bitwright::ext::{ExtOp, ExtSig, Registry};
23//! use bitwright::{BitVec, BinOp, Context, ContextConfig, Width};
24//!
25//! /// Unsigned average rounding down, without overflow.
26//! struct AvgU;
27//! impl ExtOp for AvgU {
28//!     fn name(&self) -> &str { "acme.avgu" }
29//!     fn signature(&self, args: &[Width]) -> Result<ExtSig, String> {
30//!         match args {
31//!             [a, b] if a == b => Ok(ExtSig::new(&[("avg", *a)])),
32//!             _ => Err("two operands of one width".into()),
33//!         }
34//!     }
35//!     fn eval(&self, args: &[BitVec], out: &mut [BitVec]) {
36//!         let (a, b) = (&args[0], &args[1]);
37//!         let both = BitVec::apply_bin(BinOp::And, a, b).unwrap();
38//!         let either = BitVec::apply_bin(BinOp::Xor, a, b).unwrap();
39//!         let one = BitVec::one(a.width());
40//!         let half = BitVec::apply_bin(BinOp::LShr, &either, &one).unwrap();
41//!         out[0] = BitVec::apply_bin(BinOp::Add, &both, &half).unwrap();
42//!     }
43//! }
44//!
45//! let registry = Arc::new(Registry::builder().register(AvgU)?.build());
46//! let mut cx = Context::with_registry(ContextConfig::default(), registry.clone());
47//! let avg = registry.id("acme.avgu").unwrap();
48//! let x = cx.symbol("x", Width::W8)?;
49//! let c = cx.constant_u64(Width::W8, 200)?;
50//! let d = cx.constant_u64(Width::W8, 100)?;
51//! let folded = cx.ext(avg, &[c, d])?;
52//! assert_eq!(cx.as_const(folded[0])?.and_then(|v| v.to_u64()), Some(150));
53//! let open = cx.ext(avg, &[x, d])?;
54//! assert_eq!(cx.display(open[0]).to_string(), "@acme.avgu(x, 100:8)");
55//! # Ok::<(), Box<dyn std::error::Error>>(())
56//! ```
57
58use core::fmt;
59use std::borrow::Cow;
60use std::collections::HashMap;
61use std::sync::Arc;
62
63use crate::error::Error;
64use crate::expr::{Context, Expr};
65use crate::facts::KnownBits;
66use crate::hash::combine;
67use crate::{BitVec, Width};
68
69/// The most arguments an extension call takes.
70pub const MAX_ARGS: usize = 3;
71/// The most outputs an extension operation has.
72pub const MAX_OUTPUTS: usize = 8;
73/// The most operations a registry holds.
74pub const MAX_OPS: usize = 256;
75
76/// The outputs of an extension operation at some argument widths: a role name and a width each.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ExtSig {
79    outputs: Vec<(Cow<'static, str>, Width)>,
80}
81
82impl ExtSig {
83    /// Outputs by role name and width, in order (1 to 8 of them).
84    pub fn new(outputs: &[(&'static str, Width)]) -> ExtSig {
85        ExtSig {
86            outputs: outputs
87                .iter()
88                .map(|&(r, w)| (Cow::Borrowed(r), w))
89                .collect(),
90        }
91    }
92
93    /// The outputs: role name and width.
94    pub fn outputs(&self) -> impl Iterator<Item = (&str, Width)> {
95        self.outputs.iter().map(|(r, w)| (&**r, *w))
96    }
97
98    /// The number of outputs.
99    pub fn len(&self) -> usize {
100        self.outputs.len()
101    }
102
103    /// Whether there are no outputs (never valid).
104    pub fn is_empty(&self) -> bool {
105        self.outputs.is_empty()
106    }
107
108    /// The width of output `k`.
109    pub fn width(&self, k: usize) -> Option<Width> {
110        self.outputs.get(k).map(|(_, w)| *w)
111    }
112}
113
114/// Properties of an extension operation the builder and simplifier may use.
115#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
116#[non_exhaustive]
117pub struct ExtTraits {
118    /// The first two arguments may be swapped (they are put in canonical order).
119    pub commutative: bool,
120    /// The simplifier leaves the arguments as they are (the call is an atom, arguments
121    /// included).
122    pub opaque: bool,
123}
124
125setters!(ExtTraits {
126    with_commutative: commutative: bool,
127    with_opaque: opaque: bool,
128});
129
130/// A host-defined operation. Every method must be deterministic; `eval` must be total.
131pub trait ExtOp: Send + Sync + 'static {
132    /// A namespaced name: `acme.avgu`. Letters, digits, `_` and `.`, starting with a letter.
133    fn name(&self) -> &str;
134
135    /// Bumped whenever `eval` changes; it enters every structural hash and cache key.
136    fn revision(&self) -> u32 {
137        1
138    }
139
140    /// The outputs at these argument widths (1 to 3 arguments), or why they are not accepted.
141    fn signature(&self, args: &[Width]) -> Result<ExtSig, String>;
142
143    /// The outputs for these arguments. `out` holds one value per output, of the declared
144    /// widths (zero); write every one.
145    fn eval(&self, args: &[BitVec], out: &mut [BitVec]);
146
147    /// Known bits of the outputs from known bits of the arguments. `out` starts unknown; leave an
148    /// output unknown when nothing is known. Must be sound: every value `eval` gives for
149    /// arguments inside `args` must be inside `out`.
150    fn known_bits(&self, _args: &[KnownBits], _out: &mut [KnownBits]) {}
151
152    /// The outputs built from bitwright's own operators, if the operation has such a definition
153    /// (it must agree with `eval`).
154    fn expand(&self, _cx: &mut Context, _args: &[Expr]) -> Option<Result<Vec<Expr>, Error>> {
155        None
156    }
157
158    /// An SMT-LIB term for output `output`, given the argument terms and widths; `None` exports
159    /// it as an uninterpreted function.
160    fn smtlib(&self, _output: u8, _args: &[&str], _widths: &[Width]) -> Option<String> {
161        None
162    }
163
164    /// Properties the builder and simplifier may use.
165    fn traits(&self) -> ExtTraits {
166        ExtTraits::default()
167    }
168}
169
170/// The arguments of an extension call (1 to 3).
171#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
172pub struct ExtArgs {
173    args: [Expr; MAX_ARGS],
174    len: u8,
175}
176
177impl ExtArgs {
178    /// `args` must hold 1 to 3 handles.
179    pub(crate) fn new(args: &[Expr]) -> ExtArgs {
180        let mut a = [args[0]; MAX_ARGS];
181        a[..args.len()].copy_from_slice(args);
182        ExtArgs {
183            args: a,
184            len: args.len() as u8,
185        }
186    }
187
188    /// The arguments in order.
189    pub fn as_slice(&self) -> &[Expr] {
190        &self.args[..usize::from(self.len)]
191    }
192}
193
194/// An operation in a [`Registry`]: its position, and a tag of its name and revision, so an id
195/// used with a registry that has another operation at that position is refused.
196#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
197pub struct ExtId {
198    index: u8,
199    tag: u32,
200}
201
202impl ExtId {
203    /// The position of the operation in its registry.
204    pub fn index(self) -> usize {
205        usize::from(self.index)
206    }
207
208    pub(crate) fn raw(self) -> u8 {
209        self.index
210    }
211
212    /// An id no registry has (for a node whose registry is gone; never built by the builder).
213    pub(crate) fn dangling() -> ExtId {
214        ExtId {
215            index: u8::MAX,
216            tag: 0,
217        }
218    }
219}
220
221/// Why an operation was refused by [`RegistryBuilder::register`].
222#[derive(Clone, Debug, PartialEq, Eq)]
223#[non_exhaustive]
224pub enum ExtError {
225    /// The name is empty or not of the allowed form.
226    BadName(String),
227    /// Another operation has the name.
228    Duplicate(String),
229    /// The registry is full.
230    TooMany,
231    /// The self-test found the operation breaking its contract (the operation, the reason).
232    Contract(String, String),
233}
234
235impl fmt::Display for ExtError {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match self {
238            ExtError::BadName(n) => write!(f, "`{n}` is not a valid extension operation name"),
239            ExtError::Duplicate(n) => write!(f, "`{n}` is already registered"),
240            ExtError::TooMany => write!(f, "a registry holds at most {MAX_OPS} operations"),
241            ExtError::Contract(n, why) => write!(f, "`{n}` breaks its contract: {why}"),
242        }
243    }
244}
245
246impl std::error::Error for ExtError {}
247
248/// An immutable set of extension operations.
249#[derive(Clone, Default)]
250pub struct Registry {
251    ops: Vec<Arc<dyn ExtOp>>,
252    by_name: HashMap<String, ExtId>,
253    /// Per operation: a hash of its name and revision (enters structural hashes).
254    hashes: Vec<u64>,
255}
256
257impl fmt::Debug for Registry {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        f.debug_list()
260            .entries(
261                self.ops
262                    .iter()
263                    .map(|o| format!("{}#{}", o.name(), o.revision())),
264            )
265            .finish()
266    }
267}
268
269impl Registry {
270    /// A builder.
271    pub fn builder() -> RegistryBuilder {
272        RegistryBuilder::default()
273    }
274
275    /// The operation named `name`.
276    pub fn id(&self, name: &str) -> Option<ExtId> {
277        self.by_name.get(name).copied()
278    }
279
280    /// The operation `id`, if this registry has it (at that position, by name and revision).
281    pub fn op(&self, id: ExtId) -> Option<&dyn ExtOp> {
282        (self.id_at(id.index)? == id).then(|| self.ops.get(id.index()).map(|o| &**o))?
283    }
284
285    /// The id of the operation at position `index`.
286    pub(crate) fn id_at(&self, index: u8) -> Option<ExtId> {
287        let h = *self.hashes.get(usize::from(index))?;
288        Some(ExtId {
289            index,
290            tag: h as u32,
291        })
292    }
293
294    /// The operation at position `index` (for nodes, which store only the position).
295    pub(crate) fn op_at(&self, index: u8) -> Option<&dyn ExtOp> {
296        self.ops.get(usize::from(index)).map(|o| &**o)
297    }
298
299    /// The number of operations.
300    pub fn len(&self) -> usize {
301        self.ops.len()
302    }
303
304    /// Whether there are none.
305    pub fn is_empty(&self) -> bool {
306        self.ops.is_empty()
307    }
308
309    /// Every operation, in registration order.
310    pub fn ids(&self) -> impl Iterator<Item = ExtId> + '_ {
311        (0..self.ops.len()).filter_map(|i| self.id_at(i as u8))
312    }
313
314    /// The hash of the operation at position `index` (its name and revision).
315    pub(crate) fn hash_at(&self, index: u8) -> u64 {
316        self.hashes.get(usize::from(index)).copied().unwrap_or(0)
317    }
318}
319
320/// Collects operations into a [`Registry`].
321#[derive(Default, Debug)]
322pub struct RegistryBuilder {
323    reg: Registry,
324}
325
326impl RegistryBuilder {
327    /// Adds `op` after its contract self-test.
328    pub fn register(self, op: impl ExtOp) -> Result<Self, ExtError> {
329        check(&op)?;
330        self.register_unchecked(op)
331    }
332
333    /// Adds `op` without its contract self-test (names are still checked). For a host that
334    /// registers the same operations in many registries, and runs [`check`] on each of them once,
335    /// in its own tests: the self-test samples hundreds of evaluations, too many to repeat per
336    /// registry. A broken operation registered this way makes facts and folds wrong.
337    pub fn register_unchecked(mut self, op: impl ExtOp) -> Result<Self, ExtError> {
338        let name = op.name().to_string();
339        let good = name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
340            && name
341                .chars()
342                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
343            && !name.ends_with('.')
344            && !name.contains("..");
345        if !good {
346            return Err(ExtError::BadName(name));
347        }
348        if self.reg.by_name.contains_key(&name) {
349            return Err(ExtError::Duplicate(name));
350        }
351        if self.reg.ops.len() >= MAX_OPS {
352            return Err(ExtError::TooMany);
353        }
354        let index = self.reg.ops.len() as u8;
355        let h = name
356            .bytes()
357            .fold(combine(0x6578_7400, u64::from(op.revision())), |h, b| {
358                combine(h, u64::from(b))
359            });
360        self.reg.hashes.push(h);
361        let id = ExtId {
362            index,
363            tag: h as u32,
364        };
365        self.reg.by_name.insert(name, id);
366        self.reg.ops.push(Arc::new(op));
367        Ok(self)
368    }
369
370    /// The registry.
371    pub fn build(self) -> Registry {
372        self.reg
373    }
374}
375
376/// The contract self-test [`RegistryBuilder::register`] runs (see the module docs): `Ok` if the
377/// sampled checks found no breach. For hosts that register with
378/// [`register_unchecked`](RegistryBuilder::register_unchecked) and test their operations once.
379pub fn check(op: &dyn ExtOp) -> Result<(), ExtError> {
380    self_test(op).map_err(|why| ExtError::Contract(op.name().to_string(), why))
381}
382
383/// Checks the declared outputs, and runs `eval` for them, validating widths.
384pub(crate) fn run_eval(op: &dyn ExtOp, args: &[BitVec]) -> Result<Vec<BitVec>, String> {
385    let widths: Vec<Width> = args.iter().map(|a| a.width()).collect();
386    let sig = op.signature(&widths)?;
387    if sig.is_empty() || sig.len() > MAX_OUTPUTS {
388        return Err(format!(
389            "{} outputs declared (1 to {MAX_OUTPUTS})",
390            sig.len()
391        ));
392    }
393    let mut out: Vec<BitVec> = sig.outputs().map(|(_, w)| BitVec::zero(w)).collect();
394    op.eval(args, &mut out);
395    for (k, (v, (_, w))) in out.iter().zip(sig.outputs()).enumerate() {
396        if v.width() != w {
397            return Err(format!(
398                "output {k} has {} bits, declared {}",
399                v.width().bits(),
400                w.bits()
401            ));
402        }
403    }
404    Ok(out)
405}
406
407/// Runs `known_bits`, validating widths and consistency.
408pub(crate) fn run_known_bits(
409    op: &dyn ExtOp,
410    sig: &ExtSig,
411    args: &[KnownBits],
412) -> Result<Vec<KnownBits>, String> {
413    let mut out: Vec<KnownBits> = sig.outputs().map(|(_, w)| KnownBits::unknown(w)).collect();
414    op.known_bits(args, &mut out);
415    for (k, (v, (_, w))) in out.iter().zip(sig.outputs()).enumerate() {
416        if v.width() != w {
417            return Err(format!(
418                "known bits of output {k} have {} bits, declared {}",
419                v.width().bits(),
420                w.bits()
421            ));
422        }
423    }
424    Ok(out)
425}
426
427/// A small deterministic generator for the self-test.
428struct Rng(u64);
429
430impl Rng {
431    fn next(&mut self) -> u64 {
432        self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
433        let mut z = self.0;
434        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
435        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
436        z ^ (z >> 31)
437    }
438
439    fn value(&mut self, w: Width) -> BitVec {
440        let limbs: Vec<u64> = (0..8)
441            .map(|_| match self.next() % 5 {
442                0 => 0,
443                1 => u64::MAX,
444                _ => self.next(),
445            })
446            .collect();
447        BitVec::wrapping_from_limbs(w, &limbs)
448    }
449}
450
451/// Argument widths the self-test tries: for one argument every width of the battery, for two
452/// every pair, for three every triple with at least two equal (a value, a count, a carry...).
453fn width_tuples() -> Vec<Vec<u16>> {
454    const W: [u16; 11] = [1, 3, 8, 13, 16, 32, 64, 65, 128, 256, 512];
455    let mut out: Vec<Vec<u16>> = W.iter().map(|&a| vec![a]).collect();
456    for &a in &W {
457        for &b in &W {
458            out.push(vec![a, b]);
459        }
460    }
461    for &a in &W {
462        for &b in &W {
463            for t in [vec![a, a, b], vec![a, b, a], vec![b, a, a]] {
464                if !out.contains(&t) {
465                    out.push(t);
466                }
467            }
468        }
469    }
470    out
471}
472
473/// The most argument-width tuples the self-test exercises (the first accepted, in the order of
474/// [`width_tuples`], which starts from single and equal widths).
475const TESTED_TUPLES: usize = 64;
476
477/// The contract self-test (see the module docs): at every argument-width tuple the signature
478/// accepts (up to [`TESTED_TUPLES`] of them), declared output widths, determinism, known-bits
479/// soundness against `eval`, a commutative operation's symmetry, and `expand ≡ eval`.
480fn self_test(op: &dyn ExtOp) -> Result<(), String> {
481    use crate::facts::known::{bv_and, bv_not};
482    let mut rng = Rng(0x5e1f_7e57 ^ u64::from(op.revision()));
483    let commutative = op.traits().commutative;
484    let mut accepted = 0;
485    for tuple in width_tuples() {
486        if accepted >= TESTED_TUPLES {
487            break;
488        }
489        let widths: Vec<Width> = tuple
490            .iter()
491            .map(|&w| Width::new(w).map_err(|e| e.to_string()))
492            .collect::<Result<_, _>>()?;
493        let Ok(sig) = op.signature(&widths) else {
494            continue;
495        };
496        if sig.is_empty() || sig.len() > MAX_OUTPUTS {
497            return Err(format!(
498                "{} outputs declared (1 to {MAX_OUTPUTS})",
499                sig.len()
500            ));
501        }
502        accepted += 1;
503        let at = || format!(" at argument widths {tuple:?}");
504        for _ in 0..16 {
505            let args: Vec<BitVec> = widths.iter().map(|&w| rng.value(w)).collect();
506            let a = run_eval(op, &args).map_err(|e| e + &at())?;
507            if run_eval(op, &args)? != a {
508                return Err(format!("eval is not deterministic{}", at()));
509            }
510            if commutative && widths.len() >= 2 && widths[0] == widths[1] {
511                let mut swapped = args.clone();
512                swapped.swap(0, 1);
513                if run_eval(op, &swapped)? != a {
514                    return Err(format!(
515                        "declared commutative, but swapping the first two arguments changes \
516                         the result{}",
517                        at()
518                    ));
519                }
520            }
521            // Known bits: random partial knowledge of the arguments, checked on random
522            // completions.
523            let masks: Vec<BitVec> = widths.iter().map(|&w| rng.value(w)).collect();
524            let kb: Vec<KnownBits> = args
525                .iter()
526                .zip(&masks)
527                .map(|(v, m)| {
528                    KnownBits::new(bv_and(&bv_not(v), m), bv_and(v, m))
529                        .unwrap_or(KnownBits::unknown(v.width()))
530                })
531                .collect();
532            let out = run_known_bits(op, &sig, &kb).map_err(|e| e + &at())?;
533            for _ in 0..4 {
534                let inst: Vec<BitVec> = args
535                    .iter()
536                    .zip(&masks)
537                    .map(|(v, m)| {
538                        let r = rng.value(v.width());
539                        crate::facts::known::bv_or(&bv_and(v, m), &bv_and(&r, &bv_not(m)))
540                    })
541                    .collect();
542                for (k, (v, o)) in run_eval(op, &inst)?.iter().zip(&out).enumerate() {
543                    if !o.contains(v) {
544                        return Err(format!("known bits of output {k} exclude {v}{}", at()));
545                    }
546                }
547            }
548        }
549        // expand ≡ eval.
550        let mut cx = Context::new();
551        let syms: Vec<Expr> = widths
552            .iter()
553            .enumerate()
554            .map(|(i, &w)| cx.symbol(format!("a{i}").as_str(), w))
555            .collect::<Result<_, _>>()
556            .map_err(|e| e.to_string())?;
557        if let Some(ex) = op.expand(&mut cx, &syms) {
558            let ex = ex.map_err(|e| format!("expand failed: {e}{}", at()))?;
559            if ex.len() != sig.len() {
560                return Err(format!(
561                    "expand gave {} outputs, not {}{}",
562                    ex.len(),
563                    sig.len(),
564                    at()
565                ));
566            }
567            for _ in 0..8 {
568                let args: Vec<BitVec> = widths.iter().map(|&w| rng.value(w)).collect();
569                let env: Vec<(crate::SymbolKey, BitVec)> = args
570                    .iter()
571                    .enumerate()
572                    .map(|(i, v)| (crate::SymbolKey::from(format!("a{i}").as_str()), *v))
573                    .collect();
574                let got = cx.eval(&ex, &env[..]).map_err(|e| e.to_string())?;
575                if got != run_eval(op, &args)? {
576                    return Err(format!("expand disagrees with eval{}", at()));
577                }
578            }
579        }
580    }
581    if accepted == 0 {
582        return Err("no signature accepted at any tested argument widths".into());
583    }
584    Ok(())
585}