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