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::SyntaxNodeExt;
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 optional = key_type.is_optional();
491        Ok(MapType::new(
492            Type::Primitive(key_type.kind().into(), optional),
493            self.convert_type(&value_type)?,
494        ))
495    }
496
497    /// Converts an AST struct definition into a struct type.
498    ///
499    /// If the type could not created, an error with the relevant diagnostic is
500    /// returned.
501    pub fn convert_struct_type<N: TreeNode>(
502        &mut self,
503        definition: &v1::StructDefinition<N>,
504    ) -> Result<StructType, Diagnostic> {
505        Ok(StructType {
506            name: Arc::new(definition.name().text().to_string()),
507            members: Arc::new(
508                definition
509                    .members()
510                    .map(|d| Ok((d.name().text().to_string(), self.convert_type(&d.ty())?)))
511                    .collect::<Result<_, _>>()?,
512            ),
513        })
514    }
515}
516
517impl From<v1::PrimitiveTypeKind> for PrimitiveType {
518    fn from(value: v1::PrimitiveTypeKind) -> Self {
519        match value {
520            v1::PrimitiveTypeKind::Boolean => Self::Boolean,
521            v1::PrimitiveTypeKind::Integer => Self::Integer,
522            v1::PrimitiveTypeKind::Float => Self::Float,
523            v1::PrimitiveTypeKind::String => Self::String,
524            v1::PrimitiveTypeKind::File => Self::File,
525            v1::PrimitiveTypeKind::Directory => Self::Directory,
526        }
527    }
528}
529
530/// Represents context to an expression type evaluator.
531pub trait EvaluationContext {
532    /// Gets the supported version of the document being evaluated.
533    fn version(&self) -> SupportedVersion;
534
535    /// Gets the type of the given name in scope.
536    ///
537    /// - If the name is a variable, returns the type of that variable. For
538    ///   example, returns the type of `foo` in the expression `foo.bar`.
539    /// - If the name refers to a custom type, returns a type name reference to
540    ///   that custom type. For example, returns a type name reference to
541    ///   `Status` in the expression `Status.Active` (where `Status`) is an
542    ///   enum.
543    fn resolve_name(&self, name: &str, span: Span) -> Option<Type>;
544
545    /// Resolves a type name to a type.
546    ///
547    /// For example, returns the type of `MyStruct` in the expression `MyStruct
548    /// a = MyStruct { ... }`.
549    fn resolve_type_name(&mut self, name: &str, span: Span) -> Result<Type, Diagnostic>;
550
551    /// Gets the task associated with the evaluation context.
552    ///
553    /// This is only `Some` when evaluating a task `hints` section.
554    fn task(&self) -> Option<&Task>;
555
556    /// Gets the diagnostics configuration for the evaluation.
557    fn diagnostics_config(&self) -> DiagnosticsConfig;
558
559    /// Adds a diagnostic.
560    fn add_diagnostic(&mut self, diagnostic: Diagnostic);
561}
562
563/// Represents an evaluator of expression types.
564#[derive(Debug)]
565pub struct ExprTypeEvaluator<'a, C> {
566    /// The context for the evaluator.
567    context: &'a mut C,
568    /// The nested count of placeholder evaluation.
569    ///
570    /// This is incremented immediately before a placeholder expression is
571    /// evaluated and decremented immediately after.
572    ///
573    /// If the count is non-zero, special evaluation behavior is enabled for
574    /// string interpolation.
575    placeholders: usize,
576}
577
578impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> {
579    /// Constructs a new expression type evaluator.
580    pub fn new(context: &'a mut C) -> Self {
581        Self {
582            context,
583            placeholders: 0,
584        }
585    }
586
587    /// Evaluates the type of the given expression in the given scope.
588    ///
589    /// Returns `None` if the type of the expression is indeterminate.
590    pub fn evaluate_expr<N: TreeNode + SyntaxNodeExt>(&mut self, expr: &Expr<N>) -> Option<Type> {
591        match expr {
592            Expr::Literal(expr) => self.evaluate_literal_expr(expr),
593            Expr::NameRef(r) => {
594                let name = r.name();
595                self.context.resolve_name(name.text(), name.span())
596            }
597            Expr::Parenthesized(expr) => self.evaluate_expr(&expr.expr()),
598            Expr::If(expr) => self.evaluate_if_expr(expr),
599            Expr::LogicalNot(expr) => self.evaluate_logical_not_expr(expr),
600            Expr::Negation(expr) => self.evaluate_negation_expr(expr),
601            Expr::LogicalOr(expr) => self.evaluate_logical_or_expr(expr),
602            Expr::LogicalAnd(expr) => self.evaluate_logical_and_expr(expr),
603            Expr::Equality(expr) => {
604                let (lhs, rhs) = expr.operands();
605                self.evaluate_comparison_expr(ComparisonOperator::Equality, &lhs, &rhs, expr.span())
606            }
607            Expr::Inequality(expr) => {
608                let (lhs, rhs) = expr.operands();
609                self.evaluate_comparison_expr(
610                    ComparisonOperator::Inequality,
611                    &lhs,
612                    &rhs,
613                    expr.span(),
614                )
615            }
616            Expr::Less(expr) => {
617                let (lhs, rhs) = expr.operands();
618                self.evaluate_comparison_expr(ComparisonOperator::Less, &lhs, &rhs, expr.span())
619            }
620            Expr::LessEqual(expr) => {
621                let (lhs, rhs) = expr.operands();
622                self.evaluate_comparison_expr(
623                    ComparisonOperator::LessEqual,
624                    &lhs,
625                    &rhs,
626                    expr.span(),
627                )
628            }
629            Expr::Greater(expr) => {
630                let (lhs, rhs) = expr.operands();
631                self.evaluate_comparison_expr(ComparisonOperator::Greater, &lhs, &rhs, expr.span())
632            }
633            Expr::GreaterEqual(expr) => {
634                let (lhs, rhs) = expr.operands();
635                self.evaluate_comparison_expr(
636                    ComparisonOperator::GreaterEqual,
637                    &lhs,
638                    &rhs,
639                    expr.span(),
640                )
641            }
642            Expr::Addition(expr) => {
643                let (lhs, rhs) = expr.operands();
644                self.evaluate_numeric_expr(NumericOperator::Addition, expr.span(), &lhs, &rhs)
645            }
646            Expr::Subtraction(expr) => {
647                let (lhs, rhs) = expr.operands();
648                self.evaluate_numeric_expr(NumericOperator::Subtraction, expr.span(), &lhs, &rhs)
649            }
650            Expr::Multiplication(expr) => {
651                let (lhs, rhs) = expr.operands();
652                self.evaluate_numeric_expr(NumericOperator::Multiplication, expr.span(), &lhs, &rhs)
653            }
654            Expr::Division(expr) => {
655                let (lhs, rhs) = expr.operands();
656                self.evaluate_numeric_expr(NumericOperator::Division, expr.span(), &lhs, &rhs)
657            }
658            Expr::Modulo(expr) => {
659                let (lhs, rhs) = expr.operands();
660                self.evaluate_numeric_expr(NumericOperator::Modulo, expr.span(), &lhs, &rhs)
661            }
662            Expr::Exponentiation(expr) => {
663                let (lhs, rhs) = expr.operands();
664                self.evaluate_numeric_expr(NumericOperator::Exponentiation, expr.span(), &lhs, &rhs)
665            }
666            Expr::Call(expr) => self.evaluate_call_expr(expr),
667            Expr::Index(expr) => self.evaluate_index_expr(expr),
668            Expr::Access(expr) => self.evaluate_access_expr(expr),
669        }
670    }
671
672    /// Evaluates the type of a literal expression.
673    fn evaluate_literal_expr<N: TreeNode + SyntaxNodeExt>(
674        &mut self,
675        expr: &LiteralExpr<N>,
676    ) -> Option<Type> {
677        match expr {
678            LiteralExpr::Boolean(_) => Some(PrimitiveType::Boolean.into()),
679            LiteralExpr::Integer(_) => Some(PrimitiveType::Integer.into()),
680            LiteralExpr::Float(_) => Some(PrimitiveType::Float.into()),
681            LiteralExpr::String(s) => {
682                for p in s.parts() {
683                    if let StringPart::Placeholder(p) = p {
684                        self.check_placeholder(&p);
685                    }
686                }
687
688                Some(PrimitiveType::String.into())
689            }
690            LiteralExpr::Array(expr) => Some(self.evaluate_literal_array(expr)),
691            LiteralExpr::Pair(expr) => Some(self.evaluate_literal_pair(expr)),
692            LiteralExpr::Map(expr) => Some(self.evaluate_literal_map(expr)),
693            LiteralExpr::Object(expr) => Some(self.evaluate_literal_object(expr)),
694            LiteralExpr::Struct(expr) => self.evaluate_literal_struct(expr),
695            LiteralExpr::None(_) => Some(Type::None),
696            LiteralExpr::Hints(expr) => self.evaluate_literal_hints(expr),
697            LiteralExpr::Input(expr) => self.evaluate_literal_input(expr),
698            LiteralExpr::Output(expr) => self.evaluate_literal_output(expr),
699        }
700    }
701
702    /// Checks a placeholder expression.
703    pub(crate) fn check_placeholder<N: TreeNode + SyntaxNodeExt>(
704        &mut self,
705        placeholder: &Placeholder<N>,
706    ) {
707        self.placeholders += 1;
708
709        // Evaluate the placeholder expression and check that the resulting type is
710        // coercible to string for interpolation
711        let expr = placeholder.expr();
712        if let Some(ty) = self.evaluate_expr(&expr) {
713            if let Some(option) = placeholder.option() {
714                let valid = match option {
715                    PlaceholderOption::Sep(_) => {
716                        ty == Type::Union
717                            || ty == Type::None
718                            || matches!(&ty,
719                        Type::Compound(CompoundType::Array(array_ty), _)
720                        if matches!(array_ty.element_type(), Type::Primitive(_, false) | Type::Union))
721                    }
722                    PlaceholderOption::Default(_) => {
723                        matches!(ty, Type::Primitive(..) | Type::Union | Type::None)
724                    }
725                    PlaceholderOption::TrueFalse(_) => {
726                        matches!(
727                            ty,
728                            Type::Primitive(PrimitiveType::Boolean, _) | Type::Union | Type::None
729                        )
730                    }
731                };
732
733                if !valid {
734                    self.context.add_diagnostic(invalid_placeholder_option(
735                        &ty,
736                        expr.span(),
737                        &option,
738                    ));
739                }
740            } else {
741                match ty {
742                    Type::Primitive(..)
743                    | Type::Union
744                    | Type::None
745                    | Type::Compound(CompoundType::Custom(CustomType::Enum(_)), _) => {}
746                    _ => {
747                        self.context
748                            .add_diagnostic(cannot_coerce_to_string(&ty, expr.span()));
749                    }
750                }
751            }
752        }
753
754        self.placeholders -= 1;
755    }
756
757    /// Evaluates the type of a literal array expression.
758    fn evaluate_literal_array<N: TreeNode + SyntaxNodeExt>(
759        &mut self,
760        expr: &LiteralArray<N>,
761    ) -> Type {
762        // Look at the first array element to determine the element type
763        // The remaining elements must have a common type
764        let mut elements = expr.elements();
765        match elements
766            .next()
767            .and_then(|e| Some((self.evaluate_expr(&e)?, e.span())))
768        {
769            Some((mut expected, mut expected_span)) => {
770                // Ensure the remaining element types share a common type
771                for expr in elements {
772                    if let Some(actual) = self.evaluate_expr(&expr) {
773                        match expected.common_type(&actual) {
774                            Some(ty) => {
775                                expected = ty;
776                                expected_span = expr.span();
777                            }
778                            _ => {
779                                self.context.add_diagnostic(no_common_type(
780                                    &expected,
781                                    expected_span,
782                                    &actual,
783                                    expr.span(),
784                                ));
785                            }
786                        }
787                    }
788                }
789
790                ArrayType::new(expected).into()
791            }
792            // Treat empty array as `Array[Union]`
793            None => ArrayType::new(Type::Union).into(),
794        }
795    }
796
797    /// Evaluates the type of a literal pair expression.
798    fn evaluate_literal_pair<N: TreeNode + SyntaxNodeExt>(
799        &mut self,
800        expr: &LiteralPair<N>,
801    ) -> Type {
802        let (left, right) = expr.exprs();
803        let left = self.evaluate_expr(&left).unwrap_or(Type::Union);
804        let right = self.evaluate_expr(&right).unwrap_or(Type::Union);
805        PairType::new(left, right).into()
806    }
807
808    /// Evaluates the type of a literal map expression.
809    fn evaluate_literal_map<N: TreeNode + SyntaxNodeExt>(&mut self, expr: &LiteralMap<N>) -> Type {
810        let map_item_type = |item: LiteralMapItem<N>| {
811            let (key, value) = item.key_value();
812            let expected_key = self.evaluate_expr(&key)?;
813            match expected_key {
814                Type::Primitive(..) | Type::None | Type::Union => {
815                    // OK
816                }
817                _ => {
818                    self.context
819                        .add_diagnostic(map_key_not_primitive(key.span(), &expected_key));
820                    return None;
821                }
822            }
823
824            Some((
825                expected_key,
826                key.span(),
827                self.evaluate_expr(&value)?,
828                value.span(),
829            ))
830        };
831
832        let mut items = expr.items();
833        match items.next().and_then(map_item_type) {
834            Some((
835                mut expected_key,
836                mut expected_key_span,
837                mut expected_value,
838                mut expected_value_span,
839            )) => {
840                // Ensure the remaining items types share common types
841                for item in items {
842                    let (key, value) = item.key_value();
843                    if let Some(actual_key) = self.evaluate_expr(&key)
844                        && let Some(actual_value) = self.evaluate_expr(&value)
845                    {
846                        match expected_key.common_type(&actual_key) {
847                            Some(ty) => {
848                                expected_key = ty;
849                                expected_key_span = key.span();
850                            }
851                            _ => {
852                                self.context.add_diagnostic(no_common_type(
853                                    &expected_key,
854                                    expected_key_span,
855                                    &actual_key,
856                                    key.span(),
857                                ));
858                            }
859                        }
860
861                        match expected_value.common_type(&actual_value) {
862                            Some(ty) => {
863                                expected_value = ty;
864                                expected_value_span = value.span();
865                            }
866                            _ => {
867                                self.context.add_diagnostic(no_common_type(
868                                    &expected_value,
869                                    expected_value_span,
870                                    &actual_value,
871                                    value.span(),
872                                ));
873                            }
874                        }
875                    }
876                }
877
878                MapType::new(expected_key, expected_value).into()
879            }
880            // Treat as `Map[Union, Union]`
881            None => MapType::new(Type::Union, Type::Union).into(),
882        }
883    }
884
885    /// Evaluates the type of a literal object expression.
886    fn evaluate_literal_object<N: TreeNode + SyntaxNodeExt>(
887        &mut self,
888        expr: &LiteralObject<N>,
889    ) -> Type {
890        // Validate the member expressions
891        for item in expr.items() {
892            let (_, v) = item.name_value();
893            self.evaluate_expr(&v);
894        }
895
896        Type::Object
897    }
898
899    /// Evaluates the type of a literal struct expression.
900    fn evaluate_literal_struct<N: TreeNode + SyntaxNodeExt>(
901        &mut self,
902        expr: &LiteralStruct<N>,
903    ) -> Option<Type> {
904        let name = expr.name();
905        match self.context.resolve_type_name(name.text(), name.span()) {
906            Ok(ty) => {
907                let ty = match ty {
908                    Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), false) => ty,
909                    _ => panic!("type should be a required struct"),
910                };
911
912                // Keep track of which members are present in the expression
913                let mut present = vec![false; ty.members().len()];
914
915                // Validate the member types
916                for item in expr.items() {
917                    let (n, v) = item.name_value();
918                    match ty.members.get_full(n.text()) {
919                        Some((index, _, expected)) => {
920                            present[index] = true;
921                            if let Some(actual) = self.evaluate_expr(&v)
922                                && !actual.is_coercible_to(expected)
923                            {
924                                self.context.add_diagnostic(type_mismatch(
925                                    expected,
926                                    n.span(),
927                                    &actual,
928                                    v.span(),
929                                ));
930                            }
931                        }
932                        _ => {
933                            // Not a struct member
934                            self.context
935                                .add_diagnostic(not_a_struct_member(name.text(), &n));
936                        }
937                    }
938                }
939
940                // Find the first unspecified member that is required, if any
941                let mut unspecified = present
942                    .iter()
943                    .enumerate()
944                    .filter_map(|(i, present)| {
945                        if *present {
946                            return None;
947                        }
948
949                        let (name, ty) = &ty.members.get_index(i).unwrap();
950                        if ty.is_optional() {
951                            return None;
952                        }
953
954                        Some(name.as_str())
955                    })
956                    .peekable();
957
958                if unspecified.peek().is_some() {
959                    let mut members = String::new();
960                    let mut count = 0;
961                    while let Some(member) = unspecified.next() {
962                        match (unspecified.peek().is_none(), count) {
963                            (true, c) if c > 1 => members.push_str(", and "),
964                            (true, 1) => members.push_str(" and "),
965                            (false, c) if c > 0 => members.push_str(", "),
966                            _ => {}
967                        }
968
969                        write!(&mut members, "`{member}`").ok();
970                        count += 1;
971                    }
972
973                    self.context
974                        .add_diagnostic(missing_struct_members(&name, count, &members));
975                }
976
977                Some(Type::Compound(
978                    CompoundType::Custom(CustomType::Struct(ty)),
979                    false,
980                ))
981            }
982            Err(diagnostic) => {
983                self.context.add_diagnostic(diagnostic);
984                None
985            }
986        }
987    }
988
989    /// Evaluates a `runtime` section item.
990    pub(crate) fn evaluate_runtime_item<N: TreeNode + SyntaxNodeExt>(
991        &mut self,
992        name: &Ident<N::Token>,
993        expr: &Expr<N>,
994    ) {
995        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
996        if !self.evaluate_requirement(name, expr, &expr_ty) {
997            // Always use object types for `runtime` section `inputs` and `outputs` keys as
998            // only `hints` sections can use input/output hidden types
999            if let Some(expected) = task_hint_types(self.context.version(), name.text(), false)
1000                && !expected
1001                    .iter()
1002                    .any(|target| expr_ty.is_coercible_to(target))
1003            {
1004                self.context.add_diagnostic(multiple_type_mismatch(
1005                    expected,
1006                    name.span(),
1007                    &expr_ty,
1008                    expr.span(),
1009                ));
1010            }
1011        }
1012    }
1013
1014    /// Evaluates a `requirements` section item.
1015    pub(crate) fn evaluate_requirements_item<N: TreeNode + SyntaxNodeExt>(
1016        &mut self,
1017        name: &Ident<N::Token>,
1018        expr: &Expr<N>,
1019    ) {
1020        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
1021        self.evaluate_requirement(name, expr, &expr_ty);
1022    }
1023
1024    /// Evaluates a requirement in either a `requirements` section or a legacy
1025    /// `runtime` section.
1026    ///
1027    /// Returns `true` if the name matched a requirement or `false` if it did
1028    /// not.
1029    fn evaluate_requirement<N: TreeNode>(
1030        &mut self,
1031        name: &Ident<N::Token>,
1032        expr: &Expr<N>,
1033        expr_ty: &Type,
1034    ) -> bool {
1035        if let Some(expected) = task_requirement_types(self.context.version(), name.text()) {
1036            if !expected
1037                .iter()
1038                .any(|target| expr_ty.is_coercible_to(target))
1039            {
1040                self.context.add_diagnostic(multiple_type_mismatch(
1041                    expected,
1042                    name.span(),
1043                    expr_ty,
1044                    expr.span(),
1045                ));
1046            }
1047
1048            return true;
1049        }
1050
1051        false
1052    }
1053
1054    /// Evaluates the type of a literal hints expression.
1055    fn evaluate_literal_hints<N: TreeNode + SyntaxNodeExt>(
1056        &mut self,
1057        expr: &LiteralHints<N>,
1058    ) -> Option<Type> {
1059        self.context.task()?;
1060
1061        for item in expr.items() {
1062            self.evaluate_hints_item(&item.name(), &item.expr())
1063        }
1064
1065        Some(Type::Hidden(HiddenType::Hints))
1066    }
1067
1068    /// Evaluates a hints item, whether in task `hints` section or a `hints`
1069    /// literal expression.
1070    pub(crate) fn evaluate_hints_item<N: TreeNode + SyntaxNodeExt>(
1071        &mut self,
1072        name: &Ident<N::Token>,
1073        expr: &Expr<N>,
1074    ) {
1075        let expr_ty = self.evaluate_expr(expr).unwrap_or(Type::Union);
1076        if let Some(expected) = task_hint_types(self.context.version(), name.text(), true)
1077            && !expected
1078                .iter()
1079                .any(|target| expr_ty.is_coercible_to(target))
1080        {
1081            self.context.add_diagnostic(multiple_type_mismatch(
1082                expected,
1083                name.span(),
1084                &expr_ty,
1085                expr.span(),
1086            ));
1087        }
1088    }
1089
1090    /// Evaluates the type of a literal input expression.
1091    fn evaluate_literal_input<N: TreeNode + SyntaxNodeExt>(
1092        &mut self,
1093        expr: &LiteralInput<N>,
1094    ) -> Option<Type> {
1095        // Check to see if inputs literals are supported in the evaluation scope
1096        self.context.task()?;
1097
1098        // Evaluate the items of the literal
1099        for item in expr.items() {
1100            self.evaluate_literal_io_item(item.names(), item.expr(), Io::Input);
1101        }
1102
1103        Some(Type::Hidden(HiddenType::Input))
1104    }
1105
1106    /// Evaluates the type of a literal output expression.
1107    fn evaluate_literal_output<N: TreeNode + SyntaxNodeExt>(
1108        &mut self,
1109        expr: &LiteralOutput<N>,
1110    ) -> Option<Type> {
1111        // Check to see if output literals are supported in the evaluation scope
1112        self.context.task()?;
1113
1114        // Evaluate the items of the literal
1115        for item in expr.items() {
1116            self.evaluate_literal_io_item(item.names(), item.expr(), Io::Output);
1117        }
1118
1119        Some(Type::Hidden(HiddenType::Output))
1120    }
1121
1122    /// Evaluates a literal input/output item.
1123    fn evaluate_literal_io_item<N: TreeNode + SyntaxNodeExt>(
1124        &mut self,
1125        names: impl Iterator<Item = Ident<N::Token>>,
1126        expr: Expr<N>,
1127        io: Io,
1128    ) {
1129        let mut names = names.enumerate().peekable();
1130        let expr_ty = self.evaluate_expr(&expr).unwrap_or(Type::Union);
1131
1132        // The first name should be an input/output and then the remainder should be a
1133        // struct member
1134        let mut span = None;
1135        let mut s: Option<&StructType> = None;
1136        while let Some((i, name)) = names.next() {
1137            // The first name is an input or an output
1138            let ty = if i == 0 {
1139                span = Some(name.span());
1140
1141                match if io == Io::Input {
1142                    self.context
1143                        .task()
1144                        .expect("should have task")
1145                        .inputs()
1146                        .get(name.text())
1147                        .map(|i| i.ty())
1148                } else {
1149                    self.context
1150                        .task()
1151                        .expect("should have task")
1152                        .outputs()
1153                        .get(name.text())
1154                        .map(|o| o.ty())
1155                } {
1156                    Some(ty) => ty,
1157                    None => {
1158                        self.context.add_diagnostic(unknown_task_io(
1159                            self.context.task().expect("should have task").name(),
1160                            &name,
1161                            io,
1162                        ));
1163                        break;
1164                    }
1165                }
1166            } else {
1167                // Every other name is a struct member
1168                let start = span.unwrap().start();
1169                span = Some(Span::new(start, name.span().end() - start));
1170                let s = s.unwrap();
1171                match s.members.get(name.text()) {
1172                    Some(ty) => ty,
1173                    None => {
1174                        self.context
1175                            .add_diagnostic(not_a_struct_member(&s.name, &name));
1176                        break;
1177                    }
1178                }
1179            };
1180
1181            match ty {
1182                Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), _) => s = Some(ty),
1183                _ if names.peek().is_some() => {
1184                    self.context.add_diagnostic(not_a_struct(&name, i == 0));
1185                    break;
1186                }
1187                _ => {
1188                    // It's ok for the last one to not name a struct
1189                }
1190            }
1191        }
1192
1193        // If we bailed out early above, calculate the entire span of the name
1194        if let Some((_, last)) = names.last() {
1195            let start = span.unwrap().start();
1196            span = Some(Span::new(start, last.span().end() - start));
1197        }
1198
1199        // The type of every item should be `hints`
1200        if !expr_ty.is_coercible_to(&Type::Hidden(HiddenType::Hints)) {
1201            self.context.add_diagnostic(type_mismatch(
1202                &Type::Hidden(HiddenType::Hints),
1203                span.expect("should have span"),
1204                &expr_ty,
1205                expr.span(),
1206            ));
1207        }
1208    }
1209
1210    /// Evaluates the type of an `if` expression.
1211    fn evaluate_if_expr<N: TreeNode + SyntaxNodeExt>(&mut self, expr: &IfExpr<N>) -> Option<Type> {
1212        let (cond_expr, true_expr, false_expr) = expr.exprs();
1213
1214        // The conditional should be a boolean
1215        let cond_ty = self.evaluate_expr(&cond_expr).unwrap_or(Type::Union);
1216        if !cond_ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1217            self.context
1218                .add_diagnostic(if_conditional_mismatch(&cond_ty, cond_expr.span()));
1219        }
1220
1221        // Check that the two expressions have the same type
1222        let true_ty = self.evaluate_expr(&true_expr).unwrap_or(Type::Union);
1223        let false_ty = self.evaluate_expr(&false_expr).unwrap_or(Type::Union);
1224
1225        match (true_ty, false_ty) {
1226            (Type::Union, Type::Union) => None,
1227            (Type::Union, false_ty) => Some(false_ty),
1228            (true_ty, Type::Union) => Some(true_ty),
1229            (true_ty, false_ty) => match true_ty.common_type(&false_ty) {
1230                Some(ty) => Some(ty),
1231                _ => {
1232                    self.context.add_diagnostic(type_mismatch(
1233                        &true_ty,
1234                        true_expr.span(),
1235                        &false_ty,
1236                        false_expr.span(),
1237                    ));
1238
1239                    None
1240                }
1241            },
1242        }
1243    }
1244
1245    /// Evaluates the type of a `logical not` expression.
1246    fn evaluate_logical_not_expr<N: TreeNode + SyntaxNodeExt>(
1247        &mut self,
1248        expr: &LogicalNotExpr<N>,
1249    ) -> Option<Type> {
1250        // The operand should be a boolean
1251        let operand = expr.operand();
1252        let ty = self.evaluate_expr(&operand).unwrap_or(Type::Union);
1253        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1254            self.context
1255                .add_diagnostic(logical_not_mismatch(&ty, operand.span()));
1256        }
1257
1258        Some(PrimitiveType::Boolean.into())
1259    }
1260
1261    /// Evaluates the type of a negation expression.
1262    fn evaluate_negation_expr<N: TreeNode + SyntaxNodeExt>(
1263        &mut self,
1264        expr: &NegationExpr<N>,
1265    ) -> Option<Type> {
1266        // The operand should be a int or float
1267        let operand = expr.operand();
1268        let ty = self.evaluate_expr(&operand)?;
1269
1270        // If the type is `Int`, treat it as `Int`
1271        // This is checked first as `Int` is coercible to `Float`
1272        if ty.eq(&PrimitiveType::Integer.into()) {
1273            return Some(PrimitiveType::Integer.into());
1274        }
1275
1276        if !ty.is_coercible_to(&PrimitiveType::Float.into()) {
1277            self.context
1278                .add_diagnostic(negation_mismatch(&ty, operand.span()));
1279            // Type is indeterminate as the expression may evaluate to more than one type
1280            return None;
1281        }
1282
1283        Some(PrimitiveType::Float.into())
1284    }
1285
1286    /// Evaluates the type of a `logical or` expression.
1287    fn evaluate_logical_or_expr<N: TreeNode + SyntaxNodeExt>(
1288        &mut self,
1289        expr: &LogicalOrExpr<N>,
1290    ) -> Option<Type> {
1291        // Both operands should be booleans
1292        let (lhs, rhs) = expr.operands();
1293
1294        let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union);
1295        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1296            self.context
1297                .add_diagnostic(logical_or_mismatch(&ty, lhs.span()));
1298        }
1299
1300        let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union);
1301        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1302            self.context
1303                .add_diagnostic(logical_or_mismatch(&ty, rhs.span()));
1304        }
1305
1306        Some(PrimitiveType::Boolean.into())
1307    }
1308
1309    /// Evaluates the type of a `logical and` expression.
1310    fn evaluate_logical_and_expr<N: TreeNode + SyntaxNodeExt>(
1311        &mut self,
1312        expr: &LogicalAndExpr<N>,
1313    ) -> Option<Type> {
1314        // Both operands should be booleans
1315        let (lhs, rhs) = expr.operands();
1316
1317        let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union);
1318        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1319            self.context
1320                .add_diagnostic(logical_and_mismatch(&ty, lhs.span()));
1321        }
1322
1323        let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union);
1324        if !ty.is_coercible_to(&PrimitiveType::Boolean.into()) {
1325            self.context
1326                .add_diagnostic(logical_and_mismatch(&ty, rhs.span()));
1327        }
1328
1329        Some(PrimitiveType::Boolean.into())
1330    }
1331
1332    /// Evaluates the type of a comparison expression.
1333    fn evaluate_comparison_expr<N: TreeNode + SyntaxNodeExt>(
1334        &mut self,
1335        op: ComparisonOperator,
1336        lhs: &Expr<N>,
1337        rhs: &Expr<N>,
1338        span: Span,
1339    ) -> Option<Type> {
1340        let lhs_ty = self.evaluate_expr(lhs).unwrap_or(Type::Union);
1341        let rhs_ty = self.evaluate_expr(rhs).unwrap_or(Type::Union);
1342
1343        // Check for comparison to `None` or `Union` and allow it
1344        if lhs_ty.is_union() || lhs_ty.is_none() || rhs_ty.is_union() || rhs_ty.is_none() {
1345            return Some(PrimitiveType::Boolean.into());
1346        }
1347
1348        // Check LHS and RHS for being coercible to one of the supported primitive types
1349        for expected in [
1350            Type::from(PrimitiveType::Boolean),
1351            PrimitiveType::Integer.into(),
1352            PrimitiveType::Float.into(),
1353            PrimitiveType::String.into(),
1354            PrimitiveType::File.into(),
1355            PrimitiveType::Directory.into(),
1356        ] {
1357            // Only support equality/inequality comparisons for `File` and `Directory`
1358            if op != ComparisonOperator::Equality
1359                && op != ComparisonOperator::Inequality
1360                && (matches!(
1361                    lhs_ty.as_primitive(),
1362                    Some(PrimitiveType::File) | Some(PrimitiveType::Directory)
1363                ) || matches!(
1364                    rhs_ty.as_primitive(),
1365                    Some(PrimitiveType::File) | Some(PrimitiveType::Directory)
1366                ))
1367            {
1368                continue;
1369            }
1370
1371            if lhs_ty.is_coercible_to(&expected) && rhs_ty.is_coercible_to(&expected) {
1372                return Some(PrimitiveType::Boolean.into());
1373            }
1374
1375            let expected = expected.optional();
1376            if lhs_ty.is_coercible_to(&expected) && rhs_ty.is_coercible_to(&expected) {
1377                return Some(PrimitiveType::Boolean.into());
1378            }
1379        }
1380
1381        // For equality comparisons, check LHS and RHS being object and compound types
1382        if op == ComparisonOperator::Equality || op == ComparisonOperator::Inequality {
1383            // Check for object
1384            if (lhs_ty.is_coercible_to(&Type::Object) && rhs_ty.is_coercible_to(&Type::Object))
1385                || (lhs_ty.is_coercible_to(&Type::OptionalObject)
1386                    && rhs_ty.is_coercible_to(&Type::OptionalObject))
1387            {
1388                return Some(PrimitiveType::Boolean.into());
1389            }
1390
1391            // Check for other compound types
1392            let equal = match (&lhs_ty, &rhs_ty) {
1393                (
1394                    Type::Compound(CompoundType::Array(a), _),
1395                    Type::Compound(CompoundType::Array(b), _),
1396                ) => a == b,
1397                (
1398                    Type::Compound(CompoundType::Pair(a), _),
1399                    Type::Compound(CompoundType::Pair(b), _),
1400                ) => a == b,
1401                (
1402                    Type::Compound(CompoundType::Map(a), _),
1403                    Type::Compound(CompoundType::Map(b), _),
1404                ) => a == b,
1405                (
1406                    Type::Compound(CompoundType::Custom(CustomType::Struct(a)), _),
1407                    Type::Compound(CompoundType::Custom(CustomType::Struct(b)), _),
1408                ) => a == b,
1409                (
1410                    Type::Compound(CompoundType::Custom(CustomType::Enum(a)), _),
1411                    Type::Compound(CompoundType::Custom(CustomType::Enum(b)), _),
1412                ) => a == b,
1413                _ => false,
1414            };
1415
1416            if equal {
1417                return Some(PrimitiveType::Boolean.into());
1418            }
1419        }
1420
1421        // A type mismatch at this point
1422        self.context.add_diagnostic(comparison_mismatch(
1423            op,
1424            span,
1425            &lhs_ty,
1426            lhs.span(),
1427            &rhs_ty,
1428            rhs.span(),
1429        ));
1430        Some(PrimitiveType::Boolean.into())
1431    }
1432
1433    /// Evaluates the type of a numeric expression.
1434    fn evaluate_numeric_expr<N: TreeNode + SyntaxNodeExt>(
1435        &mut self,
1436        op: NumericOperator,
1437        span: Span,
1438        lhs: &Expr<N>,
1439        rhs: &Expr<N>,
1440    ) -> Option<Type> {
1441        let lhs_ty = self.evaluate_expr(lhs).unwrap_or(Type::Union);
1442        let rhs_ty = self.evaluate_expr(rhs).unwrap_or(Type::Union);
1443
1444        // If both sides are `Int`, the result is `Int`
1445        if lhs_ty.eq(&PrimitiveType::Integer.into()) && rhs_ty.eq(&PrimitiveType::Integer.into()) {
1446            return Some(PrimitiveType::Integer.into());
1447        }
1448
1449        // If both sides are coercible to `Float`, the result is `Float`
1450        if !lhs_ty.is_union()
1451            && lhs_ty.is_coercible_to(&PrimitiveType::Float.into())
1452            && !rhs_ty.is_union()
1453            && rhs_ty.is_coercible_to(&PrimitiveType::Float.into())
1454        {
1455            return Some(PrimitiveType::Float.into());
1456        }
1457
1458        // For addition, also support `String` on one or both sides of any primitive
1459        // type that isn't `Boolean`; in placeholder expressions, allow the
1460        // other side to also be optional
1461        if op == NumericOperator::Addition {
1462            let allow_optional = self.placeholders > 0;
1463            let other = if (!lhs_ty.is_optional() || allow_optional)
1464                && lhs_ty
1465                    .as_primitive()
1466                    .map(|p| p == PrimitiveType::String)
1467                    .unwrap_or(false)
1468            {
1469                Some((lhs_ty.is_optional(), &rhs_ty, rhs.span()))
1470            } else if (!rhs_ty.is_optional() || allow_optional)
1471                && rhs_ty
1472                    .as_primitive()
1473                    .map(|p| p == PrimitiveType::String)
1474                    .unwrap_or(false)
1475            {
1476                Some((rhs_ty.is_optional(), &lhs_ty, lhs.span()))
1477            } else {
1478                None
1479            };
1480
1481            if let Some((optional, other, span)) = other {
1482                if (!other.is_optional() || allow_optional)
1483                    && other
1484                        .as_primitive()
1485                        .map(|p| p != PrimitiveType::Boolean)
1486                        .unwrap_or(other.is_union() || (allow_optional && other.is_none()))
1487                {
1488                    let ty: Type = PrimitiveType::String.into();
1489                    if optional || other.is_optional() {
1490                        return Some(ty.optional());
1491                    }
1492
1493                    return Some(ty);
1494                }
1495
1496                self.context
1497                    .add_diagnostic(string_concat_mismatch(other, span));
1498                return None;
1499            }
1500        }
1501
1502        if !lhs_ty.is_union() && !rhs_ty.is_union() {
1503            self.context.add_diagnostic(numeric_mismatch(
1504                op,
1505                span,
1506                &lhs_ty,
1507                lhs.span(),
1508                &rhs_ty,
1509                rhs.span(),
1510            ));
1511        }
1512
1513        None
1514    }
1515
1516    /// Evaluates the type of a call expression.
1517    fn evaluate_call_expr<N: TreeNode + SyntaxNodeExt>(
1518        &mut self,
1519        expr: &CallExpr<N>,
1520    ) -> Option<Type> {
1521        let target = expr.target();
1522        match STDLIB.function(target.text()) {
1523            Some(f) => {
1524                // Evaluate the argument expressions
1525                let mut count = 0;
1526                let mut arguments = [const { Type::Union }; MAX_PARAMETERS];
1527
1528                for arg in expr.arguments() {
1529                    if count < MAX_PARAMETERS {
1530                        arguments[count] = self.evaluate_expr(&arg).unwrap_or(Type::Union);
1531                    }
1532
1533                    count += 1;
1534                }
1535
1536                match target.text() {
1537                    "find" | "matches" | "sub" => {
1538                        // above function expect the pattern as 2nd argument
1539                        if let Some(Expr::Literal(LiteralExpr::String(pattern_literal))) =
1540                            expr.arguments().nth(1)
1541                            && let Some(value) = pattern_literal.text()
1542                        {
1543                            let pattern = value.text().to_string();
1544                            if let Err(e) = regex::Regex::new(&pattern) {
1545                                self.context.add_diagnostic(invalid_regex_pattern(
1546                                    target.text(),
1547                                    value.text(),
1548                                    &e,
1549                                    pattern_literal.span(),
1550                                ));
1551                            }
1552                        }
1553                    }
1554                    _ => {}
1555                }
1556
1557                let arguments = &arguments[..count.min(MAX_PARAMETERS)];
1558                if count <= MAX_PARAMETERS {
1559                    match f.bind(self.context.version(), arguments) {
1560                        Ok(binding) => {
1561                            if let Some(severity) =
1562                                self.context.diagnostics_config().unnecessary_function_call
1563                                && !expr.inner().is_rule_excepted(UNNECESSARY_FUNCTION_CALL)
1564                            {
1565                                self.check_unnecessary_call(
1566                                    &target,
1567                                    arguments,
1568                                    expr.arguments().map(|e| e.span()),
1569                                    severity,
1570                                );
1571                            }
1572                            return Some(binding.return_type().clone());
1573                        }
1574                        Err(FunctionBindError::RequiresVersion(minimum)) => {
1575                            self.context.add_diagnostic(unsupported_function(
1576                                minimum,
1577                                target.text(),
1578                                target.span(),
1579                            ));
1580                        }
1581                        Err(FunctionBindError::TooFewArguments(minimum)) => {
1582                            self.context.add_diagnostic(too_few_arguments(
1583                                target.text(),
1584                                target.span(),
1585                                minimum,
1586                                count,
1587                            ));
1588                        }
1589                        Err(FunctionBindError::TooManyArguments(maximum)) => {
1590                            self.context.add_diagnostic(too_many_arguments(
1591                                target.text(),
1592                                target.span(),
1593                                maximum,
1594                                count,
1595                                expr.arguments().skip(maximum).map(|e| e.span()),
1596                            ));
1597                        }
1598                        Err(FunctionBindError::ArgumentTypeMismatch { index, expected }) => {
1599                            self.context.add_diagnostic(argument_type_mismatch(
1600                                target.text(),
1601                                &expected,
1602                                &arguments[index],
1603                                expr.arguments()
1604                                    .nth(index)
1605                                    .map(|e| e.span())
1606                                    .expect("should have span"),
1607                            ));
1608                        }
1609                        Err(FunctionBindError::Ambiguous { first, second }) => {
1610                            self.context.add_diagnostic(ambiguous_argument(
1611                                target.text(),
1612                                target.span(),
1613                                &first,
1614                                &second,
1615                            ));
1616                        }
1617                    }
1618                } else {
1619                    // Exceeded the maximum number of arguments to any function
1620                    match f.param_min_max(self.context.version()) {
1621                        Some((_, max)) => {
1622                            assert!(max <= MAX_PARAMETERS);
1623                            self.context.add_diagnostic(too_many_arguments(
1624                                target.text(),
1625                                target.span(),
1626                                max,
1627                                count,
1628                                expr.arguments().skip(max).map(|e| e.span()),
1629                            ));
1630                        }
1631                        None => {
1632                            self.context.add_diagnostic(unsupported_function(
1633                                f.minimum_version(),
1634                                target.text(),
1635                                target.span(),
1636                            ));
1637                        }
1638                    }
1639                }
1640
1641                Some(f.realize_unconstrained_return_type(arguments))
1642            }
1643            None => {
1644                self.context
1645                    .add_diagnostic(unknown_function(target.text(), target.span()));
1646                None
1647            }
1648        }
1649    }
1650
1651    /// Evaluates the type of an index expression.
1652    fn evaluate_index_expr<N: TreeNode + SyntaxNodeExt>(
1653        &mut self,
1654        expr: &IndexExpr<N>,
1655    ) -> Option<Type> {
1656        let (target, index) = expr.operands();
1657
1658        // Determine the expected index type and result type of the expression
1659        let target_ty = self.evaluate_expr(&target)?;
1660        let (expected_index_ty, result_ty) = match &target_ty {
1661            Type::Compound(CompoundType::Array(ty), _) => (
1662                Some(PrimitiveType::Integer.into()),
1663                Some(ty.element_type().clone()),
1664            ),
1665            Type::Compound(CompoundType::Map(ty), _) => {
1666                (Some(ty.key_type().clone()), Some(ty.value_type().clone()))
1667            }
1668            _ => (None, None),
1669        };
1670
1671        // Check that the index type is the expected one
1672        if let Some(expected_index_ty) = expected_index_ty {
1673            let index_ty = self.evaluate_expr(&index).unwrap_or(Type::Union);
1674            if !index_ty.is_coercible_to(&expected_index_ty) {
1675                self.context.add_diagnostic(index_type_mismatch(
1676                    &expected_index_ty,
1677                    &index_ty,
1678                    index.span(),
1679                ));
1680            }
1681        }
1682
1683        match result_ty {
1684            Some(ty) => Some(ty),
1685            None => {
1686                self.context
1687                    .add_diagnostic(cannot_index(&target_ty, target.span()));
1688                None
1689            }
1690        }
1691    }
1692
1693    /// Evaluates the type of an access expression.
1694    fn evaluate_access_expr<N: TreeNode + SyntaxNodeExt>(
1695        &mut self,
1696        expr: &AccessExpr<N>,
1697    ) -> Option<Type> {
1698        let (target, name) = expr.operands();
1699        let ty = self.evaluate_expr(&target)?;
1700
1701        match &ty {
1702            Type::Hidden(HiddenType::TaskPreEvaluation) => {
1703                return match task_member_type_pre_evaluation(name.text()) {
1704                    Some(ty) => Some(ty),
1705                    None => {
1706                        self.context.add_diagnostic(not_a_task_member(&name));
1707                        return None;
1708                    }
1709                };
1710            }
1711            Type::Hidden(HiddenType::TaskPostEvaluation) => {
1712                return match task_member_type_post_evaluation(self.context.version(), name.text()) {
1713                    Some(ty) => Some(ty),
1714                    None => {
1715                        self.context.add_diagnostic(not_a_task_member(&name));
1716                        return None;
1717                    }
1718                };
1719            }
1720            Type::Hidden(HiddenType::PreviousTaskData) => {
1721                return match previous_task_data_member_type(name.text()) {
1722                    Some(ty) => Some(ty),
1723                    None => {
1724                        self.context
1725                            .add_diagnostic(not_a_previous_task_data_member(&name));
1726                        return None;
1727                    }
1728                };
1729            }
1730            Type::Compound(CompoundType::Custom(CustomType::Struct(ty)), _) => {
1731                if let Some(ty) = ty.members.get(name.text()) {
1732                    return Some(ty.clone());
1733                }
1734
1735                self.context
1736                    .add_diagnostic(not_a_struct_member(ty.name(), &name));
1737                return None;
1738            }
1739            Type::Compound(CompoundType::Pair(ty), _) => {
1740                // Support `left` and `right` accessors for pairs
1741                return match name.text() {
1742                    "left" => Some(ty.left_type().clone()),
1743                    "right" => Some(ty.right_type().clone()),
1744                    _ => {
1745                        self.context.add_diagnostic(not_a_pair_accessor(&name));
1746                        None
1747                    }
1748                };
1749            }
1750            Type::Call(ty) => {
1751                if let Some(output) = ty.outputs().get(name.text()) {
1752                    return Some(output.ty().clone());
1753                }
1754
1755                self.context
1756                    .add_diagnostic(unknown_call_io(ty, &name, Io::Output));
1757                return None;
1758            }
1759            Type::TypeNameRef(custom_ty) => match custom_ty {
1760                CustomType::Struct(_) => {
1761                    self.context
1762                        .add_diagnostic(cannot_access(&ty, target.span()));
1763                    return None;
1764                }
1765                CustomType::Enum(_) => {
1766                    return Some(Type::from(CompoundType::Custom(custom_ty.clone())));
1767                }
1768            },
1769            _ => {}
1770        }
1771
1772        // Check to see if it's coercible to object; if so, treat as `Union` as it's
1773        // indeterminate
1774        if ty.is_coercible_to(&Type::OptionalObject) {
1775            return Some(Type::Union);
1776        }
1777
1778        self.context
1779            .add_diagnostic(cannot_access(&ty, target.span()));
1780        None
1781    }
1782
1783    /// Checks for unnecessary function calls.
1784    fn check_unnecessary_call<T: TreeToken>(
1785        &mut self,
1786        target: &Ident<T>,
1787        arguments: &[Type],
1788        mut spans: impl Iterator<Item = Span>,
1789        severity: Severity,
1790    ) {
1791        let (label, span, fix) = match target.text() {
1792            "select_first" => {
1793                if let Some(ty) = arguments[0].as_array().map(|a| a.element_type()) {
1794                    if ty.is_optional() || ty.is_union() {
1795                        return;
1796                    }
1797                    (
1798                        format!("array element type `{ty}` is not optional"),
1799                        spans.next().expect("should have span"),
1800                        "replace the function call with the array's first element",
1801                    )
1802                } else {
1803                    return;
1804                }
1805            }
1806            "select_all" => {
1807                if let Some(ty) = arguments[0].as_array().map(|a| a.element_type()) {
1808                    if ty.is_optional() || ty.is_union() {
1809                        return;
1810                    }
1811                    (
1812                        format!("array element type `{ty}` is not optional"),
1813                        spans.next().expect("should have span"),
1814                        "replace the function call with the array itself",
1815                    )
1816                } else {
1817                    return;
1818                }
1819            }
1820            "defined" => {
1821                if arguments[0].is_optional() || arguments[0].is_union() {
1822                    return;
1823                }
1824
1825                (
1826                    format!("type `{ty}` is not optional", ty = arguments[0]),
1827                    spans.next().expect("should have span"),
1828                    "replace the function call with `true`",
1829                )
1830            }
1831            _ => return,
1832        };
1833
1834        self.context.add_diagnostic(
1835            unnecessary_function_call(target.text(), target.span(), &label, span)
1836                .with_severity(severity)
1837                .with_fix(fix),
1838        )
1839    }
1840}