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