Skip to main content

wdl_analysis/types/
v1.rs

1//! Type conversion helpers for a V1 AST.
2
3use std::fmt;
4use std::fmt::Write;
5use std::sync::LazyLock;
6
7use wdl_ast::AstNode;
8use wdl_ast::AstToken;
9use wdl_ast::Diagnostic;
10use wdl_ast::Ident;
11use wdl_ast::Severity;
12use wdl_ast::Span;
13use wdl_ast::SupportedVersion;
14use wdl_ast::TreeNode;
15use wdl_ast::v1;
16use wdl_ast::v1::AccessExpr;
17use wdl_ast::v1::CallExpr;
18use wdl_ast::v1::Expr;
19use wdl_ast::v1::IfExpr;
20use wdl_ast::v1::IndexExpr;
21use wdl_ast::v1::LiteralArray;
22use wdl_ast::v1::LiteralExpr;
23use wdl_ast::v1::LiteralHints;
24use wdl_ast::v1::LiteralInput;
25use wdl_ast::v1::LiteralMap;
26use wdl_ast::v1::LiteralMapItem;
27use wdl_ast::v1::LiteralObject;
28use wdl_ast::v1::LiteralOutput;
29use wdl_ast::v1::LiteralPair;
30use wdl_ast::v1::LiteralStruct;
31use wdl_ast::v1::LogicalAndExpr;
32use wdl_ast::v1::LogicalNotExpr;
33use wdl_ast::v1::LogicalOrExpr;
34use wdl_ast::v1::NegationExpr;
35use wdl_ast::v1::Placeholder;
36use wdl_ast::v1::PlaceholderOption;
37use wdl_ast::v1::StringPart;
38use wdl_ast::v1::TASK_FIELD_ATTEMPT;
39use wdl_ast::v1::TASK_FIELD_CONTAINER;
40use wdl_ast::v1::TASK_FIELD_CPU;
41use wdl_ast::v1::TASK_FIELD_DISKS;
42use wdl_ast::v1::TASK_FIELD_END_TIME;
43use wdl_ast::v1::TASK_FIELD_EXT;
44use wdl_ast::v1::TASK_FIELD_FPGA;
45use wdl_ast::v1::TASK_FIELD_GPU;
46use wdl_ast::v1::TASK_FIELD_ID;
47use wdl_ast::v1::TASK_FIELD_MAX_RETRIES;
48use wdl_ast::v1::TASK_FIELD_MEMORY;
49use wdl_ast::v1::TASK_FIELD_META;
50use wdl_ast::v1::TASK_FIELD_NAME;
51use wdl_ast::v1::TASK_FIELD_PARAMETER_META;
52use wdl_ast::v1::TASK_FIELD_PREVIOUS;
53use wdl_ast::v1::TASK_FIELD_RETURN_CODE;
54use wdl_ast::v1::TASK_HINT_CACHEABLE;
55use wdl_ast::v1::TASK_HINT_DISKS;
56use wdl_ast::v1::TASK_HINT_FPGA;
57use wdl_ast::v1::TASK_HINT_GPU;
58use wdl_ast::v1::TASK_HINT_INPUTS;
59use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL;
60use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS;
61use wdl_ast::v1::TASK_HINT_MAX_CPU;
62use wdl_ast::v1::TASK_HINT_MAX_CPU_ALIAS;
63use wdl_ast::v1::TASK_HINT_MAX_MEMORY;
64use wdl_ast::v1::TASK_HINT_MAX_MEMORY_ALIAS;
65use wdl_ast::v1::TASK_HINT_OUTPUTS;
66use wdl_ast::v1::TASK_HINT_SHORT_TASK;
67use wdl_ast::v1::TASK_HINT_SHORT_TASK_ALIAS;
68use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER;
69use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER_ALIAS;
70use wdl_ast::v1::TASK_REQUIREMENT_CPU;
71use wdl_ast::v1::TASK_REQUIREMENT_DISKS;
72use wdl_ast::v1::TASK_REQUIREMENT_FPGA;
73use wdl_ast::v1::TASK_REQUIREMENT_GPU;
74use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES;
75use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS;
76use wdl_ast::v1::TASK_REQUIREMENT_MEMORY;
77use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES;
78use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS;
79use wdl_ast::version::V1;
80use wdl_grammar::SyntaxKind;
81
82use super::ArrayType;
83use super::CompoundType;
84use super::HiddenType;
85use super::MapType;
86use super::Optional;
87use super::PairType;
88use super::PrimitiveType;
89use super::StructType;
90use super::Type;
91use super::TypeNameResolver;
92use crate::Exceptable;
93use crate::UnnecessaryFunctionCall;
94use crate::config::DiagnosticsConfig;
95use crate::diagnostics::Io;
96use crate::diagnostics::ambiguous_argument;
97use crate::diagnostics::argument_type_mismatch;
98use crate::diagnostics::cannot_access;
99use crate::diagnostics::cannot_coerce_to_string;
100use crate::diagnostics::cannot_index;
101use crate::diagnostics::comparison_mismatch;
102use crate::diagnostics::if_conditional_mismatch;
103use crate::diagnostics::index_type_mismatch;
104use crate::diagnostics::invalid_placeholder_option;
105use crate::diagnostics::invalid_regex_pattern;
106use crate::diagnostics::logical_and_mismatch;
107use crate::diagnostics::logical_not_mismatch;
108use crate::diagnostics::logical_or_mismatch;
109use crate::diagnostics::map_key_not_primitive;
110use crate::diagnostics::missing_struct_members;
111use crate::diagnostics::multiple_type_mismatch;
112use crate::diagnostics::negation_mismatch;
113use crate::diagnostics::no_common_type;
114use crate::diagnostics::not_a_pair_accessor;
115use crate::diagnostics::not_a_previous_task_data_member;
116use crate::diagnostics::not_a_struct;
117use crate::diagnostics::not_a_struct_member;
118use crate::diagnostics::not_a_task_member;
119use crate::diagnostics::not_an_enum_choice;
120use crate::diagnostics::numeric_mismatch;
121use crate::diagnostics::string_concat_mismatch;
122use crate::diagnostics::too_few_arguments;
123use crate::diagnostics::too_many_arguments;
124use crate::diagnostics::type_mismatch;
125use crate::diagnostics::unknown_call_io;
126use crate::diagnostics::unknown_function;
127use crate::diagnostics::unknown_task_io;
128use crate::diagnostics::unnecessary_function_call;
129use crate::diagnostics::unsupported_function;
130use crate::document::Task;
131use crate::stdlib::FunctionBindError;
132use crate::stdlib::MAX_PARAMETERS;
133use crate::stdlib::STDLIB;
134use crate::types::Coercible;
135use crate::types::CustomType;
136
137/// Gets the type of a `task` variable member for pre-evaluation contexts.
138///
139/// This is used in requirements, hints, and runtime sections where
140/// `task.previous` and `task.attempt` are available.
141///
142/// Returns [`None`] if the given member name is unknown.
143pub fn task_member_type_pre_evaluation(name: &str) -> Option<Type> {
144    match name {
145        TASK_FIELD_NAME | TASK_FIELD_ID => Some(PrimitiveType::String.into()),
146        TASK_FIELD_ATTEMPT => Some(PrimitiveType::Integer.into()),
147        TASK_FIELD_META | TASK_FIELD_PARAMETER_META | TASK_FIELD_EXT => Some(Type::Object),
148        TASK_FIELD_PREVIOUS => Some(Type::Hidden(HiddenType::PreviousTaskData)),
149        _ => None,
150    }
151}
152
153/// Gets the type of a `task` variable member for post-evaluation contexts.
154///
155/// This is used in command and output sections. Not all `task` fields are
156/// immediately available, however.
157///
158/// Returns [`None`] if the given member name is unknown.
159pub fn task_member_type_post_evaluation(version: SupportedVersion, name: &str) -> Option<Type> {
160    match name {
161        TASK_FIELD_NAME | TASK_FIELD_ID => Some(PrimitiveType::String.into()),
162        TASK_FIELD_CONTAINER => Some(Type::from(PrimitiveType::String).optional()),
163        TASK_FIELD_CPU => Some(PrimitiveType::Float.into()),
164        TASK_FIELD_MEMORY | TASK_FIELD_ATTEMPT | TASK_FIELD_RETURN_CODE => {
165            Some(PrimitiveType::Integer.into())
166        }
167        TASK_FIELD_GPU | TASK_FIELD_FPGA => Some(STDLIB.array_string_type().clone().into()),
168        TASK_FIELD_DISKS => Some(STDLIB.map_string_int_type().clone().into()),
169        TASK_FIELD_END_TIME => Some(Type::from(PrimitiveType::Integer).optional()),
170        TASK_FIELD_META | TASK_FIELD_PARAMETER_META | TASK_FIELD_EXT => Some(Type::Object),
171        TASK_FIELD_MAX_RETRIES if version >= SupportedVersion::V1(V1::Three) => {
172            Some(PrimitiveType::Integer.into())
173        }
174        TASK_FIELD_PREVIOUS if version >= SupportedVersion::V1(V1::Three) => {
175            Some(Type::Hidden(HiddenType::PreviousTaskData))
176        }
177        _ => None,
178    }
179}
180
181/// Gets the type of a `task.previous` member.
182///
183/// Returns [`None`] if the given member name is unknown.
184pub fn previous_task_data_member_type(name: &str) -> Option<Type> {
185    match name {
186        TASK_FIELD_MEMORY => Some(Type::from(PrimitiveType::Integer).optional()),
187        TASK_FIELD_CPU => Some(Type::from(PrimitiveType::Float).optional()),
188        TASK_FIELD_CONTAINER => Some(Type::from(PrimitiveType::String).optional()),
189        TASK_FIELD_GPU | TASK_FIELD_FPGA => {
190            Some(Type::from(STDLIB.array_string_type().clone()).optional())
191        }
192        TASK_FIELD_DISKS => Some(Type::from(STDLIB.map_string_int_type().clone()).optional()),
193        TASK_FIELD_MAX_RETRIES => Some(Type::from(PrimitiveType::Integer).optional()),
194        _ => None,
195    }
196}
197
198/// Gets the execution types of a task requirement supported by Sprocket.
199///
200/// Returns a slice of types or `None` if the given name is not a requirement.
201///
202/// Static analysis may skip type checking for requirements that are not
203/// formally typed by a particular WDL version.
204pub fn task_requirement_types(version: SupportedVersion, name: &str) -> Option<&'static [Type]> {
205    /// The types for the `container` requirement.
206    static CONTAINER_TYPES: LazyLock<Box<[Type]>> = LazyLock::new(|| {
207        Box::new([
208            PrimitiveType::String.into(),
209            STDLIB.array_string_type().clone().into(),
210        ])
211    });
212    /// The types for the `cpu` requirement.
213    const CPU_TYPES: &[Type] = &[
214        Type::Primitive(PrimitiveType::Integer, false),
215        Type::Primitive(PrimitiveType::Float, false),
216    ];
217    /// The types for the `memory` requirement.
218    const MEMORY_TYPES: &[Type] = &[
219        Type::Primitive(PrimitiveType::Integer, false),
220        Type::Primitive(PrimitiveType::String, false),
221    ];
222    /// The types for the `gpu` requirement.
223    const GPU_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Boolean, false)];
224    /// The types for the `fpga` requirement.
225    const FPGA_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Boolean, false)];
226    /// The types for the `disks` requirement.
227    static DISKS_TYPES: LazyLock<Box<[Type]>> = LazyLock::new(|| {
228        Box::new([
229            PrimitiveType::Integer.into(),
230            PrimitiveType::String.into(),
231            STDLIB.array_string_type().clone().into(),
232        ])
233    });
234    /// The types for the `max_retries` requirement.
235    const MAX_RETRIES_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Integer, false)];
236    /// The types for the `return_codes` requirement.
237    static RETURN_CODES_TYPES: LazyLock<Box<[Type]>> = LazyLock::new(|| {
238        Box::new([
239            PrimitiveType::Integer.into(),
240            PrimitiveType::String.into(),
241            STDLIB.array_int_type().clone().into(),
242        ])
243    });
244
245    match name {
246        TASK_REQUIREMENT_CONTAINER | TASK_REQUIREMENT_CONTAINER_ALIAS => Some(&CONTAINER_TYPES),
247        TASK_REQUIREMENT_CPU => Some(CPU_TYPES),
248        TASK_REQUIREMENT_DISKS => Some(&DISKS_TYPES),
249        TASK_REQUIREMENT_GPU => Some(GPU_TYPES),
250        TASK_REQUIREMENT_FPGA if version >= SupportedVersion::V1(V1::Two) => Some(FPGA_TYPES),
251        TASK_REQUIREMENT_MAX_RETRIES if version >= SupportedVersion::V1(V1::Two) => {
252            Some(MAX_RETRIES_TYPES)
253        }
254        TASK_REQUIREMENT_MAX_RETRIES_ALIAS => Some(MAX_RETRIES_TYPES),
255        TASK_REQUIREMENT_MEMORY => Some(MEMORY_TYPES),
256        TASK_REQUIREMENT_RETURN_CODES if version >= SupportedVersion::V1(V1::Two) => {
257            Some(&RETURN_CODES_TYPES)
258        }
259        TASK_REQUIREMENT_RETURN_CODES_ALIAS => Some(&RETURN_CODES_TYPES),
260        _ => None,
261    }
262}
263
264/// Gets the types of a task hint.
265///
266/// Returns a slice of types or `None` if the given name is not a reserved hint.
267pub fn task_hint_types(
268    version: SupportedVersion,
269    name: &str,
270    use_hidden_types: bool,
271) -> Option<&'static [Type]> {
272    /// The types for the `disks` hint.
273    static DISKS_TYPES: LazyLock<Box<[Type]>> = LazyLock::new(|| {
274        Box::new([
275            PrimitiveType::String.into(),
276            STDLIB.map_string_string_type().clone().into(),
277        ])
278    });
279    /// The types for the `fpga` hint.
280    const FPGA_TYPES: &[Type] = &[
281        Type::Primitive(PrimitiveType::Integer, false),
282        Type::Primitive(PrimitiveType::String, false),
283    ];
284    /// The types for the `gpu` hint.
285    const GPU_TYPES: &[Type] = &[
286        Type::Primitive(PrimitiveType::Integer, false),
287        Type::Primitive(PrimitiveType::String, false),
288    ];
289    /// The types for the `inputs` hint.
290    const INPUTS_TYPES: &[Type] = &[Type::Object];
291    /// The types for the `inputs` hint (with hidden types).
292    const INPUTS_HIDDEN_TYPES: &[Type] = &[Type::Hidden(HiddenType::Input)];
293    /// The types for the `localization_optional` hint.
294    const LOCALIZATION_OPTIONAL_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Boolean, false)];
295    /// The types for the `max_cpu` hint.
296    const MAX_CPU_TYPES: &[Type] = &[
297        Type::Primitive(PrimitiveType::Integer, false),
298        Type::Primitive(PrimitiveType::Float, false),
299    ];
300    /// The types for the `max_memory` hint.
301    const MAX_MEMORY_TYPES: &[Type] = &[
302        Type::Primitive(PrimitiveType::Integer, false),
303        Type::Primitive(PrimitiveType::String, false),
304    ];
305    /// The types for the `outputs` hint.
306    const OUTPUTS_TYPES: &[Type] = &[Type::Object];
307    /// The types for the `outputs` hint (with hidden types).
308    const OUTPUTS_HIDDEN_TYPES: &[Type] = &[Type::Hidden(HiddenType::Output)];
309    /// The types for the `short_task` hint.
310    const SHORT_TASK_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Boolean, false)];
311    /// The types for the `cacheable` hint
312    const CACHEABLE_TYPES: &[Type] = &[Type::Primitive(PrimitiveType::Boolean, false)];
313
314    match name {
315        TASK_HINT_DISKS => Some(&DISKS_TYPES),
316        TASK_HINT_FPGA if version >= SupportedVersion::V1(V1::Two) => Some(FPGA_TYPES),
317        TASK_HINT_GPU => Some(GPU_TYPES),
318        TASK_HINT_INPUTS if use_hidden_types && version >= SupportedVersion::V1(V1::Two) => {
319            Some(INPUTS_HIDDEN_TYPES)
320        }
321        TASK_HINT_INPUTS => Some(INPUTS_TYPES),
322        TASK_HINT_LOCALIZATION_OPTIONAL if version >= SupportedVersion::V1(V1::Two) => {
323            Some(LOCALIZATION_OPTIONAL_TYPES)
324        }
325        TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS => Some(LOCALIZATION_OPTIONAL_TYPES),
326        TASK_HINT_MAX_CPU if version >= SupportedVersion::V1(V1::Two) => Some(MAX_CPU_TYPES),
327        TASK_HINT_MAX_CPU_ALIAS => Some(MAX_CPU_TYPES),
328        TASK_HINT_MAX_MEMORY if version >= SupportedVersion::V1(V1::Two) => Some(MAX_MEMORY_TYPES),
329        TASK_HINT_MAX_MEMORY_ALIAS => Some(MAX_MEMORY_TYPES),
330        TASK_HINT_OUTPUTS if use_hidden_types && version >= SupportedVersion::V1(V1::Two) => {
331            Some(OUTPUTS_HIDDEN_TYPES)
332        }
333        TASK_HINT_OUTPUTS => Some(OUTPUTS_TYPES),
334        TASK_HINT_SHORT_TASK if version >= SupportedVersion::V1(V1::Two) => Some(SHORT_TASK_TYPES),
335        TASK_HINT_SHORT_TASK_ALIAS => Some(SHORT_TASK_TYPES),
336        TASK_HINT_CACHEABLE => Some(CACHEABLE_TYPES),
337        _ => None,
338    }
339}
340
341/// Represents a comparison operator.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum ComparisonOperator {
344    /// The `==` operator.
345    Equality,
346    /// The `!=` operator.
347    Inequality,
348    /// The `>` operator.
349    Less,
350    /// The `<=` operator.
351    LessEqual,
352    /// The `>` operator.
353    Greater,
354    /// The `>=` operator.
355    GreaterEqual,
356}
357
358impl fmt::Display for ComparisonOperator {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        write!(
361            f,
362            "{}",
363            match self {
364                Self::Equality => "==",
365                Self::Inequality => "!=",
366                Self::Less => "<",
367                Self::LessEqual => "<=",
368                Self::Greater => ">",
369                Self::GreaterEqual => ">=",
370            }
371        )
372    }
373}
374
375/// Represents a numeric operator.
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum NumericOperator {
378    /// The `+` operator.
379    Addition,
380    /// The `-` operator.
381    Subtraction,
382    /// The `*` operator.
383    Multiplication,
384    /// The `/` operator.
385    Division,
386    /// The `%` operator.
387    Modulo,
388    /// The `**` operator.
389    Exponentiation,
390}
391
392impl fmt::Display for NumericOperator {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        write!(
395            f,
396            "{}",
397            match self {
398                Self::Addition => "addition",
399                Self::Subtraction => "subtraction",
400                Self::Multiplication => "multiplication",
401                Self::Division => "division",
402                Self::Modulo => "remainder",
403                Self::Exponentiation => "exponentiation",
404            }
405        )
406    }
407}
408
409/// Used to convert AST types into diagnostic types.
410#[derive(Debug)]
411pub struct AstTypeConverter<R>(R);
412
413impl<R> AstTypeConverter<R>
414where
415    R: TypeNameResolver,
416{
417    /// Constructs a new AST type converter.
418    pub fn new(resolver: R) -> Self {
419        Self(resolver)
420    }
421
422    /// Converts a V1 AST type into an analysis type.
423    ///
424    /// If a type could not created, an error with the relevant diagnostic is
425    /// returned.
426    pub fn convert_type<N: TreeNode>(&mut self, ty: &v1::Type<N>) -> Result<Type, Diagnostic> {
427        let optional = ty.is_optional();
428
429        let ty: Type = match ty {
430            v1::Type::Map(ty) => {
431                let ty = self.convert_map_type(ty)?;
432                ty.into()
433            }
434            v1::Type::Array(ty) => {
435                let ty = self.convert_array_type(ty)?;
436                ty.into()
437            }
438            v1::Type::Pair(ty) => {
439                let ty = self.convert_pair_type(ty)?;
440                ty.into()
441            }
442            v1::Type::Object(_) => Type::Object,
443            v1::Type::Ref(r) => {
444                let name = r.name();
445                self.0.resolve(name.text(), name.span())?
446            }
447            v1::Type::Primitive(ty) => Type::Primitive(ty.kind().into(), false),
448        };
449
450        if optional { Ok(ty.optional()) } else { Ok(ty) }
451    }
452
453    /// Converts an AST array type to a diagnostic array type.
454    ///
455    /// If a type could not created, an error with the relevant diagnostic is
456    /// returned.
457    pub fn convert_array_type<N: TreeNode>(
458        &mut self,
459        ty: &v1::ArrayType<N>,
460    ) -> Result<ArrayType, Diagnostic> {
461        let element_type = self.convert_type(&ty.element_type())?;
462        if ty.is_non_empty() {
463            Ok(ArrayType::non_empty(element_type))
464        } else {
465            Ok(ArrayType::new(element_type))
466        }
467    }
468
469    /// Converts an AST pair type into a diagnostic pair type.
470    ///
471    /// If a type could not created, an error with the relevant diagnostic is
472    /// returned.
473    pub fn convert_pair_type<N: TreeNode>(
474        &mut self,
475        ty: &v1::PairType<N>,
476    ) -> Result<PairType, Diagnostic> {
477        let (left_type, right_type) = ty.types();
478        Ok(PairType::new(
479            self.convert_type(&left_type)?,
480            self.convert_type(&right_type)?,
481        ))
482    }
483
484    /// Creates an AST map type into a diagnostic map type.
485    ///
486    /// If a type could not created, an error with the relevant diagnostic is
487    /// returned.
488    pub fn convert_map_type<N: TreeNode>(
489        &mut self,
490        ty: &v1::MapType<N>,
491    ) -> Result<MapType, Diagnostic> {
492        let (key_type, value_type) = ty.types();
493        let key_type =
494            Type::Primitive(PrimitiveType::from(key_type.kind()), key_type.is_optional());
495
496        // The key type cannot be optional
497        if key_type.is_optional() {
498            return Err(map_key_not_primitive(ty.types().0.span(), &key_type));
499        }
500
501        Ok(MapType::new(key_type, self.convert_type(&value_type)?))
502    }
503
504    /// Converts an AST struct definition into a struct type.
505    ///
506    /// If the type could not created, an error with the relevant diagnostic is
507    /// returned.
508    pub fn convert_struct_type<N: TreeNode>(
509        &mut self,
510        definition: &v1::StructDefinition<N>,
511    ) -> Result<StructType, Diagnostic> {
512        Ok(StructType::new(
513            definition.name().text().to_string(),
514            definition
515                .members()
516                .map(|d| Ok((d.name().text().to_string(), self.convert_type(&d.ty())?)))
517                .collect::<Result<Vec<_>, Diagnostic>>()?,
518        ))
519    }
520}
521
522impl From<v1::PrimitiveTypeKind> for PrimitiveType {
523    fn from(value: v1::PrimitiveTypeKind) -> Self {
524        match value {
525            v1::PrimitiveTypeKind::Boolean => Self::Boolean,
526            v1::PrimitiveTypeKind::Integer => Self::Integer,
527            v1::PrimitiveTypeKind::Float => Self::Float,
528            v1::PrimitiveTypeKind::String => Self::String,
529            v1::PrimitiveTypeKind::File => Self::File,
530            v1::PrimitiveTypeKind::Directory => Self::Directory,
531        }
532    }
533}
534
535/// Represents context to an expression type evaluator.
536pub trait EvaluationContext {
537    /// Gets the supported version of the document being evaluated.
538    fn version(&self) -> SupportedVersion;
539
540    /// Gets the type of the given name in scope.
541    ///
542    /// - If the name is a variable, returns the type of that variable. For
543    ///   example, returns the type of `foo` in the expression `foo.bar`.
544    /// - If the name refers to a custom type, returns a type name reference to
545    ///   that custom type. For example, returns a type name reference to
546    ///   `Status` in the expression `Status.Active` (where `Status`) is an
547    ///   enum.
548    fn resolve_name(&mut self, name: &str, span: Span) -> Option<Type>;
549
550    /// Resolves a type name to a type.
551    ///
552    /// For example, returns the type of `MyStruct` in the expression `MyStruct
553    /// a = MyStruct { ... }`.
554    fn resolve_type_name(&mut self, name: &str, span: Span) -> Result<Type, Diagnostic>;
555
556    /// Gets the task associated with the evaluation context.
557    ///
558    /// This is only `Some` when evaluating a task `hints` section.
559    fn task(&self) -> Option<&Task>;
560
561    /// Gets the diagnostics configuration for the evaluation.
562    fn diagnostics_config(&self) -> DiagnosticsConfig;
563
564    /// Adds a diagnostic.
565    fn add_diagnostic(&mut self, diagnostic: Diagnostic);
566
567    /// Same as [`Self::add_diagnostic()`], but check for `except` comments
568    /// first.
569    fn exceptable_add_diagnostic<N: TreeNode + Exceptable>(
570        &mut self,
571        diagnostic: Diagnostic,
572        element: &N,
573        exceptable_nodes: &Option<&'static [SyntaxKind]>,
574    );
575}
576
577/// Represents an evaluator of expression types.
578#[derive(Debug)]
579pub struct ExprTypeEvaluator<'a, C> {
580    /// The context for the evaluator.
581    context: &'a mut C,
582    /// The nested count of placeholder evaluation.
583    ///
584    /// This is incremented immediately before a placeholder expression is
585    /// evaluated and decremented immediately after.
586    ///
587    /// If the count is non-zero, special evaluation behavior is enabled for
588    /// string interpolation.
589    placeholders: usize,
590}
591
592impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> {
593    /// Constructs a new expression type evaluator.
594    pub fn new(context: &'a mut C) -> Self {
595        Self {
596            context,
597            placeholders: 0,
598        }
599    }
600
601    /// Evaluates the type of the given expression in the given scope.
602    ///
603    /// Returns `None` if the type of the expression is indeterminate.
604    pub fn evaluate_expr<N: TreeNode + Exceptable>(&mut self, expr: &Expr<N>) -> Option<Type> {
605        match expr {
606            Expr::Literal(expr) => self.evaluate_literal_expr(expr),
607            Expr::NameRef(r) => {
608                let name = r.name();
609                self.context.resolve_name(name.text(), name.span())
610            }
611            Expr::Parenthesized(expr) => self.evaluate_expr(&expr.expr()),
612            Expr::If(expr) => self.evaluate_if_expr(expr),
613            Expr::LogicalNot(expr) => self.evaluate_logical_not_expr(expr),
614            Expr::Negation(expr) => self.evaluate_negation_expr(expr),
615            Expr::LogicalOr(expr) => self.evaluate_logical_or_expr(expr),
616            Expr::LogicalAnd(expr) => self.evaluate_logical_and_expr(expr),
617            Expr::Equality(expr) => {
618                let (lhs, rhs) = expr.operands();
619                self.evaluate_comparison_expr(ComparisonOperator::Equality, &lhs, &rhs, expr.span())
620            }
621            Expr::Inequality(expr) => {
622                let (lhs, rhs) = expr.operands();
623                self.evaluate_comparison_expr(
624                    ComparisonOperator::Inequality,
625                    &lhs,
626                    &rhs,
627                    expr.span(),
628                )
629            }
630            Expr::Less(expr) => {
631                let (lhs, rhs) = expr.operands();
632                self.evaluate_comparison_expr(ComparisonOperator::Less, &lhs, &rhs, expr.span())
633            }
634            Expr::LessEqual(expr) => {
635                let (lhs, rhs) = expr.operands();
636                self.evaluate_comparison_expr(
637                    ComparisonOperator::LessEqual,
638                    &lhs,
639                    &rhs,
640                    expr.span(),
641                )
642            }
643            Expr::Greater(expr) => {
644                let (lhs, rhs) = expr.operands();
645                self.evaluate_comparison_expr(ComparisonOperator::Greater, &lhs, &rhs, expr.span())
646            }
647            Expr::GreaterEqual(expr) => {
648                let (lhs, rhs) = expr.operands();
649                self.evaluate_comparison_expr(
650                    ComparisonOperator::GreaterEqual,
651                    &lhs,
652                    &rhs,
653                    expr.span(),
654                )
655            }
656            Expr::Addition(expr) => {
657                let (lhs, rhs) = expr.operands();
658                self.evaluate_numeric_expr(NumericOperator::Addition, expr.span(), &lhs, &rhs)
659            }
660            Expr::Subtraction(expr) => {
661                let (lhs, rhs) = expr.operands();
662                self.evaluate_numeric_expr(NumericOperator::Subtraction, expr.span(), &lhs, &rhs)
663            }
664            Expr::Multiplication(expr) => {
665                let (lhs, rhs) = expr.operands();
666                self.evaluate_numeric_expr(NumericOperator::Multiplication, expr.span(), &lhs, &rhs)
667            }
668            Expr::Division(expr) => {
669                let (lhs, rhs) = expr.operands();
670                self.evaluate_numeric_expr(NumericOperator::Division, expr.span(), &lhs, &rhs)
671            }
672            Expr::Modulo(expr) => {
673                let (lhs, rhs) = expr.operands();
674                self.evaluate_numeric_expr(NumericOperator::Modulo, expr.span(), &lhs, &rhs)
675            }
676            Expr::Exponentiation(expr) => {
677                let (lhs, rhs) = expr.operands();
678                self.evaluate_numeric_expr(NumericOperator::Exponentiation, expr.span(), &lhs, &rhs)
679            }
680            Expr::Call(expr) => self.evaluate_call_expr(expr),
681            Expr::Index(expr) => self.evaluate_index_expr(expr),
682            Expr::Access(expr) => self.evaluate_access_expr(expr),
683        }
684    }
685
686    /// Evaluates the type of a literal expression.
687    fn evaluate_literal_expr<N: TreeNode + Exceptable>(
688        &mut self,
689        expr: &LiteralExpr<N>,
690    ) -> Option<Type> {
691        match expr {
692            LiteralExpr::Boolean(_) => Some(PrimitiveType::Boolean.into()),
693            LiteralExpr::Integer(_) => Some(PrimitiveType::Integer.into()),
694            LiteralExpr::Float(_) => Some(PrimitiveType::Float.into()),
695            LiteralExpr::String(s) => {
696                for p in s.parts() {
697                    if let StringPart::Placeholder(p) = p {
698                        self.check_placeholder(&p);
699                    }
700                }
701
702                Some(PrimitiveType::String.into())
703            }
704            LiteralExpr::Array(expr) => Some(self.evaluate_literal_array(expr)),
705            LiteralExpr::Pair(expr) => Some(self.evaluate_literal_pair(expr)),
706            LiteralExpr::Map(expr) => Some(self.evaluate_literal_map(expr)),
707            LiteralExpr::Object(expr) => Some(self.evaluate_literal_object(expr)),
708            LiteralExpr::Struct(expr) => self.evaluate_literal_struct(expr),
709            LiteralExpr::None(_) => Some(Type::None),
710            LiteralExpr::Hints(expr) => self.evaluate_literal_hints(expr),
711            LiteralExpr::Input(expr) => self.evaluate_literal_input(expr),
712            LiteralExpr::Output(expr) => self.evaluate_literal_output(expr),
713        }
714    }
715
716    /// Checks a placeholder expression.
717    pub(crate) fn check_placeholder<N: TreeNode + Exceptable>(
718        &mut self,
719        placeholder: &Placeholder<N>,
720    ) {
721        self.placeholders += 1;
722
723        // Evaluate the placeholder expression and check that the resulting type is
724        // coercible to string for interpolation
725        let expr = placeholder.expr();
726        if let Some(ty) = self.evaluate_expr(&expr) {
727            if let Some(option) = placeholder.option() {
728                let valid = match option {
729                    PlaceholderOption::Sep(_) => {
730                        ty == Type::Union
731                            || ty == Type::None
732                            || matches!(&ty,
733                        Type::Compound(CompoundType::Array(array_ty), _)
734                        if matches!(array_ty.element_type(), Type::Primitive(_, false) | Type::Union))
735                    }
736                    PlaceholderOption::Default(_) => {
737                        matches!(ty, Type::Primitive(..) | Type::Union | Type::None)
738                    }
739                    PlaceholderOption::TrueFalse(_) => {
740                        matches!(
741                            ty,
742                            Type::Primitive(PrimitiveType::Boolean, _) | Type::Union | Type::None
743                        )
744                    }
745                };
746
747                if !valid {
748                    self.context.add_diagnostic(invalid_placeholder_option(
749                        &ty,
750                        expr.span(),
751                        &option,
752                    ));
753                }
754            } else {
755                match ty {
756                    Type::Primitive(..)
757                    | Type::Union
758                    | Type::None
759                    | Type::Compound(CompoundType::Custom(CustomType::Enum(_)), _) => {}
760                    _ => {
761                        self.context
762                            .add_diagnostic(cannot_coerce_to_string(&ty, expr.span()));
763                    }
764                }
765            }
766        }
767
768        self.placeholders -= 1;
769    }
770
771    /// Evaluates the type of a literal array expression.
772    fn evaluate_literal_array<N: TreeNode + Exceptable>(&mut self, expr: &LiteralArray<N>) -> Type {
773        // Look at the first array element to determine the element type
774        // The remaining elements must have a common type
775        let mut elements = expr.elements();
776        match elements
777            .next()
778            .and_then(|e| Some((self.evaluate_expr(&e)?, e.span())))
779        {
780            Some((mut expected, mut expected_span)) => {
781                // Ensure the remaining element types share a common type
782                for expr in elements {
783                    if let Some(actual) = self.evaluate_expr(&expr) {
784                        match expected.common_type(&actual) {
785                            Some(ty) => {
786                                expected = ty;
787                                expected_span = expr.span();
788                            }
789                            _ => {
790                                self.context.add_diagnostic(no_common_type(
791                                    &expected,
792                                    expected_span,
793                                    &actual,
794                                    expr.span(),
795                                ));
796                            }
797                        }
798                    }
799                }
800
801                ArrayType::new(expected).into()
802            }
803            // Treat empty array as `Array[Union]`
804            None => ArrayType::new(Type::Union).into(),
805        }
806    }
807
808    /// Evaluates the type of a literal pair expression.
809    fn evaluate_literal_pair<N: TreeNode + Exceptable>(&mut self, expr: &LiteralPair<N>) -> Type {
810        let (left, right) = expr.exprs();
811        let left = self.evaluate_expr(&left).unwrap_or(Type::Union);
812        let right = self.evaluate_expr(&right).unwrap_or(Type::Union);
813        PairType::new(left, right).into()
814    }
815
816    /// Evaluates the type of a literal map expression.
817    fn evaluate_literal_map<N: TreeNode + Exceptable>(&mut self, expr: &LiteralMap<N>) -> Type {
818        let map_item_type = |item: LiteralMapItem<N>| {
819            let (key, value) = item.key_value();
820            let expected_key = self.evaluate_expr(&key)?;
821            match expected_key {
822                Type::Primitive(_, false) | Type::Union => {
823                    // OK
824                }
825                _ => {
826                    self.context
827                        .add_diagnostic(map_key_not_primitive(key.span(), &expected_key));
828                    return None;
829                }
830            }
831
832            Some((
833                expected_key,
834                key.span(),
835                self.evaluate_expr(&value)?,
836                value.span(),
837            ))
838        };
839
840        let mut items = expr.items();
841        match items.next().and_then(map_item_type) {
842            Some((
843                mut expected_key,
844                mut expected_key_span,
845                mut expected_value,
846                mut expected_value_span,
847            )) => {
848                // Ensure the remaining items types share common types
849                for item in items {
850                    let (key, value) = item.key_value();
851                    if let Some(actual_key) = self.evaluate_expr(&key)
852                        && let Some(actual_value) = self.evaluate_expr(&value)
853                    {
854                        // The key must be a non-optional primitive type or union
855                        match actual_key {
856                            Type::Primitive(_, false) | Type::Union => {
857                                match expected_key.common_type(&actual_key) {
858                                    Some(ty) => {
859                                        expected_key = ty;
860                                        expected_key_span = key.span();
861                                    }
862                                    _ => {
863                                        self.context.add_diagnostic(no_common_type(
864                                            &expected_key,
865                                            expected_key_span,
866                                            &actual_key,
867                                            key.span(),
868                                        ));
869                                    }
870                                }
871                            }
872                            _ => {
873                                self.context
874                                    .add_diagnostic(map_key_not_primitive(key.span(), &actual_key));
875                            }
876                        }
877
878                        match expected_value.common_type(&actual_value) {
879                            Some(ty) => {
880                                expected_value = ty;
881                                expected_value_span = value.span();
882                            }
883                            _ => {
884                                self.context.add_diagnostic(no_common_type(
885                                    &expected_value,
886                                    expected_value_span,
887                                    &actual_value,
888                                    value.span(),
889                                ));
890                            }
891                        }
892                    }
893                }
894
895                MapType::new(expected_key, expected_value).into()
896            }
897            // Treat as `Map[Union, Union]`
898            None => MapType::new(Type::Union, Type::Union).into(),
899        }
900    }
901
902    /// Evaluates the type of a literal object expression.
903    fn evaluate_literal_object<N: TreeNode + Exceptable>(
904        &mut self,
905        expr: &LiteralObject<N>,
906    ) -> Type {
907        // Validate the member expressions
908        for item in expr.items() {
909            let (_, v) = item.name_value();
910            self.evaluate_expr(&v);
911        }
912
913        Type::Object
914    }
915
916    /// Evaluates the type of a literal struct expression.
917    fn evaluate_literal_struct<N: TreeNode + Exceptable>(
918        &mut self,
919        expr: &LiteralStruct<N>,
920    ) -> Option<Type> {
921        let name = expr.name();
922        match self.context.resolve_type_name(name.text(), name.span()) {
923            Ok(ty) => {
924                let ty = match &ty {
925                    Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), false) => ty,
926                    _ => panic!("type should be a required struct"),
927                };
928
929                // Keep track of which members are present in the expression
930                let mut present = vec![false; ty.members().len()];
931
932                // Validate the member types
933                for item in expr.items() {
934                    let (n, v) = item.name_value();
935                    match ty.members().get_full(n.text()) {
936                        Some((index, _, expected)) => {
937                            present[index] = true;
938                            if let Some(actual) = self.evaluate_expr(&v)
939                                && !actual.is_coercible_to(expected)
940                            {
941                                self.context.add_diagnostic(type_mismatch(
942                                    expected,
943                                    n.span(),
944                                    &actual,
945                                    v.span(),
946                                ));
947                            }
948                        }
949                        _ => {
950                            // Not a struct member
951                            self.context
952                                .add_diagnostic(not_a_struct_member(name.text(), &n));
953                        }
954                    }
955                }
956
957                // Find the first unspecified member that is required, if any
958                let mut unspecified = present
959                    .iter()
960                    .enumerate()
961                    .filter_map(|(i, present)| {
962                        if *present {
963                            return None;
964                        }
965
966                        let (name, member_ty) = ty.members().get_index(i).unwrap();
967                        if member_ty.is_optional() {
968                            return None;
969                        }
970
971                        Some(name.as_str())
972                    })
973                    .peekable();
974
975                if unspecified.peek().is_some() {
976                    let mut members = String::new();
977                    let mut count = 0;
978                    while let Some(member) = unspecified.next() {
979                        match (unspecified.peek().is_none(), count) {
980                            (true, c) if c > 1 => members.push_str(", and "),
981                            (true, 1) => members.push_str(" and "),
982                            (false, c) if c > 0 => members.push_str(", "),
983                            _ => {}
984                        }
985
986                        write!(&mut members, "`{member}`").ok();
987                        count += 1;
988                    }
989
990                    self.context
991                        .add_diagnostic(missing_struct_members(&name, count, &members));
992                }
993
994                Some(Type::Compound(
995                    CompoundType::Custom(CustomType::Struct(ty.clone())),
996                    false,
997                ))
998            }
999            Err(diagnostic) => {
1000                self.context.add_diagnostic(diagnostic);
1001                None
1002            }
1003        }
1004    }
1005
1006    /// Evaluates a `runtime` section item.
1007    pub(crate) fn evaluate_runtime_item<N: TreeNode + Exceptable>(
1008        &mut self,
1009        name: &Ident<N::Token>,
1010        expr: &Expr<N>,
1011    ) {
1012        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
1013
1014        // The `cpu`, `gpu`, `disks`, `maxRetries`, and `returnCodes` keys are not
1015        // formally typed until WDL 1.1 (see the WDL 1.0 specification's runtime
1016        // section, which only gives recommended conventions for `docker` and
1017        // `memory`). Some of these names are shared with differently-typed
1018        // `hints` keys (e.g. `gpu` and `disks`), so simply letting the
1019        // `task_requirement_types` lookup fail for WDL 1.0 documents isn't
1020        // sufficient; doing so would incorrectly fall through to checking
1021        // against the `hints` types below. Instead, skip type checking for
1022        // these keys entirely when the document version is older than 1.1.
1023        // See https://github.com/stjude-rust-labs/sprocket/issues/811.
1024        if self.context.version() < SupportedVersion::V1(V1::One)
1025            && matches!(
1026                name.text(),
1027                TASK_REQUIREMENT_CPU
1028                    | TASK_REQUIREMENT_GPU
1029                    | TASK_REQUIREMENT_DISKS
1030                    | TASK_REQUIREMENT_MAX_RETRIES_ALIAS
1031                    | TASK_REQUIREMENT_RETURN_CODES_ALIAS
1032            )
1033        {
1034            return;
1035        }
1036
1037        if !self.evaluate_requirement(name, expr, &expr_ty) {
1038            // Always use object types for `runtime` section `inputs` and `outputs` keys as
1039            // only `hints` sections can use input/output hidden types
1040            if let Some(expected) = task_hint_types(self.context.version(), name.text(), false)
1041                && !expected
1042                    .iter()
1043                    .any(|target| expr_ty.is_coercible_to(target))
1044            {
1045                self.context.add_diagnostic(multiple_type_mismatch(
1046                    expected,
1047                    name.span(),
1048                    &expr_ty,
1049                    expr.span(),
1050                ));
1051            }
1052        }
1053    }
1054
1055    /// Evaluates a `requirements` section item.
1056    pub(crate) fn evaluate_requirements_item<N: TreeNode + Exceptable>(
1057        &mut self,
1058        name: &Ident<N::Token>,
1059        expr: &Expr<N>,
1060    ) {
1061        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
1062        self.evaluate_requirement(name, expr, &expr_ty);
1063    }
1064
1065    /// Evaluates a requirement in either a `requirements` section or a legacy
1066    /// `runtime` section.
1067    ///
1068    /// Returns `true` if the name matched a requirement or `false` if it did
1069    /// not.
1070    fn evaluate_requirement<N: TreeNode>(
1071        &mut self,
1072        name: &Ident<N::Token>,
1073        expr: &Expr<N>,
1074        expr_ty: &Type,
1075    ) -> bool {
1076        if let Some(expected) = task_requirement_types(self.context.version(), name.text()) {
1077            if !expected
1078                .iter()
1079                .any(|target| expr_ty.is_coercible_to(target))
1080            {
1081                self.context.add_diagnostic(multiple_type_mismatch(
1082                    expected,
1083                    name.span(),
1084                    expr_ty,
1085                    expr.span(),
1086                ));
1087            }
1088
1089            return true;
1090        }
1091
1092        false
1093    }
1094
1095    /// Evaluates the type of a literal hints expression.
1096    fn evaluate_literal_hints<N: TreeNode + Exceptable>(
1097        &mut self,
1098        expr: &LiteralHints<N>,
1099    ) -> Option<Type> {
1100        self.context.task()?;
1101
1102        for item in expr.items() {
1103            self.evaluate_hints_item(&item.name(), &item.expr())
1104        }
1105
1106        Some(Type::Hidden(HiddenType::Hints))
1107    }
1108
1109    /// Evaluates a hints item, whether in task `hints` section or a `hints`
1110    /// literal expression.
1111    pub(crate) fn evaluate_hints_item<N: TreeNode + Exceptable>(
1112        &mut self,
1113        name: &Ident<N::Token>,
1114        expr: &Expr<N>,
1115    ) {
1116        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
1117        if let Some(expected) = task_hint_types(self.context.version(), name.text(), true)
1118            && !expected
1119                .iter()
1120                .any(|target| expr_ty.is_coercible_to(target))
1121        {
1122            self.context.add_diagnostic(multiple_type_mismatch(
1123                expected,
1124                name.span(),
1125                &expr_ty,
1126                expr.span(),
1127            ));
1128        }
1129    }
1130
1131    /// Evaluates the type of a literal input expression.
1132    fn evaluate_literal_input<N: TreeNode + Exceptable>(
1133        &mut self,
1134        expr: &LiteralInput<N>,
1135    ) -> Option<Type> {
1136        // Check to see if inputs literals are supported in the evaluation scope
1137        self.context.task()?;
1138
1139        // Evaluate the items of the literal
1140        for item in expr.items() {
1141            self.evaluate_literal_io_item(item.names(), item.expr(), Io::Input);
1142        }
1143
1144        Some(Type::Hidden(HiddenType::Input))
1145    }
1146
1147    /// Evaluates the type of a literal output expression.
1148    fn evaluate_literal_output<N: TreeNode + Exceptable>(
1149        &mut self,
1150        expr: &LiteralOutput<N>,
1151    ) -> Option<Type> {
1152        // Check to see if output literals are supported in the evaluation scope
1153        self.context.task()?;
1154
1155        // Evaluate the items of the literal
1156        for item in expr.items() {
1157            self.evaluate_literal_io_item(item.names(), item.expr(), Io::Output);
1158        }
1159
1160        Some(Type::Hidden(HiddenType::Output))
1161    }
1162
1163    /// Evaluates a literal input/output item.
1164    fn evaluate_literal_io_item<N: TreeNode + Exceptable>(
1165        &mut self,
1166        names: impl Iterator<Item = Ident<N::Token>>,
1167        expr: Expr<N>,
1168        io: Io,
1169    ) {
1170        let mut names = names.enumerate().peekable();
1171        let expr_ty = self.evaluate_expr(&expr).unwrap_or(Type::Union);
1172
1173        // The first name should be an input/output and then the remainder should be a
1174        // struct member
1175        let mut span = None;
1176        let mut s: Option<&StructType> = None;
1177        while let Some((i, name)) = names.next() {
1178            // The first name is an input or an output
1179            let ty = if i == 0 {
1180                span = Some(name.span());
1181
1182                match if io == Io::Input {
1183                    self.context
1184                        .task()
1185                        .expect("should have task")
1186                        .inputs()
1187                        .get(name.text())
1188                        .map(|i| i.ty())
1189                } else {
1190                    self.context
1191                        .task()
1192                        .expect("should have task")
1193                        .outputs()
1194                        .get(name.text())
1195                        .map(|o| o.ty())
1196                } {
1197                    Some(ty) => ty,
1198                    None => {
1199                        self.context.add_diagnostic(unknown_task_io(
1200                            self.context.task().expect("should have task").name(),
1201                            &name,
1202                            io,
1203                        ));
1204                        break;
1205                    }
1206                }
1207            } else {
1208                // Every other name is a struct member
1209                let start = span.unwrap().start();
1210                span = Some(Span::new(start, name.span().end() - start));
1211                let s = s.unwrap();
1212                match s.members().get(name.text()) {
1213                    Some(ty) => ty,
1214                    None => {
1215                        self.context
1216                            .add_diagnostic(not_a_struct_member(s.name(), &name));
1217                        break;
1218                    }
1219                }
1220            };
1221
1222            match ty {
1223                Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), _) => s = Some(ty),
1224                _ if names.peek().is_some() => {
1225                    self.context.add_diagnostic(not_a_struct(&name, i == 0));
1226                    break;
1227                }
1228                _ => {
1229                    // It's ok for the last one to not name a struct
1230                }
1231            }
1232        }
1233
1234        // If we bailed out early above, calculate the entire span of the name
1235        if let Some((_, last)) = names.last() {
1236            let start = span.unwrap().start();
1237            span = Some(Span::new(start, last.span().end() - start));
1238        }
1239
1240        // The type of every item should be `hints`
1241        if !expr_ty.is_coercible_to(&Type::Hidden(HiddenType::Hints)) {
1242            self.context.add_diagnostic(type_mismatch(
1243                &Type::Hidden(HiddenType::Hints),
1244                span.expect("should have span"),
1245                &expr_ty,
1246                expr.span(),
1247            ));
1248        }
1249    }
1250
1251    /// Evaluates the type of an `if` expression.
1252    fn evaluate_if_expr<N: TreeNode + Exceptable>(&mut self, expr: &IfExpr<N>) -> Option<Type> {
1253        let (cond_expr, true_expr, false_expr) = expr.exprs();
1254
1255        // The conditional should be a boolean
1256        let cond_ty = self.evaluate_expr(&cond_expr).unwrap_or(Type::Union);
1257        if !cond_ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1258            self.context
1259                .add_diagnostic(if_conditional_mismatch(&cond_ty, cond_expr.span()));
1260        }
1261
1262        // Check that the two expressions have the same type
1263        let true_ty = self.evaluate_expr(&true_expr).unwrap_or(Type::Union);
1264        let false_ty = self.evaluate_expr(&false_expr).unwrap_or(Type::Union);
1265
1266        match (true_ty, false_ty) {
1267            (Type::Union, Type::Union) => None,
1268            (Type::Union, false_ty) => Some(false_ty),
1269            (true_ty, Type::Union) => Some(true_ty),
1270            (true_ty, false_ty) => match true_ty.common_type(&false_ty) {
1271                Some(ty) => Some(ty),
1272                _ => {
1273                    self.context.add_diagnostic(type_mismatch(
1274                        &true_ty,
1275                        true_expr.span(),
1276                        &false_ty,
1277                        false_expr.span(),
1278                    ));
1279
1280                    None
1281                }
1282            },
1283        }
1284    }
1285
1286    /// Evaluates the type of a `logical not` expression.
1287    fn evaluate_logical_not_expr<N: TreeNode + Exceptable>(
1288        &mut self,
1289        expr: &LogicalNotExpr<N>,
1290    ) -> Option<Type> {
1291        // The operand should be a boolean
1292        let operand = expr.operand();
1293        let ty = self.evaluate_expr(&operand).unwrap_or(Type::Union);
1294        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1295            self.context
1296                .add_diagnostic(logical_not_mismatch(&ty, operand.span()));
1297        }
1298
1299        Some(PrimitiveType::Boolean.into())
1300    }
1301
1302    /// Evaluates the type of a negation expression.
1303    fn evaluate_negation_expr<N: TreeNode + Exceptable>(
1304        &mut self,
1305        expr: &NegationExpr<N>,
1306    ) -> Option<Type> {
1307        // The operand should be a int or float
1308        let operand = expr.operand();
1309        let ty = self.evaluate_expr(&operand)?;
1310
1311        // If the type is `Int`, treat it as `Int`
1312        // This is checked first as `Int` is coercible to `Float`
1313        if ty.eq(&PrimitiveType::Integer.into()) {
1314            return Some(PrimitiveType::Integer.into());
1315        }
1316
1317        if !ty.is_coercible_to(&PrimitiveType::Float.into()) {
1318            self.context
1319                .add_diagnostic(negation_mismatch(&ty, operand.span()));
1320            // Type is indeterminate as the expression may evaluate to more than one type
1321            return None;
1322        }
1323
1324        Some(PrimitiveType::Float.into())
1325    }
1326
1327    /// Evaluates the type of a `logical or` expression.
1328    fn evaluate_logical_or_expr<N: TreeNode + Exceptable>(
1329        &mut self,
1330        expr: &LogicalOrExpr<N>,
1331    ) -> Option<Type> {
1332        // Both operands should be booleans
1333        let (lhs, rhs) = expr.operands();
1334
1335        let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union);
1336        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1337            self.context
1338                .add_diagnostic(logical_or_mismatch(&ty, lhs.span()));
1339        }
1340
1341        let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union);
1342        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1343            self.context
1344                .add_diagnostic(logical_or_mismatch(&ty, rhs.span()));
1345        }
1346
1347        Some(PrimitiveType::Boolean.into())
1348    }
1349
1350    /// Evaluates the type of a `logical and` expression.
1351    fn evaluate_logical_and_expr<N: TreeNode + Exceptable>(
1352        &mut self,
1353        expr: &LogicalAndExpr<N>,
1354    ) -> Option<Type> {
1355        // Both operands should be booleans
1356        let (lhs, rhs) = expr.operands();
1357
1358        let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union);
1359        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1360            self.context
1361                .add_diagnostic(logical_and_mismatch(&ty, lhs.span()));
1362        }
1363
1364        let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union);
1365        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1366            self.context
1367                .add_diagnostic(logical_and_mismatch(&ty, rhs.span()));
1368        }
1369
1370        Some(PrimitiveType::Boolean.into())
1371    }
1372
1373    /// Evaluates the type of a comparison expression.
1374    fn evaluate_comparison_expr<N: TreeNode + Exceptable>(
1375        &mut self,
1376        op: ComparisonOperator,
1377        lhs: &Expr<N>,
1378        rhs: &Expr<N>,
1379        span: Span,
1380    ) -> Option<Type> {
1381        let lhs_ty = self.evaluate_expr(lhs).unwrap_or(Type::Union);
1382        let rhs_ty = self.evaluate_expr(rhs).unwrap_or(Type::Union);
1383
1384        // Check for comparison to `None` or `Union` and allow it
1385        if lhs_ty.is_union() || lhs_ty.is_none() || rhs_ty.is_union() || rhs_ty.is_none() {
1386            return Some(PrimitiveType::Boolean.into());
1387        }
1388
1389        // Check LHS and RHS for being coercible to one of the supported primitive types
1390        for expected in [
1391            Type::from(PrimitiveType::Boolean),
1392            PrimitiveType::Integer.into(),
1393            PrimitiveType::Float.into(),
1394            PrimitiveType::String.into(),
1395            PrimitiveType::File.into(),
1396            PrimitiveType::Directory.into(),
1397        ] {
1398            // Only support equality/inequality comparisons for `File` and `Directory`
1399            if op != ComparisonOperator::Equality
1400                && op != ComparisonOperator::Inequality
1401                && (matches!(
1402                    lhs_ty.as_primitive(),
1403                    Some(PrimitiveType::File) | Some(PrimitiveType::Directory)
1404                ) || matches!(
1405                    rhs_ty.as_primitive(),
1406                    Some(PrimitiveType::File) | Some(PrimitiveType::Directory)
1407                ))
1408            {
1409                continue;
1410            }
1411
1412            if lhs_ty.is_coercible_to(&expected) && rhs_ty.is_coercible_to(&expected) {
1413                return Some(PrimitiveType::Boolean.into());
1414            }
1415
1416            let expected = expected.optional();
1417            if lhs_ty.is_coercible_to(&expected) && rhs_ty.is_coercible_to(&expected) {
1418                return Some(PrimitiveType::Boolean.into());
1419            }
1420        }
1421
1422        // For equality comparisons, check LHS and RHS being object and compound types
1423        if op == ComparisonOperator::Equality || op == ComparisonOperator::Inequality {
1424            // Check for object
1425            if (lhs_ty.is_coercible_to(&Type::Object) && rhs_ty.is_coercible_to(&Type::Object))
1426                || (lhs_ty.is_coercible_to(&Type::OptionalObject)
1427                    && rhs_ty.is_coercible_to(&Type::OptionalObject))
1428            {
1429                return Some(PrimitiveType::Boolean.into());
1430            }
1431
1432            // Check for other compound types
1433            let equal = match (&lhs_ty, &rhs_ty) {
1434                (
1435                    Type::Compound(CompoundType::Array(a), _),
1436                    Type::Compound(CompoundType::Array(b), _),
1437                ) => a == b,
1438                (
1439                    Type::Compound(CompoundType::Pair(a), _),
1440                    Type::Compound(CompoundType::Pair(b), _),
1441                ) => a == b,
1442                (
1443                    Type::Compound(CompoundType::Map(a), _),
1444                    Type::Compound(CompoundType::Map(b), _),
1445                ) => a == b,
1446                (
1447                    Type::Compound(CompoundType::Custom(CustomType::Struct(a)), _),
1448                    Type::Compound(CompoundType::Custom(CustomType::Struct(b)), _),
1449                ) => a == b,
1450                (
1451                    Type::Compound(CompoundType::Custom(CustomType::Enum(a)), _),
1452                    Type::Compound(CompoundType::Custom(CustomType::Enum(b)), _),
1453                ) => a == b,
1454                _ => false,
1455            };
1456
1457            if equal {
1458                return Some(PrimitiveType::Boolean.into());
1459            }
1460        }
1461
1462        // A type mismatch at this point
1463        self.context.add_diagnostic(comparison_mismatch(
1464            op,
1465            span,
1466            &lhs_ty,
1467            lhs.span(),
1468            &rhs_ty,
1469            rhs.span(),
1470        ));
1471        Some(PrimitiveType::Boolean.into())
1472    }
1473
1474    /// Evaluates the type of a numeric expression.
1475    fn evaluate_numeric_expr<N: TreeNode + Exceptable>(
1476        &mut self,
1477        op: NumericOperator,
1478        span: Span,
1479        lhs: &Expr<N>,
1480        rhs: &Expr<N>,
1481    ) -> Option<Type> {
1482        let lhs_ty = self.evaluate_expr(lhs).unwrap_or(Type::Union);
1483        let rhs_ty = self.evaluate_expr(rhs).unwrap_or(Type::Union);
1484
1485        // If both sides are `Int`, the result is `Int`
1486        if lhs_ty.eq(&PrimitiveType::Integer.into()) && rhs_ty.eq(&PrimitiveType::Integer.into()) {
1487            return Some(PrimitiveType::Integer.into());
1488        }
1489
1490        // If both sides are coercible to `Float`, the result is `Float`
1491        if !lhs_ty.is_union()
1492            && lhs_ty.is_coercible_to(&PrimitiveType::Float.into())
1493            && !rhs_ty.is_union()
1494            && rhs_ty.is_coercible_to(&PrimitiveType::Float.into())
1495        {
1496            return Some(PrimitiveType::Float.into());
1497        }
1498
1499        // For addition, also support `String` on one or both sides of any primitive
1500        // type that isn't `Boolean`; in placeholder expressions, allow the
1501        // other side to also be optional
1502        if op == NumericOperator::Addition {
1503            let allow_optional = self.placeholders > 0;
1504            let other = if (!lhs_ty.is_optional() || allow_optional)
1505                && lhs_ty
1506                    .as_primitive()
1507                    .map(|p| p == PrimitiveType::String)
1508                    .unwrap_or(false)
1509            {
1510                Some((lhs_ty.is_optional(), &rhs_ty, rhs.span()))
1511            } else if (!rhs_ty.is_optional() || allow_optional)
1512                && rhs_ty
1513                    .as_primitive()
1514                    .map(|p| p == PrimitiveType::String)
1515                    .unwrap_or(false)
1516            {
1517                Some((rhs_ty.is_optional(), &lhs_ty, lhs.span()))
1518            } else {
1519                None
1520            };
1521
1522            if let Some((optional, other, span)) = other {
1523                if (!other.is_optional() || allow_optional)
1524                    && other
1525                        .as_primitive()
1526                        .map(|p| p != PrimitiveType::Boolean)
1527                        .unwrap_or(other.is_union() || (allow_optional && other.is_none()))
1528                {
1529                    let ty: Type = PrimitiveType::String.into();
1530                    if optional || other.is_optional() {
1531                        return Some(ty.optional());
1532                    }
1533
1534                    return Some(ty);
1535                }
1536
1537                self.context
1538                    .add_diagnostic(string_concat_mismatch(other, span));
1539                return None;
1540            }
1541        }
1542
1543        if !lhs_ty.is_union() && !rhs_ty.is_union() {
1544            self.context.add_diagnostic(numeric_mismatch(
1545                op,
1546                span,
1547                &lhs_ty,
1548                lhs.span(),
1549                &rhs_ty,
1550                rhs.span(),
1551            ));
1552        }
1553
1554        None
1555    }
1556
1557    /// Evaluates the type of a call expression.
1558    fn evaluate_call_expr<N: TreeNode + Exceptable>(&mut self, expr: &CallExpr<N>) -> Option<Type> {
1559        let target = expr.target();
1560        let Some(f) = STDLIB.function(target.text()) else {
1561            self.context
1562                .add_diagnostic(unknown_function(target.text(), target.span()));
1563            return None;
1564        };
1565
1566        // Evaluate the argument expressions
1567        let mut count = 0;
1568        let mut arguments = [const { Type::Union }; MAX_PARAMETERS];
1569
1570        for arg in expr.arguments() {
1571            if count < MAX_PARAMETERS {
1572                arguments[count] = self.evaluate_expr(&arg).unwrap_or(Type::Union);
1573            }
1574
1575            count += 1;
1576        }
1577
1578        match target.text() {
1579            "find" | "matches" | "sub" => {
1580                // above function expect the pattern as 2nd argument
1581                if let Some(Expr::Literal(LiteralExpr::String(pattern_literal))) =
1582                    expr.arguments().nth(1)
1583                    && let Some(value) = pattern_literal.text()
1584                {
1585                    let pattern = value.text().to_string();
1586                    if let Err(e) = regex::Regex::new(&pattern) {
1587                        self.context.add_diagnostic(invalid_regex_pattern(
1588                            target.text(),
1589                            value.text(),
1590                            &e,
1591                            pattern_literal.span(),
1592                        ));
1593                    }
1594                }
1595            }
1596            _ => {}
1597        }
1598
1599        let arguments = &arguments[..count.min(MAX_PARAMETERS)];
1600        if count <= MAX_PARAMETERS {
1601            match f.bind(self.context.version(), arguments) {
1602                Ok(binding) => {
1603                    if let Some(severity) =
1604                        self.context.diagnostics_config().unnecessary_function_call
1605                    {
1606                        self.check_unnecessary_call(expr, arguments, severity);
1607                    }
1608                    return Some(binding.return_type().clone());
1609                }
1610                Err(FunctionBindError::RequiresVersion(minimum)) => {
1611                    self.context.add_diagnostic(unsupported_function(
1612                        minimum,
1613                        target.text(),
1614                        target.span(),
1615                    ));
1616                }
1617                Err(FunctionBindError::TooFewArguments(minimum)) => {
1618                    self.context.add_diagnostic(too_few_arguments(
1619                        target.text(),
1620                        target.span(),
1621                        minimum,
1622                        count,
1623                    ));
1624                }
1625                Err(FunctionBindError::TooManyArguments(maximum)) => {
1626                    self.context.add_diagnostic(too_many_arguments(
1627                        target.text(),
1628                        target.span(),
1629                        maximum,
1630                        count,
1631                        expr.arguments().skip(maximum).map(|e| e.span()),
1632                    ));
1633                }
1634                Err(FunctionBindError::ArgumentTypeMismatch { index, expected }) => {
1635                    self.context.add_diagnostic(argument_type_mismatch(
1636                        target.text(),
1637                        &expected,
1638                        &arguments[index],
1639                        expr.arguments()
1640                            .nth(index)
1641                            .map(|e| e.span())
1642                            .expect("should have span"),
1643                    ));
1644                }
1645                Err(FunctionBindError::Ambiguous { first, second }) => {
1646                    self.context.add_diagnostic(ambiguous_argument(
1647                        target.text(),
1648                        target.span(),
1649                        &first,
1650                        &second,
1651                    ));
1652                }
1653            }
1654        } else {
1655            // Exceeded the maximum number of arguments to any function
1656            match f.param_min_max(self.context.version()) {
1657                Some((_, max)) => {
1658                    assert!(max <= MAX_PARAMETERS);
1659                    self.context.add_diagnostic(too_many_arguments(
1660                        target.text(),
1661                        target.span(),
1662                        max,
1663                        count,
1664                        expr.arguments().skip(max).map(|e| e.span()),
1665                    ));
1666                }
1667                None => {
1668                    self.context.add_diagnostic(unsupported_function(
1669                        f.minimum_version(),
1670                        target.text(),
1671                        target.span(),
1672                    ));
1673                }
1674            }
1675        }
1676
1677        Some(f.realize_unconstrained_return_type(arguments))
1678    }
1679
1680    /// Evaluates the type of an index expression.
1681    fn evaluate_index_expr<N: TreeNode + Exceptable>(
1682        &mut self,
1683        expr: &IndexExpr<N>,
1684    ) -> Option<Type> {
1685        let (target, index) = expr.operands();
1686
1687        // Determine the expected index type and result type of the expression
1688        let target_ty = self.evaluate_expr(&target)?;
1689        let (expected_index_ty, result_ty) = match &target_ty {
1690            Type::Compound(CompoundType::Array(ty), _) => (
1691                Some(PrimitiveType::Integer.into()),
1692                Some(ty.element_type().clone()),
1693            ),
1694            Type::Compound(CompoundType::Map(ty), _) => {
1695                (Some(ty.key_type().clone()), Some(ty.value_type().clone()))
1696            }
1697            _ => (None, None),
1698        };
1699
1700        // Check that the index type is the expected one
1701        if let Some(expected_index_ty) = expected_index_ty {
1702            let index_ty = self.evaluate_expr(&index).unwrap_or(Type::Union);
1703            if !index_ty.is_coercible_to(&expected_index_ty) {
1704                self.context.add_diagnostic(index_type_mismatch(
1705                    &expected_index_ty,
1706                    &index_ty,
1707                    index.span(),
1708                ));
1709            }
1710        }
1711
1712        match result_ty {
1713            Some(ty) => Some(ty),
1714            None => {
1715                self.context
1716                    .add_diagnostic(cannot_index(&target_ty, target.span()));
1717                None
1718            }
1719        }
1720    }
1721
1722    /// Evaluates the type of an access expression.
1723    fn evaluate_access_expr<N: TreeNode + Exceptable>(
1724        &mut self,
1725        expr: &AccessExpr<N>,
1726    ) -> Option<Type> {
1727        let (target, name) = expr.operands();
1728        let ty = self.evaluate_expr(&target)?;
1729
1730        match &ty {
1731            Type::Hidden(HiddenType::TaskPreEvaluation) => {
1732                return match task_member_type_pre_evaluation(name.text()) {
1733                    Some(ty) => Some(ty),
1734                    None => {
1735                        self.context.add_diagnostic(not_a_task_member(&name));
1736                        return None;
1737                    }
1738                };
1739            }
1740            Type::Hidden(HiddenType::TaskPostEvaluation) => {
1741                return match task_member_type_post_evaluation(self.context.version(), name.text()) {
1742                    Some(ty) => Some(ty),
1743                    None => {
1744                        self.context.add_diagnostic(not_a_task_member(&name));
1745                        return None;
1746                    }
1747                };
1748            }
1749            Type::Hidden(HiddenType::PreviousTaskData) => {
1750                return match previous_task_data_member_type(name.text()) {
1751                    Some(ty) => Some(ty),
1752                    None => {
1753                        self.context
1754                            .add_diagnostic(not_a_previous_task_data_member(&name));
1755                        return None;
1756                    }
1757                };
1758            }
1759            Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), _) => {
1760                if let Some(ty) = ty.members().get(name.text()) {
1761                    return Some(ty.clone());
1762                }
1763
1764                self.context
1765                    .add_diagnostic(not_a_struct_member(ty.name(), &name));
1766                return None;
1767            }
1768            Type::Compound(CompoundType::Pair(ty), _) => {
1769                // Support `left` and `right` accessors for pairs
1770                return match name.text() {
1771                    "left" => Some(ty.left_type().clone()),
1772                    "right" => Some(ty.right_type().clone()),
1773                    _ => {
1774                        self.context.add_diagnostic(not_a_pair_accessor(&name));
1775                        None
1776                    }
1777                };
1778            }
1779            Type::Call(ty) => {
1780                if let Some(output) = ty.outputs().get(name.text()) {
1781                    return Some(output.ty().clone());
1782                }
1783
1784                self.context
1785                    .add_diagnostic(unknown_call_io(ty, &name, Io::Output));
1786                return None;
1787            }
1788            Type::TypeNameRef(ref_ty) => match ref_ty.ty() {
1789                CustomType::Struct(_) => {
1790                    self.context
1791                        .add_diagnostic(cannot_access(&ty, target.span()));
1792                    return None;
1793                }
1794                CustomType::Enum(ty) => {
1795                    if !ty.choices().iter().any(|n| n == name.text()) {
1796                        self.context
1797                            .add_diagnostic(not_an_enum_choice(ref_ty.name(), &name));
1798                        return None;
1799                    }
1800
1801                    return Some(ref_ty.ty().clone().into());
1802                }
1803            },
1804            _ => {}
1805        }
1806
1807        // Check to see if it's coercible to object; if so, treat as `Union` as it's
1808        // indeterminate
1809        if ty.is_coercible_to(&Type::OptionalObject) {
1810            return Some(Type::Union);
1811        }
1812
1813        self.context
1814            .add_diagnostic(cannot_access(&ty, target.span()));
1815        None
1816    }
1817
1818    /// Checks for unnecessary function calls.
1819    fn check_unnecessary_call<N: TreeNode + Exceptable>(
1820        &mut self,
1821        call: &CallExpr<N>,
1822        arguments: &[Type],
1823        severity: Severity,
1824    ) {
1825        let target = call.target();
1826        let mut arg_spans = call.arguments().map(|arg| arg.span());
1827
1828        let (label, span, fix) = match target.text() {
1829            "select_first" => {
1830                if let Some(ty) = arguments[0].as_array().map(|a| a.element_type()) {
1831                    if ty.is_optional() || ty.is_union() {
1832                        return;
1833                    }
1834                    (
1835                        format!("array element {ty:#} is not optional"),
1836                        arg_spans.next().expect("should have span"),
1837                        "replace the function call with the array's first element",
1838                    )
1839                } else {
1840                    return;
1841                }
1842            }
1843            "select_all" => {
1844                if let Some(ty) = arguments[0].as_array().map(|a| a.element_type()) {
1845                    if ty.is_optional() || ty.is_union() {
1846                        return;
1847                    }
1848                    (
1849                        format!("array element {ty:#} is not optional"),
1850                        arg_spans.next().expect("should have span"),
1851                        "replace the function call with the array itself",
1852                    )
1853                } else {
1854                    return;
1855                }
1856            }
1857            "defined" => {
1858                if arguments[0].is_optional() || arguments[0].is_union() {
1859                    return;
1860                }
1861
1862                (
1863                    format!("{ty:#} is not optional", ty = arguments[0]),
1864                    arg_spans.next().expect("should have span"),
1865                    "replace the function call with `true`",
1866                )
1867            }
1868            _ => return,
1869        };
1870
1871        self.context.exceptable_add_diagnostic(
1872            unnecessary_function_call(target.text(), target.span(), &label, span)
1873                .with_severity(severity)
1874                .with_fix(fix),
1875            call.inner(),
1876            &UnnecessaryFunctionCall::EXCEPTABLE_NODES,
1877        )
1878    }
1879}