Skip to main content

sva_engine/instantiate/
mod.rs

1// Concern: holds one instance per argument tuple and resolves a name inside one | Non-concern: building the instances (build.rs), writing a resolved walk out (resolved.rs) | IO: (path) -> a body, a Node
2
3mod build;
4mod edges;
5mod resolved;
6
7use std::collections::{BTreeMap, BTreeSet, HashMap};
8use std::sync::Arc;
9
10use sva_ast::{Address, Arg, BinOp, ByteSpan, Defined, Expr, Literal};
11
12use crate::error::EngineError;
13use crate::time::Grid;
14use sva_ast::is_builtin;
15
16use build::Journal;
17pub use build::{from_roots, instantiate};
18use edges::Edges;
19
20pub(crate) type ScopeId = u32;
21
22/// Scope 0 binds nothing, reserved for defaults: a default stands outside every invocation.
23pub(crate) const NO_PARAMS: ScopeId = 0;
24
25#[derive(Clone, Copy, Debug)]
26pub struct Thunk<'a> {
27    pub(crate) expr: &'a Expr,
28    pub(crate) scope: ScopeId,
29}
30
31/// An expression owned where it was written: a node's line and each child taken down to it.
32#[derive(Clone, Debug)]
33pub(crate) struct Written {
34    defined: Arc<Defined>,
35    path: Arc<[u32]>,
36}
37
38impl Written {
39    /// `line` 0 is the body, `k` the default line `k - 1`.
40    pub(crate) fn of(defined: &Arc<Defined>, line: u32) -> Written {
41        Written {
42            defined: Arc::clone(defined),
43            path: [line].into(),
44        }
45    }
46
47    pub(crate) fn expr(&self) -> &Expr {
48        let (line, path) = self.path.split_first().expect("a line");
49        let top = match *line {
50            0 => &self.defined.body,
51            k => &self.defined.defaults[k as usize - 1].1,
52        };
53        let mut at = top;
54        for k in path {
55            at = child(at, *k).expect("a path down this expression");
56        }
57        at
58    }
59
60    /// Whether this is an argument a call was given by position, not by name.
61    pub(crate) fn is_positional(&self) -> bool {
62        let Some((last, upper)) = self.path[1..].split_last() else {
63            return false;
64        };
65        let parent = Written {
66            defined: Arc::clone(&self.defined),
67            path: [&self.path[..1], upper].concat().into(),
68        };
69        match parent.expr() {
70            Expr::Call { args, .. } => matches!(args.get(*last as usize), Some(Arg::Pos(_))),
71            _ => false,
72        }
73    }
74}
75
76/// A child's number on a walk down, as [`child`] reads it.
77#[derive(Clone, Copy)]
78pub(crate) enum Child {
79    First,
80    Right,
81    Arg(u32),
82    Bind(u32),
83}
84
85impl Child {
86    pub(crate) fn index(self) -> u32 {
87        match self {
88            Child::First => 0,
89            Child::Right => 1,
90            Child::Arg(k) => k,
91            Child::Bind(k) => k + 1,
92        }
93    }
94}
95
96fn child(e: &Expr, k: u32) -> Option<&Expr> {
97    let k = k as usize;
98    match e {
99        Expr::Bin(_, l, r) => [l, r].get(k).map(|c| &***c),
100        Expr::Call { args, .. } => args.get(k).map(|(Arg::Pos(x) | Arg::Named(_, x))| x),
101        Expr::Ref { arg, binds, .. } => match k {
102            0 => Some(arg),
103            k => binds.get(k - 1).map(|(_, x)| x),
104        },
105        Expr::SelfRef { arg, .. } | Expr::Indexed { arg, .. } => (k == 0).then_some(&**arg),
106        Expr::Lit(_) | Expr::Var(_) => None,
107    }
108}
109
110#[derive(Clone, Debug)]
111pub(crate) struct Bound {
112    pub(crate) written: Written,
113    pub(crate) scope: ScopeId,
114}
115
116impl Bound {
117    pub(crate) fn thunk(&self) -> Thunk<'_> {
118        Thunk {
119            expr: self.written.expr(),
120            scope: self.scope,
121        }
122    }
123}
124
125/// Sorted by name: the tuple names the instance. Held while owned or read in.
126#[derive(Debug)]
127pub(crate) struct Scope {
128    pub(crate) vars: Vec<(String, Bound)>,
129    keys: Vec<u64>,
130    seen: u64,
131    chained: bool,
132    held: u32,
133    /// The child instance each invocation scanned in it names, by where it is written.
134    sites: HashMap<usize, String>,
135}
136
137/// One instance: what it reads, who reads it, where it was first invoked.
138#[derive(Debug)]
139pub(crate) struct Instance {
140    pub(crate) body: Bound,
141    pub(crate) file: String,
142    given: Vec<String>,
143    site: (String, Option<ByteSpan>),
144    held: u32,
145    scanned: bool,
146}
147
148/// A name's length and first seven bytes in one word, so a lookup costs integer compares.
149#[inline]
150pub(crate) fn packed(name: &str) -> u64 {
151    let bytes = name.as_bytes();
152    let mut key = (bytes.len() as u64) << 56;
153    for (at, byte) in bytes.iter().take(7).enumerate() {
154        key |= u64::from(*byte) << (at * 8);
155    }
156    key
157}
158
159#[inline]
160fn bit(key: u64) -> u64 {
161    1u64 << ((key ^ (key >> 32)) & 63)
162}
163
164impl Scope {
165    pub(crate) fn new(vars: Vec<(String, Bound)>, chained: bool) -> Scope {
166        let keys: Vec<u64> = vars.iter().map(|(k, _)| packed(k)).collect();
167        let seen = keys.iter().fold(0, |acc, k| acc | bit(*k));
168        Scope {
169            vars,
170            keys,
171            seen,
172            chained,
173            held: 0,
174            sites: HashMap::new(),
175        }
176    }
177
178    /// One bit says no before a key is read. Two names of a length share a key past the
179    /// seventh byte, so every match is offered the name.
180    #[inline]
181    pub(crate) fn get(&self, name: &str, key: u64) -> Option<&Bound> {
182        if self.seen & bit(key) == 0 {
183            return None;
184        }
185        self.keys
186            .iter()
187            .enumerate()
188            .find(|(at, k)| **k == key && (name.len() <= 7 || self.vars[*at].0 == name))
189            .map(|(at, _)| &self.vars[at].1)
190    }
191}
192
193/// `time` is DYNAMIC: `p(t - d)` moves every `t` under the parameter, however deep. `sp` is a
194/// step of `grid`.
195#[derive(Clone, Copy)]
196pub struct Cx<'a> {
197    pub(crate) scope: ScopeId,
198    pub(crate) time: Option<&'a Time<'a>>,
199    pub(crate) grid: Grid,
200}
201
202pub(crate) struct Time<'a> {
203    pub(crate) expr: &'a Expr,
204    pub(crate) cx: Cx<'a>,
205}
206
207enum Move<'a> {
208    Here(&'a Expr, Cx<'a>),
209    Shifted(&'a Expr, ScopeId, &'a Expr),
210}
211
212impl<'a> Cx<'a> {
213    pub(crate) fn under(self, scope: ScopeId) -> Cx<'a> {
214        Cx { scope, ..self }
215    }
216
217    pub(crate) fn on(self, grid: Grid) -> Cx<'a> {
218        Cx { grid, ..self }
219    }
220}
221
222/// Read from sva-ast so the two crates cannot resolve a path two ways.
223pub fn resolve_ref_path(referencing: &str, ref_path: &str) -> Result<String, EngineError> {
224    sva_ast::resolve_ref_path(referencing, ref_path)
225        .ok_or_else(|| EngineError::RefAboveRoot(referencing.to_string(), ref_path.to_string()))
226}
227
228/// `@kick.comb(delay=0.03)` is `@comb(t, x=@kick, delay=0.03)`: one name, so a file never
229/// states a parameter order.
230pub const SIGNAL_PARAM: &str = "x";
231
232/// Bounds BREADTH: a chain of functions each invoking the next several times multiplies.
233pub const MAX_INSTANCES: usize = 4096;
234
235/// A `Read` names the child INSTANCE, so this is reachable only past [`Instances::follow`].
236#[derive(Clone, Copy)]
237pub enum Node<'a> {
238    Lit(&'a Literal),
239    Name(&'a str),
240    Bin(BinOp, &'a Expr, &'a Expr),
241    Call {
242        name: &'a str,
243        args: &'a [Arg],
244        span: ByteSpan,
245    },
246    Read {
247        path: &'a str,
248        arg: &'a Expr,
249        address: Address,
250        span: ByteSpan,
251    },
252    Own {
253        arg: &'a Expr,
254        address: Address,
255        span: ByteSpan,
256    },
257    /// `x[i]`: `of`, what `x` is bound to, read at index `arg`.
258    Signal {
259        name: &'a str,
260        of: Thunk<'a>,
261        arg: &'a Expr,
262        span: ByteSpan,
263    },
264}
265
266/// One file's body beside what its parameters stand for: the body is shared, the scope is not.
267/// An instance lasts while a root or a reader holds it.
268#[derive(Debug)]
269pub struct Instances {
270    pub(crate) scopes: Vec<Option<Scope>>,
271    free: Vec<ScopeId>,
272    nodes: BTreeMap<String, Instance>,
273    edges: Edges,
274    pub(crate) own_terms: BTreeMap<String, String>,
275    files: BTreeMap<String, BTreeSet<String>>,
276    journal: Journal,
277    pub(crate) time: Expr,
278    /// The rate in use, whose step `sp` is.
279    pub(crate) rate: u32,
280}
281
282impl Instances {
283    pub fn new(rate: u32) -> Instances {
284        Instances {
285            scopes: vec![Some(Scope::new(Vec::new(), false))],
286            free: Vec::new(),
287            nodes: BTreeMap::new(),
288            edges: Edges::default(),
289            own_terms: BTreeMap::new(),
290            files: BTreeMap::new(),
291            journal: Journal::default(),
292            time: Expr::Var("t".to_string()),
293            rate,
294        }
295    }
296
297    pub(crate) fn rate(&self) -> u32 {
298        self.rate
299    }
300
301    pub(crate) fn grid(&self) -> Grid {
302        Grid::of(self.rate)
303    }
304
305    pub(crate) fn cx(&self, scope: ScopeId) -> Cx<'_> {
306        Cx {
307            scope,
308            time: None,
309            grid: self.grid(),
310        }
311    }
312
313    pub fn origin(&self, instance: &str) -> Option<&str> {
314        self.nodes.get(instance).map(|held| held.file.as_str())
315    }
316
317    pub fn deps(&self, path: &str) -> &[String] {
318        self.edges.of(path)
319    }
320
321    pub(crate) fn readers(&self, path: &str) -> impl Iterator<Item = &str> {
322        self.edges.readers(path)
323    }
324
325    /// `name` held as `held`, filed under its file: the one way an instance is added.
326    fn insert(&mut self, name: String, held: Instance) {
327        let files = self.files.entry(held.file.clone()).or_default();
328        files.insert(name.clone());
329        self.nodes.insert(name, held);
330    }
331
332    /// `name` let go, reading nothing and filed nowhere: the one way an instance goes.
333    fn remove(&mut self, name: &str) -> (Instance, Vec<String>) {
334        let held = self.nodes.remove(name).expect("an instance held");
335        let files = self
336            .files
337            .get_mut(&held.file)
338            .expect("its file's instances");
339        files.remove(name);
340        if files.is_empty() {
341            self.files.remove(&held.file);
342        }
343        (held, self.edges.cleared(name))
344    }
345
346    pub(crate) fn scope(&self, id: ScopeId) -> &Scope {
347        self.scopes[id as usize]
348            .as_ref()
349            .expect("a scope something holds")
350    }
351
352    pub fn paths(&self) -> impl Iterator<Item = &str> {
353        self.nodes.keys().map(String::as_str)
354    }
355
356    pub fn holds(&self, path: &str) -> bool {
357        self.nodes.contains_key(path)
358    }
359
360    /// A bare file name is the file read as a root, on its own terms; a call site's
361    /// instance carries the bindings that made it.
362    pub fn instance_of(&self, target: &str) -> Result<String, EngineError> {
363        if self.holds(target) {
364            return Ok(target.to_string());
365        }
366        if let Some(own) = self.own_terms.get(target) {
367            return Ok(own.clone());
368        }
369        sole(
370            target,
371            self.instances_of(target).map(|p| (p.clone(), p)).collect(),
372        )
373    }
374
375    pub fn instances_of<'a>(&'a self, file: &'a str) -> impl Iterator<Item = String> + 'a {
376        let held = self.files.get(file).into_iter().flatten();
377        held.map(String::clone)
378    }
379
380    pub fn at<'a>(&'a self, path: &str) -> Option<(&'a Expr, Cx<'a>)> {
381        let thunk = self.nodes.get(path)?.body.thunk();
382        Some((thunk.expr, self.cx(thunk.scope)))
383    }
384
385    pub fn bindings<'a>(&'a self, path: &str) -> Option<Vec<(&'a str, &'a Expr, Cx<'a>)>> {
386        let scope = self.nodes.get(path)?.body.scope;
387        Some(
388            self.vars(scope)
389                .map(|(name, value)| (name, value.expr, self.cx(value.scope)))
390                .collect(),
391        )
392    }
393
394    /// Each name `scope` binds, and what it stands for.
395    pub(crate) fn vars(&self, scope: ScopeId) -> impl Iterator<Item = (&str, Thunk<'_>)> {
396        let vars = self.scope(scope).vars.iter();
397        vars.map(|(name, bound)| (name.as_str(), bound.thunk()))
398    }
399
400    /// Every name any scope binds.
401    pub(crate) fn bound_names(&self) -> impl Iterator<Item = &str> {
402        let vars = self
403            .scopes
404            .iter()
405            .flatten()
406            .flat_map(|scope| scope.vars.iter());
407        vars.map(|(name, _)| name.as_str())
408    }
409}
410
411/// The one node a written name stands for: a file one argument tuple expanded is that
412/// instance, and a file several tuples share is none of them.
413pub(crate) fn sole<T>(target: &str, mut held: Vec<(String, T)>) -> Result<T, EngineError> {
414    match held.len() {
415        0 => Err(EngineError::UnknownNode(target.to_string())),
416        1 => Ok(held.remove(0).1),
417        _ => Err(EngineError::AmbiguousNode(
418            target.to_string(),
419            held.into_iter().map(|(name, _)| name).collect(),
420        )),
421    }
422}
423
424fn implicit_time(name: &str) -> bool {
425    sva_ast::FINITE_DIFFERENCE.contains(&name)
426        || sva_ast::MODAL.contains(&name)
427        || matches!(name, "noise" | "stft" | "istft")
428}
429
430impl Instances {
431    #[inline]
432    pub(crate) fn binds(&self, scope: ScopeId, name: &str) -> Option<Thunk<'_>> {
433        let bound = self.scope(scope).get(name, packed(name))?;
434        Some(bound.thunk())
435    }
436
437    /// A bound name continues in what it stands for; anything else is read where it stands.
438    #[inline]
439    pub fn follow<'a, R>(
440        &'a self,
441        e: &'a Expr,
442        cx: Cx<'a>,
443        go: impl FnOnce(&'a Expr, Cx<'_>) -> R,
444    ) -> Option<R> {
445        match self.step(e, cx)? {
446            Move::Here(e2, cx2) => Some(go(e2, cx2)),
447            Move::Shifted(e2, scope, when) => {
448                let moved = Time { expr: when, cx };
449                Some(go(
450                    e2,
451                    Cx {
452                        scope,
453                        time: Some(&moved),
454                        grid: cx.grid,
455                    },
456                ))
457            }
458        }
459    }
460
461    /// A walk of two at once needs both shifts in one frame, so it cannot take the closure.
462    #[inline(always)]
463    fn step<'a>(&'a self, e: &'a Expr, cx: Cx<'a>) -> Option<Move<'a>> {
464        match e {
465            Expr::Var(name) if name == "t" => cx.time.map(|t| Move::Here(t.expr, t.cx)),
466            Expr::Var(name) => self
467                .binds(cx.scope, name)
468                .map(|b| Move::Here(b.expr, cx.under(b.scope))),
469            Expr::Call { name, args, .. } => {
470                let bound = self.binds(cx.scope, name)?;
471                let [Arg::Pos(when)] = args.as_slice() else {
472                    return None;
473                };
474                Some(Move::Shifted(bound.expr, bound.scope, when))
475            }
476            _ => None,
477        }
478    }
479
480    #[inline]
481    pub fn node<'a>(&'a self, e: &'a Expr, cx: Cx<'_>) -> Node<'a> {
482        match e {
483            Expr::Lit(l) => Node::Lit(l),
484            Expr::Var(name) => Node::Name(name),
485            Expr::Bin(op, l, r) => Node::Bin(*op, l, r),
486            Expr::SelfRef { arg, address, span } => Node::Own {
487                arg,
488                address: *address,
489                span: *span,
490            },
491            Expr::Ref {
492                path,
493                arg,
494                address,
495                span,
496                ..
497            } => Node::Read {
498                path: self.site(e, cx.scope, path),
499                arg,
500                address: *address,
501                span: *span,
502            },
503            Expr::Indexed { name, arg, span } => match self.binds(cx.scope, name) {
504                Some(of) => Node::Signal {
505                    name,
506                    of,
507                    arg,
508                    span: *span,
509                },
510                None => Node::Name(name),
511            },
512            Expr::Call { name, args, span } if is_builtin(name) => Node::Call {
513                name,
514                args,
515                span: *span,
516            },
517            Expr::Call { name, span, .. } => Node::Read {
518                path: self.site(e, cx.scope, name),
519                arg: &self.time,
520                address: Address::Time,
521                span: *span,
522            },
523        }
524    }
525
526    fn site<'a>(&'a self, e: &Expr, scope: ScopeId, written: &'a str) -> &'a str {
527        match self
528            .scope(scope)
529            .sites
530            .get(&(std::ptr::from_ref(e) as usize))
531        {
532            Some(child) => child,
533            None => written,
534        }
535    }
536
537    /// A parameter's signal at the reader's own `t`: no shift the reader is under moves it.
538    pub(crate) fn signal<'a>(&self, of: Thunk<'a>, cx: Cx<'a>) -> Cx<'a> {
539        Cx {
540            scope: of.scope,
541            time: None,
542            grid: cx.grid,
543        }
544    }
545
546    pub(crate) fn is_now(&self, e: &Expr, cx: Cx) -> bool {
547        if let Some(r) = self.follow(e, cx, |e2, cx2| self.is_now(e2, cx2)) {
548            return r;
549        }
550        matches!(self.node(e, cx), Node::Name(n) if n == "t")
551    }
552
553    /// One hop down; `self(...)` is no edge.
554    fn direct_deps(&self, path: &str) -> Vec<String> {
555        let (e, cx) = self.at(path).expect("an instance");
556        let mut out = Vec::new();
557        self.direct_refs(e, cx, &mut out);
558        out.sort();
559        out.dedup();
560        out
561    }
562
563    fn direct_refs(&self, e: &Expr, cx: Cx, out: &mut Vec<String>) {
564        if self
565            .follow(e, cx, |e2, cx2| self.direct_refs(e2, cx2, out))
566            .is_some()
567        {
568            return;
569        }
570        match self.node(e, cx) {
571            Node::Lit(_) | Node::Name(_) => {}
572            Node::Bin(_, l, r) => {
573                self.direct_refs(l, cx, out);
574                self.direct_refs(r, cx, out);
575            }
576            Node::Call { args, .. } => {
577                for a in args {
578                    let (Arg::Pos(x) | Arg::Named(_, x)) = a;
579                    self.direct_refs(x, cx, out);
580                }
581            }
582            Node::Read { path, arg, .. } => {
583                out.push(path.to_string());
584                self.direct_refs(arg, cx, out);
585            }
586            Node::Own { arg, .. } => self.direct_refs(arg, cx, out),
587            Node::Signal { of, arg, .. } => {
588                self.direct_refs(of.expr, self.signal(of, cx), out);
589                self.direct_refs(arg, cx, out);
590            }
591        }
592    }
593
594    pub fn reads_self(&self, path: &str) -> bool {
595        self.at(path).is_some_and(|(e, cx)| self.holds_self(e, cx))
596    }
597
598    pub(crate) fn holds_self(&self, e: &Expr, cx: Cx) -> bool {
599        if let Some(r) = self.follow(e, cx, |e2, cx2| self.holds_self(e2, cx2)) {
600            return r;
601        }
602        match self.node(e, cx) {
603            Node::Lit(_) | Node::Name(_) => false,
604            Node::Own { .. } => true,
605            Node::Bin(_, l, r) => self.holds_self(l, cx) || self.holds_self(r, cx),
606            Node::Read { arg, .. } | Node::Signal { arg, .. } => self.holds_self(arg, cx),
607            Node::Call { args, .. } => args.iter().any(|a| {
608                let (Arg::Pos(x) | Arg::Named(_, x)) = a;
609                self.holds_self(x, cx)
610            }),
611        }
612    }
613
614    /// A value depending on WHICH sample is written rather than on the time handed to it.
615    pub(crate) fn position_dependent(&self, e: &Expr, cx: Cx) -> Option<String> {
616        if let Some(r) = self.follow(e, cx, |e2, cx2| self.position_dependent(e2, cx2)) {
617            return r;
618        }
619        match self.node(e, cx) {
620            Node::Lit(_) | Node::Name(_) => None,
621            Node::Own { .. } => Some("self".to_string()),
622            Node::Bin(_, l, r) => self
623                .position_dependent(l, cx)
624                .or_else(|| self.position_dependent(r, cx)),
625            Node::Read { arg, .. } | Node::Signal { arg, .. } => self.position_dependent(arg, cx),
626            Node::Call { name, args, .. } => {
627                if crate::vocabulary::shape(name).is_some() || name == "crop" || implicit_time(name)
628                {
629                    return Some(name.to_string());
630                }
631                args.iter().find_map(|a| {
632                    let (Arg::Pos(x) | Arg::Named(_, x)) = a;
633                    self.position_dependent(x, cx)
634                })
635            }
636        }
637    }
638
639    /// Two arguments are one tuple when they MEAN the same, walked both at once.
640    pub(crate) fn same<'x, 'y>(&self, a: &Expr, ax: Cx<'x>, b: &Expr, by: Cx<'y>) -> bool {
641        if let Some(moved) = self.step(a, ax) {
642            return match moved {
643                Move::Here(a2, ax2) => self.same(a2, ax2, b, by),
644                Move::Shifted(a2, scope, when) => {
645                    let moved = Time { expr: when, cx: ax };
646                    self.same(
647                        a2,
648                        Cx {
649                            scope,
650                            time: Some(&moved),
651                            grid: ax.grid,
652                        },
653                        b,
654                        by,
655                    )
656                }
657            };
658        }
659        if let Some(moved) = self.step(b, by) {
660            return match moved {
661                Move::Here(b2, by2) => self.same(a, ax, b2, by2),
662                Move::Shifted(b2, scope, when) => {
663                    let moved = Time { expr: when, cx: by };
664                    self.same(
665                        a,
666                        ax,
667                        b2,
668                        Cx {
669                            scope,
670                            time: Some(&moved),
671                            grid: by.grid,
672                        },
673                    )
674                }
675            };
676        }
677        match (self.node(a, ax), self.node(b, by)) {
678            (Node::Lit(x), Node::Lit(y)) => x == y,
679            (Node::Name(x), Node::Name(y)) => x == y,
680            (Node::Bin(o1, l1, r1), Node::Bin(o2, l2, r2)) => {
681                o1 == o2 && self.same(l1, ax, l2, by) && self.same(r1, ax, r2, by)
682            }
683            (
684                Node::Own {
685                    arg: x, address: i, ..
686                },
687                Node::Own {
688                    arg: y, address: j, ..
689                },
690            ) => i == j && self.same(x, ax, y, by),
691            (
692                Node::Read {
693                    path: p,
694                    arg: x,
695                    address: i,
696                    ..
697                },
698                Node::Read {
699                    path: q,
700                    arg: y,
701                    address: j,
702                    ..
703                },
704            ) => p == q && i == j && self.same(x, ax, y, by),
705            (Node::Signal { of: f, arg: x, .. }, Node::Signal { of: g, arg: y, .. }) => {
706                self.same(f.expr, self.signal(f, ax), g.expr, self.signal(g, by))
707                    && self.same(x, ax, y, by)
708            }
709            (
710                Node::Call {
711                    name: n1, args: a1, ..
712                },
713                Node::Call {
714                    name: n2, args: a2, ..
715                },
716            ) => {
717                n1 == n2
718                    && a1.len() == a2.len()
719                    && a1.iter().zip(a2).all(|(x, y)| match (x, y) {
720                        (Arg::Pos(x), Arg::Pos(y)) => self.same(x, ax, y, by),
721                        (Arg::Named(k1, x), Arg::Named(k2, y)) => {
722                            k1 == k2 && self.same(x, ax, y, by)
723                        }
724                        _ => false,
725                    })
726            }
727            _ => false,
728        }
729    }
730
731    pub(crate) fn same_binds(&self, scope: ScopeId, binds: &[(String, Bound)]) -> bool {
732        let held = &self.scope(scope).vars;
733        held.len() == binds.len()
734            && held.iter().zip(binds).all(|((k1, v1), (k2, v2))| {
735                let (v1, v2) = (v1.thunk(), v2.thunk());
736                k1 == k2 && self.same(v1.expr, self.cx(v1.scope), v2.expr, self.cx(v2.scope))
737            })
738    }
739}