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}