Skip to main content

croma_core/parse/
note.rs

1//! Note, rest, accidental, octave, length, chord, and grace parsing.
2
3use crate::diagnostic::Span;
4use crate::lower::invalid_tuplet_warning;
5use crate::model::{Accidental, Fraction, RestVisibility};
6use crate::parse::music::{
7    MusicLineParser, invalid_length_warning, is_note_letter, redundant_chord_tie_warning,
8};
9use crate::syntax::{
10    AccidentalSyntax, BrokenRhythmDirection, BrokenRhythmSyntax, ChordMemberSyntax, ChordSyntax,
11    GraceElementSyntax, GraceGroupSyntax, LengthSyntax, MalformedSyntax, MalformedSyntaxKind,
12    MultiMeasureRestSyntax, MusicItem, MusicTokenKind, NoteSyntax, OctaveMark, OctaveMarkSyntax,
13    PitchSyntax, RestSyntax, SlurDirection, SlurSyntax, SpannedNumber, TieSyntax, TupletSyntax,
14};
15
16impl<'line> MusicLineParser<'line> {
17    pub(super) fn parse_note(&mut self, accidental: Option<AccidentalSyntax>) {
18        let attachments = self.take_pending_attachments();
19        let core_start = accidental
20            .as_ref()
21            .map(|accidental| accidental.span.start - self.line_offset)
22            .unwrap_or(self.index);
23        let start = attachments
24            .span_start()
25            .map(|start| start - self.line_offset)
26            .unwrap_or(core_start)
27            .min(core_start);
28        let pitch_start = self.index;
29        let Some(step) = self.bump_char() else {
30            return;
31        };
32        let pitch_span = self.span(pitch_start, self.index);
33        self.push_token(MusicTokenKind::Pitch, pitch_span);
34
35        let octave_marks = self.parse_octave_marks();
36        let length = self.parse_length_suffix();
37        let end = length
38            .as_ref()
39            .map(|length| length.span.end - self.line_offset)
40            .or_else(|| {
41                octave_marks
42                    .last()
43                    .map(|mark| mark.span.end - self.line_offset)
44            })
45            .unwrap_or(self.index);
46        let note = NoteSyntax {
47            span: self.span(start, end),
48            attachments,
49            accidental,
50            pitch: PitchSyntax {
51                step,
52                span: pitch_span,
53            },
54            octave_marks,
55            length,
56        };
57        self.items.push(MusicItem::Note(note));
58    }
59
60    pub(super) fn parse_accidental_token(&mut self) -> Option<AccidentalSyntax> {
61        let start = self.index;
62        let sign = if self.starts_with("__") {
63            self.index += 2;
64            Accidental::DoubleFlat
65        } else if self.starts_with("^^") {
66            self.index += 2;
67            Accidental::DoubleSharp
68        } else {
69            match self.bump_char()? {
70                '_' => Accidental::Flat,
71                '^' => Accidental::Sharp,
72                '=' => Accidental::Natural,
73                _ => return None,
74            }
75        };
76        let span = self.span(start, self.index);
77        self.push_token(MusicTokenKind::Accidental, span);
78        Some(AccidentalSyntax { sign, span })
79    }
80
81    pub(super) fn parse_octave_marks(&mut self) -> Vec<OctaveMarkSyntax> {
82        let mut marks = Vec::new();
83        while let Some(ch @ ('\'' | ',')) = self.peek_char() {
84            let start = self.index;
85            self.bump_char();
86            let span = self.span(start, self.index);
87            self.push_token(MusicTokenKind::OctaveMark, span);
88            marks.push(OctaveMarkSyntax {
89                mark: if ch == '\'' {
90                    OctaveMark::Raise
91                } else {
92                    OctaveMark::Lower
93                },
94                span,
95            });
96        }
97        marks
98    }
99
100    pub(super) fn parse_rest(&mut self, visibility: RestVisibility) {
101        let attachments = self.take_pending_attachments();
102        let start = self.index;
103        self.bump_char();
104        let marker_span = self.span(start, self.index);
105        self.push_token(MusicTokenKind::Rest, marker_span);
106        let length = self.parse_length_suffix();
107        let span = length
108            .as_ref()
109            .map(|length| {
110                Span::new(
111                    attachments.span_start().unwrap_or(marker_span.start),
112                    length.span.end,
113                )
114            })
115            .unwrap_or_else(|| {
116                Span::new(
117                    attachments.span_start().unwrap_or(marker_span.start),
118                    marker_span.end,
119                )
120            });
121        self.items.push(MusicItem::Rest(RestSyntax {
122            span,
123            attachments,
124            visibility,
125            marker_span,
126            length,
127        }));
128    }
129
130    pub(super) fn parse_multi_measure_rest(&mut self, visibility: RestVisibility) {
131        self.flush_pending_attachments();
132        let start = self.index;
133        self.bump_char();
134        let marker_span = self.span(start, self.index);
135        self.push_token(MusicTokenKind::MultiMeasureRest, marker_span);
136        let count = self.parse_number_token();
137        if let Some(count) = count {
138            self.push_token(MusicTokenKind::Length, count.span);
139        }
140        let span = count
141            .map(|count| Span::new(marker_span.start, count.span.end))
142            .unwrap_or(marker_span);
143        self.items
144            .push(MusicItem::MultiMeasureRest(MultiMeasureRestSyntax {
145                span,
146                visibility,
147                marker_span,
148                count,
149            }));
150    }
151
152    pub(super) fn parse_chord(&mut self, recover_barline_members_as_notes: bool) {
153        let attachments = self.take_pending_attachments();
154        let start = attachments
155            .span_start()
156            .map(|start| start - self.line_offset)
157            .unwrap_or(self.index);
158        let open_start = self.index;
159        self.bump_char();
160        let open_span = self.span(open_start, self.index);
161        let mut members = Vec::new();
162        let mut closed = false;
163        let mut stopped_at_barline = false;
164        // Per-member slurs (`[(C(E] [C)E)]`, ABC 2.1 §4.11: "Both ties and
165        // slurs may be used into, out of and between chords"). A `(` inside the
166        // bracket opens a slur on the member that follows it; a `)` closes one on
167        // the member that precedes it. Each binds to its own member (not the
168        // chord head/tail), mirroring the per-member `tie` handling below.
169        // `pending_member_starts` holds `(`s seen since the last member, to attach
170        // to the next member; `deferred_slur_ends` holds a `)` with no preceding
171        // member (malformed), re-emitted after the chord as a voice-level item.
172        let mut pending_member_starts: Vec<SlurSyntax> = Vec::new();
173        let mut deferred_slur_ends = Vec::new();
174
175        while let Some(ch) = self.peek_char() {
176            match ch {
177                ']' => {
178                    self.bump_char();
179                    closed = true;
180                    break;
181                }
182                ch if ch.is_whitespace() => self.parse_whitespace(),
183                '|' => {
184                    // A chord can never span a bar line (ABC 2.1 §4.18: chords
185                    // are note groups within a measure). An unclosed `[` would
186                    // otherwise consume the following bar lines and discard the
187                    // intervening measures. Stop the member scan at the bar
188                    // line, leaving `self.index` AT the `|` so the caller's main
189                    // loop re-reads it as a bar line. `closed` stays false, so
190                    // the unclosed-chord recovery below preserves the partial
191                    // `[...` run actually consumed.
192                    stopped_at_barline = true;
193                    break;
194                }
195                '^' | '_' | '=' => {
196                    let Some(accidental) = self.parse_accidental_token() else {
197                        continue;
198                    };
199                    if self.peek_char().is_some_and(is_note_letter) {
200                        if let Some(note) = self.parse_note_syntax(Some(accidental)) {
201                            let tie = self.parse_chord_member_tie();
202                            members.push(ChordMemberSyntax {
203                                span: note.span,
204                                note,
205                                tie,
206                                slur_starts: std::mem::take(&mut pending_member_starts),
207                                slur_ends: Vec::new(),
208                            });
209                        }
210                    } else {
211                        self.push_malformed(
212                            accidental.span,
213                            MalformedSyntaxKind::DanglingAccidental,
214                            "abc.music.malformed_accidental",
215                            "Accidentals must appear immediately before a chord member note",
216                        );
217                    }
218                }
219                'A'..='G' | 'a'..='g' => {
220                    if let Some(note) = self.parse_note_syntax(None) {
221                        let tie = self.parse_chord_member_tie();
222                        members.push(ChordMemberSyntax {
223                            span: note.span,
224                            note,
225                            tie,
226                            slur_starts: std::mem::take(&mut pending_member_starts),
227                            slur_ends: Vec::new(),
228                        });
229                    }
230                }
231                '-' => {
232                    // A tie marker that is not directly after a member note
233                    // (e.g. `[-CE]` or a doubled `--`). This is a misplaced or
234                    // redundant marker: attach it to the most recent untied
235                    // member if one exists, otherwise drop it — and always warn,
236                    // since the strict parser never recovers silently.
237                    if let Some(tie) = self.parse_chord_member_tie() {
238                        self.diagnostics.push(redundant_chord_tie_warning(tie.span));
239                        if let Some(last) = members.last_mut()
240                            && last.tie.is_none()
241                        {
242                            last.tie = Some(tie);
243                        }
244                    }
245                }
246                '"' => self.parse_quoted_text(),
247                '{' => self.parse_grace_group(),
248                '!' | '+' => self.parse_decoration(ch),
249                '.' => self.parse_dot(),
250                '~' | 'H' | 'L' | 'M' | 'O' | 'P' | 'S' | 'T' | 'u' | 'v' => {
251                    self.parse_shorthand_decoration()
252                }
253                ch if self.is_user_symbol(ch) => self.parse_shorthand_decoration(),
254                '(' if !self
255                    .peek_next_char()
256                    .is_some_and(|next| next.is_ascii_digit()) =>
257                {
258                    let slur_start = self.index;
259                    self.bump_char();
260                    let span = self.span(slur_start, self.index);
261                    self.push_token(MusicTokenKind::Slur, span);
262                    pending_member_starts.push(SlurSyntax {
263                        span,
264                        dotted: false,
265                        direction: SlurDirection::Start,
266                    });
267                }
268                ')' => {
269                    let slur_start = self.index;
270                    self.bump_char();
271                    let span = self.span(slur_start, self.index);
272                    self.push_token(MusicTokenKind::Slur, span);
273                    let end = SlurSyntax {
274                        span,
275                        dotted: false,
276                        direction: SlurDirection::End,
277                    };
278                    // `)` closes a slur on the member it follows; with no preceding
279                    // member it is deferred to a voice-level item after the chord.
280                    if let Some(last) = members.last_mut() {
281                        last.slur_ends.push(end);
282                    } else {
283                        deferred_slur_ends.push(end);
284                    }
285                }
286                _ => {
287                    self.parse_malformed_single(
288                        MalformedSyntaxKind::UnknownToken,
289                        "abc.music.unknown_chord_token",
290                        "Unknown chord-member token was preserved and skipped",
291                    );
292                }
293            }
294        }
295
296        let close_span = closed.then(|| self.span(self.index - 1, self.index));
297        if !closed {
298            let span = self.span(start, self.index);
299            self.push_token(MusicTokenKind::Malformed, span);
300            self.push_malformed(
301                span,
302                MalformedSyntaxKind::UnclosedChord,
303                "abc.music.unclosed_chord",
304                "Chord group was preserved and skipped",
305            );
306            if recover_barline_members_as_notes && stopped_at_barline {
307                for member in members {
308                    for slur in member.slur_starts {
309                        self.items.push(MusicItem::Slur(slur));
310                    }
311                    self.items.push(MusicItem::Note(member.note));
312                    if let Some(tie) = member.tie {
313                        self.items.push(MusicItem::Tie(tie));
314                    }
315                    for slur in member.slur_ends {
316                        self.items.push(MusicItem::Slur(slur));
317                    }
318                }
319                for slur in deferred_slur_ends {
320                    self.items.push(MusicItem::Slur(slur));
321                }
322            }
323            return;
324        }
325
326        let length = self.parse_length_suffix();
327        let end = length
328            .as_ref()
329            .map(|length| length.span.end - self.line_offset)
330            .unwrap_or(self.index);
331        let span = self.span(start, end);
332        self.push_token(MusicTokenKind::Chord, span);
333        self.items.push(MusicItem::Chord(ChordSyntax {
334            span,
335            attachments,
336            open_span,
337            close_span,
338            members,
339            length,
340        }));
341        for slur in deferred_slur_ends {
342            self.items.push(MusicItem::Slur(slur));
343        }
344        // A `(` after the last member with no member to bind to (`[C(]`) opens a
345        // slur on the FOLLOWING note, so re-emit it as a voice-level item.
346        for slur in pending_member_starts {
347            self.items.push(MusicItem::Slur(slur));
348        }
349    }
350
351    /// Consume an optional tie marker (`-`) following a chord member note and
352    /// return it as a [`TieSyntax`]. ABC 2.1 §4.11 allows ties on chord members
353    /// (`[DA-]`). Returns `None` when the next character is not a tie.
354    fn parse_chord_member_tie(&mut self) -> Option<TieSyntax> {
355        if self.peek_char() != Some('-') {
356            return None;
357        }
358        let start = self.index;
359        self.bump_char();
360        let span = self.span(start, self.index);
361        self.push_token(MusicTokenKind::Tie, span);
362        Some(TieSyntax {
363            span,
364            dotted: false,
365        })
366    }
367
368    pub(super) fn parse_grace_group(&mut self) {
369        // Attachments pending at `{` (quoted text, decorations) belong to the
370        // MAIN note that follows the grace group (`"F"{AB}c` binds "F" to
371        // `c`), so stash them while the inner elements parse — otherwise the
372        // first grace note inside the braces would steal them.
373        let stashed = std::mem::take(&mut self.pending_attachments);
374        let start = self.index;
375        self.bump_char();
376        let slash_span = if self.peek_char() == Some('/') {
377            let slash_start = self.index;
378            self.bump_char();
379            Some(self.span(slash_start, self.index))
380        } else {
381            None
382        };
383        let mut elements = Vec::new();
384        let mut closed = false;
385
386        while let Some(ch) = self.peek_char() {
387            match ch {
388                '}' => {
389                    self.bump_char();
390                    closed = true;
391                    break;
392                }
393                ch if ch.is_whitespace() => self.parse_whitespace(),
394                '^' | '_' | '=' => {
395                    let Some(accidental) = self.parse_accidental_token() else {
396                        continue;
397                    };
398                    if self.peek_char().is_some_and(is_note_letter) {
399                        if let Some(note) = self.parse_note_syntax(Some(accidental)) {
400                            elements.push(GraceElementSyntax::Note(note));
401                        }
402                    } else {
403                        let malformed = MalformedSyntax {
404                            span: accidental.span,
405                            kind: MalformedSyntaxKind::DanglingAccidental,
406                        };
407                        elements.push(GraceElementSyntax::Malformed(malformed.clone()));
408                        self.push_malformed(
409                            malformed.span,
410                            malformed.kind,
411                            "abc.music.malformed_accidental",
412                            "Accidentals must appear immediately before a grace note",
413                        );
414                    }
415                }
416                'A'..='G' | 'a'..='g' => {
417                    if let Some(note) = self.parse_note_syntax(None) {
418                        elements.push(GraceElementSyntax::Note(note));
419                    }
420                }
421                'z' => {
422                    let rest = self.parse_rest_syntax(RestVisibility::Visible);
423                    elements.push(GraceElementSyntax::Rest(rest));
424                }
425                'x' => {
426                    let rest = self.parse_rest_syntax(RestVisibility::Invisible);
427                    elements.push(GraceElementSyntax::Rest(rest));
428                }
429                '(' => {
430                    let slur = self.parse_grace_slur(SlurDirection::Start);
431                    elements.push(GraceElementSyntax::Slur(slur));
432                }
433                ')' => {
434                    let slur = self.parse_grace_slur(SlurDirection::End);
435                    elements.push(GraceElementSyntax::Slur(slur));
436                }
437                '[' => {
438                    self.parse_chord(false);
439                    if let Some(MusicItem::Chord(chord)) = self.items.pop() {
440                        elements.push(GraceElementSyntax::Chord(chord));
441                    }
442                }
443                '"' => self.parse_quoted_text(),
444                '!' | '+' => self.parse_decoration(ch),
445                '.' => self.parse_dot(),
446                '~' | 'H' | 'L' | 'M' | 'O' | 'P' | 'S' | 'T' | 'u' | 'v' => {
447                    self.parse_shorthand_decoration()
448                }
449                ch if self.is_user_symbol(ch) => self.parse_shorthand_decoration(),
450                _ => {
451                    let start = self.index;
452                    self.bump_char();
453                    let malformed = MalformedSyntax {
454                        span: self.span(start, self.index),
455                        kind: MalformedSyntaxKind::UnknownToken,
456                    };
457                    elements.push(GraceElementSyntax::Malformed(malformed.clone()));
458                    self.push_malformed(
459                        malformed.span,
460                        malformed.kind,
461                        "abc.music.unknown_grace_token",
462                        "Unknown grace-group token was preserved and skipped",
463                    );
464                }
465            }
466        }
467
468        // Restore the caller's pending bundle, then re-append any leftover
469        // inner pendings (e.g. trailing quoted text right before `}`) so they
470        // bind to the next main note in source order.
471        let inner_leftovers = std::mem::take(&mut self.pending_attachments);
472        self.pending_attachments = stashed;
473        self.pending_attachments.extend(inner_leftovers);
474
475        let span = self.span(start, self.index);
476        if closed {
477            self.push_token(MusicTokenKind::GraceGroup, span);
478            self.pending_attachments
479                .grace_groups
480                .push(GraceGroupSyntax {
481                    span,
482                    slash_span,
483                    elements,
484                });
485        } else {
486            self.push_token(MusicTokenKind::Malformed, span);
487            self.push_malformed(
488                span,
489                MalformedSyntaxKind::UnclosedGraceGroup,
490                "abc.music.unclosed_grace",
491                "Grace group was preserved and skipped",
492            );
493        }
494    }
495
496    fn parse_grace_slur(&mut self, direction: SlurDirection) -> SlurSyntax {
497        let start = self.index;
498        self.bump_char();
499        let span = self.span(start, self.index);
500        self.push_token(MusicTokenKind::Slur, span);
501        SlurSyntax {
502            span,
503            dotted: false,
504            direction,
505        }
506    }
507
508    pub(super) fn parse_note_syntax(
509        &mut self,
510        accidental: Option<AccidentalSyntax>,
511    ) -> Option<NoteSyntax> {
512        let attachments = self.take_pending_attachments();
513        let core_start = accidental
514            .as_ref()
515            .map(|accidental| accidental.span.start - self.line_offset)
516            .unwrap_or(self.index);
517        let start = attachments
518            .span_start()
519            .map(|start| start - self.line_offset)
520            .unwrap_or(core_start)
521            .min(core_start);
522        let pitch_start = self.index;
523        let step = self.bump_char()?;
524        let pitch_span = self.span(pitch_start, self.index);
525        self.push_token(MusicTokenKind::Pitch, pitch_span);
526
527        let octave_marks = self.parse_octave_marks();
528        let length = self.parse_length_suffix();
529        let end = length
530            .as_ref()
531            .map(|length| length.span.end - self.line_offset)
532            .or_else(|| {
533                octave_marks
534                    .last()
535                    .map(|mark| mark.span.end - self.line_offset)
536            })
537            .unwrap_or(self.index);
538        Some(NoteSyntax {
539            span: self.span(start, end),
540            attachments,
541            accidental,
542            pitch: PitchSyntax {
543                step,
544                span: pitch_span,
545            },
546            octave_marks,
547            length,
548        })
549    }
550
551    pub(super) fn parse_rest_syntax(&mut self, visibility: RestVisibility) -> RestSyntax {
552        let attachments = self.take_pending_attachments();
553        let start = self.index;
554        self.bump_char();
555        let marker_span = self.span(start, self.index);
556        self.push_token(MusicTokenKind::Rest, marker_span);
557        let length = self.parse_length_suffix();
558        let span = length
559            .as_ref()
560            .map(|length| {
561                Span::new(
562                    attachments.span_start().unwrap_or(marker_span.start),
563                    length.span.end,
564                )
565            })
566            .unwrap_or_else(|| {
567                Span::new(
568                    attachments.span_start().unwrap_or(marker_span.start),
569                    marker_span.end,
570                )
571            });
572        RestSyntax {
573            span,
574            attachments,
575            visibility,
576            marker_span,
577            length,
578        }
579    }
580
581    pub(super) fn parse_length_suffix(&mut self) -> Option<LengthSyntax> {
582        let start = self.index;
583        let numerator = self.parse_number_token();
584        let mut slash_count = 0u8;
585        let mut denominator = None;
586
587        if self.peek_char() == Some('/') {
588            while self.peek_char() == Some('/') {
589                slash_count = slash_count.saturating_add(1);
590                self.bump_char();
591            }
592            denominator = self.parse_number_token();
593        }
594
595        if numerator.is_none() && slash_count == 0 {
596            return None;
597        }
598
599        let end = self.index;
600        let span = self.span(start, end);
601        self.push_token(MusicTokenKind::Length, span);
602        let numerator_value = numerator.map(|number| number.value).unwrap_or(1);
603        let denominator_value = match (slash_count, denominator) {
604            (0, _) => 1,
605            (_, Some(number)) if number.value == 0 => {
606                self.diagnostics.push(invalid_length_warning(
607                    number.span,
608                    "Length denominator cannot be zero; recovered as denominator 1",
609                ));
610                1
611            }
612            (_, Some(number)) => number.value,
613            (slashes, None) => slash_denominator(slashes).unwrap_or_else(|| {
614                self.diagnostics.push(invalid_length_warning(
615                    span,
616                    "Length slash shorthand is too long; recovered as denominator 1",
617                ));
618                1
619            }),
620        };
621        let numerator_value = if let Some(number) = numerator
622            && number.value == 0
623        {
624            self.diagnostics.push(invalid_length_warning(
625                number.span,
626                "Length numerator cannot be zero; recovered as numerator 1",
627            ));
628            1
629        } else {
630            numerator_value
631        };
632
633        Some(LengthSyntax {
634            span,
635            raw: self.text[start..end].to_owned(),
636            numerator,
637            slash_count,
638            denominator,
639            multiplier: Fraction::new(numerator_value, denominator_value),
640        })
641    }
642
643    pub(super) fn parse_number_token(&mut self) -> Option<SpannedNumber> {
644        let start = self.index;
645        while self.peek_char().is_some_and(|ch| ch.is_ascii_digit()) {
646            self.bump_char();
647        }
648        if start == self.index {
649            return None;
650        }
651        let span = self.span(start, self.index);
652        let raw = &self.text[start..self.index];
653        let value = raw.parse().unwrap_or_else(|_| {
654            self.diagnostics.push(invalid_length_warning(
655                span,
656                "Number is too large; recovered as 1",
657            ));
658            1
659        });
660        Some(SpannedNumber { value, span })
661    }
662
663    pub(super) fn parse_broken_rhythm(&mut self) {
664        let start = self.index;
665        let Some(marker) = self.bump_char() else {
666            return;
667        };
668        while self.peek_char() == Some(marker) {
669            self.bump_char();
670        }
671        let span = self.span(start, self.index);
672        self.push_token(MusicTokenKind::BrokenRhythm, span);
673        self.items.push(MusicItem::BrokenRhythm(BrokenRhythmSyntax {
674            span,
675            direction: if marker == '<' {
676                BrokenRhythmDirection::LeftShorter
677            } else {
678                BrokenRhythmDirection::RightShorter
679            },
680            count: u8::try_from(self.index - start).unwrap_or(u8::MAX),
681        }));
682    }
683
684    pub(super) fn parse_tuplet(&mut self) {
685        // A tuplet marker precedes its first note, so pending attachments
686        // ride across it and bind to that note (`"F"(3CDE` binds "F" to `C`).
687        let start = self.index;
688        self.bump_char();
689        let Some(p) = self.parse_number_token() else {
690            let span = self.span(start, self.index);
691            self.push_token(MusicTokenKind::Malformed, span);
692            self.push_malformed(
693                span,
694                MalformedSyntaxKind::InvalidTuplet,
695                "abc.music.invalid_tuplet",
696                "Tuplet specifier must start with a number",
697            );
698            return;
699        };
700
701        let mut q = None;
702        let mut r = None;
703        if self.peek_char() == Some(':') {
704            self.bump_char();
705            q = self.parse_number_token();
706            if self.peek_char() == Some(':') {
707                self.bump_char();
708                r = self.parse_number_token();
709            }
710        }
711
712        let span = self.span(start, self.index);
713        self.push_token(MusicTokenKind::Tuplet, span);
714        if !(2..=9).contains(&p.value) {
715            self.diagnostics.push(invalid_tuplet_warning(span));
716        }
717        self.items
718            .push(MusicItem::Tuplet(TupletSyntax { span, p, q, r }));
719    }
720}
721
722fn slash_denominator(slash_count: u8) -> Option<u32> {
723    1u32.checked_shl(u32::from(slash_count))
724}