wdl_engine/
diagnostics.rs

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