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