vj-codegen 0.2.0

LLVM code generator of the vj SystemVerilog simulator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
//! `std::randomize()`: randomization of scope variables (constraints plan C21;
//! LRM 1800-2023 18.12, 18.12.1).
//!
//! `[std::]randomize(a, b, c) with { ... }` randomizes variables that belong to
//! no class: module variables, locals and arguments of the enclosing
//! subroutine. It "behaves exactly the same as a class randomize method" (18.12)
//! over the call's arguments, with these differences:
//!
//! * the random variables are exactly the arguments, and every other name in the
//!   `with` block is a *state* variable (18.12.1): a constant of the solve whose
//!   value is read when the call is made;
//! * there is no object, so no `rand_mode`, no `pre_randomize`/`post_randomize`
//!   and no constraint blocks, only the inline block;
//! * called with no argument it "shall not change the value of any variable but
//!   instead it shall check its constraints" (18.12): the block is a checker;
//! * the result is 1 when every variable was given a valid value, 0 when the
//!   constraints cannot be met (then no variable is changed).
//!
//! The implementation reuses the whole class machinery. At the call site the
//! arguments are declared to the constraint builder as random variables
//! ([`Builder::declare_var_forced`]), every other name the block reads is
//! classified a call-scope local and captured as a state exactly as for
//! `obj.randomize() with {}` (`crate::classes_randomize`), and the result is a
//! [`Program`] whose variable `i` lives at a word offset of a *scratch object*
//! the runtime allocates for the call ([`registry::randomize_scope`]). The
//! solved values come back in the call's buffer and are stored into the
//! arguments only when the solve succeeded. The generator is the calling
//! process's, so the draw is as reproducible as `$urandom` (LRM 18.13).
//!
//! A queue or dynamic array argument is random in size and elements (LRM
//! 18.4): the scratch object holds a copy of its collection handle (one word,
//! [`registry::ProgArray`] at `Home::Instance`), the solve replaces the copy
//! with the new contents, and the call site stores that into the variable only
//! on success. A collection the block merely reads travels the same way and is
//! freed after the call.
//!
//! Integral, real, unpacked aggregate and queue/dynamic-array variables can be
//! arguments; every other form is a typed error naming the reason. `null` and a
//! restricted `with (x, y)` list are illegal in a scope randomize (LRM 18.7,
//! footnote 43).

use inkwell::IntPredicate;
use sv_lang::kinds::{ExpressionKind, SymbolKind};
use vj_runtime::collections_rt::CollOp;
use vj_runtime::objects_rt::ObjOp;
use vj_sim::constraints::build::{Builder, Declared};
use vj_sim::constraints::registry::{self, Home, Outcome, ProgState, ProgVar, Program};
use vj_sim::objects::FieldSlot;
use vj_sim::symid::SymbolUid;

use crate::classes::{load_packed_words, obj_call, store_packed_words, word_ptr};
use crate::classes_randomize::{find_local_occurrence, restriction_list};
use crate::error::LoweringError;
use crate::expr::{LowerCtx, LoweredValue, entry_alloca};
use crate::ops::OpBuilder;

/// Whether the `randomize` call `call` is a scope randomize rather than a method
/// call of an object (LRM 18.12): it has no receiver, and it is either written
/// `std::randomize` or appears where `randomize` cannot mean the enclosing
/// class's method (`in_method` false: a module, interface or free subroutine).
/// A bare `randomize(...)` inside a class method is that class's own method.
pub(crate) fn is_scope_call(
    call: sv_lang::Expression<'_>,
    has_receiver: bool,
    in_method: bool,
) -> bool {
    !has_receiver && (!in_method || is_std_qualified(call))
}

/// Whether the callee of `call` is spelled `std::randomize`.
fn is_std_qualified(call: sv_lang::Expression<'_>) -> bool {
    use sv_lang::AstNode;
    use sv_lang::kinds::SyntaxKind;
    use sv_lang::nodes::{
        ArrayOrRandomizeMethodExpressionSyntax, IdentifierNameSyntax, InvocationExpressionSyntax,
        ScopedNameSyntax,
    };

    crate::classes::with_design_trees(|trees| {
        for tree in trees {
            let Some(node) = call.syntax(tree) else {
                continue;
            };
            // `randomize(..) with {..}` wraps the invocation; without a `with`
            // the call is the invocation itself.
            let invocation = match ArrayOrRandomizeMethodExpressionSyntax::cast(node) {
                Some(with) => with.method().syntax(),
                None => node,
            };
            let callee = match InvocationExpressionSyntax::cast(invocation) {
                Some(inv) => inv.left().syntax(),
                None => invocation,
            };
            if callee.kind() != SyntaxKind::ScopedName {
                return false;
            }
            return ScopedNameSyntax::cast(callee)
                .and_then(|s| IdentifierNameSyntax::cast(s.left().syntax()))
                .and_then(|n| n.identifier())
                .is_some_and(|t| t.text() == "std");
        }
        false
    })
}

/// One argument of a scope randomize: the expression the call names it by and
/// the variable it denotes.
struct ScopeVar<'d> {
    expr: sv_lang::Expression<'d>,
    sym: sv_lang::Symbol<'d>,
    /// A `real`/`shortreal` variable (a random real of the call, LRM 18.4).
    real: bool,
    /// An unpacked struct or fixed-size unpacked array: a flat value whose
    /// leaves are the random variables (LRM 18.4, 18.12).
    aggregate: bool,
    /// A queue or dynamic array: its size and every element are random
    /// (LRM 18.4, 18.12). The scratch object holds a copy of its collection
    /// handle ([`registry::ProgArray`]).
    coll: bool,
}

impl ScopeVar<'_> {
    /// A fixed-size unpacked array (its rows are stored one by one, unlike a
    /// struct, which is one flat value).
    fn is_array(&self) -> bool {
        self.aggregate
            && self.sym.kind() != SymbolKind::ClassProperty
            && self
                .sym
                .value_type()
                .is_some_and(|t| t.canonical().is_unpacked_array())
    }
}

/// Reads the call's arguments as scope variables (LRM 18.12): each must be the
/// plain name of an integral or floating-point variable of the enclosing scope.
fn parse_vars<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    args: &[sv_lang::Expression<'d>],
) -> Result<Vec<ScopeVar<'d>>, LoweringError> {
    let fail =
        |why: &str| LoweringError::unsupported_at(&format!("std::randomize(): {why}"), &cx.fn_sym);
    let mut vars: Vec<ScopeVar<'d>> = Vec::new();
    for &arg in args {
        // slang binds an output-like argument as `var = <formal>`
        let a = match arg.left() {
            Some(lhs) if arg.kind() == ExpressionKind::Assignment => lhs,
            _ => arg,
        };
        if a.kind() == ExpressionKind::NullLiteral {
            return Err(fail(
                "`null` is illegal in a scope randomize (LRM 18.7, footnote 43); call it with \
                 no argument to check its constraints",
            ));
        }
        let sym = if a.kind() == ExpressionKind::NamedValue {
            a.referenced_symbol()
        } else {
            None
        };
        let Some(sym) = sym else {
            return Err(fail(
                "an argument must be the plain name of a variable (LRM 18.12); an element, a \
                 member, a hierarchical name or another expression is not supported",
            ));
        };
        match sym.kind() {
            SymbolKind::Variable | SymbolKind::FormalArgument => {}
            // a property of the enclosing class is a variable of the method's scope
            // (LRM 18.12): it is read and stored through `this` like any name
            SymbolKind::ClassProperty => {}
            other => {
                return Err(fail(&format!(
                    "`{}` is a {other:?}, not a variable (LRM 18.12)",
                    sym.name()
                )));
            }
        }
        let real = sym.value_type().is_some_and(|t| t.is_floating());
        let integral = sym
            .value_type()
            .is_some_and(|t| t.is_integral() && !t.is_floating());
        let aggregate = sym
            .value_type()
            .is_some_and(vj_sim::constraints::build::is_aggregate_type);
        let coll_ty = sym
            .value_type()
            .and_then(|t| crate::collections::coll_of(&t));
        let coll = coll_ty.is_some_and(|c| !c.assoc);
        if let Some(c) = coll_ty {
            if c.assoc {
                return Err(fail(&format!(
                    "`{}` is an associative array: its keys are never randomized (LRM 18.4) and \
                     a scope randomize of one is not supported",
                    sym.name()
                )));
            }
            if c.bound != 0 {
                return Err(fail(&format!(
                    "`{}` is a bounded queue: a scope randomize does not honor its bound \
                     (LRM 7.10.5), use an unbounded queue or a dynamic array",
                    sym.name()
                )));
            }
            if c.obj {
                return Err(fail(&format!(
                    "`{}` holds class handles, which cannot be randomized (LRM 18.4)",
                    sym.name()
                )));
            }
        }
        if !integral && !real && !aggregate && !coll {
            return Err(fail(&format!(
                "`{}` is not an integral, real, unpacked struct, fixed-size array, queue or \
                 dynamic array variable; a string or class handle cannot be randomized by a \
                 scope randomize in this build",
                sym.name()
            )));
        }
        if !vars.iter().any(|v| v.sym.uid() == sym.uid()) {
            vars.push(ScopeVar {
                expr: a,
                sym,
                real,
                aggregate,
                coll,
            });
        }
    }
    Ok(vars)
}

/// Words one value of `width` bits takes in the call's buffer (4-state packed,
/// as every value of the object store).
fn words_of(width: u32) -> usize {
    (2 * width as usize).div_ceil(64).max(1)
}

/// The spec of one scope randomize call site, prepared at compile time.
struct ScopeSite<'d> {
    spec_id: u32,
    /// Each captured local's expression at the call and its word count, in the
    /// spec's order.
    captured: Vec<(sv_lang::Expression<'d>, usize, u32)>,
    capture_words: usize,
    /// Words of the scratch object the solved variables come back in.
    out_words: usize,
    /// For each distinct argument: where its value is in the output words.
    slots: Vec<FieldSlot>,
    /// Each argument's stored width when it is a scalar real (64 or 32).
    real_width: Vec<Option<u32>>,
    /// Each collection the constraints reach, in the program's array order:
    /// its scratch word (an offset into the output words), the expression
    /// that names it at the call, and whether it is random (an argument) or
    /// only read (state).
    arrays: Vec<SiteArray<'d>>,
}

/// One queue or dynamic array of a scope randomize.
struct SiteArray<'d> {
    word: usize,
    expr: sv_lang::Expression<'d>,
    rand: bool,
}

/// Builds and registers the [`registry::Spec`] of one call.
fn build_site<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    inline: Option<sv_lang::SemNode<'d>>,
    vars: &[ScopeVar<'d>],
) -> Result<ScopeSite<'d>, LoweringError> {
    let fail =
        |why: &str| LoweringError::unsupported_at(&format!("std::randomize(): {why}"), &cx.fn_sym);
    let listed: Vec<vj_sim::symid::SymbolId> = vars.iter().map(|v| v.sym.uid()).collect();
    // Everything the block reads that is not an argument is a variable of the
    // call's scope: a state variable, captured when the call is made (18.12.1).
    let is_local = |x: sv_lang::Expression<'_>| -> bool {
        x.referenced_symbol()
            .is_some_and(|s| !listed.contains(&s.uid()))
    };
    // What each argument declared: one id for a scalar, one per random leaf for
    // an aggregate.
    let mut decl: Vec<Vec<Declared>> = Vec::new();
    // The array id each queue / dynamic array argument declared.
    let mut array_id: Vec<Option<u32>> = Vec::new();
    let built = crate::classes::with_design_trees(|trees| {
        let mut b = Builder::new(trees);
        for v in vars {
            if v.coll {
                let id = b
                    .declare_array_forced(v.sym)
                    .map_err(|e| format!("`{}`: {e}", v.sym.name()))?;
                decl.push(Vec::new());
                array_id.push(Some(id));
                continue;
            }
            array_id.push(None);
            let ds = if v.aggregate {
                b.declare_aggregate_forced(v.sym)
            } else if v.real {
                b.declare_real_forced(v.sym)
                    .map(|i| vec![Declared::Real(i)])
            } else {
                b.declare_var_forced(v.sym).map(|i| vec![Declared::Int(i)])
            }
            .map_err(|e| format!("`{}`: {e}", v.sym.name()))?;
            decl.push(ds);
        }
        if let Some(node) = inline {
            let block = node
                .as_constraint()
                .ok_or_else(|| "internal: the constraints are not a constraint node".to_string())?;
            b.set_local_classifier(is_local);
            b.add_constraint(block)
                .map_err(|e| format!("constraint: {e}"))?;
        }
        b.finish().map_err(|e| e.to_string())
    })
    .map_err(|why| fail(&why))?;
    let set = built;
    // One slot per argument in the scratch object, one after another; the
    // random leaves of an aggregate are windows into its slot.
    let mut offset = 0usize;
    let mut slots = Vec::with_capacity(vars.len());
    let mut real_width = Vec::with_capacity(vars.len());
    let mut owner_int: Vec<usize> = vec![usize::MAX; set.vars.len()];
    let mut owner_real: Vec<usize> = vec![usize::MAX; set.real_vars.len()];
    for (k, (v, ds)) in vars.iter().zip(&decl).enumerate() {
        if v.coll {
            // the collection handle: one raw word, not a 4-state packed value
            slots.push(FieldSlot { offset, words: 1 });
            offset += 1;
            real_width.push(None);
            continue;
        }
        let (width, rw) = if v.aggregate {
            let w = v
                .sym
                .value_type()
                .and_then(|t| vj_sim::state::storage::storage_width(&t))
                .ok_or_else(|| fail(&format!("`{}` has no stored width", v.sym.name())))?;
            (w as u32, None)
        } else {
            match ds[0] {
                Declared::Int(i) => (set.vars[i as usize].ty.width, None),
                Declared::Real(i) => {
                    let w = set.real_vars[i as usize].kind.width();
                    (w, Some(w))
                }
            }
        };
        for d in ds {
            let (owner, i) = match *d {
                Declared::Int(i) => (&mut owner_int, i),
                Declared::Real(i) => (&mut owner_real, i),
            };
            *owner
                .get_mut(i as usize)
                .ok_or_else(|| fail("internal: random variable ids out of step"))? = k;
        }
        let slot = FieldSlot {
            offset,
            words: words_of(width),
        };
        offset += slot.words;
        slots.push(slot);
        real_width.push(rw);
    }
    if owner_int.contains(&usize::MAX) || owner_real.contains(&usize::MAX) {
        return Err(fail("internal: random variable ids out of step"));
    }
    let prog_vars: Vec<ProgVar> = set
        .vars
        .iter()
        .enumerate()
        .map(|(i, v)| ProgVar {
            ty: v.ty,
            slot: slots[owner_int[i]],
            home: Home::Instance,
            prop_index: None,
            domain_conjunct: set.domain_conjunct[i],
            randc: v.randc,
            window: v.window,
        })
        .collect();
    let real_vars: Vec<registry::ProgRealVar> = set
        .real_vars
        .iter()
        .enumerate()
        .map(|(i, v)| registry::ProgRealVar {
            kind: v.kind,
            slot: slots[owner_real[i]],
            home: Home::Instance,
            prop_index: None,
            window: v.window,
        })
        .collect();
    // Every collection the constraints reach has one scratch word holding a
    // copy of its handle: an argument at the slot it already has, a collection
    // the block only reads (state) at a word after the arguments.
    let mut prog_arrays = Vec::with_capacity(set.arrays.len());
    let mut site_arrays = Vec::with_capacity(set.arrays.len());
    for (ai, a) in set.arrays.iter().enumerate() {
        if a.nested || a.template_of.is_some() {
            return Err(fail(&format!(
                "`{}`: an array of dynamic arrays in a scope randomize (`std::randomize`, \
                 `randomize()` of a local): only a class property's rows are solved (C14)",
                a.name
            )));
        }
        let arg = array_id.iter().position(|id| *id == Some(ai as u32));
        let (word, expr) = match arg {
            Some(k) => (slots[k].offset, vars[k].expr),
            None => {
                let node = inline
                    .ok_or_else(|| fail("internal: a collection read without constraints"))?;
                let ex =
                    find_local_occurrence(node, &a.symbol, &[], &is_local).ok_or_else(|| {
                        fail(&format!(
                            "internal: could not find the read of `{}` to capture",
                            a.name
                        ))
                    })?;
                offset += 1;
                (offset - 1, ex)
            }
        };
        prog_arrays.push(registry::ProgArray {
            elem_ty: a.elem_ty,
            elem_words: (2 * a.elem_ty.width as usize).div_ceil(64).max(1),
            slot: FieldSlot {
                offset: word,
                words: 1,
            },
            home: Home::Instance,
            rand: a.rand,
            prop_index: None,
            elem_domain: a.elem_domain.clone(),
            assoc: a.assoc,
            nested: a.nested,
            template_of: a.template_of,
        });
        site_arrays.push(SiteArray {
            word,
            expr,
            rand: a.rand,
        });
    }
    let mut states = Vec::with_capacity(set.states.len());
    for (k, s) in set.states.iter().enumerate() {
        if !set.captures.iter().any(|c| c.state as usize == k) {
            return Err(fail(&format!(
                "internal: `{}` is read as object state in a scope randomize",
                s.name
            )));
        }
        states.push(ProgState {
            root_bit: 0,
            ty: s.ty,
            slot: FieldSlot {
                offset: 0,
                words: 1,
            },
            home: Home::Instance,
            via: Vec::new(),
            window: None,
            class_of: false,
        });
    }
    let frag = Program {
        vars: prog_vars,
        states,
        conjuncts: set.conjuncts,
        before: set.before,
        dists: set.dists,
        funcs: set.funcs,
        softs: set.softs,
        real_vars,
        real: set.real,
        arrays: prog_arrays,
        ..Program::default()
    };
    let mut captured = Vec::new();
    let mut caps = Vec::new();
    for c in &set.captures {
        let node = inline.ok_or_else(|| fail("internal: a capture without constraints"))?;
        let ex = find_local_occurrence(node, &c.symbol, &c.hops, &is_local).ok_or_else(|| {
            fail(&format!(
                "internal: could not find the read of `{}` to capture",
                c.name
            ))
        })?;
        let cap = registry::Capture {
            state: c.state,
            width: c.ty.width,
        };
        captured.push((ex, cap.words(), c.ty.width));
        caps.push(cap);
    }
    let capture_words = captured.iter().map(|(_, n, _)| n).sum();
    let spec_id = registry::register_spec(registry::Spec {
        frag,
        captures: caps,
        only: None,
    });
    Ok(ScopeSite {
        spec_id,
        captured,
        capture_words,
        out_words: offset,
        slots,
        real_width,
        arrays: site_arrays,
    })
}

/// Lowers a scope randomize to an `int` (1 on success, 0 when the constraints
/// cannot be met, LRM 18.12).
pub(crate) fn lower_scope_randomize<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    e: sv_lang::Expression<'d>,
) -> Result<LoweredValue<'ctx>, LoweringError> {
    let at = e.source_range().start;
    let inline = e.randomize_inline_constraints();
    if restriction_list(e).is_some_and(|l| !l.is_empty()) {
        return Err(LoweringError::unsupported_at(
            "std::randomize(): a parenthesized identifier list after `with` is illegal in a \
             scope randomize (LRM 18.7, footnote 43)",
            &cx.fn_sym,
        ));
    }
    let vars = parse_vars(cx, &e.operands())?;
    let site = build_site(cx, inline, &vars)?;

    // The call's buffer: the captured locals' packed values, then the words the
    // solved variables come back in.
    let i64t = cx.ctx.i64_type();
    let capture_words = site.capture_words as u32;
    let total = capture_words + site.out_words as u32;
    let buf = if total == 0 {
        cx.ctx
            .ptr_type(inkwell::AddressSpace::default())
            .const_null()
    } else {
        entry_alloca(cx, i64t.array_type(total), "scope_rand")
    };
    // the locals are read now, at the call (LRM 18.12.1: state variables)
    let mut next = 0u32;
    for (ex, n, w) in &site.captured {
        // a real local is captured as its stored bits (64, or 32 for shortreal)
        let pv = crate::real::to_storage(
            cx,
            crate::expr::lower_expr(cx, *ex)?,
            *w,
            "a constraint reads a variable that is neither integral nor real",
        )?;
        let part = word_ptr(cx, buf, total, next);
        store_packed_words(cx, part, pv, *n as u32);
        next += *n as u32;
    }
    if vars.iter().any(|v| v.aggregate) && total > 0 {
        // the whole buffer is read back by the runtime: no stale words
        for k in 0..total {
            cx.b.build_store(word_ptr(cx, buf, total, k), i64t.const_zero())
                .unwrap();
        }
    }
    // An aggregate's current value goes in first: the solve writes only its
    // random leaves, so the others (a `string` member) keep their value.
    for (v, slot) in vars.iter().zip(&site.slots) {
        if v.aggregate {
            let cur = if v.is_array() {
                crate::arrays::pack_array_value(cx, v.expr)?
            } else {
                crate::expr::lower_expr(cx, v.expr)?.into_packed(
                    "a scope randomize aggregate is not a flat value",
                    &cx.fn_sym,
                )?
            };
            let part = word_ptr(cx, buf, total, capture_words + slot.offset as u32);
            store_packed_words(cx, part, cur, slot.words as u32);
        }
    }
    // A queue / dynamic array goes in as a copy of its handle (copy-on-write,
    // so the variable keeps its contents until the solve succeeds); the solve
    // replaces the copy with the new contents.
    for arr in &site.arrays {
        let c = crate::collections::coll_of_expr(&arr.expr).ok_or_else(|| {
            LoweringError::unsupported_at(
                "std::randomize(): internal: a collection argument without a collection type",
                &cx.fn_sym,
            )
        })?;
        let h = crate::collections::handle_of(cx, arr.expr)?;
        let copy = crate::collections::call(
            cx,
            &c,
            CollOp::Clone,
            h,
            crate::collections::Args::default(),
        );
        cx.b.build_store(
            word_ptr(cx, buf, total, capture_words + arr.word as u32),
            copy,
        )
        .unwrap();
    }
    let code = obj_call(
        cx,
        ObjOp::ScopeRandomize,
        i64t.const_zero(),
        i64t.const_int(site.out_words as u64, false),
        i64t.const_int(u64::from(site.spec_id), false),
        buf,
    );
    for outcome in (0..).map_while(Outcome::from_code) {
        let Some(msg) = outcome.error_message() else {
            continue;
        };
        let hit =
            cx.b.build_int_compare(
                IntPredicate::EQ,
                code,
                i64t.const_int(outcome.code(), false),
                "scope_rand_err",
            )
            .unwrap();
        crate::stmt_shared::error_if(cx, hit, at, msg)?;
    }
    let solved =
        cx.b.build_int_compare(
            IntPredicate::EQ,
            code,
            i64t.const_int(Outcome::Solved.code(), false),
            "scope_rand_ok",
        )
        .unwrap();
    // The copies the solve did not consume are freed: a collection the block
    // only reads always, a random one when the solve failed.
    if !site.arrays.is_empty() {
        let f =
            cx.b.get_insert_block()
                .and_then(|bb| bb.get_parent())
                .expect("lowering happens inside a function");
        let drop_bb = cx.ctx.append_basic_block(f, "scope_rand_drop");
        let keep_bb = cx.ctx.append_basic_block(f, "scope_rand_keep");
        cx.b.build_conditional_branch(solved, keep_bb, drop_bb)
            .unwrap();
        for (bb, only_state) in [(drop_bb, false), (keep_bb, true)] {
            cx.b.position_at_end(bb);
            for arr in site.arrays.iter().filter(|a| a.rand != only_state) {
                let c = crate::collections::coll_of_expr(&arr.expr).expect("checked above");
                let w =
                    cx.b.build_load(
                        i64t,
                        word_ptr(cx, buf, total, capture_words + arr.word as u32),
                        "scope_rand_copy",
                    )
                    .unwrap()
                    .into_int_value();
                crate::collections::call(
                    cx,
                    &c,
                    CollOp::Release,
                    w,
                    crate::collections::Args::default(),
                );
            }
            if bb == drop_bb {
                cx.b.build_unconditional_branch(keep_bb).unwrap();
            }
        }
        // `keep_bb` is the join: positioned there, it was filled above only
        // with the state releases, and the code below continues in it.
    }
    // Nothing is assigned unless the solve succeeded (LRM 18.6.1).
    if !vars.is_empty() {
        let f =
            cx.b.get_insert_block()
                .and_then(|bb| bb.get_parent())
                .expect("lowering happens inside a function");
        let write = cx.ctx.append_basic_block(f, "scope_rand_write");
        let done = cx.ctx.append_basic_block(f, "scope_rand_done");
        cx.b.build_conditional_branch(solved, write, done).unwrap();
        cx.b.position_at_end(write);
        for arr in site.arrays.iter().filter(|a| a.rand) {
            let w =
                cx.b.build_load(
                    i64t,
                    word_ptr(cx, buf, total, capture_words + arr.word as u32),
                    "scope_rand_new",
                )
                .unwrap()
                .into_int_value();
            // the variable's old contents are freed as it is overwritten
            let new = LoweredValue::Packed(crate::collections::handle_value(cx, w));
            if crate::classes::is_class_field_lvalue(arr.expr) {
                // a class property frees the collection it held only on an
                // assignment (`classes::store_field`'s `assign`)
                crate::classes::try_assign_field(cx, arr.expr, new)?;
            } else {
                crate::collections::store_releasing(cx, arr.expr, new)?;
            }
        }
        for ((v, slot), rw) in vars.iter().zip(&site.slots).zip(&site.real_width) {
            if v.coll {
                // stored above, with its old contents freed
                continue;
            }
            if let Some(width) = *rw {
                // the stored bits of the solved real
                let part = word_ptr(cx, buf, total, capture_words + slot.offset as u32);
                let pv = load_packed_words(cx, part, width, cx.repr, slot.words as u32, false);
                crate::expr::store_to(
                    cx,
                    v.expr,
                    LoweredValue::Real(crate::real::bits_to_real(cx, pv)),
                )?;
                continue;
            }
            if v.is_array() {
                // an unpacked array comes back element by element
                let width = v
                    .sym
                    .value_type()
                    .and_then(|t| vj_sim::state::storage::storage_width(&t))
                    .unwrap_or(0) as u32;
                let part = word_ptr(cx, buf, total, capture_words + slot.offset as u32);
                let pv = load_packed_words(cx, part, width, cx.repr, slot.words as u32, false);
                crate::arrays::store_array_from_packed(cx, v.expr, pv)?;
                continue;
            }
            // the variable's own representation and signedness
            let cur = crate::expr::lower_expr(cx, v.expr)?.into_packed(
                "a scope randomize variable is not an integral value",
                &cx.fn_sym,
            )?;
            let part = word_ptr(cx, buf, total, capture_words + slot.offset as u32);
            let pv =
                load_packed_words(cx, part, cur.width, cur.repr, slot.words as u32, cur.signed);
            crate::expr::store_to(cx, v.expr, LoweredValue::Packed(pv))?;
        }
        cx.b.build_unconditional_branch(done).unwrap();
        cx.b.position_at_end(done);
    }
    let i32t = cx.ctx.i32_type();
    let raw =
        cx.b.build_int_z_extend(solved, i32t, "scope_rand_ret")
            .unwrap();
    let op = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    };
    Ok(LoweredValue::Packed(op.pack(
        raw,
        i32t.const_zero(),
        32,
        cx.repr,
        true,
    )))
}