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