vj-codegen 0.1.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
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
//! Sampled value functions, lowered (sva T16/T17/T18; LRM 1800-2023 16.5.1,
//! 16.9.3, 16.9.4). The model and its hidden state are described in
//! `sv_sim_sim::sva::sampled`; this module is the two compiled halves of it.
//!
//! **Use site** ([`lower_sampled_call`]): a call is the call's argument
//! re-evaluated with its signal reads redirected to hidden cells. The
//! enclosing segment primes those cells into `cx.locals` exactly like an
//! external signal (`process_lower::external_refs_in_segment`), and
//! [`with_views`] temporarily stores a cell's value into the signal's own
//! shadow while the argument lowers, so every other expression lowering
//! stays untouched and unaware. The argument's loads are emitted while the
//! view is installed, so restoring the shadow afterwards cannot change them.
//!
//! **Tick guard** ([`emit_guards`]): per clocked call site one function
//! `i8 guard(handle)` that is 1 when the site's clocking event occurred in the
//! time step that is ending. The engine's end-of-step hook calls it before
//! overwriting the P cells, so the event's old value is the signal's P cell
//! and its new value the signal itself. `iff` and `$past`'s gating expression
//! read the sampled (P) values, which is the value the event control itself
//! saw (the signals a clocked process updates by NBA change only after it).

use std::collections::{BTreeSet, HashMap};

use inkwell::AddressSpace;
use inkwell::context::Context;
use inkwell::module::Module;
use inkwell::values::IntValue;
use sv_lang::kinds::SymbolKind;
use sv_lang::{Design, EdgeKind, Expression, KnownSystemName, Symbol, Walk};
use sv_sim_sim::sva::global_clock::global_clocking_of;
use sv_sim_sim::sva::sampled::{
    ClockEvent, SampledCall, SampledOp, clock_event_of, decode, guard_name, op_of, p_id, site_loc,
    sites_of_block, strict_site_signals_in, use_views,
};
use sv_sim_sim::symid::{SymbolId, SymbolUid};

use crate::accessor::{AccessorFns, build_get_scalar, build_get_wide};
use crate::error::LoweringError;
use crate::expr::{LowerCtx, LoweredValue, MemAccessorCtx, lower_condition, lower_expr};
use crate::module::{member_path, resolve_member_path};
use crate::ops::OpBuilder;
use crate::process_lower::{ProcessLoweringPlan, SignalSlot, SignalSlotClass, mem_signals_of};
use crate::value::{PackedValue, ValueRepr};

/// Lowers `e`, a call to the system function `name`, when it is a sampled
/// value function; `None` for any other.
pub(crate) fn lower_sampled_call<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    name: KnownSystemName,
    e: Expression<'d>,
) -> Option<Result<LoweredValue<'ctx>, LoweringError>> {
    op_of(name)?;
    Some(lower(cx, e))
}

fn unsupported(cx: &LowerCtx<'_, '_, '_>, what: &str) -> LoweringError {
    LoweringError::unsupported_at(what, &cx.fn_sym)
}

fn lower<'ctx, 'd>(
    cx: &LowerCtx<'ctx, 'd, '_>,
    e: Expression<'d>,
) -> Result<LoweredValue<'ctx>, LoweringError> {
    let call = decode(e)
        .map_err(|m| unsupported(cx, &m.0))?
        .ok_or_else(|| unsupported(cx, "not a sampled value function"))?;
    let no_future = || {
        unsupported(
            cx,
            "$future_gclk/$rising_gclk/$falling_gclk/$steady_gclk/$changing_gclk need a value at a \
             FUTURE global clock tick (LRM 16.9.4: they are legal only in a property or sequence \
             expression), so they have no procedural form",
        )
    };
    let in_subroutine = cx.fn_sym.kind() == SymbolKind::Subroutine;
    if cx.fn_sym.kind() != SymbolKind::ProceduralBlock && !in_subroutine {
        return Err(unsupported(
            cx,
            "a sampled value function outside a procedural block or a module-level subroutine \
             (a continuous assignment body) is not supported: its value history is kept per \
             procedural block or subroutine",
        ));
    }
    let args = strict_site_signals_in(call.arg).map_err(|m| unsupported(cx, &m.0))?;
    let views = use_views(&cx.fn_sym.uid(), &call, &args);
    // A subroutine owns its sites' cells (they depend on the clock alone, never
    // on the caller, LRM 16.9.3), so every call shares them: they are read from
    // STATE through the instance handle, not primed by a calling segment.
    let cells = if in_subroutine {
        subroutine_cells(cx, &views)?
    } else {
        HashMap::new()
    };
    if views
        .next
        .iter()
        .any(|(_, cell)| !cx.locals.contains_key(cell) && !cells.contains_key(cell))
    {
        return Err(no_future());
    }
    let cur = with_views(cx, &cells, &views.cur, || lower_expr(cx, call.arg))?;
    if call.op.is_future() {
        // 16.9.4: the value at the next global clock tick, or how it compares
        // with the value now (as `$rose`.. compare now with the previous tick).
        let next = with_views(cx, &cells, &views.next, || lower_expr(cx, call.arg))?;
        if call.op == SampledOp::Future {
            return Ok(next);
        }
        let what = "a future value change function of a real, string or >128-bit value";
        let cur = cur.into_packed(what, &cx.fn_sym)?;
        let next = next.into_packed(what, &cx.fn_sym)?;
        let op = match call.op {
            SampledOp::Rising => SampledOp::Rose,
            SampledOp::Falling => SampledOp::Fell,
            SampledOp::Steady => SampledOp::Stable,
            _ => SampledOp::Changed,
        };
        return Ok(LoweredValue::Packed(value_change(cx, op, next, cur)));
    }
    match call.op {
        SampledOp::Sampled | SampledOp::Past => Ok(cur),
        SampledOp::Rose | SampledOp::Fell | SampledOp::Stable | SampledOp::Changed => {
            let what = "a value change function of a real, string or >128-bit value";
            let cur = cur.into_packed(what, &cx.fn_sym)?;
            let prev = with_views(cx, &cells, &views.prev, || lower_expr(cx, call.arg))?
                .into_packed(what, &cx.fn_sym)?;
            Ok(LoweredValue::Packed(value_change(cx, call.op, cur, prev)))
        }
        SampledOp::Future
        | SampledOp::Rising
        | SampledOp::Falling
        | SampledOp::Steady
        | SampledOp::Changing => unreachable!("lowered above"),
    }
}

/// `$rose`/`$fell`/`$stable`/`$changed` of the sampled value `cur` against
/// the previous tick's `prev` (16.9.3): the first two look at the LSB only,
/// the last two compare the whole value with `===`, so an X/Z that appears or
/// disappears is a change.
fn value_change<'ctx>(
    cx: &LowerCtx<'ctx, '_, '_>,
    op: SampledOp,
    cur: PackedValue<'ctx>,
    prev: PackedValue<'ctx>,
) -> PackedValue<'ctx> {
    let ob = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    };
    let bit = match op {
        SampledOp::Rose => {
            let not_prev1 = cx.b.build_not(lsb_is(cx, prev, true), "prev_not1").unwrap();
            cx.b.build_and(lsb_is(cx, cur, true), not_prev1, "rose")
                .unwrap()
        }
        SampledOp::Fell => {
            let not_prev0 =
                cx.b.build_not(lsb_is(cx, prev, false), "prev_not0")
                    .unwrap();
            cx.b.build_and(lsb_is(cx, cur, false), not_prev0, "fell")
                .unwrap()
        }
        SampledOp::Stable => same_value(cx, cur, prev),
        SampledOp::Changed => {
            let same = same_value(cx, cur, prev);
            cx.b.build_not(same, "changed").unwrap()
        }
        _ => unreachable!("only the four value change functions reach here"),
    };
    ob.pack(bit, ob.i_n(1).const_zero(), 1, cx.repr, false)
}

/// Whether the LSB of `v` is a known 1 (`one`) or a known 0.
pub(crate) fn lsb_is<'ctx>(
    cx: &LowerCtx<'ctx, '_, '_>,
    v: PackedValue<'ctx>,
    one: bool,
) -> IntValue<'ctx> {
    let ob = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    };
    let i1 = ob.i_n(1);
    let val =
        cx.b.build_int_truncate(ob.lane_val(v), i1, "lsb_val")
            .unwrap();
    let unk =
        cx.b.build_int_truncate(ob.lane_unk(v), i1, "lsb_unk")
            .unwrap();
    let known = cx.b.build_not(unk, "lsb_known").unwrap();
    let want = if one {
        val
    } else {
        cx.b.build_not(val, "lsb_val_n").unwrap()
    };
    cx.b.build_and(want, known, "lsb_is").unwrap()
}

/// `a === b` as an `i1`.
pub(crate) fn same_value<'ctx>(
    cx: &LowerCtx<'ctx, '_, '_>,
    a: PackedValue<'ctx>,
    b: PackedValue<'ctx>,
) -> IntValue<'ctx> {
    let ob = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    };
    let eq = ob.caseeq4(a, b);
    cx.b.build_int_truncate(ob.lane_val(eq), ob.i_n(1), "same")
        .unwrap()
}

/// A hidden cell's shadow: pointer, width, signedness (an entry of
/// `LowerCtx::locals`).
type Cell<'ctx> = (inkwell::values::PointerValue<'ctx>, u32, bool);

/// The cells `views` reads, loaded from STATE into fresh shadows: a
/// subroutine's own cells (it has no calling segment to prime them).
fn subroutine_cells<'ctx>(
    cx: &LowerCtx<'ctx, '_, '_>,
    views: &sv_sim_sim::sva::sampled::UseViews,
) -> Result<HashMap<SymbolId, Cell<'ctx>>, LoweringError> {
    let Some(accessor) = cx.mem_accessor else {
        return Err(unsupported(
            cx,
            "a sampled value function in a subroutine called where no instance handle is \
             available",
        ));
    };
    let body = sv_sim_sim::netlist::enclosing_instance_body(&cx.fn_sym).ok_or_else(|| {
        unsupported(
            cx,
            "a sampled value function in a subroutine outside any instance",
        )
    })?;
    if !sv_sim_sim::netlist::module_items(&body)
        .iter()
        .any(|m| m.uid() == cx.fn_sym.uid())
    {
        return Err(unsupported(
            cx,
            "a sampled value function in a class method or package subroutine (its value \
             history is kept per module-level subroutine, LRM 16.9.3)",
        ));
    }
    let slots = crate::module::signal_slots_of(&body)?;
    let mut out = HashMap::new();
    for (_, id) in views.cur.iter().chain(&views.prev).chain(&views.next) {
        if out.contains_key(id) {
            continue;
        }
        let Some(info) = slots.get(id) else {
            return Err(unsupported(
                cx,
                "a sampled value function's history cell has no state slot",
            ));
        };
        let raw = match info.class {
            SignalSlotClass::Inline => {
                build_get_scalar(cx, accessor.fns, accessor.handle, info.slot, info.width)
            }
            SignalSlotClass::Wide => {
                build_get_wide(cx, accessor.fns, accessor.handle, info.slot, info.width)
            }
            SignalSlotClass::Memory { .. } => {
                return Err(unsupported(cx, "a sampled value function over a memory"));
            }
        };
        let ty = OpBuilder {
            ctx: cx.ctx,
            b: &cx.b,
        }
        .i_n(info.width * 2);
        let ptr = crate::types::entry_alloca_in(&cx.b, ty, "sva_cell");
        cx.b.build_store(ptr, raw).unwrap();
        out.insert(id.clone(), (ptr, info.width, info.signed));
    }
    Ok(out)
}

/// Runs `f` with each `(signal, cell)` pair's signal shadow holding the
/// cell's value (from `cells`, else `cx.locals`), then restores the shadows.
/// Every load `f` emits happens while the view is installed.
fn with_views<'ctx, T>(
    cx: &LowerCtx<'ctx, '_, '_>,
    cells: &HashMap<SymbolId, Cell<'ctx>>,
    views: &[(SymbolId, SymbolId)],
    f: impl FnOnce() -> Result<T, LoweringError>,
) -> Result<T, LoweringError> {
    let mut saved = Vec::with_capacity(views.len());
    for (sig, cell) in views {
        let (Some(&(sig_ptr, w, _)), Some(&(cell_ptr, cell_w, _))) = (
            cx.locals.get(sig),
            cells.get(cell).or_else(|| cx.locals.get(cell)),
        ) else {
            return Err(unsupported(
                cx,
                "a sampled value function reads a signal whose value history is not available \
                 here (an automatic or block-local variable, a hierarchical reference, or a call \
                 outside a procedural block)",
            ));
        };
        debug_assert_eq!(w, cell_w, "a hidden cell is a clone of its source");
        // A memory of a concurrent assertion's row is a local array of packed
        // elements (`shadow_memory`): its view swaps the whole array.
        let ty = match sig_ptr
            .as_instruction()
            .and_then(|i| i.get_allocated_type().ok())
        {
            Some(t) if t.is_array_type() => t,
            _ => OpBuilder {
                ctx: cx.ctx,
                b: &cx.b,
            }
            .i_n(w * 2)
            .into(),
        };
        let old = cx.b.build_load(ty, sig_ptr, "view_old").unwrap();
        let new = cx.b.build_load(ty, cell_ptr, "view_cell").unwrap();
        cx.b.build_store(sig_ptr, new).unwrap();
        saved.push((sig_ptr, old));
    }
    let r = f();
    for (ptr, old) in saved {
        cx.b.build_store(ptr, old).unwrap();
    }
    r
}

/// `edge` of the clock signal's LSB going from `prev` to `cur` (9.4.2:
/// `posedge` is 0->1/x/z or x/z->1, `negedge` the mirror image); a bare
/// `@(sig)` is any change of the whole value.
pub(crate) fn edge_occurred<'ctx>(
    cx: &LowerCtx<'ctx, '_, '_>,
    edge: EdgeKind,
    prev: PackedValue<'ctx>,
    cur: PackedValue<'ctx>,
) -> IntValue<'ctx> {
    let b = &cx.b;
    let pos = |b: &inkwell::builder::Builder<'ctx>| {
        // (prev == 0 && cur != 0) || (prev != 1 && cur == 1)
        let not_cur0 = b.build_not(lsb_is(cx, cur, false), "n_cur0").unwrap();
        let rise_a = b
            .build_and(lsb_is(cx, prev, false), not_cur0, "rise_a")
            .unwrap();
        let not_prev1 = b.build_not(lsb_is(cx, prev, true), "n_prev1").unwrap();
        let rise_b = b
            .build_and(not_prev1, lsb_is(cx, cur, true), "rise_b")
            .unwrap();
        b.build_or(rise_a, rise_b, "posedge").unwrap()
    };
    let neg = |b: &inkwell::builder::Builder<'ctx>| {
        // (prev == 1 && cur != 1) || (prev != 0 && cur == 0)
        let not_cur1 = b.build_not(lsb_is(cx, cur, true), "n_cur1").unwrap();
        let fall_a = b
            .build_and(lsb_is(cx, prev, true), not_cur1, "fall_a")
            .unwrap();
        let not_prev0 = b.build_not(lsb_is(cx, prev, false), "n_prev0").unwrap();
        let fall_b = b
            .build_and(not_prev0, lsb_is(cx, cur, false), "fall_b")
            .unwrap();
        b.build_or(fall_a, fall_b, "negedge").unwrap()
    };
    match edge {
        EdgeKind::PosEdge => pos(b),
        EdgeKind::NegEdge => neg(b),
        EdgeKind::BothEdges => {
            let p = pos(b);
            let n = neg(b);
            b.build_or(p, n, "anyedge").unwrap()
        }
        EdgeKind::None => {
            let same = same_value(cx, prev, cur);
            b.build_not(same, "changed").unwrap()
        }
    }
}

/// How a procedural block's inferred clock (16.14.6, or the default clocking
/// of 16.15) is found. The analysis is built once, on the first call that
/// needs it: it is the only consumer in a normal design.
pub(crate) struct ClockResolver<'d> {
    design: &'d Design,
    analysis: Option<sv_lang::Analysis<'d>>,
    /// The design's syntax trees, fetched on the first global clocking lookup.
    trees: Option<Vec<sv_lang::SyntaxTree>>,
}

impl<'d> ClockResolver<'d> {
    pub(crate) fn new(design: &'d Design) -> Self {
        Self {
            design,
            analysis: None,
            trees: None,
        }
    }

    /// The event of the global clocking in effect for procedural block
    /// `block` (LRM 14.14's hierarchical lookup, `$global_clock`).
    fn global_event(&mut self, block: &Symbol<'d>) -> Result<ClockEvent<'d>, LoweringError> {
        let unsup = |what: &str| LoweringError::unsupported_at(what, block);
        let body = sv_sim_sim::netlist::enclosing_instance_body(block)
            .ok_or_else(|| unsup("a procedural block outside any instance body"))?;
        let design = self.design;
        let trees = self.trees.get_or_insert_with(|| design.syntax_trees());
        let found = global_clocking_of(&body, trees).map_err(|e| unsup(&e.0))?;
        let Some(global) = found else {
            return Err(unsup(
                "a global clocking sampled value function with no global clocking declaration in \
                 scope (LRM 14.14: the lookup climbs the instance hierarchy to the top and an \
                 absent declaration is an error)",
            ));
        };
        // A declaration `levels_up > 0` above names signals of an enclosing
        // instance: they are this instance's foreign references and its
        // hidden P cell for each (`hidden_slots`) holds their history.
        let tc = global
            .block
            .clocking_block_event()
            .and_then(|n| n.as_timing_control())
            .ok_or_else(|| unsup("the global clocking declaration has no event"))?;
        clock_event_of(tc).map_err(|e| unsup(&format!("the global clocking's event: {}", e.0)))
    }

    /// The clocking event of a sampled value function with none written, called
    /// in subroutine `sub`: the default clocking (LRM 16.9.3: not in an
    /// assertion, and a subroutine is no procedure whose clock could be
    /// inferred, 16.14.6), else an error -- the function has no clock.
    fn subroutine_default_event(
        &mut self,
        sub: &Symbol<'d>,
    ) -> Result<ClockEvent<'d>, LoweringError> {
        let unsup = |what: &str| LoweringError::unsupported_at(what, sub);
        let design = self.design;
        let trees = self.trees.get_or_insert_with(|| design.syntax_trees());
        let tree = trees
            .iter()
            .find(|t| sub.syntax(t).is_some())
            .ok_or_else(|| {
                unsup("no syntax tree for a subroutine holding a sampled value function")
            })?;
        let found = sv_sim_sim::state::default_clocking_of(sub, tree)
            .map_err(|e| unsup(&format!("default clocking (LRM 14.12): {e:?}")))?;
        let Some(cb) = found else {
            return Err(unsup(
                "a sampled value function in a subroutine has no clocking event (LRM 16.9.3: a \
                 subroutine is no procedure, so no clock is inferred from it (16.14.6), and an \
                 explicit clocking event is a timing control a function body cannot hold; \
                 declare a default clocking (14.12) in the module, or use $sampled)",
            ));
        };
        let tc = cb
            .clocking_block_event()
            .and_then(|n| n.as_timing_control())
            .ok_or_else(|| unsup("the default clocking declaration has no event"))?;
        clock_event_of(tc).map_err(|e| unsup(&format!("the default clocking's event: {}", e.0)))
    }

    /// The clocking event of `call`, which is in procedural block `block`.
    fn resolve<'a>(
        &'a mut self,
        block: &Symbol<'d>,
        call: &SampledCall<'d>,
    ) -> Result<ClockEvent<'a>, LoweringError> {
        let unsup = |what: &str| LoweringError::unsupported_at(what, block);
        if let Some(ev) = call.clock {
            return Ok(ev);
        }
        if block.kind() == SymbolKind::Subroutine {
            if call.global {
                return Err(unsup(
                    "a global clocking sampled value function ($rising_gclk and the like) in a \
                     subroutine (LRM 16.9.4: legal only in a property or sequence expression)",
                ));
            }
            return self.subroutine_default_event(block);
        }
        if call.global {
            return self.global_event(block);
        }
        let (tc, twin) = self.procedure_clock(block)?;
        // A sampled value function in the action block of a static concurrent
        // assertion: the clock is the assertion's (LRM 16.9.3, 16.14.1), not
        // the hosting procedure's (which has no event control).
        let tc = tc.or_else(|| static_assertion_clock(block));
        let tc = tc.ok_or_else(|| {
            unsup(
                "no clock can be inferred for a sampled value function with no clocking event \
                 (LRM 16.14.6: the procedure shall have no blocking timing control and exactly \
                 one event control, and no default clocking applies)",
            )
        })?;
        let mut ev = sv_sim_sim::sva::sampled::clock_event_of(tc).map_err(|e| unsup(&e.0))?;
        // `@(posedge clk iff $changed(d))`: the sampled call in the guard is
        // clocked by the edge it guards, not by itself.
        if ev.iff.is_some_and(|g| holds_sampled_call(g.as_sem_node())) {
            ev.iff = None;
        }
        if twin {
            return sv_sim_sim::sva::sampled::relocate_clock(block, &ev).map_err(|e| unsup(&e.0));
        }
        Ok(ev)
    }

    /// The clock inferred for procedural block `block` (16.14.6, then the default
    /// clocking of 16.15), and whether it was inferred on an identical instance's
    /// copy of the procedure (the analysis tracks one instance of each set of
    /// identical ones), so that its nodes belong to that other instance.
    fn procedure_clock<'a>(
        &'a mut self,
        block: &Symbol<'d>,
    ) -> Result<(Option<sv_lang::TimingControl<'a>>, bool), LoweringError> {
        let unsup = |what: &str| LoweringError::unsupported_at(what, block);
        if self.analysis.is_none() {
            self.analysis = Some(
                self.design
                    .analyze(sv_lang::AnalysisFlags::NONE, 1)
                    .map_err(|e| {
                        unsup(&format!(
                            "clock inference failed to analyze the design: {e}"
                        ))
                    })?,
            );
        }
        let analysis = self.analysis.as_ref().expect("built above");
        let owner = block.uid();
        let infer = |proc: &sv_lang::AnalyzedProcedure<'a>| {
            sv_sim_sim::analysis::infer_procedural_clock(proc).map_err(|e| unsup(&e.to_string()))
        };
        let body = sv_sim_sim::netlist::enclosing_instance_body(block)
            .ok_or_else(|| unsup("a procedural block outside any instance body"))?;
        // Inference is not scoped to the module body (LRM 16.9.3, 16.14.6): a
        // procedure inside a generate block belongs to its instance, and the
        // analysis files it under the generate block's scope.
        if let Some(proc) = find_procedure(analysis, &body, owner) {
            // slang's analysis infers nothing for `@(posedge clk iff c)`, which
            // LRM 16.14.6(c)(1) admits (`edge expr1 [iff expr2]`): the
            // structural reading of the same rules decides it then.
            let tc = match infer(&proc)? {
                Some(tc) => Some(tc),
                None => sv_sim_sim::analysis::infer_structural_clock(block)
                    .map_err(|e| unsup(&e.to_string()))?,
            };
            return Ok((tc, false));
        }
        // A procedural checker instance's body (17.3) is analyzed under its
        // enclosing procedure and is not filed under any scope, so the analysis
        // cannot list its procedures: they are decided structurally.
        let mut scope = block.parent();
        while let Some(s) = scope {
            if s.kind() == SymbolKind::CheckerInstanceBody {
                let tc = sv_sim_sim::analysis::infer_structural_clock(block)
                    .map_err(|e| unsup(&e.to_string()))?;
                return Ok((tc, false));
            }
            scope = s.parent();
        }
        let path = member_path(block, &body)
            .ok_or_else(|| unsup("a procedural block that is not under its instance body"))?;
        let mut twins = Vec::new();
        self.design.root().visit(|sym| {
            if sym.kind() == SymbolKind::Instance
                && let Some(b) = sym.instance_body()
                && b != body
                && b.instance_body_has_same_type(&body)
            {
                twins.push(b);
            }
            Walk::Continue
        });
        for twin in twins {
            let Some(twin_block) = resolve_member_path(&twin, &path) else {
                continue;
            };
            let twin_uid = twin_block.uid();
            if let Some(proc) = find_procedure(analysis, &twin, twin_uid) {
                return Ok((infer(&proc)?, true));
            }
        }
        Err(unsup(
            "the clock of a sampled value function with no clocking event could not be \
             inferred: the analysis has no record of this procedure, in its instance body \
             or any of that body's generate blocks, or of an identical instance's; write the clocking event explicitly, e.g. $past(x, , , @(posedge clk))",
        ))
    }

    /// The timing control, written in `block`'s own body, that is the block's
    /// inferred clock (16.14.6); `None` when no clock is inferred from the
    /// procedure (the default clocking, an event outside the block, applies).
    pub(crate) fn inferred_node(
        &mut self,
        block: &Symbol<'d>,
    ) -> Result<Option<sv_lang::SemNode<'d>>, LoweringError> {
        let (tc, _) = self.procedure_clock(block)?;
        Ok(tc.and_then(|tc| sv_sim_sim::sva::sampled::relocate_clock_node(block, &tc)))
    }
}

/// The analysed procedure whose symbol is `owner`, among the procedures of
/// instance body `body` and of every instantiated generate block nested in it
/// (the analysis keys procedures by their immediate scope).
fn find_procedure<'a>(
    analysis: &'a sv_lang::Analysis<'_>,
    body: &Symbol<'a>,
    owner: SymbolId,
) -> Option<sv_lang::AnalyzedProcedure<'a>> {
    // A static checker instance's procedures are analyzed under its own body
    // scope (17.3: its items are flattened into the instance, but slang files
    // them by the checker's scope).
    let scopes: Vec<Symbol<'a>> = std::iter::once(*body)
        .chain(sv_sim_sim::netlist::generate_scopes(body))
        .collect();
    let checkers = scopes
        .iter()
        .flat_map(sv_sim_sim::netlist::checker_instances)
        .filter_map(|c| sv_sim_sim::netlist::checker_instance_body(&c));
    scopes
        .iter()
        .copied()
        .chain(checkers)
        .find_map(|s| analysis.procedures(s).find(|p| p.symbol().uid() == owner))
}

/// Whether a sampled value function call is anywhere under `node`.
fn holds_sampled_call(node: sv_lang::SemNode<'_>) -> bool {
    let mut stack = vec![node];
    while let Some(n) = stack.pop() {
        if let Some(e) = n.as_expression()
            && matches!(sv_sim_sim::sva::sampled::decode(e), Ok(Some(_)))
        {
            return true;
        }
        stack.extend(n.children());
    }
    false
}

/// The leading clocking event a static concurrent assertion hosted by procedural
/// block `block` writes (`assert property (@(posedge clk) ...)`), if any.
fn static_assertion_clock<'d>(block: &Symbol<'d>) -> Option<sv_lang::TimingControl<'d>> {
    let stmt =
        crate::module::static_assertion_statement(sv_sim_sim::process::cfg::proc_body(block)?)?;
    stmt.as_sem_node()
        .children()
        .into_iter()
        .find_map(|n| n.as_assertion_expr())
        .filter(|r| r.kind() == sv_lang::kinds::AssertionExprKind::Clocking)
        .and_then(|r| r.children().first().copied())
        .and_then(|n| n.as_timing_control())
}

/// Compiles the tick guard of every clocked sampled-value call site of every
/// plan's procedural block into `module`, named [`guard_name`].
pub(crate) fn emit_guards<'ctx, 'd>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    fns: &AccessorFns<'ctx>,
    plans: &[ProcessLoweringPlan<'d>],
    design: &'d Design,
) -> Result<(), LoweringError> {
    let mut resolver = ClockResolver::new(design);
    let mut bodies = BTreeSet::new();
    for plan in plans {
        // The sites of the instance's module-level subroutines (16.9.3), once
        // per instance body.
        if let Some(body) = sv_sim_sim::netlist::enclosing_instance_body(&plan.fn_sym)
            && bodies.insert(body.uid())
        {
            emit_subroutine_guards(ctx, module, fns, &mut resolver, &body)?;
        }
        if plan.fn_sym.kind() != SymbolKind::ProceduralBlock {
            continue;
        }
        let sites = sites_of_block(&plan.fn_sym)
            .map_err(|e| LoweringError::unsupported_at(&e.0, &plan.fn_sym))?;
        let owner = plan.fn_sym.uid();
        for site in sites {
            let call = site.call;
            if call.op.depth(call.ticks) == 0 || call.op.is_future() {
                continue;
            }
            let name = guard_name(&owner, &site_loc(&call.call));
            // Two plans of one block (a process split by the lowerer) share
            // its sites.
            if module.get_function(&name).is_some() {
                continue;
            }
            let ev = resolver.resolve(&plan.fn_sym, &call)?;
            compile_site_guard(
                ctx,
                module,
                fns,
                plan.fn_sym,
                &plan.signals,
                &name,
                &call,
                &ev,
            )?;
        }
    }
    Ok(())
}

/// The tick guards of the clocked sampled-value call sites in the module-level
/// subroutines of instance body `body`.
fn emit_subroutine_guards<'ctx, 'd>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    fns: &AccessorFns<'ctx>,
    resolver: &mut ClockResolver<'d>,
    body: &Symbol<'d>,
) -> Result<(), LoweringError> {
    let mut slots = None;
    for sub in subroutines_of(body) {
        let sites = sites_of_block(&sub).map_err(|e| LoweringError::unsupported_at(&e.0, &sub))?;
        let owner = sub.uid();
        for site in sites {
            let call = site.call;
            if call.op.depth(call.ticks) == 0 || call.op.is_future() {
                continue;
            }
            let name = guard_name(&owner, &site_loc(&call.call));
            if module.get_function(&name).is_some() {
                continue;
            }
            let ev = resolver.resolve(&sub, &call)?;
            if slots.is_none() {
                slots = Some(crate::module::signal_slots_of(body)?);
            }
            let table = slots.as_ref().expect("built above");
            compile_site_guard(ctx, module, fns, sub, table, &name, &call, &ev)?;
        }
    }
    Ok(())
}

/// The subroutines declared in instance body `body` (generate blocks and
/// checkers flattened), each owning the hidden history of its own sampled
/// value calls.
fn subroutines_of<'d>(body: &Symbol<'d>) -> Vec<Symbol<'d>> {
    sv_sim_sim::netlist::module_items(body)
        .into_iter()
        .filter(|m| m.kind() == SymbolKind::Subroutine)
        .collect()
}

/// Whether any plan's block or its instance's subroutines hold a sampled
/// value call (AOT output has no end-of-step hook to advance their history,
/// so it refuses them).
pub(crate) fn plans_use_sampled_values(plans: &[ProcessLoweringPlan<'_>]) -> bool {
    plans.iter().any(|p| {
        (p.fn_sym.kind() == SymbolKind::ProceduralBlock
            && sites_of_block(&p.fn_sym).map_or(true, |s| !s.is_empty()))
            || sv_sim_sim::netlist::enclosing_instance_body(&p.fn_sym).is_some_and(|b| {
                subroutines_of(&b)
                    .iter()
                    .any(|s| sites_of_block(s).map_or(true, |x| !x.is_empty()))
            })
    })
}

/// `block` is the procedural block or subroutine owning the site; `signals`
/// its instance's slot table.
#[allow(clippy::too_many_arguments)]
fn compile_site_guard<'ctx, 'd>(
    ctx: &'ctx Context,
    module: &Module<'ctx>,
    fns: &AccessorFns<'ctx>,
    block: Symbol<'d>,
    signals: &HashMap<SymbolId, SignalSlot>,
    name: &str,
    call: &SampledCall<'d>,
    ev: &ClockEvent<'d>,
) -> Result<(), LoweringError> {
    // An inferred clock's expressions come from the analysis, so `'d` is the
    // shorter of the two lifetimes (the AST handles are covariant).
    let fn_sym: Symbol<'d> = block;
    let fn_sym = &fn_sym;
    let i8_ty = ctx.i8_type();
    let ptr_ty = ctx.ptr_type(AddressSpace::default());
    let f = module.add_function(name, i8_ty.fn_type(&[ptr_ty.into()], false), None);
    f.get_nth_param(0).unwrap().set_name("handle");
    let entry = ctx.append_basic_block(f, "entry");
    let handle = f.get_nth_param(0).unwrap().into_pointer_value();
    let b = ctx.create_builder();
    b.position_at_end(entry);
    let mut cx = LowerCtx {
        ctx,
        b,
        repr: ValueRepr::FourState,
        locals: HashMap::new(),
        fn_sym: *fn_sym,
        module,
        mem_signals: mem_signals_of(signals),
        mem_accessor: Some(MemAccessorCtx { handle, fns }),
        lvalue_ref: Default::default(),
        deferred_start: Default::default(),
        unbounded: Default::default(),
        coll_temp: Default::default(),
        iter: Default::default(),
        loop_exits: Default::default(),
        block_exits: Default::default(),
        write_through: None,
        cells: Default::default(),
        copy_out: Default::default(),
        ctor_this: Default::default(),
        decl_scale: Default::default(),
        inline_this: Default::default(),
        end_flags: Default::default(),
        specify_dest: Default::default(),
        elem_refs: Default::default(),
    };

    let bad = |m: String| LoweringError::unsupported_at(&m, fn_sym);
    let mut sigs: BTreeSet<SymbolId> = strict_site_signals_in(ev.signal).map_err(|e| bad(e.0))?;
    for extra in ev.iff.into_iter().chain(call.gate) {
        sigs.extend(strict_site_signals_in(extra).map_err(|e| bad(e.0))?);
    }
    let mut views = Vec::new();
    for sig in &sigs {
        for id in [sig.clone(), p_id(sig)] {
            prime(&mut cx, fns, handle, signals, &id)?;
        }
        views.push((sig.clone(), p_id(sig)));
    }

    let what = "a clocking event on a real, string or >128-bit value";
    let cur = lower_expr(&cx, ev.signal)?.into_packed(what, fn_sym)?;
    let no_cells = HashMap::new();
    let prev = with_views(&cx, &no_cells, &views, || lower_expr(&cx, ev.signal))?
        .into_packed(what, fn_sym)?;
    let mut tick = edge_occurred(&cx, ev.edge, prev, cur);
    for cond in ev.iff.into_iter().chain(call.gate) {
        let c = with_views(&cx, &no_cells, &views, || lower_condition(&cx, cond))?;
        tick = cx.b.build_and(tick, c, "tick_and").unwrap();
    }
    let ext = cx.b.build_int_z_extend(tick, i8_ty, "tick_i8").unwrap();
    cx.b.build_return(Some(&ext)).unwrap();
    if f.verify(true) {
        Ok(())
    } else {
        Err(LoweringError {
            message: "sampled value tick guard failed LLVM verify".into(),
            at: fn_sym.hierarchical_path(),
        })
    }
}

/// Primes `id`'s shadow in `cx.locals` from its state slot.
fn prime<'ctx>(
    cx: &mut LowerCtx<'ctx, '_, '_>,
    fns: &AccessorFns<'ctx>,
    handle: inkwell::values::PointerValue<'ctx>,
    signals: &HashMap<SymbolId, SignalSlot>,
    id: &SymbolId,
) -> Result<(), LoweringError> {
    let Some(info) = signals.get(id) else {
        return Err(unsupported(
            cx,
            "a sampled value function's clock or gate reads a signal with no state slot here",
        ));
    };
    let raw = match info.class {
        SignalSlotClass::Inline => build_get_scalar(cx, fns, handle, info.slot, info.width),
        SignalSlotClass::Wide => build_get_wide(cx, fns, handle, info.slot, info.width),
        SignalSlotClass::Memory { .. } => {
            return Err(unsupported(
                cx,
                "a sampled value function's clock or gate reads a memory",
            ));
        }
    };
    let ty = OpBuilder {
        ctx: cx.ctx,
        b: &cx.b,
    }
    .i_n(info.width * 2);
    let ptr = crate::types::entry_alloca_in(&cx.b, ty, "sva_shadow");
    cx.b.build_store(ptr, raw).unwrap();
    cx.locals.insert(id.clone(), (ptr, info.width, info.signed));
    Ok(())
}