1use std::collections::{BTreeMap, BTreeSet, HashMap};
26use std::fmt::{self, Write as _};
27
28use super::front::TensorOpSpec;
29use super::graph::{Access, Dst, Elem, Op, Role, RowSel, Sym, TensorGraph};
30use crate::tensor_recognize::qwen::RowIndex;
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
34enum Sort {
35 Real,
36 Int,
37}
38
39impl Sort {
40 fn of(elem: Elem) -> Sort {
41 match elem {
42 Elem::Float => Sort::Real,
43 Elem::Int => Sort::Int,
44 }
45 }
46
47 fn smt(self) -> &'static str {
48 match self {
49 Sort::Real => "Real",
50 Sort::Int => "Int",
51 }
52 }
53
54 fn zero(self) -> &'static str {
55 match self {
56 Sort::Real => "0.0",
57 Sort::Int => "0",
58 }
59 }
60
61 fn tag(self) -> char {
62 match self {
63 Sort::Real => 'R',
64 Sort::Int => 'I',
65 }
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Hash)]
72enum At {
73 Fixed(usize),
74 Row { param: usize, pitch: usize },
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, Hash)]
79enum Map {
80 Units { pitch: usize, offset: usize, width: usize },
82 Transpose { rows: usize, cols: usize },
84 Row { param: usize, cols: usize },
86 Rows { indices: usize, cols: usize },
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Hash)]
93enum Arith {
94 Matmul { m: usize, k: usize, n: usize },
95 Binary { mul: bool, row: Option<usize> },
96 Scale { factor: u64 },
97}
98
99#[derive(Clone, Debug, PartialEq, Eq, Hash)]
101enum Node {
102 Start { name: String, sort: Sort, len: usize },
104 Zeros { sort: Sort, len: usize },
106 Apply { f: usize, args: Vec<usize>, len: usize },
108 Pick { src: usize, map: Map, len: usize },
110 Store { table: usize, src: usize, at: At, unit: Option<(usize, usize, usize)>, n: usize },
113 Real { src: usize },
115 Arith { op: Arith, args: Vec<usize>, sort: Sort, len: usize },
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
121struct Fun {
122 name: String,
123 args: Vec<Sort>,
124 sort: Sort,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
129struct Bound {
130 node: usize,
131 index: usize,
132 lo: i64,
133 hi: i64,
134 why: String,
135}
136
137#[derive(Default)]
139struct Terms {
140 nodes: Vec<Node>,
141 sorts: Vec<Sort>,
142 lens: Vec<usize>,
143 ids: HashMap<Node, usize>,
144 funs: Vec<Fun>,
145 fun_ids: HashMap<Fun, usize>,
146}
147
148impl Terms {
149 fn node(&mut self, node: Node) -> usize {
150 if let Some(&id) = self.ids.get(&node) {
151 return id;
152 }
153 let (sort, len) = match &node {
154 Node::Start { sort, len, .. } | Node::Zeros { sort, len } => (*sort, *len),
155 Node::Apply { f, len, .. } => (self.funs[*f].sort, *len),
156 Node::Pick { src, len, .. } => (self.sorts[*src], *len),
157 Node::Store { table, .. } => (self.sorts[*table], self.lens[*table]),
158 Node::Real { src } => (Sort::Real, self.lens[*src]),
159 Node::Arith { sort, len, .. } => (*sort, *len),
160 };
161 let id = self.nodes.len();
162 self.nodes.push(node.clone());
163 self.sorts.push(sort);
164 self.lens.push(len);
165 self.ids.insert(node, id);
166 id
167 }
168
169 fn fun(&mut self, fun: Fun) -> usize {
170 if let Some(&id) = self.fun_ids.get(&fun) {
171 return id;
172 }
173 self.funs.push(fun.clone());
174 self.fun_ids.insert(fun, self.funs.len() - 1);
175 self.funs.len() - 1
176 }
177
178 fn units(&mut self, src: usize, pitch: usize, offset: usize, width: usize, len: usize) -> usize {
180 let contiguous = len <= width || pitch == width;
181 if contiguous && offset == 0 && len == self.lens[src] {
182 return src;
183 }
184 let pitch = if contiguous { 0 } else { pitch };
185 let width = if contiguous { len } else { width };
186 self.node(Node::Pick { src, map: Map::Units { pitch, offset, width }, len })
187 }
188
189 fn as_sort(&mut self, src: usize, sort: Sort) -> usize {
191 match (self.sorts[src], sort) {
192 (Sort::Int, Sort::Real) => self.node(Node::Real { src }),
193 _ => src,
194 }
195 }
196}
197
198#[derive(Clone, Debug, PartialEq, Eq)]
200pub enum SmtError {
201 Missing { side: &'static str, op: usize, tensor: String },
203 Malformed { side: &'static str, op: usize, why: String },
205 Unkept { tensor: String },
207}
208
209impl fmt::Display for SmtError {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 SmtError::Missing { side, op, tensor } => write!(f, "{side} op {op} reads `{tensor}`, which holds no value when it runs"),
213 SmtError::Malformed { side, op, why } => write!(f, "{side} op {op}: {why}"),
214 SmtError::Unkept { tensor } => write!(f, "`{tensor}` is carried or observed, and the rewritten graph does not keep it"),
215 }
216 }
217}
218
219struct Run<'g> {
221 side: &'static str,
222 exact: bool,
224 g: &'g TensorGraph,
225 env: BTreeMap<String, usize>,
226 bounds: Vec<Bound>,
227}
228
229impl Run<'_> {
230 fn missing(&self, op: usize, tensor: &str) -> SmtError {
231 SmtError::Missing { side: self.side, op, tensor: tensor.to_string() }
232 }
233
234 fn malformed(&self, op: usize, why: impl Into<String>) -> SmtError {
235 SmtError::Malformed { side: self.side, op, why: why.into() }
236 }
237
238 fn len_of(&self, at: usize, t: &str) -> Result<usize, SmtError> {
239 self.g.tensors.get(t).map(|t| t.len()).ok_or_else(|| self.missing(at, t))
240 }
241
242 fn sort_of(&self, at: usize, t: &str) -> Result<Sort, SmtError> {
243 self.g.tensors.get(t).map(|t| Sort::of(t.elem)).ok_or_else(|| self.missing(at, t))
244 }
245
246 fn value(&self, at: usize, t: &str) -> Result<usize, SmtError> {
247 self.env.get(t).copied().ok_or_else(|| self.missing(at, t))
248 }
249
250 fn row(&mut self, terms: &Terms, at: usize, sel: &RowSel, pitch: usize, last: usize, len: usize, what: &str) -> Result<At, SmtError> {
253 let hi = if pitch == 0 || last >= len { 0 } else { ((len - 1 - last) / pitch) as i64 + 1 };
254 if hi < 1 {
255 return Err(self.malformed(at, format!("no row of {what} holds {} values", last + 1)));
256 }
257 match sel {
258 RowSel::Fixed(r) if *r >= 1 && (*r as i64) <= hi => Ok(At::Fixed((r - 1) * pitch)),
259 RowSel::Fixed(r) => Err(self.malformed(at, format!("row {r} of {what}"))),
260 RowSel::Param(p) => {
261 let param = self.value(at, p)?;
262 if terms.lens[param] != 1 || terms.sorts[param] != Sort::Int {
263 return Err(self.malformed(at, format!("`{p}` names the row of {what} and is not one Int")));
264 }
265 self.bounds.push(Bound { node: param, index: 0, lo: 1, hi, why: format!("`{p}` names a row of {what}") });
266 Ok(At::Row { param, pitch })
267 }
268 }
269 }
270
271 fn read(&self, terms: &mut Terms, at: usize, op: &Op, k: usize) -> Result<usize, SmtError> {
273 let whole = self.value(at, &op.ins[k])?;
274 match op.access.get(k).copied().unwrap_or(Access::Whole) {
275 Access::Whole => Ok(whole),
276 Access::Strided(s) => {
277 let len = terms.lens[whole];
278 if s.pitch == 0 || s.offset + s.width > s.pitch || len % s.pitch != 0 {
279 return Err(self.malformed(at, format!("{len} values of `{}` are no whole units of {} to read {} from {} of", op.ins[k], s.pitch, s.width, s.offset)));
280 }
281 Ok(terms.units(whole, s.pitch, s.offset, s.width, len / s.pitch * s.width))
282 }
283 }
284 }
285
286 fn land(&mut self, terms: &mut Terms, at: usize, dst: &Dst, result: usize) -> Result<(), SmtError> {
288 if let Some(name) = dst.own() {
289 self.env.insert(name.to_string(), result);
290 return Ok(());
291 }
292 let table = match self.env.get(&dst.buf) {
293 Some(&t) => t,
294 None => {
295 let (sort, len) = (self.sort_of(at, &dst.buf)?, self.len_of(at, &dst.buf)?);
296 terms.node(Node::Zeros { sort, len })
297 }
298 };
299 if terms.sorts[table] != terms.sorts[result] {
300 return Err(self.malformed(at, format!("a result of one element type landing in `{}` of another", dst.buf)));
301 }
302 let (n, len) = (terms.lens[result], terms.lens[table]);
303 let last = match dst.unit {
304 None => n.saturating_sub(1),
305 Some(u) => {
306 if u.width == 0 || n % u.width != 0 || u.offset + u.width > u.pitch {
307 return Err(self.malformed(at, format!("{n} values in shares of {} at {} of units of {}", u.width, u.offset, u.pitch)));
308 }
309 (n / u.width).saturating_sub(1) * u.pitch + u.offset + u.width - 1
310 }
311 };
312 let place = match &dst.row {
313 Some((sel, pitch)) => self.row(terms, at, sel, *pitch, last, len, &format!("`{}`", dst.buf))?,
314 None if last < len || n == 0 => At::Fixed(0),
315 None => return Err(self.malformed(at, format!("{n} values past the {len} of `{}`", dst.buf))),
316 };
317 let unit = dst.unit.map(|u| (u.pitch, u.offset, u.width));
318 let stored = terms.node(Node::Store { table, src: result, at: place, unit, n });
319 self.env.insert(dst.buf.clone(), stored);
320 Ok(())
321 }
322
323 fn op(&mut self, terms: &mut Terms, at: usize, op: &Op) -> Result<(), SmtError> {
324 match &op.sym {
325 Sym::Fused(_) => {
326 let before: BTreeSet<String> = self.env.keys().cloned().collect();
327 self.parts(terms, at, op)?;
328 let outlive: BTreeSet<&str> = op.writes().into_iter().collect();
329 self.env.retain(|k, _| before.contains(k) || outlive.contains(k.as_str()));
330 Ok(())
331 }
332 Sym::View { offset } => {
333 let src = self.read(terms, at, op, 0)?;
334 let len = self.len_of(at, &op.out)?;
335 if offset + len > terms.lens[src] {
336 return Err(self.malformed(at, format!("a view of {len} values from {offset} into {} values", terms.lens[src])));
337 }
338 let v = terms.units(src, 0, *offset, len, len);
339 self.land(terms, at, &op.dst, v)
340 }
341 Sym::Quantize(_) => {
342 let x = self.read(terms, at, op, 0)?;
343 self.land(terms, at, &op.dst, x)
344 }
345 Sym::Prim(spec) => {
346 let ins: Vec<usize> = (0..op.ins.len()).map(|k| self.read(terms, at, op, k)).collect::<Result<_, _>>()?;
347 let (result, state) = self.prim(terms, at, op, spec, &ins)?;
348 for (k, v) in state {
349 self.env.insert(op.ins[k].clone(), v);
350 }
351 match result {
352 Some(r) => self.land(terms, at, &op.dst, r),
353 None => Ok(()),
354 }
355 }
356 }
357 }
358
359 fn parts(&mut self, terms: &mut Terms, at: usize, op: &Op) -> Result<(), SmtError> {
361 let Sym::Fused(f) = &op.sym else { return self.op(terms, at, op) };
362 for part in &f.parts {
363 match part.sym {
364 Sym::Fused(_) => self.parts(terms, at, part)?,
365 _ => self.op(terms, at, part)?,
366 }
367 }
368 Ok(())
369 }
370
371 fn index(&mut self, terms: &Terms, at: usize, i: &RowIndex, op: &Op, k: usize, pitch: usize, last: usize, len: usize, what: &str) -> Result<At, SmtError> {
373 match i {
374 RowIndex::Literal(n) if *n >= 1 => self.row(terms, at, &RowSel::Fixed(*n as usize), pitch, last, len, what),
375 RowIndex::Literal(n) => Err(self.malformed(at, format!("row {n} of {what}"))),
376 RowIndex::Named(_) => {
377 let name = op.ins.get(k).ok_or_else(|| self.malformed(at, "a named row with no operand naming it"))?.clone();
378 self.row(terms, at, &RowSel::Param(name), pitch, last, len, what)
379 }
380 }
381 }
382
383 #[allow(clippy::type_complexity)]
386 fn prim(&mut self, terms: &mut Terms, at: usize, op: &Op, spec: &TensorOpSpec, ins: &[usize]) -> Result<(Option<usize>, Vec<(usize, usize)>), SmtError> {
387 let name = |k: usize| format!("`{}`", op.ins[k]);
388 match spec {
389 TensorOpSpec::HeadSlice(s) => {
390 let len = s.n / s.every.max(1) * s.width;
391 Ok((Some(terms.units(ins[0], s.every, s.from, s.width, len)), Vec::new()))
392 }
393 TensorOpSpec::Transpose(t) => {
394 let picked = terms.node(Node::Pick { src: ins[0], map: Map::Transpose { rows: t.rows, cols: t.cols }, len: t.rows * t.cols });
395 Ok((Some(picked), Vec::new()))
396 }
397 TensorOpSpec::RowGather(r) => {
398 let what = format!("{}'s {} rows", name(0), r.rows);
399 let place = self.index(terms, at, &r.index, op, 1, r.cols, r.cols - 1, r.rows * r.cols, &what)?;
400 let picked = match place {
401 At::Fixed(from) => terms.units(ins[0], 0, from, r.cols, r.cols),
402 At::Row { param, .. } => terms.node(Node::Pick { src: ins[0], map: Map::Row { param, cols: r.cols }, len: r.cols }),
403 };
404 Ok((Some(picked), Vec::new()))
405 }
406 TensorOpSpec::RowsGather(r) => {
407 let indices = ins[1];
408 if terms.sorts[indices] != Sort::Int || terms.lens[indices] < r.count {
409 return Err(self.malformed(at, format!("{} holds no {} Int row indices", name(1), r.count)));
410 }
411 for index in 0..r.count {
412 self.bounds.push(Bound { node: indices, index, lo: 1, hi: r.rows as i64, why: format!("{} names rows of {}", name(1), name(0)) });
413 }
414 let picked = terms.node(Node::Pick { src: ins[0], map: Map::Rows { indices, cols: r.cols }, len: r.count * r.cols });
415 Ok((Some(picked), Vec::new()))
416 }
417 TensorOpSpec::Join(j) => {
418 let (ha, hb) = (j.wa / j.heads.max(1), j.wb / j.heads.max(1));
419 let units = j.rows * j.heads;
420 let sort = if terms.sorts[ins[0]] == Sort::Int && terms.sorts[ins[1]] == Sort::Int { Sort::Int } else { Sort::Real };
421 let (a, b) = (terms.as_sort(ins[0], sort), terms.as_sort(ins[1], sort));
422 let zeros = terms.node(Node::Zeros { sort, len: units * (ha + hb) });
423 let with_a = terms.node(Node::Store { table: zeros, src: a, at: At::Fixed(0), unit: Some((ha + hb, 0, ha)), n: units * ha });
424 let joined = terms.node(Node::Store { table: with_a, src: b, at: At::Fixed(0), unit: Some((ha + hb, ha, hb)), n: units * hb });
425 Ok((Some(joined), Vec::new()))
426 }
427 TensorOpSpec::SetRow(r) => {
428 let n = r.cols * r.count;
429 let what = format!("{}'s {} rows", name(0), r.rows);
430 let place = self.index(terms, at, &r.index, op, 2, r.cols, n.saturating_sub(1), r.rows * r.cols, &what)?;
431 let table = ins[0];
432 if terms.sorts[table] == Sort::Int && terms.sorts[ins[1]] == Sort::Real {
433 return Err(self.malformed(at, format!("Reals written into {}, a table of Ints", name(0))));
434 }
435 let src = terms.as_sort(ins[1], terms.sorts[table]);
436 let stored = terms.node(Node::Store { table, src, at: place, unit: None, n });
437 Ok((None, vec![(0, stored)]))
438 }
439 TensorOpSpec::Matmul(_) | TensorOpSpec::Elementwise(_) | TensorOpSpec::Scale(_) if self.exact => {
440 let op_ = match spec {
441 TensorOpSpec::Matmul(m) => Arith::Matmul { m: m.m, k: m.k, n: m.n },
442 TensorOpSpec::Elementwise(e) => Arith::Binary { mul: e.op == crate::tensor_recognize::ElementwiseOp::Mul, row: e.row },
443 TensorOpSpec::Scale(s) => Arith::Scale { factor: s.factor.get().to_bits() },
444 _ => unreachable!("the arms above"),
445 };
446 let (sort, len) = (self.sort_of(at, &op.out)?, self.len_of(at, &op.out)?);
447 let args: Vec<usize> = ins.iter().map(|&i| terms.as_sort(i, sort)).collect();
448 Ok((Some(terms.node(Node::Arith { op: op_, args, sort, len })), Vec::new()))
449 }
450 _ => {
451 let args: Vec<Sort> = ins.iter().map(|&i| terms.sorts[i]).collect();
452 let shape = shape_of(spec);
453 let open = |terms: &mut Terms, out: usize, sort: Sort, len: usize| {
454 let f = terms.fun(Fun { name: format!("{}!{}!{}", spec.kind(), shape, out), args: args.clone(), sort });
455 terms.node(Node::Apply { f, args: ins.to_vec(), len })
456 };
457 let mut state = Vec::new();
458 for (n, k) in spec.state_inputs().into_iter().enumerate() {
459 let (sort, len) = (terms.sorts[ins[k]], terms.lens[ins[k]]);
460 state.push((k, open(terms, n + 1, sort, len)));
461 }
462 let (sort, len) = (self.sort_of(at, &op.out)?, self.len_of(at, &op.out)?);
463 Ok((Some(open(terms, 0, sort, len)), state))
464 }
465 }
466 }
467}
468
469fn shape_of(spec: &TensorOpSpec) -> String {
472 let text = format!("{spec:?}");
473 let mut bare = String::new();
474 let mut rest = text.as_str();
475 while let Some(i) = rest.find("Symbol(") {
476 bare.push_str(&rest[..i]);
477 rest = &rest[i..];
478 let end = rest.find(')').map_or(rest.len(), |e| e + 1);
479 rest = &rest[end..];
480 }
481 bare.push_str(rest);
482 hex(&crate::crypto_accel::sha256(bare.as_bytes())[..8])
483}
484
485fn hex(bytes: &[u8]) -> String {
486 bytes.iter().map(|b| format!("{b:02x}")).collect()
487}
488
489#[derive(Clone, Debug, PartialEq, Eq)]
493pub struct Lemma {
494 pub premises: String,
496 pub goal: String,
498}
499
500impl Lemma {
501 pub fn text(&self) -> String {
503 format!("{}{}", self.premises, self.goal)
504 }
505}
506
507#[derive(Clone, Debug, PartialEq, Eq)]
513pub struct Obligation {
514 pub lemmas: Vec<Lemma>,
516 pub compared: usize,
518 pub identical: usize,
520 pub assumed: Vec<String>,
522 header: String,
524}
525
526impl Obligation {
527 pub fn text(&self) -> String {
529 let mut t = self.header.clone();
530 for l in &self.lemmas {
531 t.push_str(&l.text());
532 }
533 t
534 }
535
536 pub fn digest(&self) -> String {
538 hex(&crate::crypto_accel::sha256(self.text().as_bytes()))
539 }
540}
541
542fn run<'g>(terms: &mut Terms, side: &'static str, exact: bool, g: &'g TensorGraph) -> Result<Run<'g>, SmtError> {
543 let mut r = Run { side, exact, g, env: BTreeMap::new(), bounds: Vec::new() };
544 for t in g.tensors.values().filter(|t| t.role != Role::Activation || t.fill.is_some()) {
545 let start = terms.node(Node::Start { name: t.name.clone(), sort: Sort::of(t.elem), len: t.len() });
546 r.env.insert(t.name.clone(), start);
547 }
548 for (at, op) in g.ops.iter().enumerate() {
549 r.op(terms, at, op)?;
550 }
551 Ok(r)
552}
553
554pub fn obligation(reference: &TensorGraph, rewritten: &TensorGraph) -> Result<Obligation, SmtError> {
558 obligation_under(reference, rewritten, false)
559}
560
561pub fn identity(lhs: &TensorGraph, rhs: &TensorGraph) -> Result<Obligation, SmtError> {
565 obligation_under(lhs, rhs, true)
566}
567
568fn obligation_under(reference: &TensorGraph, rewritten: &TensorGraph, exact: bool) -> Result<Obligation, SmtError> {
569 let mut terms = Terms::default();
570 let a = run(&mut terms, "reference", exact, reference)?;
571 let b = run(&mut terms, "rewritten", exact, rewritten)?;
572 for t in reference.tensors.values().filter(|t| t.role == Role::State || t.observed) {
573 if a.env.contains_key(&t.name) && !b.env.contains_key(&t.name) {
574 return Err(SmtError::Unkept { tensor: t.name.clone() });
575 }
576 }
577 let starts: BTreeMap<&str, usize> = reference
578 .tensors
579 .values()
580 .filter_map(|t| terms.ids.get(&Node::Start { name: t.name.clone(), sort: Sort::of(t.elem), len: t.len() }).map(|&id| (t.name.as_str(), id)))
581 .collect();
582 let goals: Vec<(&str, usize, usize)> = b
583 .env
584 .iter()
585 .filter_map(|(name, &y)| a.env.get(name).map(|&x| (name.as_str(), x, y)))
586 .filter(|(name, x, y)| starts.get(name) != Some(x) || starts.get(name) != Some(y))
587 .collect();
588 let key = |b: &Bound| (b.node, b.index, b.lo, b.hi);
589 let assumed_bounds: BTreeSet<(usize, usize, i64, i64)> = a.bounds.iter().map(key).collect();
590 let mut owed: Vec<&Bound> = b.bounds.iter().filter(|x| !assumed_bounds.contains(&key(x))).collect();
591 owed.sort();
592 owed.dedup_by_key(|b| key(b));
593 let (alike, differ): (Vec<_>, Vec<_>) = goals.iter().partition(|(_, x, y)| x == y);
594
595 let mut header = String::from("; tensor_graph::smt — the rewritten graph means what the reference means\n");
596 for (name, x, y) in &goals {
597 let _ = writeln!(header, "; {} `{name}`", if x == y { "one term on both sides:" } else { "compared:" });
598 }
599
600 let mut facts: Vec<(usize, usize, i64, i64, String)> = Vec::new();
603 for t in reference.tensors.values() {
604 let (Some((lo, hi)), Some(&node)) = (t.range, starts.get(t.name.as_str())) else { continue };
605 if t.elem != Elem::Int || (lo == i64::MIN && hi == i64::MAX) {
606 continue;
607 }
608 for index in 0..t.len() {
609 facts.push((node, index, lo, hi, format!("`{}` holds [{lo}, {hi}], the interval the program proves", t.name)));
610 }
611 }
612 let mut known = BTreeSet::new();
613 for bound in &a.bounds {
614 if known.insert(key(bound)) {
615 facts.push((bound.node, bound.index, bound.lo, bound.hi, format!("{}, as the reference reads it", bound.why)));
616 }
617 }
618
619 let mut lemmas = Vec::new();
622 let mut assumed: Vec<String> = Vec::new();
623 let mut proven: Vec<(usize, usize)> = Vec::new();
624 loop {
625 let class = classes(&terms, &proven);
626 let mut pairs: Vec<(usize, usize, &str)> = Vec::new();
627 let mut seen: BTreeSet<(usize, usize)> = BTreeSet::new();
628 let mut work: Vec<(usize, usize, &str)> = differ.iter().rev().map(|(name, x, y)| (*x, *y, *name)).collect();
629 while let Some((x, y, from)) = work.pop() {
630 if class[x] == class[y] || !seen.insert((x, y)) {
631 continue;
632 }
633 match congruent(&terms, x, y) {
634 Some(kids) => work.extend(kids.into_iter().rev().map(|(cx, cy)| (cx, cy, from))),
635 None => pairs.push((x, y, from)),
636 }
637 }
638 let Some(&(x, y, from)) = pairs.iter().min_by_key(|(x, y, _)| (*x.max(y), *x.min(y))) else { break };
639 assert!(!proven.contains(&(x.min(y), x.max(y))), "a proven pair is one class: `{from}` chosen twice");
640 let (premises, cited) = premises_of(&terms, &class, &[x, y], &[x, y], &facts, &format!("`{from}` is one term on both sides"));
641 let (lx, ly) = (cited.local[&x], cited.local[&y]);
642 let goal = if cited.arithmetic {
645 let len = terms.lens[x].max(terms.lens[y]);
646 let each: Vec<String> = (0..len).map(|i| format!("(= (select |n{lx}| {i}) (select |n{ly}| {i}))")).collect();
647 format!("(assert (not (and true {})))\n", each.join(" "))
648 } else {
649 format!("(assert (not (= |n{lx}| |n{ly}|)))\n")
650 };
651 lemmas.push(Lemma { premises, goal });
652 assumed.extend(cited.assumed);
653 proven.push((x.min(y), x.max(y)));
654 }
655 if !owed.is_empty() {
656 let nodes: Vec<usize> = owed.iter().map(|b| b.node).collect();
657 let class = classes(&terms, &proven);
658 let (mut premises, cited) = premises_of(&terms, &class, &nodes, &[], &facts, "the rewritten graph reads in range");
659 for bound in &owed {
660 let _ = writeln!(premises, "; owed: {}, as the rewritten graph reads it", bound.why);
661 }
662 let reads: Vec<String> = owed.iter().map(|b| within(cited.local[&b.node], b.index, b.lo, b.hi)).collect();
663 lemmas.push(Lemma { premises, goal: format!("(assert (not (and true {})))\n", reads.join(" ")) });
664 assumed.extend(cited.assumed);
665 }
666 let mut once = BTreeSet::new();
667 assumed.retain(|a| once.insert(a.clone()));
668 Ok(Obligation { lemmas, compared: goals.len(), identical: alike.len(), assumed, header })
669}
670
671fn congruent(terms: &Terms, x: usize, y: usize) -> Option<Vec<(usize, usize)>> {
674 match (&terms.nodes[x], &terms.nodes[y]) {
675 (Node::Apply { f: f1, args: a1, len: l1 }, Node::Apply { f: f2, args: a2, len: l2 }) if f1 == f2 && l1 == l2 && a1.len() == a2.len() => {
676 Some(a1.iter().copied().zip(a2.iter().copied()).collect())
677 }
678 (Node::Real { src: s1 }, Node::Real { src: s2 }) => Some(vec![(*s1, *s2)]),
679 _ => None,
680 }
681}
682
683fn renamed(node: &Node, f: &dyn Fn(usize) -> usize) -> Node {
685 let at = |a: &At| match a {
686 At::Fixed(s) => At::Fixed(*s),
687 At::Row { param, pitch } => At::Row { param: f(*param), pitch: *pitch },
688 };
689 match node {
690 Node::Start { .. } | Node::Zeros { .. } => node.clone(),
691 Node::Apply { f: fun, args, len } => Node::Apply { f: *fun, args: args.iter().map(|&a| f(a)).collect(), len: *len },
692 Node::Pick { src, map, len } => {
693 let map = match map {
694 Map::Row { param, cols } => Map::Row { param: f(*param), cols: *cols },
695 Map::Rows { indices, cols } => Map::Rows { indices: f(*indices), cols: *cols },
696 Map::Units { .. } | Map::Transpose { .. } => map.clone(),
697 };
698 Node::Pick { src: f(*src), map, len: *len }
699 }
700 Node::Store { table, src, at: place, unit, n } => Node::Store { table: f(*table), src: f(*src), at: at(place), unit: *unit, n: *n },
701 Node::Real { src } => Node::Real { src: f(*src) },
702 Node::Arith { op, args, sort, len } => Node::Arith { op: op.clone(), args: args.iter().map(|&a| f(a)).collect(), sort: *sort, len: *len },
703 }
704}
705
706fn classes(terms: &Terms, proven: &[(usize, usize)]) -> Vec<usize> {
711 fn root(parent: &[usize], mut x: usize) -> usize {
712 while parent[x] != x {
713 x = parent[x];
714 }
715 x
716 }
717 fn join(parent: &mut [usize], a: usize, b: usize) {
718 let (ra, rb) = (root(parent, a), root(parent, b));
719 parent[ra.max(rb)] = ra.min(rb);
720 }
721 let mut parent: Vec<usize> = (0..terms.nodes.len()).collect();
722 let mut partners: HashMap<usize, Vec<usize>> = HashMap::new();
723 for &(a, b) in proven {
724 partners.entry(a.max(b)).or_default().push(a.min(b));
725 }
726 let mut seen: HashMap<Node, usize> = HashMap::new();
727 for (n, node) in terms.nodes.iter().enumerate() {
728 let signature = renamed(node, &|k| root(&parent, k));
729 match seen.get(&signature) {
730 Some(&m) => join(&mut parent, n, m),
731 None => {
732 seen.insert(signature, n);
733 }
734 }
735 for &p in partners.get(&n).into_iter().flatten() {
736 join(&mut parent, n, p);
737 }
738 }
739 (0..terms.nodes.len()).map(|n| root(&parent, n)).collect()
740}
741
742fn reach(terms: &Terms, class: &[usize], from: usize) -> BTreeSet<usize> {
744 let mut out = BTreeSet::new();
745 let mut seen = BTreeSet::new();
746 let mut stack = vec![from];
747 while let Some(n) = stack.pop() {
748 if seen.insert(n) {
749 out.insert(class[n]);
750 stack.extend(children(&terms.nodes[n]));
751 }
752 }
753 out
754}
755
756struct Cited {
758 local: BTreeMap<usize, usize>,
759 assumed: Vec<String>,
760 arithmetic: bool,
761}
762
763fn premises_of(terms: &Terms, class: &[usize], roots: &[usize], split: &[usize], facts: &[(usize, usize, i64, i64, String)], what: &str) -> (String, Cited) {
767 let reaches: Vec<BTreeSet<usize>> = split.iter().map(|&r| reach(terms, class, r)).collect();
768 let shared = |n: usize| reaches.iter().filter(|r| r.contains(&class[n])).count() > 1;
769 let mut defined = BTreeSet::new();
770 let mut opaque: BTreeMap<usize, BTreeSet<usize>> = BTreeMap::new();
771 let mut stack: Vec<usize> = roots.to_vec();
772 let mut visited = BTreeSet::new();
773 while let Some(n) = stack.pop() {
774 if !visited.insert(n) {
775 continue;
776 }
777 let leaf = matches!(terms.nodes[n], Node::Start { .. } | Node::Zeros { .. });
778 if !roots.contains(&n) && !leaf && (shared(n) || split.is_empty()) {
779 opaque.entry(class[n]).or_default().insert(n);
780 } else {
781 defined.insert(n);
782 stack.extend(children(&terms.nodes[n]));
783 }
784 }
785 let mut names: BTreeMap<usize, usize> = BTreeMap::new();
787 for &n in &defined {
788 names.insert(n, n);
789 }
790 for members in opaque.values() {
791 let first = *members.iter().next().expect("a class has a member");
792 for &m in members {
793 names.insert(m, first);
794 }
795 }
796 let shown: BTreeSet<usize> = names.values().copied().collect();
797 let order: BTreeMap<usize, usize> = shown.iter().enumerate().map(|(k, &n)| (n, k)).collect();
798 let local: BTreeMap<usize, usize> = names.iter().map(|(&n, first)| (n, order[first])).collect();
799 let opaque_names: BTreeSet<usize> = opaque.values().map(|m| *m.iter().next().expect("a member")).collect();
800 let mut text = format!("; lemma: {what}\n(set-logic ALL)\n");
801 let mut declared: BTreeMap<&str, Sort> = BTreeMap::new();
802 let mut funs: BTreeSet<usize> = BTreeSet::new();
803 for &n in &defined {
804 match &terms.nodes[n] {
805 Node::Start { name, sort, .. } => {
806 declared.insert(name, *sort);
807 }
808 Node::Apply { f, .. } => {
809 funs.insert(*f);
810 }
811 _ => {}
812 }
813 }
814 for (name, sort) in &declared {
815 let _ = writeln!(text, "(declare-const |in!{name}| (Array Int {}))", sort.smt());
816 }
817 for &n in &opaque_names {
818 let _ = writeln!(text, "(declare-const |o!{}| (Array Int {}))", local[&n], terms.sorts[n].smt());
819 }
820 let mut fun_lines: Vec<String> = funs
821 .iter()
822 .map(|&f| {
823 let fun = &terms.funs[f];
824 let args: Vec<String> = fun.args.iter().map(|s| format!("(Array Int {})", s.smt())).collect();
825 let sig: String = fun.args.iter().map(|s| s.tag()).collect();
826 format!("(declare-fun |f!{}!{sig}| ({}) (Array Int {}))", fun.name, args.join(" "), fun.sort.smt())
827 })
828 .collect();
829 fun_lines.sort();
830 for line in fun_lines {
831 text.push_str(&line);
832 text.push('\n');
833 }
834 for &n in &shown {
835 let body = if opaque_names.contains(&n) {
836 let (sort, len) = (terms.sorts[n], terms.lens[n]);
837 format!("(lambda ((j Int)) (ite (and (<= 0 j) (< j {len})) (select |o!{}| j) {}))", local[&n], sort.zero())
838 } else {
839 expr(terms, n, &local)
840 };
841 let _ = writeln!(text, "(define-fun |n{}| () (Array Int {}) {body})", local[&n], terms.sorts[n].smt());
842 }
843 let mut assumed = Vec::new();
844 for (node, index, lo, hi, why) in facts.iter().filter(|f| local.contains_key(&f.0)) {
845 let _ = writeln!(text, "; assumed: {why}\n(assert {})", within(local[node], *index, *lo, *hi));
846 assumed.push(why.clone());
847 }
848 let arithmetic = defined.iter().any(|&n| matches!(terms.nodes[n], Node::Arith { .. }));
849 (text, Cited { local, assumed, arithmetic })
850}
851
852fn children(node: &Node) -> Vec<usize> {
853 match node {
854 Node::Start { .. } | Node::Zeros { .. } => Vec::new(),
855 Node::Apply { args, .. } => args.clone(),
856 Node::Pick { src, map, .. } => {
857 let mut c = vec![*src];
858 match map {
859 Map::Row { param, .. } => c.push(*param),
860 Map::Rows { indices, .. } => c.push(*indices),
861 Map::Units { .. } | Map::Transpose { .. } => {}
862 }
863 c
864 }
865 Node::Store { table, src, at, .. } => {
866 let mut c = vec![*table, *src];
867 if let At::Row { param, .. } = at {
868 c.push(*param);
869 }
870 c
871 }
872 Node::Real { src } => vec![*src],
873 Node::Arith { args, .. } => args.clone(),
874 }
875}
876
877fn within(node: usize, index: usize, lo: i64, hi: i64) -> String {
878 format!("(and (<= {} (select |n{node}| {index})) (<= (select |n{node}| {index}) {}))", int(lo), int(hi))
879}
880
881fn int(v: i64) -> String {
882 if v < 0 {
883 format!("(- {})", v.unsigned_abs())
884 } else {
885 v.to_string()
886 }
887}
888
889fn expr(terms: &Terms, n: usize, local: &BTreeMap<usize, usize>) -> String {
891 let (sort, len) = (terms.sorts[n], terms.lens[n]);
892 let id = |x: &usize| local[x];
893 let clamp = |body: String| format!("(lambda ((j Int)) (ite (and (<= 0 j) (< j {len})) {body} {}))", sort.zero());
894 match &terms.nodes[n] {
895 Node::Start { name, .. } => clamp(format!("(select |in!{name}| j)")),
896 Node::Zeros { .. } => format!("((as const (Array Int {})) {})", sort.smt(), sort.zero()),
897 Node::Apply { f, args, .. } => {
898 let fun = &terms.funs[*f];
899 let sig: String = fun.args.iter().map(|s| s.tag()).collect();
900 let call = if args.is_empty() {
901 format!("|f!{}!{sig}|", fun.name)
902 } else {
903 format!("(|f!{}!{sig}| {})", fun.name, args.iter().map(|a| format!("|n{}|", id(a))).collect::<Vec<_>>().join(" "))
904 };
905 clamp(format!("(select {call} j)"))
906 }
907 Node::Pick { src, map, .. } => {
908 let index = match map {
909 Map::Units { pitch: 0, offset, .. } => format!("(+ j {offset})"),
910 Map::Units { pitch, offset, width } => format!("(+ (* (div j {width}) {pitch}) {offset} (mod j {width}))"),
911 Map::Transpose { rows, cols } => format!("(+ (* (mod j {rows}) {cols}) (div j {rows}))"),
912 Map::Row { param, cols } => format!("(+ (* (- (select |n{}| 0) 1) {cols}) j)", id(param)),
913 Map::Rows { indices, cols } => format!("(+ (* (- (select |n{}| (div j {cols})) 1) {cols}) (mod j {cols}))", id(indices)),
914 };
915 clamp(format!("(select |n{}| {index})", id(src)))
916 }
917 Node::Store { table, src, at, unit, n: count } => {
918 let start = match at {
919 At::Fixed(s) => s.to_string(),
920 At::Row { param, pitch } => format!("(* (- (select |n{}| 0) 1) {pitch})", id(param)),
921 };
922 let d = format!("(- j {start})");
923 let (inside, from) = match unit {
924 None => (format!("(and (<= 0 {d}) (< {d} {count}))"), d.clone()),
925 Some((pitch, offset, width)) => (
926 format!("(and (<= 0 {d}) (< (div {d} {pitch}) {}) (<= {offset} (mod {d} {pitch})) (< (mod {d} {pitch}) {}))", count / width, offset + width),
927 format!("(+ (* (div {d} {pitch}) {width}) (- (mod {d} {pitch}) {offset}))"),
928 ),
929 };
930 clamp(format!("(ite {inside} (select |n{}| {from}) (select |n{}| j))", id(src), id(table)))
931 }
932 Node::Real { src } => clamp(format!("(to_real (select |n{}| j))", id(src))),
933 Node::Arith { op, args, .. } => {
934 let at = |k: usize, index: String| format!("(select |n{}| {index})", id(&args[k]));
935 clamp(match op {
936 Arith::Matmul { k: 0, .. } => sort.zero().to_string(),
937 Arith::Matmul { k, n: cols, .. } => {
938 let terms: Vec<String> = (0..*k)
939 .map(|kk| format!("(* {} {})", at(0, format!("(+ (* (div j {cols}) {k}) {kk})")), at(1, format!("(+ {} (mod j {cols}))", kk * cols))))
940 .collect();
941 if terms.len() == 1 {
942 terms[0].clone()
943 } else {
944 format!("(+ {})", terms.join(" "))
945 }
946 }
947 Arith::Binary { mul, row } => {
948 let b = row.map_or_else(|| at(1, "j".to_string()), |w| at(1, format!("(mod j {w})")));
949 format!("({} {} {b})", if *mul { "*" } else { "+" }, at(0, "j".to_string()))
950 }
951 Arith::Scale { factor } => format!("(* {} {})", exact_real(f64::from_bits(*factor)), at(0, "j".to_string())),
952 })
953 }
954 }
955}
956
957fn exact_real(v: f64) -> String {
959 let bits = v.to_bits();
960 let negative = bits >> 63 == 1;
961 let exponent = ((bits >> 52) & 0x7ff) as i64;
962 let fraction = bits & ((1u64 << 52) - 1);
963 let (mut mantissa, mut power) = if exponent == 0 { (fraction, -1074) } else { (fraction | (1u64 << 52), exponent - 1075) };
964 if mantissa == 0 {
965 return "0.0".to_string();
966 }
967 while mantissa % 2 == 0 && power < 0 {
968 mantissa /= 2;
969 power += 1;
970 }
971 let magnitude = if power == 0 {
972 format!("{mantissa}.0")
973 } else if power > 0 {
974 format!("(* {mantissa}.0 {}.0)", num_pow2(power as u32))
975 } else {
976 format!("(/ {mantissa}.0 {}.0)", num_pow2(-power as u32))
977 };
978 if negative {
979 format!("(- {magnitude})")
980 } else {
981 magnitude
982 }
983}
984
985fn num_pow2(e: u32) -> String {
987 let mut digits = vec![1u8];
988 for _ in 0..e {
989 let mut carry = 0u8;
990 for d in digits.iter_mut() {
991 let v = *d * 2 + carry;
992 *d = v % 10;
993 carry = v / 10;
994 }
995 if carry > 0 {
996 digits.push(carry);
997 }
998 }
999 digits.iter().rev().map(|d| (b'0' + d) as char).collect()
1000}