beck_core/
signal.rs

1//! The signal graph, as a graph.
2//!
3//! [`docs/03-type-and-effect-system.md`](../../../../../docs/03-type-and-effect-system.md) §3.7:
4//! "**The signal graph is a graph, not a pipeline.** This section reads top-to-bottom, and the
5//! programs it describes do not: `events` is decided from the state, and the state is folded from
6//! `events`. The cycle is real and it is sound."
7//!
8//! Phase 1 and Phase 2 read the graph by *recognising one shape*: find the `merge_clients()`, find
9//! the `durable`, find the `decide`, find the first client-placed signal, and inline everything
10//! between them ([`docs/19-phase-1-report.md`](../../../../../docs/19-phase-1-report.md) §19.9). That
11//! was legitimate narrowness because it announced itself — nine diagnostics refused every other
12//! shape — and it was named as debt by two phases running. It also had a hole neither report knew
13//! about: a program with *two* durable folds matched the shape, was accepted, and was sliced with
14//! both folds reading the same accumulator. See
15//! [`docs/23-incremental-views-report.md`](../../../../../docs/23-incremental-views-report.md) §23.2.
16//!
17//! This module is the replacement. It does not recognise a shape. It builds the graph the program
18//! wrote — one vertex per signal operation, including the ones nested inside a declaration —
19//! computes its strongly connected components, and hands [`crate::split`] a structure to slice.
20//! What used to be "the durable one" is now "the vertices whose op is [`Op::Durable`]", and there
21//! may be any number of them.
22//!
23//! # What a vertex is
24//!
25//! A *declared* signal contributes one vertex per prim application in its expression, not one per
26//! declaration. `todos: Signal[State] = durable(fold(apply_event, empty, events))` is two vertices
27//! — a [`Op::Durable`] over a [`Op::Fold`] — because the fold is a node in the dataflow whether or
28//! not the program gave it a name. Only the outermost carries the declared name; the inner one is
29//! labelled `todos·fold` so a diagnostic and `beck explain flow` can still point at it.
30//!
31//! That is the difference between a graph and a pattern: `map2(f, durable(fold(…)), summary)`
32//! needs no new case here, because there was never a case to begin with.
33//!
34//! # Cycles
35//!
36//! The condensation is computed by [`crate::graph::DepGraph`], which already does Tarjan
37//! iteratively over a CSR adjacency and numbers components in topological order. Reusing it rather
38//! than writing a second SCC pass is the point of it being a separate module.
39//!
40//! One rule is imposed on the result: **every cycle must contain a fold**. The `decide → durable →
41//! fold → decide` cycle is sound because the fold is where the recursion bottoms out — the
42//! accumulator is a value the slicer can take as a parameter. A cycle of pure `signal_map`s has no
43//! such point and is a program with no meaning; [`Graph::build`] refuses it by name rather than
44//! looping.
45
46use std::collections::BTreeMap;
47use std::sync::Arc;
48
49use beck_diag::{Diagnostic, Diagnostics, Span};
50
51use crate::check::Program;
52use crate::core::{Core, CoreKind, Prim};
53use crate::graph::{DepGraph, EdgeKind, GraphBuilder, GraphNode, NodeId, NodeKind};
54use crate::ty::{Tier, Ty};
55
56/// An index into [`Graph::nodes`].
57pub type SigId = usize;
58
59/// The accumulator a program with several durable folds is compiled to.
60///
61/// §3.7 fixes "one totally-ordered log per application", and the runtime holds one accumulator over
62/// it. A program that writes two `durable` folds has not asked for two logs; it has asked for two
63/// projections of one. [`crate::split`] fuses them into a record of this type, which is why the
64/// name is unwritable in the surface syntax: it is a compiler product, and no module publishes it.
65pub const FUSED_STATE: &str = "$State";
66
67/// What a vertex does. One variant per construct in §3.7's signal vocabulary.
68#[derive(Clone, Debug)]
69pub enum Op {
70    /// `merge_clients()` — the one place time and nondeterminism enter.
71    Ingress,
72    /// `presence()` — who is connected now.
73    ///
74    /// A source like [`Op::Ingress`] and a `Signal` rather than a `Stream`: connections are a value
75    /// defined at all times, not occurrences. It is the only vertex here that is neither the log
76    /// nor derived from it, which is the one fact everything else about it follows from — see
77    /// [`crate::split`] for what that forbids and [`crate::plan`] for what it costs.
78    Presence,
79    /// `awareness(f)` — every subscriber's `f(session)`, keyed by actor.
80    ///
81    /// A source for [`Op::Presence`]'s reason and with its rules: the runtime holds every
82    /// connection's `Session`, so it computes each contribution itself and this vertex reads what
83    /// it publishes. `f` is carried here rather than being a signal input because there is no
84    /// signal to read — the subscribers are the runtime's fact, not the graph's.
85    Awareness { f: Core },
86    /// `freshness()` — whether what is about to be rendered is confirmed or a guess.
87    ///
88    /// A source like [`Op::Presence`], and the other one that is not the log. The difference is
89    /// which side holds it: presence is the server's fact about its sockets, freshness is the
90    /// client's fact about its own unacknowledged commands — so this one is refused to a page
91    /// that renders on the *server* ([`crate::render`]) where presence is refused to a page that
92    /// renders on the client.
93    Freshness,
94    /// `decide(proposals, state, validate)` — §3.5's authority chokepoint, as a node.
95    Decide { validate: Core },
96    /// `fold(step, init, stream)`. The accumulator, and the point at which a cycle bottoms out.
97    Fold { step: Core, init: Core },
98    /// `gestures(step, init)` — D30's non-durable fold, and a source like [`Op::Presence`].
99    ///
100    /// A source rather than an operator over one, because the stream it folds has no other
101    /// consumer it could have: a gesture is this client's and the graph has nothing else on that
102    /// side of the seam. So the step and the initial value are carried here for [`Op::Awareness`]'s
103    /// reason — there is no signal to read — and the vertex has no inputs, which is what makes the
104    /// accumulator unreachable from the log by construction rather than by a rule.
105    Gestures { step: Core, init: Core },
106    /// `durable(signal)` — the accumulator that survives a restart, and therefore the one the log
107    /// is *of*.
108    Durable,
109    /// `signal_map(s, f)`.
110    Map { f: Core },
111    /// `map2(f, a, b)`.
112    Map2 { f: Core },
113    /// `per_session(s, f)` — §3.8's fanout point, first-class so that Phase 3 can share the
114    /// arrangement above it.
115    PerSession { f: Core },
116    /// `filter_map(s, f)` on a stream.
117    FilterMap { f: Core },
118    /// A signal declared as another signal: `mirror: Signal[T] = todos`.
119    Alias,
120}
121
122impl Op {
123    pub fn name(&self) -> &'static str {
124        match self {
125            Op::Ingress => "merge_clients",
126            Op::Presence => "presence",
127            Op::Awareness { .. } => "awareness",
128            Op::Freshness => "freshness",
129            Op::Decide { .. } => "decide",
130            Op::Fold { .. } => "fold",
131            Op::Gestures { .. } => "gestures",
132            Op::Durable => "durable",
133            Op::Map { .. } => "signal_map",
134            Op::Map2 { .. } => "map2",
135            Op::PerSession { .. } => "per_session",
136            Op::FilterMap { .. } => "filter_map",
137            Op::Alias => "alias",
138        }
139    }
140
141    /// Whether this vertex carries a `Stream` rather than a `Signal` — occurrences rather than a
142    /// value defined at all times (§3.7). A view is a function of signals, so a stream vertex on a
143    /// view's path is an error rather than a missing feature.
144    pub fn is_stream(&self) -> bool {
145        matches!(self, Op::Ingress | Op::Decide { .. } | Op::FilterMap { .. })
146    }
147}
148
149#[derive(Clone, Debug)]
150pub struct Node {
151    /// The declared name, when this vertex *is* a signal declaration rather than a sub-expression
152    /// of one.
153    pub name: Option<Arc<str>>,
154    /// What to call it in a diagnostic: the declared name, or `<parent>·<op>` for an inner vertex.
155    pub label: Arc<str>,
156    pub op: Op,
157    pub ty: Ty,
158    pub tier: Tier,
159    /// The vertices this one reads, in the order the construct takes them.
160    pub inputs: Vec<SigId>,
161    pub span: Span,
162}
163
164/// A dataflow edge whose two ends are on different tiers.
165///
166/// §4.3: "Every signal edge that crosses tiers becomes a subscription: the server side gets a diff
167/// operator, the client side a resumable `(subscription, seq)` consumer." Phase 1 and Phase 2 knew
168/// about exactly one crossing and printed a sentence about it; this enumerates them, and gives each
169/// the content-derived id a resumable subscription is keyed by.
170#[derive(Clone, Debug)]
171pub struct Cut {
172    /// The consumer — the downstream end, which subscribes.
173    pub to: SigId,
174    /// The producer — the upstream end, which diffs and streams.
175    pub from: SigId,
176    pub carries: Ty,
177    /// `blake3(module, producer, consumer, structural(carried))[..16]`, by the same rule as the
178    /// command channel's operation id: content, not names a human maintains.
179    pub id: String,
180}
181
182/// The signal graph of one program.
183#[derive(Clone, Debug)]
184pub struct Graph {
185    pub nodes: Vec<Node>,
186    /// Declared signal names to their vertices. Inner vertices are not in here — they have no name
187    /// a program can write.
188    pub by_name: BTreeMap<Arc<str>, SigId>,
189    /// The condensation, for cycles and for order.
190    pub dep: DepGraph,
191    pub cuts: Vec<Cut>,
192    /// Vertices nothing reads. A view is one; so is a materialised read model.
193    pub sinks: Vec<SigId>,
194}
195
196impl Graph {
197    pub fn node(&self, id: SigId) -> &Node {
198        &self.nodes[id]
199    }
200
201    /// Every vertex, dependencies before dependents, cycle members adjacent.
202    pub fn order(&self) -> Vec<SigId> {
203        self.dep
204            .topological()
205            .iter()
206            .map(|n| n.0 as usize)
207            .collect()
208    }
209
210    /// What reads this vertex.
211    pub fn consumers(&self, id: SigId) -> Vec<SigId> {
212        self.dep
213            .dependents(NodeId(id as u32))
214            .iter()
215            .map(|e| e.to.0 as usize)
216            .collect()
217    }
218
219    pub fn find(&self, f: impl Fn(&Op) -> bool) -> Vec<SigId> {
220        self.nodes
221            .iter()
222            .enumerate()
223            .filter(|(_, n)| f(&n.op))
224            .map(|(i, _)| i)
225            .collect()
226    }
227
228    /// The durable accumulators, in declaration order — what the log is of.
229    pub fn states(&self) -> Vec<SigId> {
230        self.find(|o| matches!(o, Op::Durable))
231    }
232
233    pub fn ingress(&self) -> Vec<SigId> {
234        self.find(|o| matches!(o, Op::Ingress))
235    }
236
237    /// The connection sets — what is *not* in the log.
238    pub fn presences(&self) -> Vec<SigId> {
239        self.find(|o| matches!(o, Op::Presence))
240    }
241
242    /// The awareness rosters — presence with a payload, and not in the log either.
243    pub fn awarenesses(&self) -> Vec<SigId> {
244        self.find(|o| matches!(o, Op::Awareness { .. }))
245    }
246
247    /// The non-durable folds — D30's client-local interface state, and the fourth thing here that
248    /// is not in the log. The strongest case of it: a gesture is not merely absent from the log,
249    /// it never travelled far enough to be a candidate for one.
250    pub fn gestures(&self) -> Vec<SigId> {
251        self.find(|o| matches!(o, Op::Gestures { .. }))
252    }
253
254    /// The freshness sources — the other thing that is not in the log, and for the same reason:
255    /// nothing about which commands a browser had not heard back about is written down anywhere.
256    pub fn freshnesses(&self) -> Vec<SigId> {
257        self.find(|o| matches!(o, Op::Freshness))
258    }
259
260    pub fn decides(&self) -> Vec<SigId> {
261        self.find(|o| matches!(o, Op::Decide { .. }))
262    }
263
264    /// The name a report should use for a vertex.
265    pub fn label(&self, id: SigId) -> &str {
266        &self.nodes[id].label
267    }
268
269    // -------------------------------------------------------------------------------------
270    // Building
271    // -------------------------------------------------------------------------------------
272
273    /// Build the graph a checked program declares, or refuse it by name.
274    pub fn build(program: &Program, diags: &mut Diagnostics) -> Option<Graph> {
275        let mut by_name = BTreeMap::new();
276        for (i, s) in program.signals.iter().enumerate() {
277            by_name.insert(s.name.clone(), i);
278        }
279
280        let mut b = Builder {
281            by_name: &by_name,
282            nodes: (0..program.signals.len()).map(|_| None).collect(),
283            labels: program.signals.iter().map(|s| s.name.clone()).collect(),
284            diags,
285            ok: true,
286        };
287        for (i, s) in program.signals.iter().enumerate() {
288            let Some((op, inputs)) = b.classify(&s.expr, &s.name, s.tier) else {
289                continue;
290            };
291            b.nodes[i] = Some(Node {
292                name: Some(s.name.clone()),
293                label: s.name.clone(),
294                op,
295                ty: s.ty.clone(),
296                tier: s.tier,
297                inputs,
298                span: s.span,
299            });
300        }
301        if !b.ok {
302            return None;
303        }
304        let nodes: Vec<Node> = b.nodes.into_iter().collect::<Option<Vec<_>>>()?;
305
306        // The condensation, from the module that already knows how to compute one.
307        let mut gb = GraphBuilder::new();
308        for n in &nodes {
309            gb.node(GraphNode {
310                name: n.label.clone(),
311                kind: NodeKind::Signal,
312                tier: n.tier,
313                effects: Vec::new(),
314                because: String::new(),
315                span: n.span,
316            });
317        }
318        for (i, n) in nodes.iter().enumerate() {
319            for &input in &n.inputs {
320                gb.edge(NodeId(i as u32), NodeId(input as u32), EdgeKind::Reads);
321            }
322        }
323        let dep = gb.finish();
324
325        // §3.7's cycle is sound because a fold is in it: the accumulator is a value, so slicing
326        // stops there. A cycle without one is a signal defined in terms of itself, and there is
327        // nothing to compute.
328        let mut ok = true;
329        for cycle in dep.cycles() {
330            if cycle
331                .iter()
332                .any(|c| matches!(nodes[c.0 as usize].op, Op::Fold { .. }))
333            {
334                continue;
335            }
336            ok = false;
337            let members: Vec<&str> = cycle
338                .iter()
339                .map(|c| nodes[c.0 as usize].label.as_ref())
340                .collect();
341            diags.push(
342                Diagnostic::error(
343                    "B0509",
344                    format!("`{}` is defined in terms of itself", members[0]),
345                    nodes[cycle[0].0 as usize].span,
346                )
347                .with_primary_label(format!("the cycle is {}", members.join(" → ")))
348                .with_note(
349                    "§3.7's `events → todos → events` cycle is sound because a `fold` is in it: an \
350                     accumulator is a value, so the recursion has a bottom. This one has no fold, \
351                     so there is no first value to compute",
352                ),
353            );
354        }
355        if !ok {
356            return None;
357        }
358
359        let sinks: Vec<SigId> = (0..nodes.len())
360            .filter(|i| dep.dependents(NodeId(*i as u32)).is_empty())
361            .collect();
362
363        let mut cuts = Vec::new();
364        for (i, n) in nodes.iter().enumerate() {
365            for &input in &n.inputs {
366                let up = &nodes[input];
367                if n.tier == Tier::Any || up.tier == Tier::Any || n.tier == up.tier {
368                    continue;
369                }
370                let carries = signal_elem(&up.ty);
371                let mut h = blake3::Hasher::new();
372                h.update(program.name.as_bytes());
373                h.update(up.label.as_bytes());
374                h.update(b"\x00");
375                h.update(n.label.as_bytes());
376                h.update(b"\x00");
377                h.update(crate::iface::structural(&carries, &program.types).as_bytes());
378                cuts.push(Cut {
379                    to: i,
380                    from: input,
381                    carries,
382                    id: h.finalize().to_hex()[..16].to_string(),
383                });
384            }
385        }
386
387        Some(Graph {
388            nodes,
389            by_name,
390            dep,
391            cuts,
392            sinks,
393        })
394    }
395}
396
397/// The element a `Signal[T]` or `Stream[T]` carries.
398pub fn signal_elem(t: &Ty) -> Ty {
399    match t {
400        Ty::Con(n, args)
401            if (n.as_ref() == Ty::STREAM || n.as_ref() == Ty::SIGNAL) && args.len() == 1 =>
402        {
403            args[0].clone()
404        }
405        other => other.clone(),
406    }
407}
408
409/// The synthetic accumulator a program with several durable folds is compiled to.
410///
411/// One field per fold, named for the signal that declared it — so `beck explain flow` and a
412/// diagnostic can say `$State.counts` and mean something the programmer wrote.
413pub fn fused_state_decl(folds: &[(Arc<str>, Ty)]) -> crate::ty::TyDecl {
414    crate::ty::TyDecl::Model {
415        name: Arc::from(FUSED_STATE),
416        params: Vec::new(),
417        fields: folds.to_vec(),
418    }
419}
420
421/// Every `durable` a set of signal declarations holds, labelled exactly as [`Graph::build`] labels
422/// it, in declaration order.
423///
424/// [`crate::split`] reads this off the graph. The **checker** has to answer the same question
425/// before a graph exists, because a `test` block's `state` is typed against the accumulator and a
426/// fused one is a type the program did not write. Both go through here so the two cannot disagree
427/// about how many folds there are or what their fields are called.
428///
429/// `resolve` is the caller's substitution: mid-check a declaration's type is still a variable, and
430/// after checking it is not.
431pub fn durables(
432    signals: &[crate::check::SignalDecl],
433    resolve: &mut dyn FnMut(&Ty) -> Ty,
434) -> Vec<(Arc<str>, Ty)> {
435    fn walk(expr: &Core, owner: &Arc<str>, out: &mut Vec<(Arc<str>, Ty)>, top: bool) {
436        let CoreKind::Prim { op, args } = &expr.kind else {
437            return;
438        };
439        if *op == Prim::Durable {
440            // The same rule [`Builder::input`] uses: the outermost vertex of a declaration carries
441            // the declared name, an inner one is `<owner>·<op>`. Only durables can collide with
442            // durables, so counting them alone gives the same suffixes the full walk does.
443            let label: Arc<str> = if top {
444                owner.clone()
445            } else {
446                let base = format!("{owner}·durable");
447                let mut candidate: Arc<str> = Arc::from(base.as_str());
448                let mut n = 2;
449                while out.iter().any(|(l, _)| *l == candidate) {
450                    candidate = Arc::from(format!("{base}{n}"));
451                    n += 1;
452                }
453                candidate
454            };
455            out.push((label, expr.ty.clone()));
456        }
457        for a in args {
458            walk(a, owner, out, false);
459        }
460    }
461    let mut out = Vec::new();
462    for s in signals {
463        walk(&s.expr, &s.name, &mut out, true);
464    }
465    // Mid-check a `durable`'s type is still a variable, so resolve before unwrapping the `Signal`.
466    for (_, ty) in out.iter_mut() {
467        *ty = signal_elem(&resolve(ty));
468    }
469    out
470}
471
472struct Builder<'a, 'd> {
473    by_name: &'a BTreeMap<Arc<str>, SigId>,
474    nodes: Vec<Option<Node>>,
475    /// Every label handed out so far. An inner vertex is named for its owner and its op, and one
476    /// declaration can hold two of the same op — `map2(f, signal_map(a, g), signal_map(b, h))` —
477    /// so the second gets a number. Labels are the graph's vertex keys, and two vertices sharing
478    /// one would silently become a single vertex.
479    labels: std::collections::BTreeSet<Arc<str>>,
480    diags: &'d mut Diagnostics,
481    ok: bool,
482}
483
484impl Builder<'_, '_> {
485    /// Turn one signal expression into an op and the vertices it reads, creating vertices for any
486    /// nested prim application on the way.
487    fn classify(&mut self, expr: &Core, owner: &Arc<str>, tier: Tier) -> Option<(Op, Vec<SigId>)> {
488        match &expr.kind {
489            CoreKind::Global(name) => {
490                let id = self.reference(name, expr)?;
491                Some((Op::Alias, vec![id]))
492            }
493            CoreKind::Prim { op, args } => match (op, args.len()) {
494                (Prim::MergeClients, 0) => Some((Op::Ingress, Vec::new())),
495                (Prim::Presence, 0) => Some((Op::Presence, Vec::new())),
496                (Prim::Awareness, 1) => Some((Op::Awareness { f: args[0].clone() }, Vec::new())),
497                (Prim::Freshness, 0) => Some((Op::Freshness, Vec::new())),
498                (Prim::Gestures, 2) => Some((
499                    Op::Gestures {
500                        step: args[0].clone(),
501                        init: args[1].clone(),
502                    },
503                    Vec::new(),
504                )),
505                (Prim::Decide, 3) => {
506                    let proposals = self.input(&args[0], owner, tier)?;
507                    let state = self.input(&args[1], owner, tier)?;
508                    Some((
509                        Op::Decide {
510                            validate: args[2].clone(),
511                        },
512                        vec![proposals, state],
513                    ))
514                }
515                (Prim::Fold, 3) => {
516                    let stream = self.input(&args[2], owner, tier)?;
517                    Some((
518                        Op::Fold {
519                            step: args[0].clone(),
520                            init: args[1].clone(),
521                        },
522                        vec![stream],
523                    ))
524                }
525                (Prim::Durable, 1) => {
526                    let inner = self.input(&args[0], owner, tier)?;
527                    Some((Op::Durable, vec![inner]))
528                }
529                (Prim::SignalMap, 2) => {
530                    let input = self.input(&args[0], owner, tier)?;
531                    Some((Op::Map { f: args[1].clone() }, vec![input]))
532                }
533                (Prim::SignalMap2, 3) => {
534                    let a = self.input(&args[1], owner, tier)?;
535                    let b = self.input(&args[2], owner, tier)?;
536                    Some((Op::Map2 { f: args[0].clone() }, vec![a, b]))
537                }
538                (Prim::PerSession, 2) => {
539                    let input = self.input(&args[0], owner, tier)?;
540                    Some((Op::PerSession { f: args[1].clone() }, vec![input]))
541                }
542                (Prim::StreamFilterMap, 2) => {
543                    let input = self.input(&args[0], owner, tier)?;
544                    Some((Op::FilterMap { f: args[1].clone() }, vec![input]))
545                }
546                (other, n) => {
547                    self.fail(
548                        Diagnostic::error(
549                            "B0507",
550                            format!("`{}` is not a signal construct", other.name()),
551                            expr.span,
552                        )
553                        .with_primary_label(format!("applied to {n} arguments here"))
554                        .with_note(
555                            "§3.7's signal vocabulary is `merge_clients`, `presence`, \
556                             `awareness`, `freshness`, `gestures`, `filter_map`, `fold`, \
557                             `durable`, `signal_map`, `map2`, `per_session` and `decide`; a \
558                             signal's expression is built from those and nothing else",
559                        ),
560                    );
561                    None
562                }
563            },
564            _ => {
565                self.fail(
566                    Diagnostic::error("B0508", "unsupported signal expression", expr.span)
567                        .with_primary_label("a signal is a node in the dataflow, not a computation")
568                        .with_note(
569                            "the computation goes in a `def`, and the signal names it: \
570                             `summary: Signal[Summary] = signal_map(counts, summarise)`",
571                        ),
572                );
573                None
574            }
575        }
576    }
577
578    /// The vertex an argument denotes: a named signal, or a fresh vertex for a nested application.
579    fn input(&mut self, expr: &Core, owner: &Arc<str>, tier: Tier) -> Option<SigId> {
580        if let CoreKind::Global(name) = &expr.kind {
581            return self.reference(name, expr);
582        }
583        let (op, inputs) = self.classify(expr, owner, tier)?;
584        let label = self.label(format!("{owner}·{}", op.name()));
585        self.nodes.push(Some(Node {
586            name: None,
587            label,
588            op,
589            ty: expr.ty.clone(),
590            tier,
591            inputs,
592            span: expr.span,
593        }));
594        Some(self.nodes.len() - 1)
595    }
596
597    fn reference(&mut self, name: &Arc<str>, at: &Core) -> Option<SigId> {
598        match self.by_name.get(name) {
599            Some(id) => Some(*id),
600            None => {
601                self.fail(
602                    Diagnostic::error(
603                        "B0506",
604                        format!("`{name}` is not a signal"),
605                        at.span,
606                    )
607                    .with_primary_label("a signal's inputs are other signals")
608                    .with_note(
609                        "a function is applied *through* a construct — `signal_map(s, f)` — rather \
610                         than named as an input",
611                    ),
612                );
613                None
614            }
615        }
616    }
617
618    fn label(&mut self, base: String) -> Arc<str> {
619        let mut candidate: Arc<str> = Arc::from(base.as_str());
620        let mut n = 2;
621        while self.labels.contains(&candidate) {
622            candidate = Arc::from(format!("{base}{n}"));
623            n += 1;
624        }
625        self.labels.insert(candidate.clone());
626        candidate
627    }
628
629    fn fail(&mut self, d: Diagnostic) {
630        self.ok = false;
631        self.diags.push(d);
632    }
633}