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