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