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}