Skip to main content

tensor_wasm_jit/
lowering_builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3
4//! SSA value-id allocator + Cranelift → lowered-id maps for the wave-2
5//! lowering driver.
6//!
7//! The wave-1 per-family `lower_*` functions (see [`crate::lower_arith`])
8//! take a `&mut HashMap<cl::Value, LoweredValueId>` and a `&mut
9//! LoweredValueId` next-id counter, and the driver is responsible for
10//! seeding the map with entry-block params before calling them. Once we
11//! have several families to chain (arith, float, memory, cf, vector, conv)
12//! plus block params, stack slots, and constants to track, passing all of
13//! that state by-reference becomes noisy.
14//!
15//! [`LoweringBuilder`] is the scratchpad that owns that state. It exposes
16//! the same `&mut HashMap` / `&mut LoweredValueId` accessors the per-family
17//! functions already speak ([`LoweringBuilder::value_map_mut`],
18//! [`LoweringBuilder::next_value_id_mut`]) so the wave-1 lowerings keep
19//! working unchanged, plus higher-level `get_or_alloc_*` helpers for the
20//! driver's preflight and for block / stack-slot tracking.
21//!
22//! # Namespaces
23//!
24//! - **Values:** Cranelift `Value` → [`LoweredValueId`], allocated from a
25//!   monotonic `next_value_id` counter starting at `0`.
26//! - **Blocks:** Cranelift `Block` → [`LoweredBlockId`], allocated from a
27//!   *separate* `next_block_id` counter starting at `0`. Block ids and
28//!   value ids share no numbering — there can be both a `LoweredValueId(0)`
29//!   and a `LoweredBlockId(0)` in the same function.
30//! - **Stack slots:** Cranelift `StackSlot` → [`LoweredValueId`]. The
31//!   per-slot id is itself a value id (alloca pointers are SSA values), but
32//!   the `StackSlot → id` map is stored separately so
33//!   [`crate::lower_memory`] can recover the alloca pointer across multiple
34//!   `stack_load`/`stack_store` ops in the same block without re-allocating.
35//!
36//! # Not `Send`
37//!
38//! Each lowering pass owns its own builder. There's no shared mutable
39//! state, no locking, and no reason to move the builder across threads.
40//! Keeping it `!Send` is enforced indirectly by `std::collections::HashMap`
41//! with the default hasher (which is `Send`, but conceptually the builder
42//! is per-pass).
43
44#![cfg(feature = "cuda-oxide-backend")]
45
46use std::collections::HashMap;
47
48use cranelift_codegen::ir as cl;
49
50use crate::lowered_ir::{LoweredBlockId, LoweredValueId};
51
52/// Scratchpad shared across per-instruction `lower_*` calls when walking a
53/// Cranelift [`cl::Function`].
54///
55/// Owns three bidirectional Cranelift → lowered-id maps (values, blocks,
56/// stack slots) plus a monotonic id allocator for each of the two id
57/// namespaces ([`LoweredValueId`] and [`LoweredBlockId`]).
58///
59/// The wave-2 lowering driver constructs a `LoweringBuilder`, pre-allocates
60/// ids for entry-block params and constants via [`Self::get_or_alloc_value`]
61/// / [`Self::bind_value`], then calls each per-family `lower_*` function in
62/// turn ([`crate::lower_arith::lower_arith_inst`] et al.) by passing the
63/// builder's internals through [`Self::value_map_mut`] and
64/// [`Self::next_value_id_mut`]. The result is an ordered list of
65/// [`crate::lowered_ir::LoweredOp`]s with consistent SSA numbering.
66///
67/// See the [module docs](self) for the namespace rules.
68pub struct LoweringBuilder {
69    /// Cranelift `Value` → lowered SSA id.
70    value_map: HashMap<cl::Value, LoweredValueId>,
71    /// Cranelift `Block` → lowered block id.
72    block_map: HashMap<cl::Block, LoweredBlockId>,
73    /// Cranelift `StackSlot` → alloca-pointer SSA id (allocated from the
74    /// same counter as `value_map`).
75    stack_slot_map: HashMap<cl::StackSlot, LoweredValueId>,
76    /// Next fresh `LoweredValueId` to hand out. Monotonically increasing.
77    next_value_id: LoweredValueId,
78    /// Next fresh `LoweredBlockId` to hand out. Monotonically increasing,
79    /// independent of `next_value_id`.
80    next_block_id: LoweredBlockId,
81}
82
83impl LoweringBuilder {
84    /// Construct an empty builder. Both id counters start at `0` and all
85    /// three maps are empty.
86    pub fn new() -> Self {
87        Self {
88            value_map: HashMap::new(),
89            block_map: HashMap::new(),
90            stack_slot_map: HashMap::new(),
91            next_value_id: 0,
92            next_block_id: 0,
93        }
94    }
95
96    /// Allocate a fresh [`LoweredValueId`] and return it. Increments the
97    /// value-id counter. Does **not** bind the id to any Cranelift `Value`;
98    /// callers wanting a `Value` → id binding should use
99    /// [`Self::get_or_alloc_value`] or [`Self::bind_value`] instead.
100    pub fn alloc_value(&mut self) -> LoweredValueId {
101        let id = self.next_value_id;
102        self.next_value_id = self
103            .next_value_id
104            .checked_add(1)
105            .expect("LoweredValueId counter overflowed u32 — function too large");
106        id
107    }
108
109    /// Allocate a fresh [`LoweredBlockId`] and return it. Increments the
110    /// block-id counter. Independent of [`Self::alloc_value`].
111    pub fn alloc_block(&mut self) -> LoweredBlockId {
112        let id = self.next_block_id;
113        self.next_block_id = self
114            .next_block_id
115            .checked_add(1)
116            .expect("LoweredBlockId counter overflowed u32 — function too large");
117        id
118    }
119
120    /// Get the [`LoweredValueId`] for a Cranelift [`cl::Value`], allocating
121    /// one if it's the first time we've seen it.
122    ///
123    /// This is the workhorse for entry-block param setup: the driver
124    /// iterates over `dfg.block_params(entry)` and calls this for each
125    /// param to seed the value-map before walking the body. It's also safe
126    /// to call repeatedly for the same value — subsequent calls return the
127    /// existing id without advancing the counter.
128    pub fn get_or_alloc_value(&mut self, v: cl::Value) -> LoweredValueId {
129        if let Some(&id) = self.value_map.get(&v) {
130            return id;
131        }
132        let id = self.alloc_value();
133        self.value_map.insert(v, id);
134        id
135    }
136
137    /// Get the [`LoweredBlockId`] for a Cranelift [`cl::Block`], allocating
138    /// one if it's the first time we've seen it.
139    ///
140    /// Used by [`crate::lower_cf`] when it encounters a branch target: the
141    /// target block may not yet have been walked, but its lowered id needs
142    /// to be embedded in the `Br`/`CondBr`/`Switch` op now.
143    pub fn get_or_alloc_block(&mut self, b: cl::Block) -> LoweredBlockId {
144        if let Some(&id) = self.block_map.get(&b) {
145            return id;
146        }
147        let id = self.alloc_block();
148        self.block_map.insert(b, id);
149        id
150    }
151
152    /// Get the alloca-pointer [`LoweredValueId`] for a Cranelift
153    /// [`cl::StackSlot`], allocating one (from the value-id counter) if
154    /// it's the first time we've seen it.
155    ///
156    /// [`crate::lower_memory`] uses this for `stack_load` / `stack_store`:
157    /// every reference to the same `StackSlot` resolves to the same alloca
158    /// pointer SSA value, so the downstream `mem2reg` pass can rewrite all
159    /// uses uniformly.
160    pub fn get_or_alloc_stack_slot(&mut self, ss: cl::StackSlot) -> LoweredValueId {
161        if let Some(&id) = self.stack_slot_map.get(&ss) {
162            return id;
163        }
164        let id = self.alloc_value();
165        self.stack_slot_map.insert(ss, id);
166        id
167    }
168
169    /// Look up an existing [`LoweredValueId`] for a Cranelift [`cl::Value`].
170    /// Returns `None` if no binding exists yet.
171    ///
172    /// Use this when you want to *probe* the map without side effects (e.g.
173    /// assertions, debugging, or when a missing operand should fail rather
174    /// than allocate). For the eager-allocate flavour use
175    /// [`Self::get_or_alloc_value`].
176    pub fn lookup_value(&self, v: cl::Value) -> Option<LoweredValueId> {
177        self.value_map.get(&v).copied()
178    }
179
180    /// Look up an existing [`LoweredBlockId`] for a Cranelift [`cl::Block`].
181    /// Returns `None` if no binding exists yet.
182    pub fn lookup_block(&self, b: cl::Block) -> Option<LoweredBlockId> {
183        self.block_map.get(&b).copied()
184    }
185
186    /// Look up an existing stack-slot alloca-pointer id for a Cranelift
187    /// [`cl::StackSlot`]. Returns `None` if no binding exists yet.
188    pub fn lookup_stack_slot(&self, ss: cl::StackSlot) -> Option<LoweredValueId> {
189        self.stack_slot_map.get(&ss).copied()
190    }
191
192    /// Bind a Cranelift [`cl::Value`] to a specific (already-allocated)
193    /// [`LoweredValueId`].
194    ///
195    /// Does **not** advance the value-id counter — this is purely a name
196    /// assignment. Returns the previous binding for `v`, if any (mirroring
197    /// [`HashMap::insert`]).
198    ///
199    /// Used by per-family `lower_*` functions that prefer the wave-1
200    /// idiom of allocating the result id via `*next_id` and inserting it
201    /// directly: those callers reach for [`Self::next_value_id_mut`] /
202    /// [`Self::value_map_mut`] today, but `bind_value` is the cleaner
203    /// option when the caller already has the id in hand.
204    pub fn bind_value(&mut self, v: cl::Value, id: LoweredValueId) -> Option<LoweredValueId> {
205        self.value_map.insert(v, id)
206    }
207
208    /// Bind a Cranelift [`cl::StackSlot`] to a specific (already-allocated)
209    /// alloca-pointer [`LoweredValueId`], returning the previous binding if
210    /// any (mirroring [`HashMap::insert`]).
211    ///
212    /// jit HIGH fix (finding 3): the W2.4 driver lifts the builder's
213    /// stack-slot map out into a local `&mut HashMap` so it can hand a
214    /// `&mut` to the memory-lowering context. Without a way to write
215    /// entries back, every new stack slot allocated during memory lowering
216    /// was DROPPED on return — so a later `stack_load`/`stack_store` of the
217    /// same slot would call [`Self::get_or_alloc_stack_slot`], miss, and
218    /// allocate a *fresh* alloca pointer, splitting one logical slot into
219    /// several distinct allocas (a miscompile). The driver now reinserts
220    /// each entry of the local map through this method after lowering, so
221    /// repeated references to one slot resolve to the same alloca id.
222    ///
223    /// Unlike [`Self::get_or_alloc_stack_slot`], this does **not** advance
224    /// the value-id counter — the id was already allocated by the memory
225    /// lowering against the same shared counter.
226    pub fn bind_stack_slot(
227        &mut self,
228        ss: cl::StackSlot,
229        id: LoweredValueId,
230    ) -> Option<LoweredValueId> {
231        self.stack_slot_map.insert(ss, id)
232    }
233
234    /// Mutable access to the inner `value_map`.
235    ///
236    /// Provided for compatibility with the wave-1 per-family lowerings
237    /// (e.g. [`crate::lower_arith::lower_arith_inst`]) that already accept
238    /// `&mut HashMap<cl::Value, LoweredValueId>` directly. The borrow is
239    /// tied to `&mut self`, so the builder cannot be otherwise mutated for
240    /// the duration.
241    pub fn value_map_mut(&mut self) -> &mut HashMap<cl::Value, LoweredValueId> {
242        &mut self.value_map
243    }
244
245    /// Mutable access to the inner `next_value_id` counter.
246    ///
247    /// Companion to [`Self::value_map_mut`]: per-family lowerings receive
248    /// both and update them in lockstep. Direct mutation here is
249    /// equivalent to calling [`Self::alloc_value`] manually.
250    pub fn next_value_id_mut(&mut self) -> &mut LoweredValueId {
251        &mut self.next_value_id
252    }
253
254    /// Read-only view of the `value_map`. Handy for tests, fingerprinting,
255    /// and the wave-2 driver's post-walk verification.
256    pub fn value_map(&self) -> &HashMap<cl::Value, LoweredValueId> {
257        &self.value_map
258    }
259
260    /// Read-only view of the `block_map`. Used by [`crate::lower_cf`] when
261    /// it only needs to *look up* branch targets that the driver has
262    /// already seeded.
263    pub fn block_map(&self) -> &HashMap<cl::Block, LoweredBlockId> {
264        &self.block_map
265    }
266
267    /// Read-only view of the `stack_slot_map`.
268    pub fn stack_slot_map(&self) -> &HashMap<cl::StackSlot, LoweredValueId> {
269        &self.stack_slot_map
270    }
271
272    /// Split-borrow accessor for the memory-lowering family.
273    ///
274    /// Returns `(&value_map, &mut stack_slot_map, &mut next_value_id)` in a
275    /// single call so the caller gets disjoint borrows of three distinct
276    /// fields — exactly the shape `MemLowerContext` needs (value map read,
277    /// stack-slot map + counter mutated).
278    ///
279    /// jit MED fix (finding 6): the W2.4 driver previously CLONED the whole
280    /// `value_map` on every memory instruction (`builder.value_map().clone()`)
281    /// because the borrow checker couldn't see the three accessors were
282    /// disjoint — making the driver O(V·M) (values × memory instructions).
283    /// This method performs the field split once, in safe code, so the
284    /// memory context borrows the live map directly with no per-instruction
285    /// allocation.
286    #[allow(clippy::type_complexity)]
287    pub fn split_borrow_for_memory(
288        &mut self,
289    ) -> (
290        &HashMap<cl::Value, LoweredValueId>,
291        &mut HashMap<cl::StackSlot, LoweredValueId>,
292        &mut LoweredValueId,
293    ) {
294        (
295            &self.value_map,
296            &mut self.stack_slot_map,
297            &mut self.next_value_id,
298        )
299    }
300}
301
302impl Default for LoweringBuilder {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use cranelift_codegen::entity::EntityRef;
312
313    /// Helper: construct a Cranelift `Value` from a raw index. Safe because
314    /// `Value` implements `EntityRef`, which exposes a public `new(usize)`
315    /// constructor — no DFG required for tests of pure id-map plumbing.
316    fn v(n: u32) -> cl::Value {
317        cl::Value::new(n as usize)
318    }
319
320    /// Helper: construct a Cranelift `Block` from a raw index.
321    fn b(n: u32) -> cl::Block {
322        cl::Block::new(n as usize)
323    }
324
325    /// Helper: construct a Cranelift `StackSlot` from a raw index.
326    fn ss(n: u32) -> cl::StackSlot {
327        cl::StackSlot::new(n as usize)
328    }
329
330    #[test]
331    fn alloc_value_returns_sequential_ids_starting_at_zero() {
332        let mut b = LoweringBuilder::new();
333        assert_eq!(b.alloc_value(), 0);
334        assert_eq!(b.alloc_value(), 1);
335        assert_eq!(b.alloc_value(), 2);
336        // Counter advanced exactly three times.
337        assert_eq!(*b.next_value_id_mut(), 3);
338    }
339
340    #[test]
341    fn alloc_block_returns_sequential_ids_starting_at_zero_independent_of_values() {
342        let mut bld = LoweringBuilder::new();
343        // Allocate a few values first; block counter must not move.
344        bld.alloc_value();
345        bld.alloc_value();
346        // Block ids still start at 0 — the namespaces are disjoint.
347        assert_eq!(bld.alloc_block(), 0);
348        assert_eq!(bld.alloc_block(), 1);
349        assert_eq!(bld.alloc_block(), 2);
350        // And further block allocations don't disturb the value counter.
351        assert_eq!(*bld.next_value_id_mut(), 2);
352    }
353
354    #[test]
355    fn get_or_alloc_value_idempotent_and_fresh_on_new() {
356        let mut bld = LoweringBuilder::new();
357        let v0 = v(0);
358        let v1 = v(1);
359
360        let id_a = bld.get_or_alloc_value(v0);
361        let id_b = bld.get_or_alloc_value(v0); // same Value
362        assert_eq!(id_a, id_b, "same Value must return same id");
363
364        let id_c = bld.get_or_alloc_value(v1);
365        assert_ne!(id_a, id_c, "different Value must return different id");
366
367        // Only two ids handed out — the second call to v0 did not advance.
368        assert_eq!(*bld.next_value_id_mut(), 2);
369    }
370
371    #[test]
372    fn get_or_alloc_block_idempotent_and_fresh_on_new() {
373        let mut bld = LoweringBuilder::new();
374        let b0 = b(0);
375        let b1 = b(1);
376
377        let id_a = bld.get_or_alloc_block(b0);
378        let id_b = bld.get_or_alloc_block(b0);
379        assert_eq!(id_a, id_b);
380
381        let id_c = bld.get_or_alloc_block(b1);
382        assert_ne!(id_a, id_c);
383
384        // Block counter advanced exactly twice.
385        let next_block_before = bld.alloc_block();
386        assert_eq!(next_block_before, 2);
387    }
388
389    #[test]
390    fn lookup_value_returns_none_for_unallocated_some_for_allocated() {
391        let mut bld = LoweringBuilder::new();
392        let v0 = v(7);
393        assert_eq!(bld.lookup_value(v0), None, "unallocated must be None");
394        let id = bld.get_or_alloc_value(v0);
395        assert_eq!(
396            bld.lookup_value(v0),
397            Some(id),
398            "after alloc, lookup must return the id"
399        );
400        // Look-up does not advance the counter.
401        let before = *bld.next_value_id_mut();
402        let _ = bld.lookup_value(v0);
403        assert_eq!(*bld.next_value_id_mut(), before);
404    }
405
406    #[test]
407    fn lookup_block_and_lookup_stack_slot_behave_like_lookup_value() {
408        let mut bld = LoweringBuilder::new();
409        let b0 = b(3);
410        let s0 = ss(5);
411        assert_eq!(bld.lookup_block(b0), None);
412        assert_eq!(bld.lookup_stack_slot(s0), None);
413        let bid = bld.get_or_alloc_block(b0);
414        let sid = bld.get_or_alloc_stack_slot(s0);
415        assert_eq!(bld.lookup_block(b0), Some(bid));
416        assert_eq!(bld.lookup_stack_slot(s0), Some(sid));
417    }
418
419    #[test]
420    fn bind_value_does_not_advance_counter() {
421        let mut bld = LoweringBuilder::new();
422        // Caller manually picks an id 42 and binds it.
423        let v0 = v(0);
424        let prev = bld.bind_value(v0, 42);
425        assert_eq!(prev, None, "first bind returns no prior");
426        // Counter must remain at 0 — bind_value does not allocate.
427        assert_eq!(*bld.next_value_id_mut(), 0);
428        // Subsequent lookup must reflect the bound id.
429        assert_eq!(bld.lookup_value(v0), Some(42));
430
431        // Rebinding returns the previous id.
432        let prev2 = bld.bind_value(v0, 99);
433        assert_eq!(prev2, Some(42));
434        assert_eq!(bld.lookup_value(v0), Some(99));
435        // And still no counter movement.
436        assert_eq!(*bld.next_value_id_mut(), 0);
437    }
438
439    #[test]
440    fn value_map_mut_and_next_value_id_mut_expose_inner_state() {
441        // Mirrors the wave-1 per-family signature: a caller that holds the
442        // builder can pass these two `&mut`s straight into
443        // `lower_arith_inst` etc.
444        let mut bld = LoweringBuilder::new();
445        let v0 = v(0);
446        let v1 = v(1);
447
448        // Seed the map the wave-1 way.
449        {
450            let map = bld.value_map_mut();
451            map.insert(v0, 100);
452            map.insert(v1, 101);
453        }
454        // And bump the counter manually.
455        {
456            let next = bld.next_value_id_mut();
457            *next = 102;
458        }
459
460        // Visible through the read-only accessors.
461        assert_eq!(bld.value_map().len(), 2);
462        assert_eq!(bld.value_map().get(&v0), Some(&100));
463        assert_eq!(bld.value_map().get(&v1), Some(&101));
464        // alloc_value now picks up from where the manual bump left off.
465        assert_eq!(bld.alloc_value(), 102);
466        assert_eq!(bld.alloc_value(), 103);
467    }
468
469    #[test]
470    fn get_or_alloc_stack_slot_idempotent_and_uses_value_counter() {
471        let mut bld = LoweringBuilder::new();
472        let s0 = ss(0);
473        let s1 = ss(1);
474
475        // Burn one value id first so we can confirm the stack-slot ids
476        // come from the same counter (not from a separate one).
477        let pre = bld.alloc_value();
478        assert_eq!(pre, 0);
479
480        let sid_a = bld.get_or_alloc_stack_slot(s0);
481        let sid_b = bld.get_or_alloc_stack_slot(s0); // repeat
482        assert_eq!(sid_a, sid_b, "same StackSlot returns same id");
483        assert_eq!(sid_a, 1, "stack-slot ids come from the value counter");
484
485        let sid_c = bld.get_or_alloc_stack_slot(s1);
486        assert_ne!(sid_a, sid_c, "different StackSlot gets a new id");
487        assert_eq!(sid_c, 2);
488
489        // The StackSlot map is its own namespace — looking up s0 as a
490        // Value must still return None.
491        assert_eq!(bld.lookup_value(v(0)), None);
492        assert_eq!(bld.lookup_stack_slot(s0), Some(1));
493        assert_eq!(bld.stack_slot_map().len(), 2);
494    }
495
496    /// jit HIGH fix (finding 3): `bind_stack_slot` writes an explicit id
497    /// without advancing the value counter, and a re-bind of the same slot
498    /// is id-stable. This is what lets the driver reinsert the local
499    /// stack-slot map after memory lowering so repeated references to one
500    /// slot resolve to the same alloca pointer.
501    #[test]
502    fn bind_stack_slot_is_id_stable_and_does_not_advance_counter() {
503        let mut bld = LoweringBuilder::new();
504        let s0 = ss(0);
505
506        // Pretend memory lowering allocated id 0 for s0 against the shared
507        // counter (advance it once to mirror that).
508        assert_eq!(bld.alloc_value(), 0);
509        let prev = bld.bind_stack_slot(s0, 0);
510        assert_eq!(prev, None, "first bind has no previous value");
511        assert_eq!(bld.lookup_stack_slot(s0), Some(0));
512
513        // bind_stack_slot must NOT touch the value counter.
514        assert_eq!(
515            bld.alloc_value(),
516            1,
517            "bind_stack_slot must not advance the value-id counter"
518        );
519
520        // Re-binding the same slot to the same id is idempotent and returns
521        // the previous id (so the driver's reinsert loop is safe to run
522        // even when the entry already existed).
523        let prev2 = bld.bind_stack_slot(s0, 0);
524        assert_eq!(prev2, Some(0));
525        assert_eq!(bld.lookup_stack_slot(s0), Some(0));
526        assert_eq!(bld.stack_slot_map().len(), 1);
527    }
528
529    /// jit MED fix (finding 6): the split-borrow accessor hands out the
530    /// LIVE maps + counter (no clone). A mutation through the returned
531    /// `&mut`s is visible on the builder afterwards, and the value map is
532    /// borrowed in place (so the driver no longer clones it per memory
533    /// instruction).
534    #[test]
535    fn split_borrow_for_memory_exposes_live_state() {
536        let mut bld = LoweringBuilder::new();
537        bld.bind_value(v(0), 7);
538
539        {
540            let (value_map, stack_slot_map, next_value_id) = bld.split_borrow_for_memory();
541            // The value map is the live one (read-only here).
542            assert_eq!(value_map.get(&v(0)), Some(&7));
543            // Mutate the stack-slot map and counter through the split
544            // borrow — exactly what the memory context does.
545            stack_slot_map.insert(ss(0), *next_value_id);
546            *next_value_id += 1;
547        }
548
549        // Mutations are visible on the builder (proving no clone was made).
550        assert_eq!(bld.lookup_stack_slot(ss(0)), Some(0));
551        assert_eq!(
552            bld.alloc_value(),
553            1,
554            "counter advanced through split borrow"
555        );
556    }
557
558    #[test]
559    fn default_matches_new() {
560        // `Default::default()` is a thin wrapper around `new()`; this test
561        // locks in the contract so a future refactor doesn't silently
562        // change the starting counters.
563        let bld_new = LoweringBuilder::new();
564        let bld_default = LoweringBuilder::default();
565        assert_eq!(bld_new.value_map().len(), bld_default.value_map().len());
566        assert_eq!(bld_new.block_map().len(), bld_default.block_map().len());
567        assert_eq!(
568            bld_new.stack_slot_map().len(),
569            bld_default.stack_slot_map().len()
570        );
571    }
572
573    #[test]
574    fn block_and_value_namespaces_are_disjoint() {
575        // A regression guard: it must be possible to have a
576        // `LoweredValueId(0)` and a `LoweredBlockId(0)` alive at the same
577        // time, referring to different entities.
578        let mut bld = LoweringBuilder::new();
579        let vid = bld.get_or_alloc_value(v(0));
580        let bid = bld.get_or_alloc_block(b(0));
581        assert_eq!(vid, 0);
582        assert_eq!(bid, 0);
583        // The types are the same `u32` alias today, but the *bindings* live
584        // in disjoint maps. Confirm by cross-lookup.
585        assert_eq!(bld.lookup_value(v(0)), Some(0));
586        assert_eq!(bld.lookup_block(b(0)), Some(0));
587        assert!(bld.lookup_stack_slot(ss(0)).is_none());
588    }
589}