Skip to main content

rune/diagnostics/
emit.rs

1//! Runtime helpers for loading code and emitting diagnostics.
2
3use core::fmt;
4
5use std::io;
6
7use codespan_reporting::diagnostic as d;
8use codespan_reporting::term;
9pub use codespan_reporting::term::termcolor;
10use codespan_reporting::term::termcolor::WriteColor;
11
12use crate::alloc::fmt::TryWrite;
13use crate::alloc::prelude::*;
14use crate::alloc::{self, String};
15use crate::ast::{Span, Spanned};
16use crate::compile::{ErrorKind, LinkerError, Location};
17use crate::diagnostics::{
18    Diagnostic, FatalDiagnostic, FatalDiagnosticKind, RuntimeDiagnostic, RuntimeDiagnosticKind,
19    WarningDiagnostic, WarningDiagnosticKind,
20};
21use crate::hash::Hash;
22use crate::runtime::DebugInfo;
23use crate::runtime::{DebugInst, Protocol, Unit, VmError, VmErrorAt, VmErrorKind};
24use crate::Context;
25use crate::{Diagnostics, SourceId, Sources};
26
27struct StackFrame {
28    source_id: SourceId,
29    span: Span,
30}
31
32/// Errors that can be raised when formatting diagnostics.
33#[derive(Debug)]
34#[non_exhaustive]
35pub enum EmitError {
36    /// Source Error.
37    Io(io::Error),
38    /// Allocation error.
39    Alloc(alloc::Error),
40    /// Codespan reporting error.
41    CodespanReporting(codespan_reporting::files::Error),
42}
43
44impl fmt::Display for EmitError {
45    #[inline]
46    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
47        match self {
48            EmitError::Io(error) => error.fmt(f),
49            EmitError::Alloc(error) => error.fmt(f),
50            EmitError::CodespanReporting(error) => error.fmt(f),
51        }
52    }
53}
54
55impl From<io::Error> for EmitError {
56    fn from(error: io::Error) -> Self {
57        EmitError::Io(error)
58    }
59}
60
61impl From<alloc::Error> for EmitError {
62    fn from(error: alloc::Error) -> Self {
63        EmitError::Alloc(error)
64    }
65}
66
67impl From<codespan_reporting::files::Error> for EmitError {
68    fn from(error: codespan_reporting::files::Error) -> Self {
69        EmitError::CodespanReporting(error)
70    }
71}
72
73impl core::error::Error for EmitError {
74    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
75        match self {
76            EmitError::Io(error) => Some(error),
77            EmitError::Alloc(error) => Some(error),
78            EmitError::CodespanReporting(error) => Some(error),
79        }
80    }
81}
82
83impl Diagnostics {
84    /// Generate formatted diagnostics capable of referencing source lines and
85    /// hints.
86    ///
87    /// See [prepare][crate::prepare] for how to use.
88    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
89    where
90        O: WriteColor,
91    {
92        if self.is_empty() {
93            return Ok(());
94        }
95
96        let config = term::Config::default();
97
98        for diagnostic in self.diagnostics() {
99            match diagnostic {
100                Diagnostic::Fatal(e) => {
101                    fatal_diagnostics_emit(e, out, sources, &config)?;
102                }
103                Diagnostic::Warning(w) => {
104                    warning_diagnostics_emit(w, out, sources, &config)?;
105                }
106                Diagnostic::Runtime(w) => {
107                    runtime_warning_diagnostics_emit(w, out, sources, &config, None, None)?;
108                }
109            }
110        }
111
112        Ok(())
113    }
114
115    /// Generate formatted diagnostics capable of referencing source lines and
116    /// hints.
117    ///
118    /// See [prepare][crate::prepare] for how to use.
119    pub fn emit_detailed<O>(
120        &self,
121        out: &mut O,
122        sources: &Sources,
123        unit: &Unit,
124        context: &Context,
125    ) -> Result<(), EmitError>
126    where
127        O: WriteColor,
128    {
129        if self.is_empty() {
130            return Ok(());
131        }
132
133        let debug_info = unit.debug_info();
134
135        let config = term::Config::default();
136
137        for diagnostic in self.diagnostics() {
138            match diagnostic {
139                Diagnostic::Fatal(e) => {
140                    fatal_diagnostics_emit(e, out, sources, &config)?;
141                }
142                Diagnostic::Warning(w) => {
143                    warning_diagnostics_emit(w, out, sources, &config)?;
144                }
145                Diagnostic::Runtime(w) => {
146                    runtime_warning_diagnostics_emit(
147                        w,
148                        out,
149                        sources,
150                        &config,
151                        debug_info,
152                        Some(context),
153                    )?;
154                }
155            }
156        }
157
158        Ok(())
159    }
160}
161
162impl VmError {
163    /// Generate formatted diagnostics capable of referencing source lines and
164    /// hints.
165    ///
166    /// See [prepare][crate::prepare] for how to use.
167    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
168    where
169        O: WriteColor,
170    {
171        let mut red = termcolor::ColorSpec::new();
172        red.set_fg(Some(termcolor::Color::Red));
173
174        let mut backtrace = vec![];
175        let config = term::Config::default();
176
177        for l in self.stacktrace() {
178            let debug_info = match l.unit.debug_info() {
179                Some(debug_info) => debug_info,
180                None => continue,
181            };
182
183            for ip in [l.ip]
184                .into_iter()
185                .chain(l.frames.iter().rev().map(|v| v.ip))
186            {
187                let debug_inst = match debug_info.instruction_at(ip) {
188                    Some(debug_inst) => debug_inst,
189                    None => continue,
190                };
191
192                let source_id = debug_inst.source_id;
193                let span = debug_inst.span;
194
195                backtrace.push(StackFrame { source_id, span });
196            }
197        }
198
199        let mut labels = rust_alloc::vec::Vec::new();
200        let mut notes = rust_alloc::vec::Vec::new();
201
202        let get = |at: &VmErrorAt| -> Option<&DebugInst> {
203            let l = self.stacktrace().get(at.index())?;
204            let debug_info = l.unit.debug_info()?;
205            let debug_inst = debug_info.instruction_at(l.ip)?;
206            Some(debug_inst)
207        };
208
209        let get_ident = |at: &VmErrorAt, hash: Hash| {
210            let l = self.stacktrace().get(at.index())?;
211            let debug_info = l.unit.debug_info()?;
212            debug_info.ident_for_hash(hash)
213        };
214
215        for at in self.chain() {
216            // Populate source-specific notes.
217            match at.kind() {
218                VmErrorKind::UnsupportedBinaryOperation { lhs, rhs, .. } => {
219                    notes.extend(vec![
220                        format!("Left hand side has type `{}`", lhs),
221                        format!("Right hand side has type `{}`", rhs),
222                    ]);
223                }
224                VmErrorKind::BadArgumentCount { actual, expected } => {
225                    notes.extend([format!("Expected `{expected}`"), format!("Got `{actual}`")]);
226                }
227                _ => {}
228            };
229
230            if let Some(&DebugInst {
231                source_id, span, ..
232            }) = get(at)
233            {
234                labels.push(
235                    d::Label::primary(source_id, span.range()).with_message(at.try_to_string()?),
236                );
237            }
238        }
239
240        if let Some(&DebugInst {
241            source_id, span, ..
242        }) = get(self.error())
243        {
244            labels.push(
245                d::Label::primary(source_id, span.range())
246                    .with_message(self.error().try_to_string()?),
247            );
248        };
249
250        for at in [self.error()].into_iter().chain(self.chain()) {
251            // Populate source-specific notes.
252            if let VmErrorKind::MissingInstanceFunction { hash, instance } = at.kind() {
253                // Undo instance function hashing to extract the hash of the
254                // name. This is an implementation detail in how hash mixing
255                // works, in that it can be reversed because we simply xor
256                // the values together with an associated function seed. But
257                // this is not guaranteed to work everywhere.
258
259                if let Some(&DebugInst {
260                    source_id, span, ..
261                }) = get(at)
262                {
263                    let instance_hash = Hash::associated_function(instance.type_hash(), *hash);
264
265                    if let Some(ident) = get_ident(at, instance_hash) {
266                        labels.push(d::Label::secondary(source_id, span.range()).with_message(
267                            format!(
268                                "This corresponds to the `{instance}::{ident}` instance function"
269                            ),
270                        ));
271                    }
272
273                    if let Some(protocol) = Protocol::from_hash(instance_hash) {
274                        labels.push(
275                            d::Label::secondary(source_id, span.range())
276                                .with_message(format!("This corresponds to the `{protocol}` protocol function for `{instance}`")),
277                        );
278                    }
279                }
280            };
281
282            // What is missing is a field, and which field it was is in the
283            // unit next to the instruction which asked for it - the error
284            // itself only carries the slot it was in.
285            if let VmErrorKind::ObjectIndexMissing { slot } = at.kind() {
286                if let Some(&DebugInst {
287                    source_id, span, ..
288                }) = get(at)
289                {
290                    if let Some(string) = self
291                        .stacktrace()
292                        .get(at.index())
293                        .and_then(|l| l.unit.lookup_string(*slot))
294                    {
295                        labels.push(
296                            d::Label::secondary(source_id, span.range())
297                                .with_message(format!("This corresponds to the field `{string}`")),
298                        );
299                    }
300                }
301            }
302        }
303
304        let diagnostic = d::Diagnostic::error()
305            .with_message(self.error().try_to_string()?)
306            .with_labels(labels)
307            .with_notes(notes);
308
309        term::emit_to_write_style(out, &config, sources, &diagnostic)?;
310
311        if !backtrace.is_empty() {
312            writeln!(out, "Backtrace:")?;
313
314            for frame in &backtrace {
315                let Some(source) = sources.get(frame.source_id) else {
316                    continue;
317                };
318
319                let (line, line_count, [prefix, mid, suffix]) = match source.line(frame.span) {
320                    Some((line, line_count, text)) => {
321                        (line.saturating_add(1), line_count.saturating_add(1), text)
322                    }
323                    None => continue,
324                };
325
326                writeln!(out, "{}:{line}:{line_count}:", source.name())?;
327                write!(out, "{prefix}")?;
328                out.set_color(&red)?;
329                write!(out, "{mid}")?;
330                out.reset()?;
331                writeln!(out, "{}", suffix.trim_end_matches(['\n', '\r']))?;
332            }
333        }
334
335        Ok(())
336    }
337}
338
339impl FatalDiagnostic {
340    /// Generate formatted diagnostics capable of referencing source lines and
341    /// hints.
342    ///
343    /// See [prepare][crate::prepare] for how to use.
344    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
345    where
346        O: WriteColor,
347    {
348        let config = term::Config::default();
349        fatal_diagnostics_emit(self, out, sources, &config)
350    }
351}
352
353impl WarningDiagnostic {
354    /// Generate formatted diagnostics capable of referencing source lines and
355    /// hints.
356    ///
357    /// See [prepare][crate::prepare] for how to use.
358    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
359    where
360        O: WriteColor,
361    {
362        let config = term::Config::default();
363        warning_diagnostics_emit(self, out, sources, &config)
364    }
365}
366
367impl RuntimeDiagnostic {
368    /// Generate formatted diagnostics capable of referencing source lines and
369    /// hints.
370    ///
371    /// See [prepare][crate::prepare] for how to use.
372    pub fn emit<O>(
373        &self,
374        out: &mut O,
375        sources: &Sources,
376        debug_info: Option<&DebugInfo>,
377        context: Option<&Context>,
378    ) -> Result<(), EmitError>
379    where
380        O: WriteColor,
381    {
382        let config = term::Config::default();
383        runtime_warning_diagnostics_emit(self, out, sources, &config, debug_info, context)
384    }
385}
386
387impl Unit {
388    /// Dump instructions in a human readable manner.
389    pub fn emit_instructions<O>(
390        &self,
391        out: &mut O,
392        sources: &Sources,
393        without_source: bool,
394    ) -> io::Result<()>
395    where
396        O: WriteColor,
397    {
398        let mut first_function = true;
399
400        for (n, inst) in self.iter_instructions() {
401            let debug = self.debug_info().and_then(|d| d.instruction_at(n));
402
403            if let Some((hash, signature)) = self.debug_info().and_then(|d| d.function_at(n)) {
404                if !std::mem::take(&mut first_function) {
405                    writeln!(out)?;
406                }
407
408                writeln!(out, "fn {signature} ({hash}):")?;
409            }
410
411            for label in debug.map(|d| d.labels.as_slice()).unwrap_or_default() {
412                writeln!(out, "{label}:")?;
413            }
414
415            write!(out, "  {n:04} = {inst}")?;
416
417            if let Some(comment) = debug.and_then(|d| d.comment.as_ref()) {
418                write!(out, " // {comment}")?;
419            }
420
421            writeln!(out)?;
422
423            if !without_source {
424                if let Some((source, span)) =
425                    debug.and_then(|d| sources.get(d.source_id).map(|s| (s, d.span)))
426                {
427                    if let Some(line) = source.source_line(span) {
428                        write!(out, "  ")?;
429                        line.write(out)?;
430                        writeln!(out)?;
431                    }
432                }
433            }
434        }
435
436        Ok(())
437    }
438}
439
440/// Helper to emit diagnostics for a warning.
441fn warning_diagnostics_emit<O>(
442    this: &WarningDiagnostic,
443    out: &mut O,
444    sources: &Sources,
445    config: &term::Config,
446) -> Result<(), EmitError>
447where
448    O: WriteColor,
449{
450    let mut notes = rust_alloc::vec::Vec::new();
451    let mut labels = rust_alloc::vec::Vec::new();
452
453    labels.push(
454        d::Label::primary(this.source_id(), this.span().range())
455            .with_message(this.try_to_string()?),
456    );
457
458    match this.kind() {
459        WarningDiagnosticKind::LetPatternMightPanic { span, .. } => {
460            if let Some(binding) = sources.source(this.source_id(), *span) {
461                let mut note = String::new();
462                writeln!(note, "Hint: Rewrite to:")?;
463                writeln!(note, "if {binding} {{")?;
464                writeln!(note, "    // ..")?;
465                writeln!(note, "}}")?;
466                notes.push(note.into_std());
467            }
468        }
469        WarningDiagnosticKind::RemoveTupleCallParams { variant, .. } => {
470            if let Some(variant) = sources.source(this.source_id(), *variant) {
471                let mut note = String::new();
472                writeln!(note, "Hint: Rewrite to `{variant}`")?;
473                notes.push(note.into_std());
474            }
475        }
476        WarningDiagnosticKind::Unreachable { cause, .. } => {
477            labels.push(
478                d::Label::secondary(this.source_id(), cause.range())
479                    .with_message("This code diverges"),
480            );
481        }
482        _ => {}
483    };
484
485    if let Some(context) = this.context() {
486        labels.push(
487            d::Label::secondary(this.source_id(), context.range()).with_message("In this context"),
488        );
489    }
490
491    // What the warning is, rather than that there is one. An error says what it
492    // is on this line and a warning said "Warning", leaving the reader to find
493    // the text on the label below it.
494    let diagnostic = d::Diagnostic::warning()
495        .with_message(this.try_to_string()?)
496        .with_labels(labels)
497        .with_notes(notes);
498
499    term::emit_to_write_style(out, config, sources, &diagnostic)?;
500    Ok(())
501}
502
503/// Helper to emit diagnostics for a runtime warning.
504fn runtime_warning_diagnostics_emit<O>(
505    this: &RuntimeDiagnostic,
506    out: &mut O,
507    sources: &Sources,
508    config: &term::Config,
509    debug_info: Option<&DebugInfo>,
510    context: Option<&Context>,
511) -> Result<(), EmitError>
512where
513    O: WriteColor,
514{
515    let mut notes = rust_alloc::vec::Vec::new();
516    let mut labels = rust_alloc::vec::Vec::new();
517    let mut message = String::new();
518
519    match this.kind {
520        RuntimeDiagnosticKind::UsedDeprecated { hash } => {
521            // try to get the function name - this needs to be improved
522            let name = match context
523                .map(|c| c.lookup_meta_by_hash(hash))
524                .and_then(|m| m.into_iter().next())
525                .and_then(|e| e.item.as_ref())
526            {
527                Some(e) => e.try_to_string()?,
528                None => hash.try_to_string()?,
529            };
530            writeln!(message, "Used deprecated function: {name}")?;
531
532            // Deprecation message if it's availble
533            if let Some(context) = context {
534                if let Some(deprecation) = context.lookup_deprecation(hash) {
535                    let mut note = String::new();
536                    writeln!(note, "Deprecated: {deprecation}")?;
537                    notes.push(note.into_std());
538                }
539            }
540
541            // Show the span, where the problem occoured
542            if let Some(inst) = debug_info.and_then(|d| d.instruction_at(this.ip)) {
543                labels.push(
544                    d::Label::primary(inst.source_id, inst.span.range())
545                        .with_message(this.try_to_string()?),
546                );
547            }
548        }
549    };
550
551    let diagnostic = d::Diagnostic::warning()
552        .with_message(message)
553        .with_labels(labels)
554        .with_notes(notes);
555
556    term::emit_to_write_style(out, config, sources, &diagnostic)?;
557    Ok(())
558}
559
560/// Custom shared helper for emitting diagnostics for a single error.
561fn fatal_diagnostics_emit<O>(
562    this: &FatalDiagnostic,
563    out: &mut O,
564    sources: &Sources,
565    config: &term::Config,
566) -> Result<(), EmitError>
567where
568    O: WriteColor,
569{
570    let mut labels = rust_alloc::vec::Vec::new();
571    let mut notes = rust_alloc::vec::Vec::new();
572
573    if let Some(span) = this.span() {
574        labels.push(
575            d::Label::primary(this.source_id(), span.range())
576                .with_message(this.kind().try_to_string()?),
577        );
578    }
579
580    match this.kind() {
581        FatalDiagnosticKind::Custom(message) => {
582            writeln!(out, "{message}")?;
583            return Ok(());
584        }
585        FatalDiagnosticKind::LinkError(error) => {
586            match error {
587                LinkerError::MissingFunction { hash, spans } => {
588                    let mut labels = rust_alloc::vec::Vec::new();
589
590                    for (span, source_id) in spans {
591                        labels.push(
592                            d::Label::primary(*source_id, span.range())
593                                .with_message("called here."),
594                        );
595                    }
596
597                    let diagnostic = d::Diagnostic::error()
598                        .with_message(format!("linker error: missing function with hash `{hash}`",))
599                        .with_labels(labels);
600
601                    term::emit_to_write_style(out, config, sources, &diagnostic)?;
602                }
603            }
604
605            return Ok(());
606        }
607        FatalDiagnosticKind::CompileError(error) => {
608            format_compile_error(
609                this,
610                sources,
611                error.span(),
612                error.kind(),
613                &mut labels,
614                &mut notes,
615            )?;
616        }
617    };
618
619    let diagnostic = d::Diagnostic::error()
620        .with_message(this.kind().try_to_string()?)
621        .with_labels(labels)
622        .with_notes(notes);
623
624    term::emit_to_write_style(out, config, sources, &diagnostic)?;
625    return Ok(());
626
627    fn format_compile_error(
628        this: &FatalDiagnostic,
629        sources: &Sources,
630        span: Span,
631        kind: &ErrorKind,
632        labels: &mut rust_alloc::vec::Vec<d::Label<SourceId>>,
633        notes: &mut rust_alloc::vec::Vec<rust_alloc::string::String>,
634    ) -> Result<(), EmitError> {
635        match kind {
636            ErrorKind::ImportCycle { path } => {
637                let mut it = path.iter();
638                let last = it.next_back();
639
640                for (step, entry) in (1..).zip(it) {
641                    labels.push(
642                        d::Label::secondary(entry.location.source_id, entry.location.span.range())
643                            .with_message(format!("Step #{} for `{}`", step, entry.item)),
644                    );
645                }
646
647                if let Some(entry) = last {
648                    labels.push(
649                        d::Label::secondary(entry.location.source_id, entry.location.span.range())
650                            .with_message(format!("Final step cycling back to `{}`", entry.item)),
651                    );
652                }
653            }
654            ErrorKind::NotVisible {
655                chain,
656                location: Location { source_id, span },
657                ..
658            } => {
659                for Location { source_id, span } in chain {
660                    labels.push(
661                        d::Label::secondary(*source_id, span.range())
662                            .with_message("Re-exported here"),
663                    );
664                }
665
666                labels.push(
667                    d::Label::secondary(*source_id, span.range()).with_message("defined here"),
668                );
669            }
670            ErrorKind::NotVisibleMod {
671                chain,
672                location: Location { source_id, span },
673                ..
674            } => {
675                for Location { source_id, span } in chain {
676                    labels.push(
677                        d::Label::secondary(*source_id, span.range())
678                            .with_message("Re-exported here"),
679                    );
680                }
681
682                labels.push(
683                    d::Label::secondary(*source_id, span.range())
684                        .with_message("Module defined here"),
685                );
686            }
687            ErrorKind::AmbiguousItem { locations, .. } => {
688                for (Location { source_id, span }, item) in locations {
689                    labels.push(
690                        d::Label::secondary(*source_id, span.range())
691                            .with_message(format!("Here as `{item}`")),
692                    );
693                }
694            }
695            ErrorKind::AmbiguousContextItem { infos, .. } => {
696                for info in infos.as_ref() {
697                    labels.push(
698                        d::Label::secondary(this.source_id, span.range())
699                            .with_message(format!("Could be `{info}`")),
700                    );
701                }
702            }
703            ErrorKind::DuplicateObjectKey { existing, object } => {
704                labels.push(
705                    d::Label::secondary(this.source_id(), existing.range())
706                        .with_message("Previously defined here"),
707                );
708
709                labels.push(
710                    d::Label::secondary(this.source_id(), object.range())
711                        .with_message("Object being defined here"),
712                );
713            }
714            ErrorKind::ModAlreadyLoaded { existing, .. } => {
715                let (existing_source_id, existing_span) = *existing;
716
717                labels.push(
718                    d::Label::secondary(existing_source_id, existing_span.range())
719                        .with_message("Previously loaded here"),
720                );
721            }
722            ErrorKind::ExpectedBlockSemiColon { followed_span } => {
723                labels.push(
724                    d::Label::secondary(this.source_id(), followed_span.range())
725                        .with_message("Because this immediately follows"),
726                );
727
728                let binding = sources.source(this.source_id(), span);
729
730                if let Some(binding) = binding {
731                    let mut note = String::new();
732                    writeln!(note, "Hint: Rewrite to `{binding};`")?;
733                    notes.push(note.into_std());
734                }
735            }
736            ErrorKind::VariableMoved { moved_at, .. } => {
737                labels.push(
738                    d::Label::secondary(this.source_id(), moved_at.range())
739                        .with_message("Moved here"),
740                );
741            }
742            ErrorKind::NestedTest { nested_span } => {
743                labels.push(
744                    d::Label::secondary(this.source_id(), nested_span.range())
745                        .with_message("Nested in here"),
746                );
747            }
748            ErrorKind::NestedBench { nested_span } => {
749                labels.push(
750                    d::Label::secondary(this.source_id(), nested_span.range())
751                        .with_message("Nested in here"),
752                );
753            }
754            ErrorKind::PatternMissingFields { fields, .. } => {
755                let pl = if fields.len() == 1 { "field" } else { "fields" };
756
757                let fields = fields.join(", ");
758
759                labels.push(
760                    d::Label::secondary(this.source_id(), span.range())
761                        .with_message(format!("Missing {pl}: {fields}")),
762                );
763
764                notes.push(
765                    "You can also make the pattern non-exhaustive by adding `..`"
766                        .try_to_string()?
767                        .into_std(),
768                );
769            }
770            ErrorKind::ConflictingLabels { existing, .. } => {
771                labels.push(
772                    d::Label::secondary(this.source_id(), existing.range())
773                        .with_message("Existing label here"),
774                );
775            }
776            ErrorKind::DuplicateSelectDefault { existing, .. } => {
777                labels.push(
778                    d::Label::secondary(this.source_id(), existing.range())
779                        .with_message("Existing branch here"),
780                );
781            }
782            _ => (),
783        }
784
785        Ok(())
786    }
787}