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 "cannot call" diagnostic.
68pub fn cannot_call<T: TreeToken>(target: &Ident<T>) -> Diagnostic {
69    Diagnostic::error(format!(
70        "function `{target}` can only be called from task outputs",
71        target = target.text()
72    ))
73    .with_highlight(target.span())
74}
75
76/// Creates a "runtime type mismatch" diagnostic.
77pub fn runtime_type_mismatch(
78    e: anyhow::Error,
79    expected: &Type,
80    expected_span: Span,
81    actual: &Type,
82    actual_span: Span,
83) -> Diagnostic {
84    let e = e.context(format!(
85        "type mismatch: expected type `{expected}`, but found type `{actual}`"
86    ));
87
88    Diagnostic::error(format!("{e:#}"))
89        .with_label(format!("this is type `{actual}`"), actual_span)
90        .with_label(format!("this expects type `{expected}`"), expected_span)
91}
92
93/// Creates an "if conditional mismatch" diagnostic.
94pub fn if_conditional_mismatch(e: anyhow::Error, actual: &Type, actual_span: Span) -> Diagnostic {
95    let e = e.context(format!(
96        "type mismatch: expected `if` conditional expression to be type `Boolean`, but found type \
97         `{actual}`"
98    ));
99
100    Diagnostic::error(format!("{e:#}")).with_label(format!("this is type `{actual}`"), actual_span)
101}
102
103/// Creates an "array index out of range" diagnostic.
104pub fn array_index_out_of_range(
105    index: i64,
106    count: usize,
107    span: Span,
108    target_span: Span,
109) -> Diagnostic {
110    Diagnostic::error(format!("array index {index} is out of range"))
111        .with_highlight(span)
112        .with_label(
113            if count == 0 {
114                "this array is empty".to_string()
115            } else {
116                format!(
117                    "this array has only {count} element{s}",
118                    s = if count == 1 { "" } else { "s" }
119                )
120            },
121            target_span,
122        )
123}
124
125/// Creates a "map key not found" diagnostic.
126pub fn map_key_not_found(span: Span) -> Diagnostic {
127    Diagnostic::error("the map does not contain an entry for the specified key")
128        .with_highlight(span)
129}
130
131/// Creates a "not an object member" diagnostic.
132pub fn not_an_object_member<T: TreeToken>(member: &Ident<T>) -> Diagnostic {
133    Diagnostic::error(format!(
134        "object does not have a member named `{member}`",
135        member = member.text()
136    ))
137    .with_highlight(member.span())
138}
139
140/// Creates an "exponentiation requirement" diagnostic.
141pub fn exponentiation_requirement(span: Span) -> Diagnostic {
142    Diagnostic::error("use of the exponentiation operator requires WDL version 1.2")
143        .with_highlight(span)
144}
145
146/// Creates a "multi-line string requirement" diagnostic.
147pub fn multiline_string_requirement(span: Span) -> Diagnostic {
148    Diagnostic::error("use of multi-line strings requires WDL version 1.2").with_highlight(span)
149}
150
151/// Creates a "function call failed" diagnostic.
152pub fn function_call_failed(name: &str, error: impl fmt::Display, span: Span) -> Diagnostic {
153    Diagnostic::error(format!("call to function `{name}` failed: {error}")).with_highlight(span)
154}
155
156/// Creates a "input/output/declaration evaluation failed" diagnostic.
157pub fn decl_evaluation_failed(
158    e: anyhow::Error,
159    name: &str,
160    task: bool,
161    decl_name: &str,
162    io: Option<Io>,
163    span: Span,
164) -> Diagnostic {
165    let e = e.context(format!(
166        "failed to evaluate {decl_kind} `{decl_name}` for {kind} `{name}`",
167        kind = if task { "task" } else { "workflow" },
168        decl_kind = match io {
169            Some(Io::Input) => "input",
170            Some(Io::Output) => "output",
171            None => "declaration",
172        },
173    ));
174
175    Diagnostic::error(format!("{e:#}")).with_highlight(span)
176}
177
178/// Creates a "task localization failed" diagnostic.
179pub fn task_localization_failed(e: anyhow::Error, name: &str, span: Span) -> Diagnostic {
180    Diagnostic::error(format!(
181        "{e:#}",
182        e = e.context(format!("failed to localize inputs for task `{name}`"))
183    ))
184    .with_highlight(span)
185}
186
187/// Creates a "task execution failed" diagnostic.
188pub fn task_execution_failed(e: anyhow::Error, name: &str, id: &str, span: Span) -> Diagnostic {
189    Diagnostic::error(if name != id {
190        format!("task execution failed for task `{name}` (id `{id}`): {e:#}")
191    } else {
192        format!("task execution failed for task `{name}`: {e:#}")
193    })
194    .with_label("this task failed to execute", span)
195}