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, RuntimeWarningDiagnostic,
19    RuntimeWarningDiagnosticKind, 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::RuntimeWarning(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::RuntimeWarning(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
283        let diagnostic = d::Diagnostic::error()
284            .with_message(self.error().try_to_string()?)
285            .with_labels(labels)
286            .with_notes(notes);
287
288        term::emit(out, &config, sources, &diagnostic)?;
289
290        if !backtrace.is_empty() {
291            writeln!(out, "Backtrace:")?;
292
293            for frame in &backtrace {
294                let Some(source) = sources.get(frame.source_id) else {
295                    continue;
296                };
297
298                let (line, line_count, [prefix, mid, suffix]) = match source.line(frame.span) {
299                    Some((line, line_count, text)) => {
300                        (line.saturating_add(1), line_count.saturating_add(1), text)
301                    }
302                    None => continue,
303                };
304
305                writeln!(out, "{}:{line}:{line_count}:", source.name())?;
306                write!(out, "{prefix}")?;
307                out.set_color(&red)?;
308                write!(out, "{mid}")?;
309                out.reset()?;
310                writeln!(out, "{}", suffix.trim_end_matches(['\n', '\r']))?;
311            }
312        }
313
314        Ok(())
315    }
316}
317
318impl FatalDiagnostic {
319    /// Generate formatted diagnostics capable of referencing source lines and
320    /// hints.
321    ///
322    /// See [prepare][crate::prepare] for how to use.
323    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
324    where
325        O: WriteColor,
326    {
327        let config = term::Config::default();
328        fatal_diagnostics_emit(self, out, sources, &config)
329    }
330}
331
332impl WarningDiagnostic {
333    /// Generate formatted diagnostics capable of referencing source lines and
334    /// hints.
335    ///
336    /// See [prepare][crate::prepare] for how to use.
337    pub fn emit<O>(&self, out: &mut O, sources: &Sources) -> Result<(), EmitError>
338    where
339        O: WriteColor,
340    {
341        let config = term::Config::default();
342        warning_diagnostics_emit(self, out, sources, &config)
343    }
344}
345
346impl RuntimeWarningDiagnostic {
347    /// Generate formatted diagnostics capable of referencing source lines and
348    /// hints.
349    ///
350    /// See [prepare][crate::prepare] for how to use.
351    pub fn emit<O>(
352        &self,
353        out: &mut O,
354        sources: &Sources,
355        debug_info: Option<&DebugInfo>,
356        context: Option<&Context>,
357    ) -> Result<(), EmitError>
358    where
359        O: WriteColor,
360    {
361        let config = term::Config::default();
362        runtime_warning_diagnostics_emit(self, out, sources, &config, debug_info, context)
363    }
364}
365
366impl Unit {
367    /// Dump instructions in a human readable manner.
368    pub fn emit_instructions<O>(
369        &self,
370        out: &mut O,
371        sources: &Sources,
372        without_source: bool,
373    ) -> io::Result<()>
374    where
375        O: WriteColor,
376    {
377        let mut first_function = true;
378
379        for (n, inst) in self.iter_instructions() {
380            let debug = self.debug_info().and_then(|d| d.instruction_at(n));
381
382            if let Some((hash, signature)) = self.debug_info().and_then(|d| d.function_at(n)) {
383                if !std::mem::take(&mut first_function) {
384                    writeln!(out)?;
385                }
386
387                writeln!(out, "fn {signature} ({hash}):")?;
388            }
389
390            for label in debug.map(|d| d.labels.as_slice()).unwrap_or_default() {
391                writeln!(out, "{label}:")?;
392            }
393
394            write!(out, "  {n:04} = {inst}")?;
395
396            if let Some(comment) = debug.and_then(|d| d.comment.as_ref()) {
397                write!(out, " // {comment}")?;
398            }
399
400            writeln!(out)?;
401
402            if !without_source {
403                if let Some((source, span)) =
404                    debug.and_then(|d| sources.get(d.source_id).map(|s| (s, d.span)))
405                {
406                    if let Some(line) = source.source_line(span) {
407                        write!(out, "  ")?;
408                        line.write(out)?;
409                        writeln!(out)?;
410                    }
411                }
412            }
413        }
414
415        Ok(())
416    }
417}
418
419/// Helper to emit diagnostics for a warning.
420fn warning_diagnostics_emit<O>(
421    this: &WarningDiagnostic,
422    out: &mut O,
423    sources: &Sources,
424    config: &term::Config,
425) -> Result<(), EmitError>
426where
427    O: WriteColor,
428{
429    let mut notes = rust_alloc::vec::Vec::new();
430    let mut labels = rust_alloc::vec::Vec::new();
431
432    labels.push(
433        d::Label::primary(this.source_id(), this.span().range())
434            .with_message(this.try_to_string()?),
435    );
436
437    match this.kind() {
438        WarningDiagnosticKind::LetPatternMightPanic { span, .. } => {
439            if let Some(binding) = sources.source(this.source_id(), *span) {
440                let mut note = String::new();
441                writeln!(note, "Hint: Rewrite to:")?;
442                writeln!(note, "if {binding} {{")?;
443                writeln!(note, "    // ..")?;
444                writeln!(note, "}}")?;
445                notes.push(note.into_std());
446            }
447        }
448        WarningDiagnosticKind::RemoveTupleCallParams { variant, .. } => {
449            if let Some(variant) = sources.source(this.source_id(), *variant) {
450                let mut note = String::new();
451                writeln!(note, "Hint: Rewrite to `{variant}`")?;
452                notes.push(note.into_std());
453            }
454        }
455        WarningDiagnosticKind::Unreachable { cause, .. } => {
456            labels.push(
457                d::Label::secondary(this.source_id(), cause.range())
458                    .with_message("This code diverges"),
459            );
460        }
461        _ => {}
462    };
463
464    if let Some(context) = this.context() {
465        labels.push(
466            d::Label::secondary(this.source_id(), context.range()).with_message("In this context"),
467        );
468    }
469
470    let diagnostic = d::Diagnostic::warning()
471        .with_message("Warning")
472        .with_labels(labels)
473        .with_notes(notes);
474
475    term::emit(out, config, sources, &diagnostic)?;
476    Ok(())
477}
478
479/// Helper to emit diagnostics for a runtime warning.
480fn runtime_warning_diagnostics_emit<O>(
481    this: &RuntimeWarningDiagnostic,
482    out: &mut O,
483    sources: &Sources,
484    config: &term::Config,
485    debug_info: Option<&DebugInfo>,
486    context: Option<&Context>,
487) -> Result<(), EmitError>
488where
489    O: WriteColor,
490{
491    let mut notes = rust_alloc::vec::Vec::new();
492    let mut labels = rust_alloc::vec::Vec::new();
493    let mut message = String::new();
494
495    match this.kind {
496        RuntimeWarningDiagnosticKind::UsedDeprecated { hash } => {
497            // try to get the function name - this needs to be improved
498            let name = match context
499                .map(|c| c.lookup_meta_by_hash(hash))
500                .and_then(|m| m.into_iter().next())
501                .and_then(|e| e.item.as_ref())
502            {
503                Some(e) => e.try_to_string()?,
504                None => hash.try_to_string()?,
505            };
506            writeln!(message, "Used deprecated function: {name}")?;
507
508            // Deprecation message if it's availble
509            if let Some(context) = context {
510                if let Some(deprecation) = context.lookup_deprecation(hash) {
511                    let mut note = String::new();
512                    writeln!(note, "Deprecated: {deprecation}")?;
513                    notes.push(note.into_std());
514                }
515            }
516
517            // Show the span, where the problem occoured
518            if let Some(inst) = debug_info.and_then(|d| d.instruction_at(this.ip)) {
519                labels.push(
520                    d::Label::primary(inst.source_id, inst.span.range())
521                        .with_message(this.try_to_string()?),
522                );
523            }
524        }
525    };
526
527    let diagnostic = d::Diagnostic::warning()
528        .with_message(message)
529        .with_labels(labels)
530        .with_notes(notes);
531
532    term::emit(out, config, sources, &diagnostic)?;
533    Ok(())
534}
535
536/// Custom shared helper for emitting diagnostics for a single error.
537fn fatal_diagnostics_emit<O>(
538    this: &FatalDiagnostic,
539    out: &mut O,
540    sources: &Sources,
541    config: &term::Config,
542) -> Result<(), EmitError>
543where
544    O: WriteColor,
545{
546    let mut labels = rust_alloc::vec::Vec::new();
547    let mut notes = rust_alloc::vec::Vec::new();
548
549    if let Some(span) = this.span() {
550        labels.push(
551            d::Label::primary(this.source_id(), span.range())
552                .with_message(this.kind().try_to_string()?),
553        );
554    }
555
556    match this.kind() {
557        FatalDiagnosticKind::Internal(message) => {
558            writeln!(out, "internal error: {message}")?;
559            return Ok(());
560        }
561        FatalDiagnosticKind::LinkError(error) => {
562            match error {
563                LinkerError::MissingFunction { hash, spans } => {
564                    let mut labels = rust_alloc::vec::Vec::new();
565
566                    for (span, source_id) in spans {
567                        labels.push(
568                            d::Label::primary(*source_id, span.range())
569                                .with_message("called here."),
570                        );
571                    }
572
573                    let diagnostic = d::Diagnostic::error()
574                        .with_message(format!("linker error: missing function with hash `{hash}`",))
575                        .with_labels(labels);
576
577                    term::emit(out, config, sources, &diagnostic)?;
578                }
579            }
580
581            return Ok(());
582        }
583        FatalDiagnosticKind::CompileError(error) => {
584            format_compile_error(
585                this,
586                sources,
587                error.span(),
588                error.kind(),
589                &mut labels,
590                &mut notes,
591            )?;
592        }
593    };
594
595    let diagnostic = d::Diagnostic::error()
596        .with_message(this.kind().try_to_string()?)
597        .with_labels(labels)
598        .with_notes(notes);
599
600    term::emit(out, config, sources, &diagnostic)?;
601    return Ok(());
602
603    fn format_compile_error(
604        this: &FatalDiagnostic,
605        sources: &Sources,
606        span: Span,
607        kind: &ErrorKind,
608        labels: &mut rust_alloc::vec::Vec<d::Label<SourceId>>,
609        notes: &mut rust_alloc::vec::Vec<rust_alloc::string::String>,
610    ) -> Result<(), EmitError> {
611        match kind {
612            ErrorKind::ImportCycle { path } => {
613                let mut it = path.iter();
614                let last = it.next_back();
615
616                for (step, entry) in (1..).zip(it) {
617                    labels.push(
618                        d::Label::secondary(entry.location.source_id, entry.location.span.range())
619                            .with_message(format!("Step #{} for `{}`", step, entry.item)),
620                    );
621                }
622
623                if let Some(entry) = last {
624                    labels.push(
625                        d::Label::secondary(entry.location.source_id, entry.location.span.range())
626                            .with_message(format!("Final step cycling back to `{}`", entry.item)),
627                    );
628                }
629            }
630            ErrorKind::NotVisible {
631                chain,
632                location: Location { source_id, span },
633                ..
634            } => {
635                for Location { source_id, span } in chain {
636                    labels.push(
637                        d::Label::secondary(*source_id, span.range())
638                            .with_message("Re-exported here"),
639                    );
640                }
641
642                labels.push(
643                    d::Label::secondary(*source_id, span.range()).with_message("defined here"),
644                );
645            }
646            ErrorKind::NotVisibleMod {
647                chain,
648                location: Location { source_id, span },
649                ..
650            } => {
651                for Location { source_id, span } in chain {
652                    labels.push(
653                        d::Label::secondary(*source_id, span.range())
654                            .with_message("Re-exported here"),
655                    );
656                }
657
658                labels.push(
659                    d::Label::secondary(*source_id, span.range())
660                        .with_message("Module defined here"),
661                );
662            }
663            ErrorKind::AmbiguousItem { locations, .. } => {
664                for (Location { source_id, span }, item) in locations {
665                    labels.push(
666                        d::Label::secondary(*source_id, span.range())
667                            .with_message(format!("Here as `{item}`")),
668                    );
669                }
670            }
671            ErrorKind::AmbiguousContextItem { infos, .. } => {
672                for info in infos.as_ref() {
673                    labels.push(
674                        d::Label::secondary(this.source_id, span.range())
675                            .with_message(format!("Could be `{info}`")),
676                    );
677                }
678            }
679            ErrorKind::DuplicateObjectKey { existing, object } => {
680                labels.push(
681                    d::Label::secondary(this.source_id(), existing.range())
682                        .with_message("Previously defined here"),
683                );
684
685                labels.push(
686                    d::Label::secondary(this.source_id(), object.range())
687                        .with_message("Object being defined here"),
688                );
689            }
690            ErrorKind::ModAlreadyLoaded { existing, .. } => {
691                let (existing_source_id, existing_span) = *existing;
692
693                labels.push(
694                    d::Label::secondary(existing_source_id, existing_span.range())
695                        .with_message("Previously loaded here"),
696                );
697            }
698            ErrorKind::ExpectedBlockSemiColon { followed_span } => {
699                labels.push(
700                    d::Label::secondary(this.source_id(), followed_span.range())
701                        .with_message("Because this immediately follows"),
702                );
703
704                let binding = sources.source(this.source_id(), span);
705
706                if let Some(binding) = binding {
707                    let mut note = String::new();
708                    writeln!(note, "Hint: Rewrite to `{binding};`")?;
709                    notes.push(note.into_std());
710                }
711            }
712            ErrorKind::VariableMoved { moved_at, .. } => {
713                labels.push(
714                    d::Label::secondary(this.source_id(), moved_at.range())
715                        .with_message("Moved here"),
716                );
717            }
718            ErrorKind::NestedTest { nested_span } => {
719                labels.push(
720                    d::Label::secondary(this.source_id(), nested_span.range())
721                        .with_message("Nested in here"),
722                );
723            }
724            ErrorKind::NestedBench { nested_span } => {
725                labels.push(
726                    d::Label::secondary(this.source_id(), nested_span.range())
727                        .with_message("Nested in here"),
728                );
729            }
730            ErrorKind::PatternMissingFields { fields, .. } => {
731                let pl = if fields.len() == 1 { "field" } else { "fields" };
732
733                let fields = fields.join(", ");
734
735                labels.push(
736                    d::Label::secondary(this.source_id(), span.range())
737                        .with_message(format!("Missing {pl}: {fields}")),
738                );
739
740                notes.push(
741                    "You can also make the pattern non-exhaustive by adding `..`"
742                        .try_to_string()?
743                        .into_std(),
744                );
745            }
746            ErrorKind::ConflictingLabels { existing, .. } => {
747                labels.push(
748                    d::Label::secondary(this.source_id(), existing.range())
749                        .with_message("Existing label here"),
750                );
751            }
752            ErrorKind::DuplicateSelectDefault { existing, .. } => {
753                labels.push(
754                    d::Label::secondary(this.source_id(), existing.range())
755                        .with_message("Existing branch here"),
756                );
757            }
758            _ => (),
759        }
760
761        Ok(())
762    }
763}