use crate::diagnostic::{Diagnostic, RecoveryNote, Severity, Span};
use crate::lower::accidental::{KeyAccidentalPolicy, MeasureAccidental, key_accidental_policy};
use crate::lower::{
abc_broken_rhythm_reference, abc_chord_reference, abc_slur_reference, duration_overflow_warning,
};
use crate::model::{
Accidental, AccidentalMark, AlignedLyric, AnnotationPlacementModel, BarlineKind,
DecorationAttachment, DecorationSourceKind, Event, EventAttachments, Fraction, GraceEvent,
GraceEventKind, GraceGroupAttachment, GraceNoteEvent, HarmonyKindText, LoweredEventAtom,
LoweredEventAtomKind, MusicXmlInstrumentRef, Pitch, RepeatEndingCloseModel, RestEvent,
SlurAttachment, SlurRole, TextAttachment, TupletAttachment, TupletRole, VoiceId,
VoicePropertiesModel,
};
use crate::musicxml::notation::decoration_notation;
use crate::parse::field::KeySignature;
use crate::syntax::{
AnnotationPlacement, AttachmentBundle, BrokenRhythmDirection, BrokenRhythmSyntax, ChordSyntax,
DecorationKind, DecorationSyntax, GraceElementSyntax, GraceGroupSyntax, LengthSyntax,
NoteSyntax, OctaveMark, OverlaySyntax, QuotedTextKind, QuotedTextSyntax, RestSyntax,
SlurDirection, SlurSyntax, TieSyntax, VariantEndingSyntax,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum LoweredEvent {
Timed(LoweredTimedEvent),
Untimed(Event),
Overlay(OverlaySyntax),
VariantEnding(VariantEndingSyntax),
VariantEndingClose(RepeatEndingCloseModel),
KeyChange(crate::model::KeySignatureModel),
MeterChange(crate::model::MeterModel),
ClefChange(crate::model::ClefChangeModel),
TempoChange(crate::model::TempoModel),
SectionLabel {
label: String,
span: Span,
},
MeasureNumber {
display_number: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LoweredTimedEvent {
pub(crate) event: LoweredEventAtom,
pub(crate) line_index: usize,
pub(crate) source_order: u32,
pub(crate) alignable: bool,
pub(crate) attachments: EventAttachments,
}
#[derive(Debug, Clone)]
pub(crate) struct ActiveTuplet {
pub(crate) pair_id: u32,
pub(crate) span: Span,
pub(crate) remaining: u32,
pub(crate) actual_notes: u32,
pub(crate) normal_notes: u32,
pub(crate) multiplier: Fraction,
pub(crate) groups: Vec<Vec<usize>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CompletedTuplet {
pub(crate) pair_id: u32,
pub(crate) span: Span,
pub(crate) actual_notes: u32,
pub(crate) normal_notes: u32,
pub(crate) groups: Vec<Vec<usize>>,
}
#[derive(Debug)]
pub(crate) struct LoweringState {
pub(crate) id: VoiceId,
pub(crate) initial_properties: VoicePropertiesModel,
pub(crate) properties: VoicePropertiesModel,
pub(crate) source_span: Span,
pub(crate) initial_key: Option<crate::model::KeySignatureModel>,
pub(crate) initial_meter: Option<crate::model::MeterModel>,
pub(crate) unit: Fraction,
pub(crate) meter_duration: Option<Fraction>,
pub(crate) compound_meter: bool,
pub(crate) lowered: Vec<LoweredEvent>,
pub(crate) time_groups: Vec<Vec<usize>>,
pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) active_tuplets: Vec<ActiveTuplet>,
pub(crate) pending_broken: Option<PendingBrokenRhythm>,
pub(crate) broken_left_available: bool,
pub(crate) key_accidentals: Vec<KeyAccidentalPolicy>,
pub(crate) current_key: Option<KeySignature>,
pub(crate) accidental_state: Vec<MeasureAccidental>,
pub(crate) pending_ties: Vec<PendingTie>,
pub(crate) next_tie_id: u32,
pub(crate) pending_slur_starts: Vec<OpenSlur>,
pub(crate) open_slurs: Vec<OpenSlur>,
pub(crate) pending_grace_slur_stops: Vec<PendingGraceSlurStop>,
pub(crate) next_slur_id: u32,
pub(crate) next_tuplet_id: u32,
pub(crate) pending_grace_groups: Vec<PendingGraceGroup>,
pub(crate) pending_chord_symbols: Vec<TextAttachment>,
pub(crate) pending_annotations: Vec<QuotedTextSyntax>,
pub(crate) pending_decorations: Vec<DecorationAttachment>,
pub(crate) pending_musicxml_instrument: Option<MusicXmlInstrumentRef>,
pub(crate) pending_musicxml_harmony_text: Option<HarmonyKindText>,
pub(crate) pending_musicxml_direction_placement: Option<AnnotationPlacementModel>,
pub(crate) pending_musicxml_lyric_extends: Vec<u32>,
pub(crate) pending_musicxml_lyric_duplicates: Vec<AlignedLyric>,
pub(crate) pending_musicxml_forward: bool,
pub(crate) pending_musicxml_tuplets: Vec<TupletAttachment>,
pub(crate) musicxml_tuplet_pair_ids: Vec<(u32, u32)>,
pub(crate) pending_musicxml_sequence_backup: Option<Fraction>,
pub(crate) pending_musicxml_after_grace: bool,
pub(crate) pending_musicxml_barline_kind: Option<BarlineKind>,
pub(crate) pending_musicxml_meter_restatement: bool,
pub(crate) pending_musicxml_key_restatement: bool,
pub(crate) pending_musicxml_time_symbol: Option<String>,
pub(crate) pending_xvoice_slurs: Vec<(u32, SlurRole, Span)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PendingBrokenRhythm {
pub(crate) span: Span,
pub(crate) left_group: Vec<usize>,
pub(crate) left_multiplier: Fraction,
pub(crate) right_multiplier: Fraction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PendingTie {
pub(crate) event_index: usize,
pub(crate) signature: (char, i8),
pub(crate) marker: TieSyntax,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct OpenSlur {
pub(crate) pair_id: u32,
pub(crate) marker: SlurSyntax,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PendingGraceSlurStop {
pub(crate) pair_id: u32,
pub(crate) marker: SlurSyntax,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PendingGraceGroup {
pub(crate) grace: GraceGroupSyntax,
pub(crate) detached_from_previous: bool,
pub(crate) force_after_previous: bool,
}
impl LoweringState {
pub(crate) fn new(
id: VoiceId,
properties: VoicePropertiesModel,
unit: Fraction,
key: Option<&KeySignature>,
meter_duration: Option<Fraction>,
compound_meter: bool,
) -> Self {
let source_span = id.span;
Self {
id,
initial_properties: properties.clone(),
properties,
source_span,
initial_key: None,
initial_meter: None,
unit,
meter_duration,
compound_meter,
lowered: Vec::new(),
time_groups: Vec::new(),
diagnostics: Vec::new(),
active_tuplets: Vec::new(),
pending_broken: None,
broken_left_available: false,
key_accidentals: key_accidental_policy(key),
current_key: key.cloned(),
accidental_state: Vec::new(),
pending_ties: Vec::new(),
next_tie_id: 1,
pending_slur_starts: Vec::new(),
open_slurs: Vec::new(),
pending_grace_slur_stops: Vec::new(),
next_slur_id: 1,
next_tuplet_id: 1,
pending_grace_groups: Vec::new(),
pending_chord_symbols: Vec::new(),
pending_annotations: Vec::new(),
pending_decorations: Vec::new(),
pending_musicxml_instrument: None,
pending_musicxml_harmony_text: None,
pending_musicxml_direction_placement: None,
pending_musicxml_lyric_extends: Vec::new(),
pending_musicxml_lyric_duplicates: Vec::new(),
pending_musicxml_forward: false,
pending_musicxml_tuplets: Vec::new(),
musicxml_tuplet_pair_ids: Vec::new(),
pending_musicxml_sequence_backup: None,
pending_musicxml_after_grace: false,
pending_musicxml_barline_kind: None,
pending_musicxml_meter_restatement: false,
pending_musicxml_key_restatement: false,
pending_musicxml_time_symbol: None,
pending_xvoice_slurs: Vec::new(),
}
}
pub(crate) fn take_timed_attachments(&mut self, bundle: &AttachmentBundle) -> EventAttachments {
let mut pending_graces = self.take_pending_grace_group_attachments();
let mut attachments = attachment_bundle_model(bundle, self);
if !pending_graces.is_empty() {
pending_graces.append(&mut attachments.grace_groups);
attachments.grace_groups = pending_graces;
}
if !self.pending_chord_symbols.is_empty() {
let mut symbols: Vec<_> = self.pending_chord_symbols.drain(..).collect();
symbols.append(&mut attachments.chord_symbols);
attachments.chord_symbols = symbols;
}
if !self.pending_annotations.is_empty() {
let mut annotations: Vec<_> = self
.pending_annotations
.drain(..)
.map(|text| annotation_attachment_model(&text))
.collect();
annotations.append(&mut attachments.annotations);
attachments.annotations = annotations;
}
if !self.pending_decorations.is_empty() {
let mut decorations: Vec<_> = self.pending_decorations.drain(..).collect();
decorations.append(&mut attachments.decorations);
attachments.decorations = decorations;
}
if attachments.instrument.is_none() {
attachments.instrument = self.pending_musicxml_instrument.take();
}
attachments
.lyric_same_note_extends
.append(&mut self.pending_musicxml_lyric_extends);
attachments
.lyric_same_note_duplicates
.append(&mut self.pending_musicxml_lyric_duplicates);
if self.pending_musicxml_forward {
attachments.musicxml_forward = true;
self.pending_musicxml_forward = false;
}
if attachments.musicxml_sequence_backup.is_none() {
attachments.musicxml_sequence_backup = self.pending_musicxml_sequence_backup.take();
}
attachments
.tuplets
.append(&mut self.pending_musicxml_tuplets);
self.drain_pending_xvoice_slurs(&mut attachments);
attachments
}
pub(crate) fn has_pending_spacer_prefix_attachments(&self) -> bool {
!self.pending_chord_symbols.is_empty()
|| !self.pending_annotations.is_empty()
|| !self.pending_decorations.is_empty()
}
fn drain_pending_xvoice_slurs(&mut self, attachments: &mut EventAttachments) {
for (pair_id, role, span) in self.pending_xvoice_slurs.drain(..) {
attachments.slurs.push(SlurAttachment {
pair_id,
role,
span,
dotted: false,
});
}
}
pub(crate) fn push_musicxml_tuplet(
&mut self,
source_pair_id: u32,
actual_notes: u32,
normal_notes: u32,
role: TupletRole,
span: Span,
) {
let pair_id = self.musicxml_tuplet_pair_id(source_pair_id);
self.pending_musicxml_tuplets.push(TupletAttachment {
pair_id,
actual_notes,
normal_notes,
role,
span,
});
if role == TupletRole::Stop {
self.musicxml_tuplet_pair_ids
.retain(|(source, _)| *source != source_pair_id);
}
}
fn musicxml_tuplet_pair_id(&mut self, source_pair_id: u32) -> u32 {
if let Some((_, pair_id)) = self
.musicxml_tuplet_pair_ids
.iter()
.find(|(source, _)| *source == source_pair_id)
{
return *pair_id;
}
let pair_id = self.next_tuplet_id;
self.next_tuplet_id = self.next_tuplet_id.saturating_add(1);
self.musicxml_tuplet_pair_ids
.push((source_pair_id, pair_id));
pair_id
}
pub(crate) fn flush_pending_barline_directions(
&mut self,
line_index: usize,
source_order: u32,
span: Span,
kind: BarlineKind,
) {
let mut attachments = EventAttachments::default();
let mut direction_span: Option<Span> = None;
if !self.pending_annotations.is_empty() {
attachments.annotations = self
.pending_annotations
.drain(..)
.map(|text| {
direction_span = Some(merge_spans(direction_span, text.span));
annotation_attachment_model(&text)
})
.collect();
}
let measure_is_note_less = !self.broken_left_available;
let mut remaining_symbols = Vec::new();
for text in self.pending_chord_symbols.drain(..) {
let keep_pending = kind == BarlineKind::Final
|| (quoted_text_may_be_harmony(text.text.as_str()) && !measure_is_note_less);
if keep_pending {
remaining_symbols.push(text);
} else {
direction_span = Some(merge_spans(direction_span, text.span));
attachments.chord_symbols.push(text);
}
}
self.pending_chord_symbols = remaining_symbols;
let mut remaining_decorations = Vec::new();
for decoration in self.pending_decorations.drain(..) {
if decoration_binds_to_barline(&decoration) {
direction_span = Some(merge_spans(direction_span, decoration.span));
attachments.decorations.push(decoration);
} else {
remaining_decorations.push(decoration);
}
}
self.pending_decorations = remaining_decorations;
if attachments.annotations.is_empty()
&& attachments.decorations.is_empty()
&& attachments.chord_symbols.is_empty()
{
return;
}
self.lowered.push(LoweredEvent::Timed(LoweredTimedEvent {
event: LoweredEventAtom {
kind: LoweredEventAtomKind::Spacer {
span: direction_span.unwrap_or(span),
},
duration: Fraction::zero(),
},
line_index,
source_order,
alignable: false,
attachments,
}));
}
pub(crate) fn push_note_group(
&mut self,
note: &NoteSyntax,
line_index: usize,
source_order: u32,
) {
let attachments = self.take_timed_attachments(¬e.attachments);
let octave = lowered_octave(note).saturating_add(voice_octave_shift(&self.properties));
let written_accidental = note.accidental.map(|accidental| accidental.sign);
let (effective_accidental, accidental_source) = self.effective_accidental(
note.pitch.step,
octave,
written_accidental,
note.accidental.map(|accidental| accidental.span),
);
let duration = self.scaled_duration(
self.unit,
length_multiplier(note.length.as_ref()),
note.span,
);
self.push_time_group(
vec![(
LoweredEventAtom {
kind: LoweredEventAtomKind::Note {
step: note.pitch.step.to_ascii_uppercase(),
octave,
accidental: written_accidental,
effective_accidental,
accidental_source,
chord: false,
span: note.span,
},
duration,
},
true,
attachments,
)],
line_index,
source_order,
);
}
pub(crate) fn push_rest_group(
&mut self,
rest: &RestSyntax,
line_index: usize,
source_order: u32,
) {
let attachments = self.take_timed_attachments(&rest.attachments);
let duration = self.scaled_duration(
self.unit,
length_multiplier(rest.length.as_ref()),
rest.span,
);
self.push_time_group(
vec![(
LoweredEventAtom {
kind: LoweredEventAtomKind::Rest {
visibility: rest.visibility,
multiple_rest: None,
span: rest.span,
},
duration,
},
false,
attachments,
)],
line_index,
source_order,
);
}
pub(crate) fn push_chord_group(
&mut self,
chord: &ChordSyntax,
line_index: usize,
source_order: u32,
) {
if chord.members.is_empty() {
return;
}
let outer_multiplier = length_multiplier(chord.length.as_ref());
let first_duration = chord.members.first().map(|member| {
length_multiplier(member.note.length.as_ref()).checked_mul(outer_multiplier)
});
if let Some(first_duration) = first_duration
&& chord.members.iter().any(|member| {
length_multiplier(member.note.length.as_ref()).checked_mul(outer_multiplier)
!= first_duration
})
{
self.diagnostics
.push(variable_chord_duration_warning(chord.span));
}
let mut pending_graces = self.take_pending_grace_group_attachments();
let mut pending_symbols: Vec<TextAttachment> =
self.pending_chord_symbols.drain(..).collect();
let mut pending_annotations: Vec<TextAttachment> = self
.pending_annotations
.drain(..)
.map(|text| annotation_attachment_model(&text))
.collect();
let mut pending_decorations: Vec<DecorationAttachment> =
self.pending_decorations.drain(..).collect();
let chord_attachments = attachment_bundle_model(&chord.attachments, self);
let mut events = Vec::with_capacity(chord.members.len());
for (index, member) in chord.members.iter().enumerate() {
let mut attachments = attachment_bundle_model(&member.note.attachments, self);
if index == 0 {
attachments.extend(chord_attachments.clone());
if !pending_graces.is_empty() {
let mut graces = std::mem::take(&mut pending_graces);
graces.append(&mut attachments.grace_groups);
attachments.grace_groups = graces;
}
if !pending_symbols.is_empty() {
let mut symbols = std::mem::take(&mut pending_symbols);
symbols.append(&mut attachments.chord_symbols);
attachments.chord_symbols = symbols;
}
if !pending_annotations.is_empty() {
let mut annotations = std::mem::take(&mut pending_annotations);
annotations.append(&mut attachments.annotations);
attachments.annotations = annotations;
}
if !pending_decorations.is_empty() {
let mut decorations = std::mem::take(&mut pending_decorations);
decorations.append(&mut attachments.decorations);
attachments.decorations = decorations;
}
attachments
.tuplets
.append(&mut self.pending_musicxml_tuplets);
attachments
.lyric_same_note_extends
.append(&mut self.pending_musicxml_lyric_extends);
attachments
.lyric_same_note_duplicates
.append(&mut self.pending_musicxml_lyric_duplicates);
self.drain_pending_xvoice_slurs(&mut attachments);
}
let octave =
lowered_octave(&member.note).saturating_add(voice_octave_shift(&self.properties));
let written_accidental = member.note.accidental.map(|accidental| accidental.sign);
let (effective_accidental, accidental_source) = self.effective_accidental(
member.note.pitch.step,
octave,
written_accidental,
member.note.accidental.map(|accidental| accidental.span),
);
let member_multiplier = self.scaled_duration(
length_multiplier(member.note.length.as_ref()),
outer_multiplier,
member.note.span,
);
let duration = self.scaled_duration(self.unit, member_multiplier, member.note.span);
events.push((
LoweredEventAtom {
kind: LoweredEventAtomKind::Note {
step: member.note.pitch.step.to_ascii_uppercase(),
octave,
accidental: written_accidental,
effective_accidental,
accidental_source,
chord: index > 0,
span: member.note.span,
},
duration,
},
index == 0,
attachments,
));
}
self.push_time_group(events, line_index, source_order);
if chord
.members
.iter()
.any(|member| !member.slur_starts.is_empty() || !member.slur_ends.is_empty())
&& let Some(group) = self.time_groups.last().cloned()
{
for (member, &event_index) in chord.members.iter().zip(group.iter()) {
for start in &member.slur_starts {
self.open_chord_member_slur(event_index, *start);
}
for end in &member.slur_ends {
self.close_chord_member_slur(event_index, *end);
}
}
}
if chord.members.iter().any(|member| member.tie.is_some())
&& let Some(group) = self.time_groups.last().cloned()
{
for (member, &event_index) in chord.members.iter().zip(group.iter()) {
if let Some(marker) = member.tie {
self.register_pending_tie(event_index, marker);
}
}
}
}
pub(crate) fn decoration_attachment_model(
&mut self,
decoration: &DecorationSyntax,
) -> DecorationAttachment {
let mut attachment = decoration_attachment_model(decoration);
attachment.placement = self.pending_musicxml_direction_placement.take();
attachment
}
pub(crate) fn push_time_group(
&mut self,
events: Vec<(LoweredEventAtom, bool, EventAttachments)>,
line_index: usize,
source_order: u32,
) {
if events.is_empty() {
return;
}
self.finish_pending_tie_if_group_is_not_note(&events);
let group_span = events[0].0.span();
let (group_multiplier, pending_broken) = self.consume_group_multiplier(group_span);
let start_index = self.lowered.len();
for (mut event, alignable, attachments) in events {
event.duration = self.scaled_duration(event.duration, group_multiplier, event.span());
self.lowered.push(LoweredEvent::Timed(LoweredTimedEvent {
event,
line_index,
source_order,
alignable,
attachments,
}));
}
let group = (start_index..self.lowered.len()).collect::<Vec<_>>();
if let Some(pending) = pending_broken {
self.apply_pending_broken_rhythm(&pending, &group);
}
self.record_tuplet_group(&group);
self.attach_pending_slur_starts(&group);
self.finish_pending_tie_if_possible(&group);
self.time_groups.push(group);
self.broken_left_available = true;
}
fn consume_group_multiplier(&mut self, span: Span) -> (Fraction, Option<PendingBrokenRhythm>) {
let pending_broken = self.pending_broken.take();
let factors = pending_broken
.iter()
.map(|pending| pending.right_multiplier)
.chain(
self.active_tuplets
.iter()
.filter(|tuplet| tuplet.remaining > 0)
.map(|tuplet| tuplet.multiplier),
)
.collect::<Vec<_>>();
let mut multiplier = Fraction::one();
for factor in factors {
multiplier = self.scaled_duration(multiplier, factor, span);
}
(multiplier, pending_broken)
}
pub(crate) fn scaled_duration(
&mut self,
base: Fraction,
factor: Fraction,
span: Span,
) -> Fraction {
base.checked_mul(factor).unwrap_or_else(|| {
self.diagnostics.push(duration_overflow_warning(span));
base
})
}
pub(crate) fn apply_broken_rhythm(&mut self, marker: BrokenRhythmSyntax) {
let (left_multiplier, right_multiplier) = broken_rhythm_multipliers(marker);
let group = match self.time_groups.last() {
Some(group) if self.broken_left_available => group,
_ => {
self.diagnostics
.push(broken_rhythm_without_left_warning(marker.span));
return;
}
};
if self.pending_broken.is_some() {
self.diagnostics
.push(overlapping_broken_rhythm_warning(marker.span));
}
self.pending_broken = Some(PendingBrokenRhythm {
span: marker.span,
left_group: group.clone(),
left_multiplier,
right_multiplier,
});
}
fn apply_pending_broken_rhythm(
&mut self,
pending: &PendingBrokenRhythm,
right_group: &[usize],
) {
let mut overflowed = false;
for index in &pending.left_group {
if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(*index) {
match timed.event.duration.checked_mul(pending.left_multiplier) {
Some(duration) => timed.event.duration = duration,
None => overflowed = true,
}
}
}
if overflowed {
self.diagnostics
.push(duration_overflow_warning(pending.span));
}
if right_group.is_empty() {
self.diagnostics
.push(broken_rhythm_without_right_warning(pending.span));
}
}
pub(crate) fn apply_slur(&mut self, slur: SlurSyntax) {
match slur.direction {
SlurDirection::Start => {
let open = OpenSlur {
pair_id: self.next_slur_id,
marker: slur,
};
self.next_slur_id = self.next_slur_id.saturating_add(1);
self.pending_slur_starts.push(open);
self.open_slurs.push(open);
}
SlurDirection::End => {
let open = if self
.open_slurs
.last()
.is_some_and(|open| open.marker.dotted != slur.dotted)
&& let Some(position) = self
.open_slurs
.iter()
.rposition(|open| open.marker.dotted == slur.dotted)
{
self.diagnostics.push(crossing_slur_warning(slur.span));
Some(self.open_slurs.remove(position))
} else {
self.open_slurs.pop()
};
if let Some(open) = open {
if self.pending_grace_group_between(open.marker.span, slur.span) {
self.pending_grace_slur_stops.push(PendingGraceSlurStop {
pair_id: open.pair_id,
marker: slur,
});
return;
}
let start_was_pending = self
.pending_slur_starts
.iter()
.any(|pending| pending.pair_id == open.pair_id);
if start_was_pending {
self.pending_slur_starts
.retain(|pending| pending.pair_id != open.pair_id);
self.diagnostics.push(unmatched_slur_warning(slur.span));
return;
}
if let Some(event_index) = self.last_note_event_index() {
let event_index = self.chord_head_event_index(event_index);
self.attach_slur(event_index, open.pair_id, SlurRole::Stop, slur);
} else {
self.diagnostics.push(unmatched_slur_warning(slur.span));
}
} else {
self.diagnostics.push(unmatched_slur_warning(slur.span));
}
}
}
}
pub(crate) fn set_key(&mut self, key: Option<&KeySignature>) {
self.preserve_tie_pitches_for_key_change();
self.key_accidentals = key_accidental_policy(key);
self.current_key = key.cloned();
}
pub(crate) fn finish_pending_broken_at_boundary(&mut self) {
if let Some(pending) = self.pending_broken.take() {
self.diagnostics
.push(broken_rhythm_without_right_warning(pending.span));
}
self.broken_left_available = false;
}
pub(crate) fn attach_pending_grace_groups_to_previous_note(&mut self) -> bool {
if self.pending_grace_groups.is_empty() || !self.broken_left_available {
return false;
}
let forced_after_grace = self
.pending_grace_groups
.iter()
.all(|group| group.force_after_previous);
let event_index = if forced_after_grace {
self.previous_timed_note_or_chord_member_event_index()
} else {
self.previous_timed_note_event_index()
};
let Some(event_index) = event_index else {
return false;
};
if !forced_after_grace
&& !self.timed_event_has_trill(event_index)
&& self.pending_grace_slur_stops.is_empty()
{
return false;
}
let graces = self.take_pending_grace_group_attachments();
if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(event_index) {
timed.attachments.after_grace_groups.extend(graces);
return true;
}
false
}
pub(crate) fn attach_pending_grace_groups_to_previous_note_if_measure_complete(
&mut self,
) -> bool {
if self.pending_grace_groups.is_empty()
|| !self
.pending_grace_groups
.iter()
.all(|group| group.detached_from_previous)
|| !self.current_measure_has_reached_meter()
{
return false;
}
let Some(event_index) = self.previous_timed_note_event_index() else {
return false;
};
let graces = self.take_pending_grace_group_attachments();
if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(event_index) {
timed.attachments.after_grace_groups.extend(graces);
return true;
}
false
}
pub(crate) fn push_pending_grace_group(
&mut self,
grace: GraceGroupSyntax,
detached_from_previous: bool,
force_after_previous: bool,
) {
self.pending_grace_groups.push(PendingGraceGroup {
grace,
detached_from_previous,
force_after_previous,
});
}
fn attach_pending_slur_starts(&mut self, group: &[usize]) {
if self.pending_slur_starts.is_empty() {
return;
}
let Some(event_index) = group.iter().copied().find(|index| {
matches!(
self.lowered.get(*index),
Some(LoweredEvent::Timed(timed))
if matches!(
timed.event.kind,
LoweredEventAtomKind::Note { .. } | LoweredEventAtomKind::Rest { .. }
)
)
}) else {
return;
};
for slur in std::mem::take(&mut self.pending_slur_starts) {
if let Some(grace_index) = self.leading_grace_group_after(event_index, slur.marker.span)
{
self.attach_slur_to_grace(
event_index,
grace_index,
slur.pair_id,
SlurRole::Start,
slur.marker,
);
} else {
self.attach_slur(event_index, slur.pair_id, SlurRole::Start, slur.marker);
}
}
}
fn leading_grace_group_after(&self, event_index: usize, slur_span: Span) -> Option<usize> {
let LoweredEvent::Timed(timed) = self.lowered.get(event_index)? else {
return None;
};
timed
.attachments
.grace_groups
.iter()
.enumerate()
.filter(|(_, grace)| grace.span.start > slur_span.start)
.min_by_key(|(_, grace)| grace.span.start)
.map(|(index, _)| index)
}
fn attach_slur_to_grace(
&mut self,
event_index: usize,
grace_index: usize,
pair_id: u32,
role: SlurRole,
marker: SlurSyntax,
) {
if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(event_index)
&& let Some(grace) = timed.attachments.grace_groups.get_mut(grace_index)
{
grace.slurs.push(SlurAttachment {
pair_id,
role,
span: marker.span,
dotted: marker.dotted,
});
}
}
fn pending_grace_group_between(&self, open_span: Span, close_span: Span) -> bool {
self.pending_grace_groups.iter().any(|group| {
group.grace.span.start > open_span.start
&& group.grace.span.end <= close_span.start
&& grace_contains_note_event(&group.grace)
})
}
fn current_measure_has_reached_meter(&self) -> bool {
let Some(expected) = self.meter_duration else {
return false;
};
let elapsed = self
.lowered
.iter()
.rev()
.take_while(|event| !matches!(event, LoweredEvent::Untimed(Event::Barline { .. })))
.fold(Fraction::zero(), |total, event| match event {
LoweredEvent::Timed(timed) => total.saturating_add(timed.event.duration),
_ => total,
});
!elapsed.less_than(expected)
}
fn take_pending_grace_group_attachments(&mut self) -> Vec<GraceGroupAttachment> {
let pending_grace_groups = self.pending_grace_groups.drain(..).collect::<Vec<_>>();
if pending_grace_groups.is_empty() {
return Vec::new();
}
let mut starts_by_group = vec![Vec::new(); pending_grace_groups.len()];
let mut remaining_starts = Vec::new();
for start in std::mem::take(&mut self.pending_slur_starts) {
if let Some(index) = pending_grace_groups
.iter()
.enumerate()
.filter(|(_, grace)| {
grace.grace.span.start > start.marker.span.start
&& grace_contains_note_event(&grace.grace)
})
.min_by_key(|(_, grace)| grace.grace.span.start)
.map(|(index, _)| index)
{
starts_by_group[index].push(start);
} else {
remaining_starts.push(start);
}
}
self.pending_slur_starts = remaining_starts;
let mut stops_by_group = vec![Vec::new(); pending_grace_groups.len()];
let mut unapplied_stops = Vec::new();
for stop in self.pending_grace_slur_stops.drain(..) {
if let Some(index) = pending_grace_groups
.iter()
.enumerate()
.filter(|(_, grace)| {
grace.grace.span.end <= stop.marker.span.start
&& grace_contains_note_event(&grace.grace)
})
.map(|(index, _)| index)
.next_back()
{
stops_by_group[index].push(stop);
} else {
unapplied_stops.push(stop);
}
}
self.pending_grace_slur_stops = unapplied_stops;
let mut attachments = Vec::with_capacity(pending_grace_groups.len());
for ((grace, starts), stops) in pending_grace_groups
.iter()
.zip(starts_by_group)
.zip(stops_by_group)
{
let mut attachment = grace_group_attachment_model(&grace.grace, self);
for start in starts {
attachment.slurs.push(SlurAttachment {
pair_id: start.pair_id,
role: SlurRole::Start,
span: start.marker.span,
dotted: start.marker.dotted,
});
}
for stop in stops {
if !attach_grace_group_slur_stop(&mut attachment, stop.pair_id, stop.marker) {
self.diagnostics
.push(unmatched_slur_warning(stop.marker.span));
}
}
attachments.push(attachment);
}
attachments
}
pub(crate) fn last_note_event_index(&self) -> Option<usize> {
self.lowered
.iter()
.enumerate()
.rev()
.find_map(|(index, event)| lowered_timed_note(Some(event)).map(|_| index))
}
fn chord_head_event_index(&self, index: usize) -> usize {
let Some(LoweredEvent::Timed(timed)) = self.lowered.get(index) else {
return index;
};
if !matches!(
timed.event.kind,
LoweredEventAtomKind::Note { chord: true, .. }
) {
return index;
}
let source_order = timed.source_order;
let line_index = timed.line_index;
let mut head = index;
for earlier in (0..index).rev() {
let Some(LoweredEvent::Timed(earlier_timed)) = self.lowered.get(earlier) else {
break;
};
if earlier_timed.source_order == source_order
&& earlier_timed.line_index == line_index
&& matches!(earlier_timed.event.kind, LoweredEventAtomKind::Note { .. })
{
head = earlier;
} else {
break;
}
}
head
}
fn previous_timed_note_event_index(&self) -> Option<usize> {
for (index, event) in self.lowered.iter().enumerate().rev() {
match event {
LoweredEvent::Timed(timed) => {
return matches!(
&timed.event.kind,
LoweredEventAtomKind::Note { chord: false, .. }
)
.then_some(index);
}
_ => continue,
}
}
None
}
fn previous_timed_note_or_chord_member_event_index(&self) -> Option<usize> {
for (index, event) in self.lowered.iter().enumerate().rev() {
match event {
LoweredEvent::Timed(timed) => {
return match &timed.event.kind {
LoweredEventAtomKind::Note { chord: false, .. } => Some(index),
LoweredEventAtomKind::Note { chord: true, .. } => {
let source_order = timed.source_order;
let line_index = timed.line_index;
let mut first_index = index;
for earlier in (0..index).rev() {
let Some(LoweredEvent::Timed(earlier_timed)) =
self.lowered.get(earlier)
else {
break;
};
if earlier_timed.source_order == source_order
&& earlier_timed.line_index == line_index
&& matches!(
&earlier_timed.event.kind,
LoweredEventAtomKind::Note { chord: true, .. }
)
{
first_index = earlier;
} else {
break;
}
}
Some(first_index)
}
_ => None,
};
}
_ => continue,
}
}
None
}
fn timed_event_has_trill(&self, event_index: usize) -> bool {
let Some(LoweredEvent::Timed(timed)) = self.lowered.get(event_index) else {
return false;
};
timed
.attachments
.decorations
.iter()
.any(|decoration| decoration.name == "trill")
}
fn attach_slur(
&mut self,
event_index: usize,
pair_id: u32,
role: SlurRole,
marker: SlurSyntax,
) {
if let Some(LoweredEvent::Timed(timed)) = self.lowered.get_mut(event_index) {
timed.attachments.slurs.push(SlurAttachment {
pair_id,
role,
span: marker.span,
dotted: marker.dotted,
});
}
}
fn open_chord_member_slur(&mut self, event_index: usize, marker: SlurSyntax) {
let open = OpenSlur {
pair_id: self.next_slur_id,
marker,
};
self.next_slur_id = self.next_slur_id.saturating_add(1);
self.open_slurs.push(open);
self.attach_slur(event_index, open.pair_id, SlurRole::Start, marker);
}
fn close_chord_member_slur(&mut self, event_index: usize, marker: SlurSyntax) {
let open = if self
.open_slurs
.last()
.is_some_and(|open| open.marker.dotted != marker.dotted)
&& let Some(position) = self
.open_slurs
.iter()
.rposition(|open| open.marker.dotted == marker.dotted)
{
self.diagnostics.push(crossing_slur_warning(marker.span));
Some(self.open_slurs.remove(position))
} else {
self.open_slurs.pop()
};
let Some(open) = open else {
self.diagnostics.push(unmatched_slur_warning(marker.span));
return;
};
let start_was_pending = self
.pending_slur_starts
.iter()
.any(|pending| pending.pair_id == open.pair_id);
if start_was_pending {
self.pending_slur_starts
.retain(|pending| pending.pair_id != open.pair_id);
self.diagnostics.push(unmatched_slur_warning(marker.span));
return;
}
self.attach_slur(event_index, open.pair_id, SlurRole::Stop, marker);
}
pub(crate) fn finish_open_constructs(&mut self) {
self.finish_pending_broken_at_boundary();
self.finish_pending_tie_at_boundary(self.source_span);
self.finish_open_tuplets_at_boundary();
for slur in std::mem::take(&mut self.open_slurs) {
self.diagnostics
.push(unclosed_slur_warning(slur.marker.span));
}
for text in std::mem::take(&mut self.pending_chord_symbols) {
self.diagnostics
.push(dangling_quoted_text_warning(text.span));
}
for text in std::mem::take(&mut self.pending_annotations) {
self.diagnostics
.push(dangling_quoted_text_warning(text.span));
}
self.attach_pending_grace_groups_to_previous_note();
for group in std::mem::take(&mut self.pending_grace_groups) {
self.diagnostics
.push(dangling_grace_group_warning(group.grace.span));
}
for decoration in std::mem::take(&mut self.pending_decorations) {
self.diagnostics
.push(dangling_quoted_text_warning(decoration.span));
}
}
}
pub(crate) fn lowered_timed_note(event: Option<&LoweredEvent>) -> Option<&LoweredTimedEvent> {
match event {
Some(LoweredEvent::Timed(timed))
if matches!(timed.event.kind, LoweredEventAtomKind::Note { .. }) =>
{
Some(timed)
}
_ => None,
}
}
pub(crate) fn is_note_atom(event: LoweredEventAtom) -> bool {
matches!(event.kind, LoweredEventAtomKind::Note { .. })
}
pub(crate) fn note_signature(kind: LoweredEventAtomKind) -> Option<(char, i8)> {
match kind {
LoweredEventAtomKind::Note { step, octave, .. } => Some((step, octave)),
LoweredEventAtomKind::Rest { .. } | LoweredEventAtomKind::Spacer { .. } => None,
}
}
fn grace_group_attachment_model(
grace: &GraceGroupSyntax,
state: &mut LoweringState,
) -> GraceGroupAttachment {
let mut events: Vec<GraceEvent> = Vec::new();
let mut pending_slur_starts = Vec::new();
let mut open_slurs = Vec::new();
let mut last_note_event_index = None;
for element in &grace.elements {
match element {
GraceElementSyntax::Slur(slur) => match slur.direction {
SlurDirection::Start => {
let open = OpenSlur {
pair_id: state.next_slur_id,
marker: *slur,
};
state.next_slur_id = state.next_slur_id.saturating_add(1);
pending_slur_starts.push(open);
open_slurs.push(open);
}
SlurDirection::End => {
let Some(open) = open_slurs.pop() else {
state.diagnostics.push(unmatched_slur_warning(slur.span));
continue;
};
let start_was_pending = pending_slur_starts
.iter()
.any(|pending| pending.pair_id == open.pair_id);
pending_slur_starts.retain(|pending| pending.pair_id != open.pair_id);
if !start_was_pending && let Some(index) = last_note_event_index {
attach_grace_slur(&mut events[index], open.pair_id, SlurRole::Stop, *slur);
} else {
state.diagnostics.push(unmatched_slur_warning(slur.span));
}
}
},
_ => {
let Some(mut event) = grace_event_model(element, state) else {
continue;
};
if is_grace_note_event(&event) {
for open in pending_slur_starts.drain(..) {
attach_grace_slur(&mut event, open.pair_id, SlurRole::Start, open.marker);
}
last_note_event_index = Some(events.len());
}
events.push(event);
}
}
}
let unclosed_pair_ids = open_slurs
.iter()
.map(|open| open.pair_id)
.collect::<Vec<_>>();
for open in open_slurs {
state
.diagnostics
.push(unclosed_slur_warning(open.marker.span));
}
if !unclosed_pair_ids.is_empty() {
for event in &mut events {
event
.slurs
.retain(|slur| !unclosed_pair_ids.contains(&slur.pair_id));
}
}
GraceGroupAttachment {
span: grace.span,
slash: grace.slash_span,
note_count: grace
.elements
.iter()
.filter(|element| {
matches!(
element,
GraceElementSyntax::Note(_) | GraceElementSyntax::Chord(_)
)
})
.count()
.try_into()
.unwrap_or(u32::MAX),
events,
slurs: Vec::new(),
}
}
fn attach_grace_slur(event: &mut GraceEvent, pair_id: u32, role: SlurRole, marker: SlurSyntax) {
event.slurs.push(SlurAttachment {
pair_id,
role,
span: marker.span,
dotted: marker.dotted,
});
}
fn attach_grace_group_slur_stop(
group: &mut GraceGroupAttachment,
pair_id: u32,
marker: SlurSyntax,
) -> bool {
let Some(event) = group
.events
.iter_mut()
.rev()
.find(|event| is_grace_note_event(event))
else {
return false;
};
attach_grace_slur(event, pair_id, SlurRole::Stop, marker);
true
}
fn grace_contains_note_event(grace: &GraceGroupSyntax) -> bool {
grace.elements.iter().any(|element| {
matches!(
element,
GraceElementSyntax::Note(_) | GraceElementSyntax::Chord(_)
)
})
}
fn is_grace_note_event(event: &GraceEvent) -> bool {
matches!(
event.kind,
GraceEventKind::Note(_) | GraceEventKind::Chord(_)
)
}
fn attachment_bundle_model(
bundle: &AttachmentBundle,
state: &mut LoweringState,
) -> EventAttachments {
EventAttachments {
grace_groups: bundle
.grace_groups
.iter()
.map(|grace| grace_group_attachment_model(grace, state))
.collect(),
after_grace_groups: Vec::new(),
chord_symbols: bundle
.chord_symbols
.iter()
.map(|text| {
chord_symbol_attachment_model(text, state.pending_musicxml_harmony_text.take())
})
.collect(),
annotations: bundle
.annotations
.iter()
.map(annotation_attachment_model)
.collect(),
decorations: bundle
.decorations
.iter()
.map(|decoration| state.decoration_attachment_model(decoration))
.collect(),
instrument: None,
lyrics: Vec::new(),
lyric_same_note_extends: Vec::new(),
lyric_same_note_duplicates: Vec::new(),
musicxml_forward: false,
musicxml_sequence_backup: None,
symbols: Vec::new(),
ties: Vec::new(),
slurs: Vec::new(),
tuplets: Vec::new(),
}
}
fn merge_spans(accumulator: Option<Span>, span: Span) -> Span {
match accumulator {
Some(current) => Span::new(current.start.min(span.start), current.end.max(span.end)),
None => span,
}
}
pub(crate) fn chord_symbol_attachment_model(
text: &QuotedTextSyntax,
musicxml_harmony_text: Option<HarmonyKindText>,
) -> TextAttachment {
TextAttachment {
text: text.text.clone(),
span: text.span,
placement: None,
musicxml_harmony_text: musicxml_harmony_text.unwrap_or(HarmonyKindText::AbcNative),
}
}
pub(crate) fn annotation_attachment_model(text: &QuotedTextSyntax) -> TextAttachment {
TextAttachment {
text: crate::text::decode_text_string(&text.text),
span: text.span,
placement: match text.kind {
QuotedTextKind::Annotation(AnnotationPlacement::Above) => {
Some(AnnotationPlacementModel::Above)
}
QuotedTextKind::Annotation(AnnotationPlacement::Below) => {
Some(AnnotationPlacementModel::Below)
}
QuotedTextKind::Annotation(AnnotationPlacement::Left) => {
Some(AnnotationPlacementModel::Left)
}
QuotedTextKind::Annotation(AnnotationPlacement::Right) => {
Some(AnnotationPlacementModel::Right)
}
QuotedTextKind::Annotation(AnnotationPlacement::Free) => {
Some(AnnotationPlacementModel::Free)
}
QuotedTextKind::ChordSymbol => None,
},
musicxml_harmony_text: HarmonyKindText::AbcNative,
}
}
fn quoted_text_may_be_harmony(text: &str) -> bool {
matches!(text.trim_start().chars().next(), Some('A'..='G'))
}
fn decoration_binds_to_barline(decoration: &DecorationAttachment) -> bool {
decoration.name != "roll" && decoration_notation(decoration).is_none()
}
pub(crate) fn decoration_attachment_model(decoration: &DecorationSyntax) -> DecorationAttachment {
DecorationAttachment {
name: decoration.name.clone(),
span: decoration.span,
source_kind: match decoration.kind {
DecorationKind::Named => DecorationSourceKind::Named,
DecorationKind::LegacyNamed => DecorationSourceKind::LegacyNamed,
DecorationKind::Shorthand => DecorationSourceKind::Shorthand,
DecorationKind::UserDefined => DecorationSourceKind::UserDefined,
},
placement: None,
}
}
fn grace_event_model(
element: &GraceElementSyntax,
state: &mut LoweringState,
) -> Option<GraceEvent> {
match element {
GraceElementSyntax::Note(note) => Some(GraceEvent {
source_span: note.span,
kind: GraceEventKind::Note(grace_note_event_model(note, state)),
slurs: Vec::new(),
}),
GraceElementSyntax::Rest(rest) => Some(GraceEvent {
source_span: rest.span,
kind: GraceEventKind::Rest(RestEvent {
visibility: rest.visibility,
}),
slurs: Vec::new(),
}),
GraceElementSyntax::Chord(chord) => Some(GraceEvent {
source_span: chord.span,
kind: GraceEventKind::Chord(
chord
.members
.iter()
.map(|member| grace_note_event_model(&member.note, state))
.collect(),
),
slurs: Vec::new(),
}),
GraceElementSyntax::Slur(_) | GraceElementSyntax::Malformed(_) => None,
}
}
fn grace_note_event_model(note: &NoteSyntax, state: &mut LoweringState) -> GraceNoteEvent {
let octave = lowered_octave(note);
let ledger_octave = octave.saturating_add(voice_octave_shift(&state.properties));
let accidental = note.accidental.map(|accidental| accidental.sign);
let (effective_accidental, _) = state.effective_accidental(
note.pitch.step,
ledger_octave,
accidental,
note.accidental.map(|accidental| accidental.span),
);
GraceNoteEvent {
pitch: Pitch {
step: note.pitch.step.to_ascii_uppercase(),
alter: effective_accidental.map(Accidental::alter).unwrap_or(0),
octave,
spelling_source: note.pitch.span,
},
written_accidental: accidental.map(|kind| AccidentalMark {
kind,
explicit: true,
courtesy: false,
source: note
.accidental
.map(|accidental| accidental.span)
.unwrap_or(note.span),
}),
decorations: note
.attachments
.decorations
.iter()
.map(decoration_attachment_model)
.collect(),
length_multiplier: length_multiplier(note.length.as_ref()),
}
}
fn lowered_octave(note: &NoteSyntax) -> i8 {
let base_octave: i32 = if note.pitch.step.is_ascii_lowercase() {
5
} else {
4
};
let adjustment = note
.octave_marks
.iter()
.map(|mark| match mark.mark {
OctaveMark::Lower => -1,
OctaveMark::Raise => 1,
})
.sum::<i32>();
(base_octave + adjustment).clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8
}
pub(crate) fn voice_octave_shift(properties: &VoicePropertiesModel) -> i8 {
let mut shift: i32 = 0;
if let Some(clef) = properties.clef.as_ref() {
shift += i32::from(clef_octave_shift(clef.text.as_str()));
}
shift += voice_octave_param(properties);
if let Some(middle) = properties.middle.as_ref() {
shift += i32::from(middle_octave_shift(middle.text.as_str()));
}
shift.clamp(-12, 12) as i8
}
pub(crate) fn clef_octave_shift(clef: &str) -> i8 {
if clef.contains("-15") {
-2
} else if clef.contains("+15") {
2
} else if clef.contains("-8") {
-1
} else if clef.contains("+8") {
1
} else {
0
}
}
pub(crate) fn voice_octave_param(properties: &VoicePropertiesModel) -> i32 {
properties
.octave
.as_ref()
.and_then(|octave| octave.text.trim().parse::<i64>().ok())
.map_or(0, |value| value.clamp(-9, 9) as i32)
}
pub(crate) fn middle_octave_shift(text: &str) -> i8 {
let text = text.trim();
let mut chars = text.chars();
let Some(note) = chars.next() else {
return 0;
};
if !note.is_ascii_alphabetic() || !matches!(note.to_ascii_uppercase(), 'A'..='G') {
return 0;
}
let octstr = &text[note.len_utf8()..];
if !octstr.chars().all(|ch| matches!(ch, ',' | '\'')) {
return 0;
}
let n_up = note.to_ascii_uppercase();
let base: i32 = if note.is_ascii_uppercase() { 4 } else { 5 };
let marks = octstr.chars().count() as i32;
let octnum = base + if octstr.contains('\'') { marks } else { -marks };
let gtrans = (if matches!(n_up, 'A' | 'F' | 'D') {
3
} else {
4
}) - octnum;
gtrans as i8
}
fn length_multiplier(length: Option<&LengthSyntax>) -> Fraction {
length
.map(|length| length.multiplier)
.unwrap_or_else(Fraction::one)
}
fn broken_rhythm_multipliers(marker: BrokenRhythmSyntax) -> (Fraction, Fraction) {
let shift = u32::from(marker.count).min(30);
let denominator = 1u32.checked_shl(shift).unwrap_or(u32::MAX).max(1);
let long = denominator
.checked_mul(2)
.and_then(|value| value.checked_sub(1))
.unwrap_or(u32::MAX);
match marker.direction {
BrokenRhythmDirection::LeftShorter => (
Fraction::new(1, denominator),
Fraction::new(long, denominator),
),
BrokenRhythmDirection::RightShorter => (
Fraction::new(long, denominator),
Fraction::new(1, denominator),
),
}
}
fn variable_chord_duration_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.chord.variable_duration",
"Chord members have different durations; members were preserved with their own durations",
span,
)
.with_spec_reference(abc_chord_reference())
.with_recovery_note(RecoveryNote::new(
"ABC chord members should use a consistent duration within one chord group.",
))
}
fn broken_rhythm_without_left_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.broken_rhythm.missing_left",
"Broken rhythm marker has no preceding time-bearing note group",
span,
)
.with_spec_reference(abc_broken_rhythm_reference())
.with_recovery_note(RecoveryNote::new(
"The marker was preserved and applied only to the following note group when possible.",
))
}
fn broken_rhythm_without_right_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.broken_rhythm.missing_right",
"Broken rhythm marker has no following time-bearing note group",
span,
)
.with_spec_reference(abc_broken_rhythm_reference())
.with_recovery_note(RecoveryNote::new(
"The marker was preserved after applying the preceding-side duration change.",
))
}
fn overlapping_broken_rhythm_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.broken_rhythm.overlap",
"Broken rhythm markers overlap before the next note group",
span,
)
.with_spec_reference(abc_broken_rhythm_reference())
.with_recovery_note(RecoveryNote::new(
"The later marker determines the following-side duration change.",
))
}
fn unmatched_slur_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.unmatched_slur",
"Slur end has no matching open slur",
span,
)
.with_spec_reference(abc_slur_reference())
.with_recovery_note(RecoveryNote::new(
"The unmatched slur marker was preserved and skipped during lowering.",
))
}
fn crossing_slur_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.crossing_slur",
"Slur close crosses another open slur",
span,
)
.with_spec_reference(abc_slur_reference())
.with_recovery_note(RecoveryNote::new(
"The slur markers were preserved and paired by nearest compatible marker.",
))
}
fn unclosed_slur_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.unclosed_slur",
"Slur start has no matching close slur",
span,
)
.with_spec_reference(abc_slur_reference())
.with_recovery_note(RecoveryNote::new(
"The open slur marker was preserved and skipped during lowering.",
))
}
fn dangling_quoted_text_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.dangling_quoted_text",
"Quoted text has no following note to attach to",
span,
)
.with_spec_reference(crate::lower::diagnostics::abc_annotation_reference())
.with_recovery_note(RecoveryNote::new(
"The quoted text was dropped at the end of the voice.",
))
}
fn dangling_grace_group_warning(span: Span) -> Diagnostic {
Diagnostic::new(
Severity::Warning,
"abc.music.dangling_grace_group",
"Grace group has no following note to decorate",
span,
)
.with_spec_reference(crate::lower::diagnostics::abc_grace_reference())
.with_recovery_note(RecoveryNote::new(
"The grace group was dropped at the end of the voice.",
))
}
#[cfg(test)]
#[path = "voice_tests.rs"]
mod tests;