Skip to main content

wdl_engine/
diagnostics.rs

1//! Implementation of evaluation diagnostics.
2
3use std::fmt;
4
5use wdl_analysis::diagnostics::Io;
6use wdl_analysis::types::Type;
7use wdl_ast::AstToken;
8use wdl_ast::Diagnostic;
9use wdl_ast::Ident;
10use wdl_ast::Span;
11use wdl_ast::TreeToken;
12
13/// Creates an "integer not in range" diagnostic.
14pub fn integer_not_in_range(span: Span) -> Diagnostic {
15    Diagnostic::error(format!(
16        "literal integer exceeds the range for a 64-bit signed integer ({min}..={max})",
17        min = i64::MIN,
18        max = i64::MAX,
19    ))
20    .with_label("this literal integer is not in range", span)
21}
22
23/// Creates an "integer negation not in range" diagnostic.
24pub fn integer_negation_not_in_range(value: i64, span: Span) -> Diagnostic {
25    Diagnostic::error(format!(
26        "negation of integer value {value} exceeds the range for a 64-bit signed integer \
27         ({min}..={max})",
28        min = i64::MIN,
29        max = i64::MAX,
30    ))
31    .with_highlight(span)
32}
33
34/// Creates a "float not in range" diagnostic.
35pub fn float_not_in_range(span: Span) -> Diagnostic {
36    Diagnostic::error(format!(
37        "literal float exceeds the range for a 64-bit float ({min:+e}..={max:+e})",
38        min = f64::MIN,
39        max = f64::MAX,
40    ))
41    .with_label("this literal float is not in range", span)
42}
43
44/// Creates a "numeric overflow" diagnostic.
45pub fn numeric_overflow(span: Span) -> Diagnostic {
46    Diagnostic::error("evaluation of arithmetic expression resulted in overflow")
47        .with_highlight(span)
48}
49
50/// Creates a "division by zero" diagnostic.
51pub fn division_by_zero(span: Span, divisor_span: Span) -> Diagnostic {
52    Diagnostic::error("attempt to divide by zero")
53        .with_highlight(span)
54        .with_label("this expression evaluated to zero", divisor_span)
55}
56
57/// Creates a "exponent not in range" diagnostic.
58pub fn exponent_not_in_range(span: Span) -> Diagnostic {
59    Diagnostic::error(format!(
60        "exponent exceeds acceptable range ({min}..={max})",
61        min = u32::MIN,
62        max = u32::MAX,
63    ))
64    .with_label("this value exceeds the range for an exponent", span)
65}
66
67/// Creates a "runtime type mismatch" diagnostic.
68pub fn runtime_type_mismatch(
69    e: anyhow::Error,
70    expected: &Type,
71    expected_span: Span,
72    actual: &Type,
73    actual_span: Span,
74) -> Diagnostic {
75    let e = e.context(format!(
76        "type mismatch: expected type `{expected}`, but found type `{actual}`"
77    ));
78
79    Diagnostic::error(format!("{e:#}"))
80        .with_label(format!("this is type `{actual}`"), actual_span)
81        .with_label(format!("this expects type `{expected}`"), expected_span)
82}
83
84/// Creates an "if conditional mismatch" diagnostic.
85pub fn if_conditional_mismatch(e: anyhow::Error, actual: &Type, actual_span: Span) -> Diagnostic {
86    let e = e.context(format!(
87        "type mismatch: expected `if` conditional expression to be type `Boolean`, but found type \
88         `{actual}`"
89    ));
90
91    Diagnostic::error(format!("{e:#}")).with_label(format!("this is type `{actual}`"), actual_span)
92}
93
94/// Creates an "array index out of range" diagnostic.
95pub fn array_index_out_of_range(
96    index: i64,
97    count: usize,
98    span: Span,
99    target_span: Span,
100) -> Diagnostic {
101    Diagnostic::error(format!("array index {index} is out of range"))
102        .with_highlight(span)
103        .with_label(
104            if count == 0 {
105                "this array is empty".to_string()
106            } else {
107                format!(
108                    "this array has only {count} element{s}",
109                    s = if count == 1 { "" } else { "s" }
110                )
111            },
112            target_span,
113        )
114}
115
116/// Creates a "map key not found" diagnostic.
117pub fn map_key_not_found(span: Span) -> Diagnostic {
118    Diagnostic::error("the map does not contain an entry for the specified key")
119        .with_highlight(span)
120}
121
122/// Creates a "not an object member" diagnostic.
123pub fn not_an_object_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
124    Diagnostic::error(format!(
125        "object does not have a member named `{member}`",
126        member = member.text()
127    ))
128    .with_highlight(member.span())
129}
130
131/// Creates an "exponentiation requirement" diagnostic.
132pub fn exponentiation_requirement(span: Span) -> Diagnostic {
133    Diagnostic::error("use of the exponentiation operator requires WDL version 1.2")
134        .with_highlight(span)
135}
136
137/// Creates a "multi-line string requirement" diagnostic.
138pub fn multiline_string_requirement(span: Span) -> Diagnostic {
139    Diagnostic::error("use of multi-line strings requires WDL version 1.2").with_highlight(span)
140}
141
142/// Creates a "function call failed" diagnostic.
143pub fn function_call_failed(name: &str, error: impl fmt::Display, span: Span) -> Diagnostic {
144    Diagnostic::error(format!("call to function `{name}` failed: {error}")).with_highlight(span)
145}
146
147/// Creates a "input/output/declaration evaluation failed" diagnostic.
148pub fn decl_evaluation_failed(
149    e: anyhow::Error,
150    name: &str,
151    task: bool,
152    decl_name: &str,
153    io: Option<Io>,
154    span: Span,
155) -> Diagnostic {
156    let e = e.context(format!(
157        "failed to evaluate {decl_kind} `{decl_name}` for {kind} `{name}`",
158        kind = if task { "task" } else { "workflow" },
159        decl_kind = match io {
160            Some(Io::Input) => "input",
161            Some(Io::Output) => "output",
162            None => "declaration",
163        },
164    ));
165
166    Diagnostic::error(format!("{e:#}")).with_highlight(span)
167}
168
169/// Creates a "task localization failed" diagnostic.
170pub fn task_localization_failed(e: anyhow::Error, name: &str, span: Span) -> Diagnostic {
171    Diagnostic::error(format!(
172        "{e:#}",
173        e = e.context(format!("failed to localize inputs for task `{name}`"))
174    ))
175    .with_highlight(span)
176}
177
178/// Creates a "task execution failed" diagnostic.
179pub fn task_execution_failed(e: &anyhow::Error, name: &str, id: &str, span: Span) -> Diagnostic {
180    Diagnostic::error(if name != id {
181        format!("task execution failed for task `{name}` (id `{id}`): {e:#}")
182    } else {
183        format!("task execution failed for task `{name}`: {e:#}")
184    })
185    .with_label("this task failed to execute", span)
186}
187
188/// Creates an "unknown enum" diagnostic.
189pub(crate) fn unknown_enum(name: &str) -> Diagnostic {
190    Diagnostic::error(format!("unknown enum `{name}`"))
191}
192
193/// Creates an "unknown enum variant" diagnostic.
194///
195/// This is distinguished from an "unknown enum variant access" diagnostic
196/// because we don't have a span to point to that contains the supposed enum
197/// variant name.
198pub(crate) fn unknown_enum_variant(enum_name: &str, variant_name: &str) -> Diagnostic {
199    Diagnostic::error(format!(
200        "unknown variant named `{variant_name}` for enum `{enum_name}`",
201    ))
202}
203
204/// Creates an "unknown enum variant access" diagnostic.
205///
206/// This is distinguished from an "unknown enum variant" diagnostic because we
207/// have a span to point to that contains the supposed enum variant name.
208pub(crate) fn unknown_enum_variant_access<T: TreeToken>(
209    enum_name: &str,
210    variant_name: &Ident<T>,
211) -> Diagnostic {
212    Diagnostic::error(format!(
213        "unknown variant named `{variant_name}` for enum `{enum_name}`",
214        variant_name = variant_name.text()
215    ))
216    .with_label("the variant is referenced here", variant_name.span())
217}