From 97a277624da8636aaba782ff5c22936a6ebda8c1 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Wed, 27 Nov 2024 11:55:41 -0800 Subject: [PATCH 01/11] refactor: refactor analysis results. This commit refactors analysis results so that an analysis document contains the identifier, URI, and the diagnostics resulting from the analysis of the document. As a consequence, the `ParseResult` enumeration was removed and merged with `AnalysisResult`. --- gauntlet/src/lib.rs | 8 +- wdl-analysis/src/analyzer.rs | 242 +++++++++++--------------------- wdl-analysis/src/document.rs | 134 ++++++++++++++---- wdl-analysis/src/document/v1.rs | 221 +++++++++++++---------------- wdl-analysis/src/graph.rs | 54 +------ wdl-analysis/src/queue.rs | 13 +- wdl-analysis/src/types/v1.rs | 129 +++++++++-------- wdl-analysis/tests/analysis.rs | 13 +- wdl-engine/src/eval/v1.rs | 50 +++++-- wdl-engine/tests/inputs.rs | 12 +- wdl-lsp/src/proto.rs | 44 +++--- wdl/src/bin/wdl.rs | 10 +- 12 files changed, 447 insertions(+), 483 deletions(-) diff --git a/gauntlet/src/lib.rs b/gauntlet/src/lib.rs index 1b365e98..17ee04bd 100644 --- a/gauntlet/src/lib.rs +++ b/gauntlet/src/lib.rs @@ -198,7 +198,7 @@ pub async fn gauntlet(args: Args) -> Result<()> { total_time += elapsed; for result in &results { - let path = result.uri().to_file_path().ok(); + let path = result.document().uri().to_file_path().ok(); let path = match &path { Some(path) => path .strip_prefix(&repo_root) @@ -211,17 +211,17 @@ pub async fn gauntlet(args: Args) -> Result<()> { let document_identifier = document::Identifier::new(repository_identifier.clone(), &path); - let diagnostics: Cow<'_, [Diagnostic]> = match result.parse_result().error() { + let diagnostics: Cow<'_, [Diagnostic]> = match result.error() { Some(e) => { vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into() } - None => result.diagnostics().into(), + None => result.document().diagnostics().into(), }; let mut actual = IndexSet::new(); if !diagnostics.is_empty() { let source = result - .parse_result() + .document() .root() .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) .unwrap_or(String::new()); diff --git a/wdl-analysis/src/analyzer.rs b/wdl-analysis/src/analyzer.rs index 4ae5d326..6f12c1bb 100644 --- a/wdl-analysis/src/analyzer.rs +++ b/wdl-analysis/src/analyzer.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use std::thread::JoinHandle; use anyhow::Context; +use anyhow::Error; use anyhow::Result; use anyhow::anyhow; use anyhow::bail; @@ -21,13 +22,11 @@ use line_index::LineIndex; use line_index::WideEncoding; use line_index::WideLineCol; use path_clean::clean; -use rowan::GreenNode; use tokio::runtime::Handle; use tokio::sync::mpsc; use tokio::sync::oneshot; use url::Url; use walkdir::WalkDir; -use wdl_ast::Diagnostic; use wdl_ast::Severity; use wdl_ast::SyntaxNode; use wdl_ast::SyntaxNodeExt; @@ -75,119 +74,22 @@ pub fn path_to_uri(path: impl AsRef) -> Option { Url::from_file_path(clean(absolute(path).ok()?)).ok() } -/// Represents the result of a parse. -#[derive(Debug, Clone)] -pub enum ParseResult { - /// There was an error parsing the document. - Error(Arc), - /// The document was parsed. - Parsed { - /// The monotonic version of the document that was parsed. - /// - /// This value comes from incremental changes to the file. - /// - /// If `None`, the parsed version had no incremental changes. - version: Option, - /// The root node of the document. - root: GreenNode, - /// The line index used to map line/column offsets to byte offsets and - /// vice versa. - lines: Arc, - }, -} - -impl ParseResult { - /// Gets the version of the parsed document. - /// - /// Returns `None` if there was an error parsing the document or the parsed - /// document had no incremental changes. - pub fn version(&self) -> Option { - match self { - Self::Error(_) => None, - Self::Parsed { version, .. } => *version, - } - } - - /// Gets the root from the parse result. - /// - /// Returns `None` if there was an error parsing the document. - pub fn root(&self) -> Option<&GreenNode> { - match self { - Self::Error(_) => None, - Self::Parsed { root, .. } => Some(root), - } - } - - /// Gets the line index from the parse result. - /// - /// Returns `None` if there was an error parsing the document. - pub fn lines(&self) -> Option<&Arc> { - match self { - Self::Error(_) => None, - Self::Parsed { lines, .. } => Some(lines), - } - } - - /// Gets the AST document of the parse result. - /// - /// Returns `None` if there was an error parsing the document. - pub fn document(&self) -> Option { - match &self { - ParseResult::Error(_) => None, - ParseResult::Parsed { root, .. } => Some( - wdl_ast::Document::cast(SyntaxNode::new_root(root.clone())) - .expect("node should cast"), - ), - } - } - - /// Gets the error parsing the document. - /// - /// Returns` None` if the document was parsed. - pub fn error(&self) -> Option<&Arc> { - match self { - Self::Error(e) => Some(e), - ParseResult::Parsed { .. } => None, - } - } -} - -impl From<&ParseState> for ParseResult { - fn from(state: &ParseState) -> Self { - match state { - ParseState::NotParsed => { - panic!("cannot create a result for an file that hasn't been parsed") - } - ParseState::Error(e) => Self::Error(e.clone()), - ParseState::Parsed { - version, - root, - lines, - diagnostics: _, - } => Self::Parsed { - version: *version, - root: root.clone(), - lines: lines.clone(), - }, - } - } -} - /// Represents the result of an analysis. /// /// Analysis results are cheap to clone. #[derive(Debug, Clone)] pub struct AnalysisResult { - /// The analysis result id. + /// The error that occurred when attempting to parse the file (e.g. the file + /// could not be opened). + error: Option>, + /// The monotonic version of the document that was parsed. + /// + /// This value comes from incremental changes to the file. /// - /// The identifier changes every time the document is analyzed. - id: Arc, - /// The URI of the analyzed document. - uri: Arc, - /// The result from parsing the file. - parse_result: ParseResult, - /// The diagnostics for the document. - diagnostics: Arc<[Diagnostic]>, + /// If `None`, the parsed version had no incremental changes. + version: Option, + /// The lines indexed for the parsed file. + lines: Option>, /// The analyzed document. document: Arc, } @@ -195,37 +97,45 @@ pub struct AnalysisResult { impl AnalysisResult { /// Constructs a new analysis result for the given graph node. pub(crate) fn new(node: &DocumentGraphNode) -> Self { - let analysis = node.analysis().expect("analysis not completed"); + let (error, version, lines) = match node.parse_state() { + ParseState::NotParsed => unreachable!("document should have been parsed"), + ParseState::Error(e) => (Some(e), None, None), + ParseState::Parsed { version, lines, .. } => (None, *version, Some(lines)), + }; Self { - id: analysis.id().clone(), - uri: node.uri().clone(), - parse_result: node.parse_state().into(), - diagnostics: analysis.diagnostics().clone(), - document: analysis.document().clone(), + error: error.cloned(), + version, + lines: lines.cloned(), + document: node + .analysis() + .expect("analysis should have completed") + .clone(), } } - /// Gets the identifier of the analysis result. + /// Gets the error that occurred when attempting to parse the document. /// - /// This value changes when a document is reanalyzed. - pub fn id(&self) -> &Arc { - &self.id - } - - /// Gets the URI of the document that was analyzed. - pub fn uri(&self) -> &Arc { - &self.uri + /// An example error would be if the file could not be opened. + /// + /// Returns `None` if the document was parsed successfully. + pub fn error(&self) -> Option<&Arc> { + self.error.as_ref() } - /// Gets the result of the parse. - pub fn parse_result(&self) -> &ParseResult { - &self.parse_result + /// Gets the incremental version of the parsed document. + /// + /// Returns `None` if there was an error parsing the document or if the + /// parsed document had no incremental changes. + pub fn version(&self) -> Option { + self.version } - /// Gets the diagnostics associated with the document. - pub fn diagnostics(&self) -> &[Diagnostic] { - &self.diagnostics + /// Gets the line index of the parsed document. + /// + /// Returns `None` if there was an error parsing the document. + pub fn lines(&self) -> Option<&Arc> { + self.lines.as_ref() } /// Gets the analyzed document. @@ -828,24 +738,30 @@ workflow test { let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 1); - assert_eq!(results[0].diagnostics().len(), 1); - assert_eq!(results[0].diagnostics()[0].rule(), None); - assert_eq!(results[0].diagnostics()[0].severity(), Severity::Error); + assert_eq!(results[0].document.diagnostics().len(), 1); + assert_eq!(results[0].document.diagnostics()[0].rule(), None); + assert_eq!( + results[0].document.diagnostics()[0].severity(), + Severity::Error + ); assert_eq!( - results[0].diagnostics()[0].message(), + results[0].document.diagnostics()[0].message(), "conflicting workflow name `test`" ); // Analyze again and ensure the analysis result id is unchanged - let id = results[0].id().clone(); + let id = results[0].document.id().clone(); let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 1); - assert_eq!(results[0].id().as_ref(), id.as_ref()); - assert_eq!(results[0].diagnostics().len(), 1); - assert_eq!(results[0].diagnostics()[0].rule(), None); - assert_eq!(results[0].diagnostics()[0].severity(), Severity::Error); + assert_eq!(results[0].document.id().as_ref(), id.as_ref()); + assert_eq!(results[0].document.diagnostics().len(), 1); + assert_eq!(results[0].document.diagnostics()[0].rule(), None); + assert_eq!( + results[0].document.diagnostics()[0].severity(), + Severity::Error + ); assert_eq!( - results[0].diagnostics()[0].message(), + results[0].document.diagnostics()[0].message(), "conflicting workflow name `test`" ); } @@ -877,11 +793,14 @@ workflow test { let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 1); - assert_eq!(results[0].diagnostics().len(), 1); - assert_eq!(results[0].diagnostics()[0].rule(), None); - assert_eq!(results[0].diagnostics()[0].severity(), Severity::Error); + assert_eq!(results[0].document.diagnostics().len(), 1); + assert_eq!(results[0].document.diagnostics()[0].rule(), None); assert_eq!( - results[0].diagnostics()[0].message(), + results[0].document.diagnostics()[0].severity(), + Severity::Error + ); + assert_eq!( + results[0].document.diagnostics()[0].message(), "conflicting workflow name `test`" ); @@ -905,18 +824,18 @@ workflow something_else { // Analyze again and ensure the analysis result id is changed and the issue // fixed - let id = results[0].id().clone(); + let id = results[0].document.id().clone(); let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 1); - assert!(results[0].id().as_ref() != id.as_ref()); - assert_eq!(results[0].diagnostics().len(), 0); + assert!(results[0].document.id().as_ref() != id.as_ref()); + assert_eq!(results[0].document.diagnostics().len(), 0); // Analyze again and ensure the analysis result id is unchanged - let id = results[0].id().clone(); + let id = results[0].document.id().clone(); let results = analyzer.analyze_document((), uri).await.unwrap(); assert_eq!(results.len(), 1); - assert!(results[0].id().as_ref() == id.as_ref()); - assert_eq!(results[0].diagnostics().len(), 0); + assert!(results[0].document.id().as_ref() == id.as_ref()); + assert_eq!(results[0].document.diagnostics().len(), 0); } #[tokio::test] @@ -946,11 +865,14 @@ workflow test { let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 1); - assert_eq!(results[0].diagnostics().len(), 1); - assert_eq!(results[0].diagnostics()[0].rule(), None); - assert_eq!(results[0].diagnostics()[0].severity(), Severity::Error); + assert_eq!(results[0].document.diagnostics().len(), 1); + assert_eq!(results[0].document.diagnostics()[0].rule(), None); + assert_eq!( + results[0].document.diagnostics()[0].severity(), + Severity::Error + ); assert_eq!( - results[0].diagnostics()[0].message(), + results[0].document.diagnostics()[0].message(), "conflicting workflow name `test`" ); @@ -970,11 +892,11 @@ workflow test { // Analyze again and ensure the analysis result id is changed and the issue was // fixed - let id = results[0].id().clone(); + let id = results[0].document.id().clone(); let results = analyzer.analyze_document((), uri).await.unwrap(); assert_eq!(results.len(), 1); - assert!(results[0].id().as_ref() != id.as_ref()); - assert_eq!(results[0].diagnostics().len(), 0); + assert!(results[0].document.id().as_ref() != id.as_ref()); + assert_eq!(results[0].document.diagnostics().len(), 0); } #[tokio::test] @@ -1020,9 +942,9 @@ workflow test { // Analyze the documents let results = analyzer.analyze(()).await.unwrap(); assert_eq!(results.len(), 3); - assert!(results[0].diagnostics().is_empty()); - assert!(results[1].diagnostics().is_empty()); - assert!(results[2].diagnostics().is_empty()); + assert!(results[0].document.diagnostics().is_empty()); + assert!(results[1].document.diagnostics().is_empty()); + assert!(results[2].document.diagnostics().is_empty()); // Analyze the documents again let results = analyzer.analyze(()).await.unwrap(); diff --git a/wdl-analysis/src/document.rs b/wdl-analysis/src/document.rs index 2a813149..a9d23b77 100644 --- a/wdl-analysis/src/document.rs +++ b/wdl-analysis/src/document.rs @@ -2,12 +2,14 @@ use std::cmp::Ordering; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use indexmap::IndexMap; use petgraph::graph::NodeIndex; use rowan::GreenNode; use url::Url; +use uuid::Uuid; use wdl_ast::Ast; use wdl_ast::AstNode; use wdl_ast::AstToken; @@ -452,12 +454,18 @@ impl Workflow { } /// Represents an analyzed WDL document. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Document { /// The root CST node of the document. /// /// This is `None` when the document could not be parsed. root: Option, + /// The document identifier. + /// + /// The identifier changes every time the document is analyzed. + id: Arc, + /// The URI of the analyzed document. + uri: Arc, /// The version of the document. version: Option, /// The namespaces in the document. @@ -470,64 +478,111 @@ pub struct Document { structs: IndexMap, /// The collection of types for the document. types: Types, + /// The diagnostics for the document. + diagnostics: Vec, } impl Document { - /// Creates a new analyzed document. - pub(crate) fn new( + /// Creates a new analyzed document from a document graph node. + pub(crate) fn from_graph_node( config: DiagnosticsConfig, graph: &DocumentGraph, index: NodeIndex, - ) -> (Self, Vec) { + ) -> Self { let node = graph.get(index); - let mut diagnostics = match node.parse_state() { + let diagnostics = match node.parse_state() { ParseState::NotParsed => panic!("node should have been parsed"), - ParseState::Error(_) => return (Default::default(), Default::default()), + ParseState::Error(_) => { + return Self::new(node.uri().clone(), None, None, Default::default()); + } ParseState::Parsed { diagnostics, .. } => { Vec::from_iter(diagnostics.as_ref().iter().cloned()) } }; - let document = node.document().expect("node should have been parsed"); - let version = match document.version_statement() { - Some(stmt) => stmt.version(), + let root = node.document().expect("node should have been parsed"); + let (version, config) = match root.version_statement() { + Some(stmt) => (stmt.version(), config.excepted_for_node(stmt.syntax())), None => { // Don't process a document with a missing version - return (Default::default(), diagnostics); + return Self::new( + node.uri().clone(), + Some(root.syntax().green().into()), + None, + diagnostics, + ); } }; - let config = - config.excepted_for_node(&version.syntax().parent().expect("token should have parent")); - - let document = match document.ast() { - Ast::Unsupported => Default::default(), + let mut document = Self::new( + node.uri().clone(), + Some(root.syntax().green().into()), + SupportedVersion::from_str(version.as_str()).ok(), + diagnostics, + ); + match root.ast() { + Ast::Unsupported => {} Ast::V1(ast) => { - v1::create_document(config, graph, index, &ast, &version, &mut diagnostics) + v1::populate_document(&mut document, config, graph, index, &ast, &version) } - }; + } // Check for unused imports if let Some(severity) = config.unused_import { - for (name, ns) in document - .namespaces() - .filter(|(_, ns)| !ns.used && !ns.excepted) - { - diagnostics.push(unused_import(name, ns.span()).with_severity(severity)); - } + let Document { + namespaces, + diagnostics, + .. + } = &mut document; + + diagnostics.extend( + namespaces + .iter() + .filter(|(_, ns)| !ns.used && !ns.excepted) + .map(|(name, ns)| unused_import(name, ns.span()).with_severity(severity)), + ); } // Sort the diagnostics by start - diagnostics.sort_by(|a, b| match (a.labels().next(), b.labels().next()) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - (Some(a), Some(b)) => a.span().start().cmp(&b.span().start()), - }); + document + .diagnostics + .sort_by(|a, b| match (a.labels().next(), b.labels().next()) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Less, + (Some(_), None) => Ordering::Greater, + (Some(a), Some(b)) => a.span().start().cmp(&b.span().start()), + }); + + document + } + + /// Constructs a new analysis document. + fn new( + uri: Arc, + root: Option, + version: Option, + diagnostics: Vec, + ) -> Self { + Self { + root, + id: Uuid::new_v4().to_string().into(), + uri, + version, + namespaces: Default::default(), + tasks: Default::default(), + workflow: Default::default(), + structs: Default::default(), + types: Default::default(), + diagnostics, + } + } - // Perform a type check - (document, diagnostics) + /// Gets the AST root for the document. + /// + /// Returns `None` if there was an error parsing the document. + pub fn root(&self) -> Option<&GreenNode> { + self.root.as_ref() } /// Gets the AST of the document. @@ -546,6 +601,18 @@ impl Document { } } + /// Gets the identifier of the document. + /// + /// This value changes when a document is reanalyzed. + pub fn id(&self) -> &Arc { + &self.id + } + + /// Gets the URI of the document. + pub fn uri(&self) -> &Arc { + &self.uri + } + /// Gets the supported version of the document. /// /// Returns `None` if the document could not be parsed or contains an @@ -596,6 +663,11 @@ impl Document { &self.types } + /// Gets the analysis diagnostics for the document. + pub fn diagnostics(&self) -> &[Diagnostic] { + &self.diagnostics + } + /// Finds a scope based on a position within the document. pub fn find_scope_by_position(&self, position: usize) -> Option> { /// Finds a scope within a collection of sorted scopes by position. diff --git a/wdl-analysis/src/document/v1.rs b/wdl-analysis/src/document/v1.rs index 164e80c6..646285b8 100644 --- a/wdl-analysis/src/document/v1.rs +++ b/wdl-analysis/src/document/v1.rs @@ -1,7 +1,6 @@ //! Conversion of a V1 AST to an analyzed document. use std::collections::HashMap; use std::collections::HashSet; -use std::str::FromStr; use std::sync::Arc; use indexmap::IndexMap; @@ -194,22 +193,19 @@ fn sort_scopes(scopes: &mut Vec) { } /// Creates a new document for a V1 AST. -pub(crate) fn create_document( +pub(crate) fn populate_document( + document: &mut Document, config: DiagnosticsConfig, graph: &DocumentGraph, index: NodeIndex, ast: &Ast, - version: &Version, - diagnostics: &mut Vec, -) -> Document { - let mut document = Document { - root: Some(ast.syntax().green().into_owned()), - version: SupportedVersion::from_str(version.as_str()).ok(), - ..Default::default() - }; - + version: &wdl_ast::Version, +) { assert!( - document.version.is_some(), + matches!( + document.version.expect("document should have a version"), + SupportedVersion::V1(_) + ), "expected a supported V1 version" ); @@ -219,10 +215,10 @@ pub(crate) fn create_document( for item in ast.items() { match item { DocumentItem::Import(import) => { - add_namespace(&mut document, graph, &import, index, version, diagnostics); + add_namespace(document, graph, &import, index, version); } DocumentItem::Struct(s) => { - add_struct(&mut document, &s, diagnostics); + add_struct(document, &s); } DocumentItem::Task(_) | DocumentItem::Workflow(_) => { continue; @@ -231,19 +227,19 @@ pub(crate) fn create_document( } // Populate the struct types now that all structs have been processed - set_struct_types(&mut document, diagnostics); + set_struct_types(document); // Now process the tasks and workflows let mut workflow = None; for item in ast.items() { match item { DocumentItem::Task(task) => { - add_task(config, &mut document, &task, diagnostics); + add_task(config, document, &task); } DocumentItem::Workflow(w) => { // Note that this doesn't populate the workflow; we delay that until after // we've seen every task in the document so that we can resolve call targets - if add_workflow(&mut document, &w, diagnostics) { + if add_workflow(document, &w) { workflow = Some(w.clone()); } } @@ -254,10 +250,8 @@ pub(crate) fn create_document( } if let Some(workflow) = workflow { - populate_workflow(config, &mut document, &workflow, diagnostics); + populate_workflow(config, document, &workflow); } - - document } /// Adds a namespace to the document. @@ -267,13 +261,12 @@ fn add_namespace( import: &ImportStatement, importer_index: NodeIndex, importer_version: &Version, - diagnostics: &mut Vec, ) { // Start by resolving the import to its document let (uri, imported) = match resolve_import(graph, import, importer_index, importer_version) { Ok(resolved) => resolved, Err(Some(diagnostic)) => { - diagnostics.push(diagnostic); + document.diagnostics.push(diagnostic); return; } Err(None) => return, @@ -284,7 +277,7 @@ fn add_namespace( let ns = match import.namespace() { Some((ns, span)) => { if let Some(prev) = document.namespaces.get(&ns) { - diagnostics.push(namespace_conflict( + document.diagnostics.push(namespace_conflict( &ns, span, prev.span, @@ -315,7 +308,7 @@ fn add_namespace( .filter_map(|a| { let (from, to) = a.names(); if !imported.structs.contains_key(from.as_str()) { - diagnostics.push(struct_not_in_document(&from)); + document.diagnostics.push(struct_not_in_document(&from)); return None; } @@ -334,12 +327,16 @@ fn add_namespace( Some(prev) => { // Import conflicts with a struct defined in this document if prev.namespace.is_none() { - diagnostics.push(struct_conflicts_with_import(aliased_name, prev.span, span)); + document.diagnostics.push(struct_conflicts_with_import( + aliased_name, + prev.span, + span, + )); continue; } if !are_structs_equal(prev, s) { - diagnostics.push(imported_struct_conflict( + document.diagnostics.push(imported_struct_conflict( aliased_name, span, prev.span, @@ -381,21 +378,17 @@ fn are_structs_equal(a: &Struct, b: &Struct) -> bool { } /// Adds a struct to the document. -fn add_struct( - document: &mut Document, - definition: &StructDefinition, - diagnostics: &mut Vec, -) { +fn add_struct(document: &mut Document, definition: &StructDefinition) { let name = definition.name(); if let Some(prev) = document.structs.get(name.as_str()) { if prev.namespace.is_some() { - diagnostics.push(struct_conflicts_with_import( + document.diagnostics.push(struct_conflicts_with_import( name.as_str(), name.span(), prev.span, )) } else { - diagnostics.push(name_conflict( + document.diagnostics.push(name_conflict( name.as_str(), Context::Struct(name.span()), Context::Struct(prev.span), @@ -409,7 +402,7 @@ fn add_struct( for decl in definition.members() { let name = decl.name(); if let Some(prev_span) = members.get(name.as_str()) { - diagnostics.push(name_conflict( + document.diagnostics.push(name_conflict( name.as_str(), Context::StructMember(name.span()), Context::StructMember(*prev_span), @@ -429,11 +422,7 @@ fn add_struct( } /// Converts an AST type to an analysis type. -fn convert_ast_type( - document: &mut Document, - ty: &wdl_ast::v1::Type, - diagnostics: &mut Vec, -) -> Type { +fn convert_ast_type(document: &mut Document, ty: &wdl_ast::v1::Type) -> Type { let mut converter = AstTypeConverter::new(&mut document.types, |name, span| { document .structs @@ -452,7 +441,7 @@ fn convert_ast_type( match converter.convert_type(ty) { Ok(ty) => ty, Err(diagnostic) => { - diagnostics.push(diagnostic); + document.diagnostics.push(diagnostic); Type::Union } } @@ -462,7 +451,6 @@ fn convert_ast_type( fn create_input_type_map( document: &mut Document, declarations: impl Iterator, - diagnostics: &mut Vec, ) -> Arc> { let mut map = HashMap::new(); for decl in declarations { @@ -472,7 +460,7 @@ fn create_input_type_map( continue; } - let ty = convert_ast_type(document, &decl.ty(), diagnostics); + let ty = convert_ast_type(document, &decl.ty()); map.insert(name.as_str().to_string(), Input { ty, required: decl.expr().is_none() && !ty.is_optional(), @@ -486,7 +474,6 @@ fn create_input_type_map( fn create_output_type_map( document: &mut Document, declarations: impl Iterator, - diagnostics: &mut Vec, ) -> Arc> { let mut map = HashMap::new(); for decl in declarations { @@ -496,7 +483,7 @@ fn create_output_type_map( continue; } - let ty = convert_ast_type(document, &decl.ty(), diagnostics); + let ty = convert_ast_type(document, &decl.ty()); map.insert(name.as_str().to_string(), Output { ty }); } @@ -504,12 +491,7 @@ fn create_output_type_map( } /// Adds a task to the document. -fn add_task( - config: DiagnosticsConfig, - document: &mut Document, - task: &TaskDefinition, - diagnostics: &mut Vec, -) { +fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefinition) { /// Helper function for creating a scope for a task section. fn create_section_scope( version: Option, @@ -530,7 +512,7 @@ fn add_task( // Check for a name conflict with another task or workflow let name = task.name(); if let Some(s) = document.tasks.get(name.as_str()) { - diagnostics.push(name_conflict( + document.diagnostics.push(name_conflict( name.as_str(), Context::Task(name.span()), Context::Task(s.name_span), @@ -538,7 +520,7 @@ fn add_task( return; } else if let Some(s) = &document.workflow { if s.name == name.as_str() { - diagnostics.push(name_conflict( + document.diagnostics.push(name_conflict( name.as_str(), Context::Task(name.span()), Context::Workflow(s.name_span), @@ -551,18 +533,20 @@ fn add_task( let inputs = create_input_type_map( document, task.input().into_iter().flat_map(|s| s.declarations()), - diagnostics, ); let outputs = create_output_type_map( document, task.output() .into_iter() .flat_map(|s| s.declarations().map(Decl::Bound)), - diagnostics, ); // Process the task in evaluation order - let graph = TaskGraphBuilder::default().build(document.version.unwrap(), task, diagnostics); + let graph = TaskGraphBuilder::default().build( + document.version.unwrap(), + task, + &mut document.diagnostics, + ); let mut scopes = vec![Scope::new(None, braced_scope_span(task))]; let mut output_scope = None; let mut command_scope = None; @@ -575,8 +559,7 @@ fn add_task( document, ScopeRefMut::new(&mut scopes, ScopeIndex(0)), &decl, - |_, n, _, _| inputs[n].ty, - diagnostics, + |_, n, _| inputs[n].ty, ) { continue; } @@ -595,7 +578,7 @@ fn add_task( } if !decl.syntax().is_rule_excepted(UNUSED_INPUT_RULE_ID) { - diagnostics.push( + document.diagnostics.push( unused_input(name.as_str(), name.span()).with_severity(severity), ); } @@ -608,8 +591,7 @@ fn add_task( document, ScopeRefMut::new(&mut scopes, ScopeIndex(0)), &decl, - |doc, _, decl, diag| convert_ast_type(doc, &decl.ty(), diag), - diagnostics, + |doc, _, decl| convert_ast_type(doc, &decl.ty()), ) { continue; } @@ -623,7 +605,7 @@ fn add_task( .is_none() && !decl.syntax().is_rule_excepted(UNUSED_DECL_RULE_ID) { - diagnostics.push( + document.diagnostics.push( unused_declaration(name.as_str(), name.span()).with_severity(severity), ); } @@ -643,8 +625,7 @@ fn add_task( document, ScopeRefMut::new(&mut scopes, scope_index), &decl, - |_, n, _, _| outputs[n].ty, - diagnostics, + |_, n, _| outputs[n].ty, ); } TaskGraphNode::Command(section) => { @@ -660,7 +641,7 @@ fn add_task( let mut context = EvaluationContext::new(document, ScopeRef::new(&scopes, scope_index), config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); for part in section.parts() { if let CommandPart::Placeholder(p) = part { evaluator.check_placeholder(&p); @@ -671,7 +652,7 @@ fn add_task( // Perform type checking on the runtime section's expressions let mut context = EvaluationContext::new(document, ScopeRef::new(&scopes, ScopeIndex(0)), config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { evaluator.evaluate_runtime_item(&item.name(), &item.expr()); } @@ -680,7 +661,7 @@ fn add_task( // Perform type checking on the requirements section's expressions let mut context = EvaluationContext::new(document, ScopeRef::new(&scopes, ScopeIndex(0)), config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { evaluator.evaluate_requirements_item(&item.name(), &item.expr()); } @@ -693,7 +674,7 @@ fn add_task( config, TaskEvaluationContext::new(name.as_str(), &inputs, &outputs), ); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { evaluator.evaluate_hints_item(&item.name(), &item.expr()) } @@ -719,8 +700,7 @@ fn add_decl( document: &mut Document, mut scope: ScopeRefMut<'_>, decl: &Decl, - ty: impl FnOnce(&mut Document, &str, &Decl, &mut Vec) -> Type, - diagnostics: &mut Vec, + ty: impl FnOnce(&mut Document, &str, &Decl) -> Type, ) -> bool { let (name, expr) = (decl.name(), decl.expr()); if scope.lookup(name.as_str()).is_some() { @@ -728,7 +708,7 @@ fn add_decl( return false; } - let ty = ty(document, name.as_str(), decl, diagnostics); + let ty = ty(document, name.as_str(), decl); scope.insert(name.as_str(), name.span(), ty); @@ -740,7 +720,6 @@ fn add_decl( &expr, ty, name.span(), - diagnostics, ); } @@ -751,22 +730,20 @@ fn add_decl( /// /// Returns `true` if the workflow was added to the document or `false` if not /// (i.e. there was a conflict). -fn add_workflow( - document: &mut Document, - workflow: &WorkflowDefinition, - diagnostics: &mut Vec, -) -> bool { +fn add_workflow(document: &mut Document, workflow: &WorkflowDefinition) -> bool { // Check for conflicts with task names or an existing workspace let name = workflow.name(); if let Some(s) = document.tasks.get(name.as_str()) { - diagnostics.push(name_conflict( + document.diagnostics.push(name_conflict( name.as_str(), Context::Workflow(name.span()), Context::Task(s.name_span), )); return false; } else if let Some(s) = &document.workflow { - diagnostics.push(duplicate_workflow(&name, s.name_span)); + document + .diagnostics + .push(duplicate_workflow(&name, s.name_span)); return false; } @@ -795,13 +772,11 @@ fn populate_workflow( config: DiagnosticsConfig, document: &mut Document, workflow: &WorkflowDefinition, - diagnostics: &mut Vec, ) { // Populate type maps for the workflow's inputs and outputs let inputs = create_input_type_map( document, workflow.input().into_iter().flat_map(|s| s.declarations()), - diagnostics, ); let outputs = create_output_type_map( document, @@ -809,7 +784,6 @@ fn populate_workflow( .output() .into_iter() .flat_map(|s| s.declarations().map(Decl::Bound)), - diagnostics, ); // Keep a map of scopes from syntax node that introduced the scope to the scope @@ -817,7 +791,7 @@ fn populate_workflow( let mut scope_indexes: HashMap = HashMap::new(); let mut scopes = vec![Scope::new(None, braced_scope_span(workflow))]; let mut output_scope = None; - let graph = WorkflowGraphBuilder::default().build(workflow, diagnostics); + let graph = WorkflowGraphBuilder::default().build(workflow, &mut document.diagnostics); for index in toposort(&graph, None).expect("graph should be acyclic") { match graph[index].clone() { @@ -827,8 +801,7 @@ fn populate_workflow( document, ScopeRefMut::new(&mut scopes, ScopeIndex(0)), &decl, - |_, n, _, _| inputs[n].ty, - diagnostics, + |_, n, _| inputs[n].ty, ) { continue; } @@ -847,7 +820,7 @@ fn populate_workflow( } if !decl.syntax().is_rule_excepted(UNUSED_INPUT_RULE_ID) { - diagnostics.push( + document.diagnostics.push( unused_input(name.as_str(), name.span()).with_severity(severity), ); } @@ -865,8 +838,7 @@ fn populate_workflow( document, ScopeRefMut::new(&mut scopes, scope_index), &decl, - |doc, _, decl, diag| convert_ast_type(doc, &decl.ty(), diag), - diagnostics, + |doc, _, decl| convert_ast_type(doc, &decl.ty()), ) { continue; } @@ -880,7 +852,7 @@ fn populate_workflow( .is_none() && !decl.syntax().is_rule_excepted(UNUSED_DECL_RULE_ID) { - diagnostics.push( + document.diagnostics.push( unused_declaration(name.as_str(), name.span()).with_severity(severity), ); } @@ -903,8 +875,7 @@ fn populate_workflow( document, ScopeRefMut::new(&mut scopes, scope_index), &decl, - |_, n, _, _| outputs[n].ty, - diagnostics, + |_, n, _| outputs[n].ty, ); } WorkflowGraphNode::Conditional(statement) => { @@ -919,7 +890,6 @@ fn populate_workflow( parent, &mut scope_indexes, &statement, - diagnostics, ); } WorkflowGraphNode::Scatter(statement) => { @@ -934,7 +904,6 @@ fn populate_workflow( parent, &mut scope_indexes, &statement, - diagnostics, ); } WorkflowGraphNode::Call(statement) => { @@ -953,7 +922,6 @@ fn populate_workflow( .as_ref() .expect("should have workflow") .allows_nested_inputs, - diagnostics, ); // Check for unused call @@ -975,7 +943,8 @@ fn populate_workflow( .map(|a| a.name()) .unwrap_or_else(|| target_name); - diagnostics + document + .diagnostics .push(unused_call(name.as_str(), name.span()).with_severity(severity)); } } @@ -1028,7 +997,6 @@ fn add_conditional_statement( parent: ScopeIndex, scope_indexes: &mut HashMap, statement: &ConditionalStatement, - diagnostics: &mut Vec, ) { let scope_index = add_scope( scopes, @@ -1039,11 +1007,13 @@ fn add_conditional_statement( // Evaluate the statement's expression; it is expected to be a boolean let expr = statement.expr(); let mut context = EvaluationContext::new(document, ScopeRef::new(scopes, scope_index), config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); let ty = evaluator.evaluate_expr(&expr).unwrap_or(Type::Union); if !ty.is_coercible_to(&document.types, &PrimitiveTypeKind::Boolean.into()) { - diagnostics.push(if_conditional_mismatch(&document.types, ty, expr.span())); + document + .diagnostics + .push(if_conditional_mismatch(&document.types, ty, expr.span())); } } @@ -1055,7 +1025,6 @@ fn add_scatter_statement( parent: ScopeIndex, scopes_indexes: &mut HashMap, statement: &ScatterStatement, - diagnostics: &mut Vec, ) { let scope_index = add_scope( scopes, @@ -1066,21 +1035,25 @@ fn add_scatter_statement( // Evaluate the statement expression; it is expected to be an array let expr = statement.expr(); let mut context = EvaluationContext::new(document, ScopeRef::new(scopes, scope_index), config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); let ty = evaluator.evaluate_expr(&expr).unwrap_or(Type::Union); let element_ty = match ty { Type::Compound(compound_ty) => { match document.types.type_definition(compound_ty.definition()) { CompoundTypeDef::Array(ty) => ty.element_type(), _ => { - diagnostics.push(type_is_not_array(&document.types, ty, expr.span())); + document + .diagnostics + .push(type_is_not_array(&document.types, ty, expr.span())); Type::Union } } } Type::Union => Type::Union, _ => { - diagnostics.push(type_is_not_array(&document.types, ty, expr.span())); + document + .diagnostics + .push(type_is_not_array(&document.types, ty, expr.span())); Type::Union } }; @@ -1098,7 +1071,6 @@ fn add_call_statement( mut scope: ScopeRefMut<'_>, statement: &CallStatement, nested_inputs_allowed: bool, - diagnostics: &mut Vec, ) { // Determine the target name let target_name = statement @@ -1113,7 +1085,7 @@ fn add_call_statement( .map(|a| a.name()) .unwrap_or_else(|| target_name.clone()); - let ty = if let Some(ty) = resolve_call_type(document, workflow_name, statement, diagnostics) { + let ty = if let Some(ty) = resolve_call_type(document, workflow_name, statement) { // Type check the call inputs let mut seen = HashSet::new(); for input in statement.inputs() { @@ -1125,7 +1097,9 @@ fn add_call_statement( .copied() .map(|i| i.ty) .unwrap_or_else(|| { - diagnostics.push(unknown_call_io(&ty, &input_name, Io::Input)); + document + .diagnostics + .push(unknown_call_io(&ty, &input_name, Io::Input)); Type::Union }); @@ -1138,7 +1112,6 @@ fn add_call_statement( &expr, expected_ty, input_name.span(), - diagnostics, ); } None => { @@ -1146,7 +1119,7 @@ fn add_call_statement( if !matches!(expected_ty, Type::Union) && !name.ty.is_coercible_to(&document.types, &expected_ty) { - diagnostics.push(call_input_type_mismatch( + document.diagnostics.push(call_input_type_mismatch( &document.types, &input_name, expected_ty, @@ -1166,7 +1139,9 @@ fn add_call_statement( if !nested_inputs_allowed { for (name, input) in ty.inputs() { if input.required && !seen.contains(name.as_str()) { - diagnostics.push(missing_call_input(ty.kind(), &target_name, name)); + document + .diagnostics + .push(missing_call_input(ty.kind(), &target_name, name)); } } } @@ -1199,7 +1174,6 @@ fn resolve_call_type( document: &mut Document, workflow_name: &str, statement: &CallStatement, - diagnostics: &mut Vec, ) -> Option { let mut targets = statement.target().names().peekable(); let mut namespace = None; @@ -1211,7 +1185,7 @@ fn resolve_call_type( } if namespace.is_some() { - diagnostics.push(only_one_namespace(target.span())); + document.diagnostics.push(only_one_namespace(target.span())); return None; } @@ -1221,7 +1195,7 @@ fn resolve_call_type( namespace = Some(&document.namespaces[target.as_str()]) } None => { - diagnostics.push(unknown_namespace(&target)); + document.diagnostics.push(unknown_namespace(&target)); return None; } } @@ -1230,7 +1204,7 @@ fn resolve_call_type( let target = namespace.map(|ns| ns.document.as_ref()).unwrap_or(document); let name = name.expect("should have name"); if namespace.is_none() && name.as_str() == workflow_name { - diagnostics.push(recursive_workflow_call(&name)); + document.diagnostics.push(recursive_workflow_call(&name)); return None; } @@ -1244,7 +1218,9 @@ fn resolve_call_type( workflow.outputs.clone(), ), _ => { - diagnostics.push(unknown_task_or_workflow(namespace.map(|ns| ns.span), &name)); + document + .diagnostics + .push(unknown_task_or_workflow(namespace.map(|ns| ns.span), &name)); return None; } } @@ -1355,7 +1331,7 @@ fn resolve_import( let import_document = import_node.document().expect("import should have parsed"); let import_scope = import_node .analysis() - .map(|a| a.document().clone()) + .cloned() .expect("import should have been analyzed"); // Check for compatible imports @@ -1380,7 +1356,7 @@ fn resolve_import( } /// Sets the struct types in the document. -fn set_struct_types(document: &mut Document, diagnostics: &mut Vec) { +fn set_struct_types(document: &mut Document) { if document.structs.is_empty() { return; } @@ -1412,7 +1388,7 @@ fn set_struct_types(document: &mut Document, diagnostics: &mut Vec) let name = definition.name(); let name_span = name.span(); let member_span = member.name().span(); - diagnostics.push(recursive_struct( + document.diagnostics.push(recursive_struct( name.as_str(), Span::new(name_span.start() + s.offset, name_span.len()), Span::new(member_span.start() + s.offset, member_span.len()), @@ -1441,7 +1417,7 @@ fn set_struct_types(document: &mut Document, diagnostics: &mut Vec) Ok(s.ty().unwrap_or(Type::Union)) } else { - diagnostics.push(unknown_type( + document.diagnostics.push(unknown_type( name, Span::new(span.start() + document.structs[index].offset, span.len()), )); @@ -1594,6 +1570,10 @@ impl crate::types::v1::EvaluationContext for EvaluationContext<'_> { fn diagnostics_config(&self) -> DiagnosticsConfig { self.config } + + fn add_diagnostic(&mut self, diagnostic: Diagnostic) { + self.document.diagnostics.push(diagnostic); + } } /// Performs a type check of an expression. @@ -1604,14 +1584,13 @@ fn type_check_expr( expr: &Expr, expected: Type, expected_span: Span, - diagnostics: &mut Vec, ) { let mut context = EvaluationContext::new(document, scope, config); - let mut evaluator = ExprTypeEvaluator::new(&mut context, diagnostics); + let mut evaluator = ExprTypeEvaluator::new(&mut context); let actual = evaluator.evaluate_expr(expr).unwrap_or(Type::Union); if !matches!(expected, Type::Union) && !actual.is_coercible_to(&document.types, &expected) { - diagnostics.push(type_mismatch( + document.diagnostics.push(type_mismatch( &document.types, expected, expected_span, @@ -1624,7 +1603,9 @@ fn type_check_expr( else if let Type::Compound(e) = expected { if let CompoundTypeDef::Array(e) = document.types.type_definition(e.definition()) { if e.is_non_empty() && expr.is_empty_array_literal() { - diagnostics.push(non_empty_array_assignment(expected_span, expr.span())); + document + .diagnostics + .push(non_empty_array_assignment(expected_span, expr.span())); } } } diff --git a/wdl-analysis/src/graph.rs b/wdl-analysis/src/graph.rs index cd35cbff..8b6bd75e 100644 --- a/wdl-analysis/src/graph.rs +++ b/wdl-analysis/src/graph.rs @@ -26,7 +26,6 @@ use tokio::runtime::Handle; use tracing::debug; use tracing::info; use url::Url; -use uuid::Uuid; use wdl_ast::Diagnostic; use wdl_ast::SyntaxNode; use wdl_ast::Validator; @@ -82,45 +81,6 @@ pub enum ParseState { }, } -/// Represents the analysis state of a document graph node. -#[derive(Debug)] -pub struct Analysis { - /// The unique identifier of the analysis. - id: Arc, - /// The analyzed document. - document: Arc, - /// The analysis diagnostics. - diagnostics: Arc<[Diagnostic]>, -} - -impl Analysis { - /// Constructs a new analysis. - pub fn new(document: Document, diagnostics: impl Into>) -> Self { - Self { - id: Arc::new(Uuid::new_v4().to_string()), - document: Arc::new(document), - diagnostics: diagnostics.into(), - } - } - - /// Gets the analysis result id. - /// - /// The identifier changes every time the document is analyzed. - pub fn id(&self) -> &Arc { - &self.id - } - - /// Gets the analyzed document. - pub fn document(&self) -> &Arc { - &self.document - } - - /// Gets the diagnostics from the analysis. - pub fn diagnostics(&self) -> &Arc<[Diagnostic]> { - &self.diagnostics - } -} - /// Represents a node in a document graph. #[derive(Debug)] pub struct DocumentGraphNode { @@ -132,10 +92,10 @@ pub struct DocumentGraphNode { change: Option, /// The parse state of the document. parse_state: ParseState, - /// The analysis of the document. + /// The analyzed document for the node. /// /// If `None`, an analysis does not exist for the current state of the node. - analysis: Option, + analysis: Option>, } impl DocumentGraphNode { @@ -216,14 +176,14 @@ impl DocumentGraphNode { self.change = None; } - /// Gets the analysis of the document node. - pub fn analysis(&self) -> Option<&Analysis> { + /// Gets the analyzed document for the node. + pub fn analysis(&self) -> Option<&Arc> { self.analysis.as_ref() } /// Marks the analysis as completed. - pub fn analysis_completed(&mut self, analysis: Analysis) { - self.analysis = Some(analysis); + pub fn analysis_completed(&mut self, document: Document) { + self.analysis = Some(Arc::new(document)); } /// Marks the document node for reanalysis. @@ -425,7 +385,7 @@ impl DocumentGraphNode { } } -/// Represents a document graph. +/// Represents a graph of WDL analyzed documents. #[derive(Debug, Default)] pub struct DocumentGraph { /// The inner directional graph. diff --git a/wdl-analysis/src/queue.rs b/wdl-analysis/src/queue.rs index 42265c30..9b4898cd 100644 --- a/wdl-analysis/src/queue.rs +++ b/wdl-analysis/src/queue.rs @@ -34,7 +34,6 @@ use crate::DiagnosticsConfig; use crate::IncrementalChange; use crate::ProgressKind; use crate::document::Document; -use crate::graph::Analysis; use crate::graph::DfsSpace; use crate::graph::DocumentGraph; use crate::graph::ParseState; @@ -469,9 +468,9 @@ where }; let mut graph = self.graph.write(); - results.extend(analyzed.into_iter().filter_map(|(index, analysis)| { + results.extend(analyzed.into_iter().filter_map(|(index, document)| { let node = graph.get_mut(index); - node.analysis_completed(analysis); + node.analysis_completed(document); if graph.include_result(index) { Some(AnalysisResult::new(graph.get(index))) @@ -481,7 +480,7 @@ where })); } - results.sort_by(|a, b| a.uri().cmp(b.uri())); + results.sort_by(|a, b| a.document().uri().cmp(b.document().uri())); Cancelable::Completed(Ok(results)) } @@ -675,10 +674,10 @@ where config: DiagnosticsConfig, graph: Arc>, index: NodeIndex, - ) -> (NodeIndex, Analysis) { + ) -> (NodeIndex, Document) { let start = Instant::now(); let graph = graph.read(); - let (document, diagnostics) = Document::new(config, &graph, index); + let document = Document::from_graph_node(config, &graph, index); info!( "analysis of `{uri}` completed in {elapsed:?}", @@ -686,6 +685,6 @@ where elapsed = start.elapsed() ); - (index, Analysis::new(document, diagnostics)) + (index, document) } } diff --git a/wdl-analysis/src/types/v1.rs b/wdl-analysis/src/types/v1.rs index fee0bced..495678ab 100644 --- a/wdl-analysis/src/types/v1.rs +++ b/wdl-analysis/src/types/v1.rs @@ -482,6 +482,9 @@ pub trait EvaluationContext { /// Gets the diagnostics configuration for the evaluation. fn diagnostics_config(&self) -> DiagnosticsConfig; + + /// Adds a diagnostic. + fn add_diagnostic(&mut self, diagnostic: Diagnostic); } /// Represents an evaluator of expression types. @@ -489,8 +492,6 @@ pub trait EvaluationContext { pub struct ExprTypeEvaluator<'a, C> { /// The context for the evaluator. context: &'a mut C, - /// The diagnostics collection for adding evaluation diagnostics. - diagnostics: &'a mut Vec, /// The nested count of placeholder evaluation. /// /// This is incremented immediately before a placeholder expression is @@ -503,10 +504,9 @@ pub struct ExprTypeEvaluator<'a, C> { impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { /// Constructs a new expression type evaluator. - pub fn new(context: &'a mut C, diagnostics: &'a mut Vec) -> Self { + pub fn new(context: &'a mut C) -> Self { Self { context, - diagnostics, placeholders: 0, } } @@ -652,7 +652,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } if !coercible { - self.diagnostics.push(cannot_coerce_to_string( + self.context.add_diagnostic(cannot_coerce_to_string( self.context.types(), ty, expr.span(), @@ -682,7 +682,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { expected = ty; expected_span = expr.span(); } else { - self.diagnostics.push(no_common_type( + self.context.add_diagnostic(no_common_type( self.context.types(), expected, expected_span, @@ -725,7 +725,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { // OK } _ => { - self.diagnostics.push(map_key_not_primitive( + self.context.add_diagnostic(map_key_not_primitive( self.context.types(), key.span(), expected_key, @@ -761,7 +761,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { expected_key = ty; expected_key_span = key.span(); } else { - self.diagnostics.push(no_common_type( + self.context.add_diagnostic(no_common_type( self.context.types(), expected_key, expected_key_span, @@ -776,7 +776,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { expected_value = ty; expected_value_span = value.span(); } else { - self.diagnostics.push(no_common_type( + self.context.add_diagnostic(no_common_type( self.context.types(), expected_value, expected_value_span, @@ -849,7 +849,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { present[index] = true; if let Some(actual) = self.evaluate_expr(&v) { if !actual.is_coercible_to(self.context.types(), &expected) { - self.diagnostics.push(type_mismatch( + self.context.add_diagnostic(type_mismatch( self.context.types(), expected, n.span(), @@ -860,8 +860,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } } else { // Not a struct member - self.diagnostics - .push(not_a_struct_member(name.as_str(), &n)); + self.context + .add_diagnostic(not_a_struct_member(name.as_str(), &n)); } } @@ -904,14 +904,14 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { count += 1; } - self.diagnostics - .push(missing_struct_members(&name, count, &members)); + self.context + .add_diagnostic(missing_struct_members(&name, count, &members)); } Some(ty) } Err(diagnostic) => { - self.diagnostics.push(diagnostic); + self.context.add_diagnostic(diagnostic); None } } @@ -928,7 +928,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.diagnostics.push(type_mismatch_custom( + self.context.add_diagnostic(type_mismatch_custom( self.context.types(), expected, name.span(), @@ -957,7 +957,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.diagnostics.push(type_mismatch_custom( + self.context.add_diagnostic(type_mismatch_custom( self.context.types(), expected, name.span(), @@ -994,7 +994,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.diagnostics.push(type_mismatch_custom( + self.context.add_diagnostic(type_mismatch_custom( self.context.types(), expected, name.span(), @@ -1056,7 +1056,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } { Some(ty) => ty, None => { - self.diagnostics.push(unknown_task_io( + self.context.add_diagnostic(unknown_task_io( self.context.task_name().expect("should have task name"), &name, io, @@ -1072,7 +1072,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { match s.members.get(name.as_str()) { Some(ty) => *ty, None => { - self.diagnostics.push(not_a_struct_member(&s.name, &name)); + self.context + .add_diagnostic(not_a_struct_member(&s.name, &name)); break; } } @@ -1094,7 +1095,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { ); } _ if names.peek().is_some() => { - self.diagnostics.push(not_a_struct(&name, i == 0)); + self.context.add_diagnostic(not_a_struct(&name, i == 0)); break; } _ => { @@ -1111,7 +1112,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { // The type of every item should be `hints` if !expr_ty.is_coercible_to(self.context.types(), &Type::Hints) { - self.diagnostics.push(type_mismatch( + self.context.add_diagnostic(type_mismatch( self.context.types(), Type::Hints, span.expect("should have span"), @@ -1128,7 +1129,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { // The conditional should be a boolean let cond_ty = self.evaluate_expr(&cond_expr).unwrap_or(Type::Union); if !cond_ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics.push(if_conditional_mismatch( + self.context.add_diagnostic(if_conditional_mismatch( self.context.types(), cond_ty, cond_expr.span(), @@ -1147,7 +1148,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { if let Some(ty) = true_ty.common_type(self.context.types_mut(), false_ty) { Some(ty) } else { - self.diagnostics.push(type_mismatch( + self.context.add_diagnostic(type_mismatch( self.context.types(), true_ty, true_expr.span(), @@ -1167,7 +1168,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { let operand = expr.operand(); let ty = self.evaluate_expr(&operand).unwrap_or(Type::Union); if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics.push(logical_not_mismatch( + self.context.add_diagnostic(logical_not_mismatch( self.context.types(), ty, operand.span(), @@ -1190,8 +1191,11 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Float.into()) { - self.diagnostics - .push(negation_mismatch(self.context.types(), ty, operand.span())); + self.context.add_diagnostic(negation_mismatch( + self.context.types(), + ty, + operand.span(), + )); // Type is indeterminate as the expression may evaluate to more than one type return None; } @@ -1206,14 +1210,14 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union); if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics - .push(logical_or_mismatch(self.context.types(), ty, lhs.span())); + self.context + .add_diagnostic(logical_or_mismatch(self.context.types(), ty, lhs.span())); } let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union); if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics - .push(logical_or_mismatch(self.context.types(), ty, rhs.span())); + self.context + .add_diagnostic(logical_or_mismatch(self.context.types(), ty, rhs.span())); } Some(PrimitiveTypeKind::Boolean.into()) @@ -1226,14 +1230,14 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { let ty = self.evaluate_expr(&lhs).unwrap_or(Type::Union); if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics - .push(logical_and_mismatch(self.context.types(), ty, lhs.span())); + self.context + .add_diagnostic(logical_and_mismatch(self.context.types(), ty, lhs.span())); } let ty = self.evaluate_expr(&rhs).unwrap_or(Type::Union); if !ty.is_coercible_to(self.context.types(), &PrimitiveTypeKind::Boolean.into()) { - self.diagnostics - .push(logical_and_mismatch(self.context.types(), ty, rhs.span())); + self.context + .add_diagnostic(logical_and_mismatch(self.context.types(), ty, rhs.span())); } Some(PrimitiveTypeKind::Boolean.into()) @@ -1334,7 +1338,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } // A type mismatch at this point - self.diagnostics.push(comparison_mismatch( + self.context.add_diagnostic(comparison_mismatch( self.context.types(), op, span, @@ -1411,14 +1415,17 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return Some(ty); } - self.diagnostics - .push(string_concat_mismatch(self.context.types(), other, span)); + self.context.add_diagnostic(string_concat_mismatch( + self.context.types(), + other, + span, + )); return None; } } if !lhs_ty.is_union() && !rhs_ty.is_union() { - self.diagnostics.push(numeric_mismatch( + self.context.add_diagnostic(numeric_mismatch( self.context.types(), op, span, @@ -1467,14 +1474,14 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return Some(binding.return_type()); } Err(FunctionBindError::RequiresVersion(minimum)) => { - self.diagnostics.push(unsupported_function( + self.context.add_diagnostic(unsupported_function( minimum, target.as_str(), target.span(), )); } Err(FunctionBindError::TooFewArguments(minimum)) => { - self.diagnostics.push(too_few_arguments( + self.context.add_diagnostic(too_few_arguments( target.as_str(), target.span(), minimum, @@ -1482,7 +1489,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { )); } Err(FunctionBindError::TooManyArguments(maximum)) => { - self.diagnostics.push(too_many_arguments( + self.context.add_diagnostic(too_many_arguments( target.as_str(), target.span(), maximum, @@ -1491,7 +1498,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { )); } Err(FunctionBindError::ArgumentTypeMismatch { index, expected }) => { - self.diagnostics.push(argument_type_mismatch( + self.context.add_diagnostic(argument_type_mismatch( self.context.types(), target.as_str(), &expected, @@ -1503,7 +1510,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { )); } Err(FunctionBindError::Ambiguous { first, second }) => { - self.diagnostics.push(ambiguous_argument( + self.context.add_diagnostic(ambiguous_argument( target.as_str(), target.span(), &first, @@ -1516,7 +1523,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { match f.param_min_max(self.context.version()) { Some((_, max)) => { assert!(max <= MAX_PARAMETERS); - self.diagnostics.push(too_many_arguments( + self.context.add_diagnostic(too_many_arguments( target.as_str(), target.span(), max, @@ -1525,7 +1532,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { )); } None => { - self.diagnostics.push(unsupported_function( + self.context.add_diagnostic(unsupported_function( f.minimum_version(), target.as_str(), target.span(), @@ -1537,8 +1544,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { Some(f.realize_unconstrained_return_type(self.context.types_mut(), arguments)) } None => { - self.diagnostics - .push(unknown_function(target.as_str(), target.span())); + self.context + .add_diagnostic(unknown_function(target.as_str(), target.span())); None } } @@ -1566,7 +1573,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { if let Some(expected_index_ty) = expected_index_ty { let index_ty = self.evaluate_expr(&index).unwrap_or(Type::Union); if !index_ty.is_coercible_to(self.context.types(), &expected_index_ty) { - self.diagnostics.push(index_type_mismatch( + self.context.add_diagnostic(index_type_mismatch( self.context.types(), expected_index_ty, index_ty, @@ -1578,8 +1585,11 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { match result_ty { Some(ty) => Some(ty), None => { - self.diagnostics - .push(cannot_index(self.context.types(), target_ty, target.span())); + self.context.add_diagnostic(cannot_index( + self.context.types(), + target_ty, + target.span(), + )); None } } @@ -1594,7 +1604,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return match task_member_type(name.as_str()) { Some(ty) => Some(ty), None => { - self.diagnostics.push(not_a_task_member(&name)); + self.context.add_diagnostic(not_a_task_member(&name)); return None; } }; @@ -1609,7 +1619,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return Some(*ty); } - self.diagnostics.push(not_a_struct_member(ty.name(), &name)); + self.context + .add_diagnostic(not_a_struct_member(ty.name(), &name)); return None; } @@ -1620,7 +1631,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { "left" => Some(ty.left_type), "right" => Some(ty.right_type), _ => { - self.diagnostics.push(not_a_pair_accessor(&name)); + self.context.add_diagnostic(not_a_pair_accessor(&name)); None } }; @@ -1632,8 +1643,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return Some(output.ty()); } - self.diagnostics - .push(unknown_call_io(ty, &name, Io::Output)); + self.context + .add_diagnostic(unknown_call_io(ty, &name, Io::Output)); return None; } } @@ -1644,8 +1655,8 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { return Some(Type::Union); } - self.diagnostics - .push(cannot_access(self.context.types(), ty, target.span())); + self.context + .add_diagnostic(cannot_access(self.context.types(), ty, target.span())); None } @@ -1727,7 +1738,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { _ => return, }; - self.diagnostics.push( + self.context.add_diagnostic( unnecessary_function_call(target.as_str(), target.span(), &label, span) .with_severity(severity) .with_fix(fix), diff --git a/wdl-analysis/tests/analysis.rs b/wdl-analysis/tests/analysis.rs index aec71d90..1d74d215 100644 --- a/wdl-analysis/tests/analysis.rs +++ b/wdl-analysis/tests/analysis.rs @@ -117,22 +117,22 @@ fn compare_results(test: &Path, results: Vec) -> Result<()> { let cwd = std::env::current_dir().expect("must have a CWD"); for result in results { // Attempt to strip the CWD from the result path - let path = result.uri().to_file_path(); + let path = result.document().uri().to_file_path(); let path: Cow<'_, str> = match &path { // Strip the CWD from the path Ok(path) => path.strip_prefix(&cwd).unwrap_or(path).to_string_lossy(), // Use the id itself if there is no path - Err(_) => result.uri().as_str().into(), + Err(_) => result.document().uri().as_str().into(), }; - let diagnostics: Cow<'_, [Diagnostic]> = match result.parse_result().error() { + let diagnostics: Cow<'_, [Diagnostic]> = match result.error() { Some(e) => vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into(), - None => result.diagnostics().into(), + None => result.document().diagnostics().into(), }; if !diagnostics.is_empty() { let source = result - .parse_result() + .document() .root() .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) .unwrap_or(String::new()); @@ -197,7 +197,8 @@ async fn main() { let results = results .iter() .filter_map(|r| { - r.uri() + r.document() + .uri() .to_file_path() .ok()? .starts_with(&base) diff --git a/wdl-engine/src/eval/v1.rs b/wdl-engine/src/eval/v1.rs index 37b66576..e4ab7b8e 100644 --- a/wdl-engine/src/eval/v1.rs +++ b/wdl-engine/src/eval/v1.rs @@ -636,26 +636,32 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { fn evaluate_if_expr(&mut self, expr: &IfExpr) -> Result { /// Used to translate an expression evaluation context to an expression /// type evaluation context. - struct TypeContext<'a, C: EvaluationContext>(&'a mut C); + struct TypeContext<'a, C: EvaluationContext> { + /// The expression evaluation context. + context: &'a mut C, + /// The diagnostics from evaluating the type of an expression. + diagnostics: Vec, + } + impl wdl_analysis::types::v1::EvaluationContext for TypeContext<'_, C> { fn version(&self) -> SupportedVersion { - self.0.version() + self.context.version() } fn types(&self) -> &wdl_analysis::types::Types { - self.0.types() + self.context.types() } fn types_mut(&mut self) -> &mut wdl_analysis::types::Types { - self.0.types_mut() + self.context.types_mut() } fn resolve_name(&self, name: &wdl_ast::Ident) -> Option { - self.0.resolve_name(name).map(|v| v.ty()).ok() + self.context.resolve_name(name).map(|v| v.ty()).ok() } fn resolve_type_name(&mut self, name: &wdl_ast::Ident) -> Result { - self.0.resolve_type_name(name) + self.context.resolve_type_name(name) } fn input(&self, _name: &str) -> Option { @@ -685,13 +691,16 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { fn diagnostics_config(&self) -> DiagnosticsConfig { DiagnosticsConfig::except_all() } + + fn add_diagnostic(&mut self, diagnostic: Diagnostic) { + self.diagnostics.push(diagnostic); + } } let (cond_expr, true_expr, false_expr) = expr.exprs(); // Evaluate the conditional expression and the true expression or the false // expression, depending on the result of the conditional expression - let mut diagnostics = Vec::new(); let cond = self.evaluate_expr(&cond_expr)?; let (value, true_ty, false_ty) = if cond .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) @@ -703,26 +712,39 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { // Evaluate the `true` expression and calculate the type of the `false` // expression let value = self.evaluate_expr(&true_expr)?; - let true_ty = value.ty(); - let false_ty = ExprTypeEvaluator::new(&mut TypeContext(self.context), &mut diagnostics) + let mut context = TypeContext { + context: self.context, + diagnostics: Vec::new(), + }; + let false_ty = ExprTypeEvaluator::new(&mut context) .evaluate_expr(&false_expr) .unwrap_or(Type::Union); + + if let Some(diagnostic) = context.diagnostics.pop() { + return Err(diagnostic); + } + + let true_ty = value.ty(); (value, true_ty, false_ty) } else { // Evaluate the `false` expression and calculate the type of the `true` // expression let value = self.evaluate_expr(&false_expr)?; - let true_ty = ExprTypeEvaluator::new(&mut TypeContext(self.context), &mut diagnostics) + let mut context = TypeContext { + context: self.context, + diagnostics: Vec::new(), + }; + let true_ty = ExprTypeEvaluator::new(&mut context) .evaluate_expr(&true_expr) .unwrap_or(Type::Union); + if let Some(diagnostic) = context.diagnostics.pop() { + return Err(diagnostic); + } + let false_ty = value.ty(); (value, true_ty, false_ty) }; - if let Some(diagnostic) = diagnostics.pop() { - return Err(diagnostic); - } - // Determine the common type of the true and false expressions // The value must be coerced to that type let ty = true_ty diff --git a/wdl-engine/tests/inputs.rs b/wdl-engine/tests/inputs.rs index 3c4949be..e0e77ab7 100644 --- a/wdl-engine/tests/inputs.rs +++ b/wdl-engine/tests/inputs.rs @@ -114,22 +114,22 @@ fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { let mut buffer = Buffer::no_color(); // Attempt to strip the CWD from the result path - let path = result.uri().to_file_path(); + let path = result.document().uri().to_file_path(); let path: Cow<'_, str> = match &path { // Strip the CWD from the path Ok(path) => path.strip_prefix(&cwd).unwrap_or(path).to_string_lossy(), // Use the id itself if there is no path - Err(_) => result.uri().as_str().into(), + Err(_) => result.document().uri().as_str().into(), }; - let diagnostics: Cow<'_, [Diagnostic]> = match result.parse_result().error() { + let diagnostics: Cow<'_, [Diagnostic]> = match result.error() { Some(e) => vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into(), - None => result.diagnostics().into(), + None => result.document().diagnostics().into(), }; if let Some(diagnostic) = diagnostics.iter().find(|d| d.severity() == Severity::Error) { let source = result - .parse_result() + .document() .root() .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) .unwrap_or_default(); @@ -213,7 +213,7 @@ async fn main() { let base = clean(absolute(test).expect("should be made absolute")); let mut results = results.iter().filter_map(|r| { - if r.uri().to_file_path().ok()?.starts_with(&base) { + if r.document().uri().to_file_path().ok()?.starts_with(&base) { Some(r.clone()) } else { None diff --git a/wdl-lsp/src/proto.rs b/wdl-lsp/src/proto.rs index 3a7e7972..97082bd6 100644 --- a/wdl-lsp/src/proto.rs +++ b/wdl-lsp/src/proto.rs @@ -119,10 +119,10 @@ pub fn document_diagnostic_report( ) -> Option { let result = results .iter() - .find(|r| r.uri().as_ref() == ¶ms.text_document.uri)?; + .find(|r| r.document().uri().as_ref() == ¶ms.text_document.uri)?; if let Some(previous) = params.previous_result_id { - if &previous == result.id().as_ref() { + if &previous == result.document().id().as_ref() { debug!( "diagnostics for document `{uri}` have not changed (client has latest)", uri = params.text_document.uri, @@ -144,15 +144,13 @@ pub fn document_diagnostic_report( } let items = result + .document() .diagnostics() .iter() .map(|d| { diagnostic( - result.uri(), - result - .parse_result() - .lines() - .expect("should have line index"), + result.document().uri(), + result.lines().expect("should have line index"), source, d, ) @@ -164,7 +162,7 @@ pub fn document_diagnostic_report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { related_documents: None, full_document_diagnostic_report: FullDocumentDiagnosticReport { - result_id: Some(result.id().as_ref().clone()), + result_id: Some(result.document().id().as_ref().clone()), items, }, }), @@ -186,23 +184,23 @@ pub fn workspace_diagnostic_report( let mut items = Vec::new(); for result in results { // Only store local file results - if result.uri().scheme() != "file" { + if result.document().uri().scheme() != "file" { continue; } - if let Some(previous) = ids.get(result.uri()) { - if previous == result.id().as_ref() { + if let Some(previous) = ids.get(result.document().uri()) { + if previous == result.document().id().as_ref() { debug!( "diagnostics for document `{uri}` have not changed (client has latest)", - uri = result.uri(), + uri = result.document().uri(), ); items.push(WorkspaceDocumentDiagnosticReport::Unchanged( WorkspaceUnchangedDocumentDiagnosticReport { - uri: result.uri().as_ref().clone(), - version: result.parse_result().version().map(|v| v as i64), + uri: result.document().uri().as_ref().clone(), + version: result.version().map(|v| v as i64), unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { - result_id: result.id().as_ref().clone(), + result_id: result.document().id().as_ref().clone(), }, }, )); @@ -212,19 +210,17 @@ pub fn workspace_diagnostic_report( debug!( "diagnostics for document `{uri}` have changed since last client request", - uri = result.uri() + uri = result.document().uri() ); let diagnostics = result + .document() .diagnostics() .iter() .filter_map(|d| { diagnostic( - result.uri(), - result - .parse_result() - .lines() - .expect("should have line index"), + result.document().uri(), + result.lines().expect("should have line index"), source, d, ) @@ -234,10 +230,10 @@ pub fn workspace_diagnostic_report( items.push(WorkspaceDocumentDiagnosticReport::Full( WorkspaceFullDocumentDiagnosticReport { - uri: result.uri().as_ref().clone(), - version: result.parse_result().version().map(|v| v as i64), + uri: result.document().uri().as_ref().clone(), + version: result.version().map(|v| v as i64), full_document_diagnostic_report: FullDocumentDiagnosticReport { - result_id: Some(result.id().as_ref().clone()), + result_id: Some(result.document().id().as_ref().clone()), items: diagnostics, }, }, diff --git a/wdl/src/bin/wdl.rs b/wdl/src/bin/wdl.rs index d69544af..53f952e6 100644 --- a/wdl/src/bin/wdl.rs +++ b/wdl/src/bin/wdl.rs @@ -129,28 +129,28 @@ async fn analyze>( let mut errors = 0; let cwd = std::env::current_dir().ok(); for result in &results { - let path = result.uri().to_file_path().ok(); + let path = result.document().uri().to_file_path().ok(); // Attempt to strip the CWD from the result path let path = match (&cwd, &path) { // Use the id itself if there is no path - (_, None) => result.uri().as_str().into(), + (_, None) => result.document().uri().as_str().into(), // Use just the path if there's no CWD (None, Some(path)) => path.to_string_lossy(), // Strip the CWD from the path (Some(cwd), Some(path)) => path.strip_prefix(cwd).unwrap_or(path).to_string_lossy(), }; - let diagnostics: Cow<'_, [Diagnostic]> = match result.parse_result().error() { + let diagnostics: Cow<'_, [Diagnostic]> = match result.error() { Some(e) => vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into(), - None => result.diagnostics().into(), + None => result.document().diagnostics().into(), }; if !diagnostics.is_empty() { errors += emit_diagnostics( &path, &result - .parse_result() + .document() .root() .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) .unwrap_or(String::new()), From cd3df120484164234c606fe16ba98f5212cb9122 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Fri, 29 Nov 2024 11:16:01 -0800 Subject: [PATCH 02/11] feat: implement WDL 1.2 task evaluation. This commit adds evaluation of WDL tasks with full 1.2 support. It completes 1.2 expression evaluation except for accessing call values; those will be implemented along with workflow evaluation. It includes several big refactorings to `wdl-analysis` to facilitate in evaluation, namely storing diagnostics, id, and uri in `Document` instead of `AnalysisResult`. Evaluation of string and command literals now properly unescape any escape sequences. Task evaluation tests were implemented and derived from examples taken directly from the WDL 1.2 spec. Storage of compound types has been changed such that empty compound types (e.g. `[]`, `{}`, `object {}`) do not cause an allocation. Whitespace stripping for multiline strings and commands has been refactored to reduce the number of allocations made. --- Cargo.toml | 1 + gauntlet/src/lib.rs | 7 +- wdl-analysis/src/diagnostics.rs | 4 +- wdl-analysis/src/document.rs | 45 +- wdl-analysis/src/document/v1.rs | 290 +- wdl-analysis/src/types.rs | 77 +- wdl-analysis/src/types/v1.rs | 95 +- wdl-analysis/tests/analysis.rs | 12 +- .../analysis/conflicting-struct-names/bar.wdl | 2 +- .../analysis/conflicting-struct-names/foo.wdl | 2 +- .../analysis/import-same-struct/a/file.wdl | 7 + .../analysis/import-same-struct/b/file.wdl | 7 + .../import-same-struct/source.diagnostics | 0 .../analysis/import-same-struct/source.wdl | 17 + wdl-ast/src/v1/expr.rs | 220 +- wdl-ast/src/v1/task.rs | 182 +- wdl-ast/tests/validation.rs | 5 +- wdl-doc/src/lib.rs | 6 +- wdl-engine/Cargo.toml | 13 +- wdl-engine/src/backend.rs | 121 + wdl-engine/src/backend/local.rs | 285 ++ wdl-engine/src/diagnostics.rs | 41 +- wdl-engine/src/engine.rs | 122 +- wdl-engine/src/eval.rs | 220 +- wdl-engine/src/eval/v1.rs | 2848 +-------------- wdl-engine/src/eval/v1/expr.rs | 3068 +++++++++++++++++ wdl-engine/src/eval/v1/task.rs | 929 +++++ wdl-engine/src/inputs.rs | 334 +- wdl-engine/src/lib.rs | 2 + wdl-engine/src/outputs.rs | 102 +- wdl-engine/src/stdlib.rs | 16 +- wdl-engine/src/stdlib/as_map.rs | 9 +- wdl-engine/src/stdlib/as_pairs.rs | 15 +- wdl-engine/src/stdlib/chunk.rs | 34 +- wdl-engine/src/stdlib/collect_by_key.rs | 13 +- wdl-engine/src/stdlib/contains.rs | 2 +- wdl-engine/src/stdlib/contains_key.rs | 21 +- wdl-engine/src/stdlib/cross.rs | 14 +- wdl-engine/src/stdlib/flatten.rs | 12 +- wdl-engine/src/stdlib/glob.rs | 20 +- wdl-engine/src/stdlib/join_paths.rs | 4 +- wdl-engine/src/stdlib/keys.rs | 24 +- wdl-engine/src/stdlib/length.rs | 2 +- wdl-engine/src/stdlib/prefix.rs | 16 +- wdl-engine/src/stdlib/quote.rs | 20 +- wdl-engine/src/stdlib/range.rs | 10 +- wdl-engine/src/stdlib/read_boolean.rs | 2 +- wdl-engine/src/stdlib/read_float.rs | 2 +- wdl-engine/src/stdlib/read_int.rs | 2 +- wdl-engine/src/stdlib/read_json.rs | 17 +- wdl-engine/src/stdlib/read_lines.rs | 9 +- wdl-engine/src/stdlib/read_map.rs | 5 +- wdl-engine/src/stdlib/read_object.rs | 2 +- wdl-engine/src/stdlib/read_objects.rs | 13 +- wdl-engine/src/stdlib/read_string.rs | 9 +- wdl-engine/src/stdlib/read_tsv.rs | 34 +- wdl-engine/src/stdlib/select_all.rs | 14 +- wdl-engine/src/stdlib/select_first.rs | 6 +- wdl-engine/src/stdlib/sep.rs | 2 +- wdl-engine/src/stdlib/size.rs | 37 +- wdl-engine/src/stdlib/squote.rs | 16 +- wdl-engine/src/stdlib/stderr.rs | 2 +- wdl-engine/src/stdlib/stdout.rs | 2 +- wdl-engine/src/stdlib/suffix.rs | 16 +- wdl-engine/src/stdlib/transpose.rs | 22 +- wdl-engine/src/stdlib/unzip.rs | 12 +- wdl-engine/src/stdlib/values.rs | 9 +- wdl-engine/src/stdlib/write_json.rs | 4 +- wdl-engine/src/stdlib/write_lines.rs | 8 +- wdl-engine/src/stdlib/write_map.rs | 8 +- wdl-engine/src/stdlib/write_object.rs | 18 +- wdl-engine/src/stdlib/write_objects.rs | 44 +- wdl-engine/src/stdlib/write_tsv.rs | 36 +- wdl-engine/src/stdlib/zip.rs | 14 +- wdl-engine/src/units.rs | 40 +- wdl-engine/src/value.rs | 1336 +++++-- wdl-engine/tests/inputs.rs | 155 +- .../tests/inputs/mising-call-input/error.txt | 4 - .../error.txt | 2 +- .../inputs.json | 0 .../source.wdl | 0 wdl-engine/tests/tasks.rs | 438 +++ .../tests/tasks/array-access/inputs.json | 4 + .../tests/tasks/array-access/outputs.json | 3 + .../tests/tasks/array-access/source.wdl | 14 + wdl-engine/tests/tasks/array-access/stderr | 0 wdl-engine/tests/tasks/array-access/stdout | 0 .../tasks/array-map-equality/inputs.json | 1 + .../tasks/array-map-equality/outputs.json | 6 + .../tests/tasks/array-map-equality/source.wdl | 15 + .../tests/tasks/array-map-equality/stderr | 0 .../tests/tasks/array-map-equality/stdout | 0 .../tests/tasks/compare-coerced/inputs.json | 1 + .../tests/tasks/compare-coerced/outputs.json | 3 + .../tests/tasks/compare-coerced/source.wdl | 14 + wdl-engine/tests/tasks/compare-coerced/stderr | 0 wdl-engine/tests/tasks/compare-coerced/stdout | 0 .../tests/tasks/compare-optionals/inputs.json | 1 + .../tasks/compare-optionals/outputs.json | 6 + .../tests/tasks/compare-optionals/source.wdl | 19 + .../tests/tasks/compare-optionals/stderr | 0 .../tests/tasks/compare-optionals/stdout | 0 .../tests/tasks/concat-optional/inputs.json | 1 + .../tests/tasks/concat-optional/outputs.json | 4 + .../tests/tasks/concat-optional/source.wdl | 21 + wdl-engine/tests/tasks/concat-optional/stderr | 0 wdl-engine/tests/tasks/concat-optional/stdout | 0 .../tests/tasks/declarations/inputs.json | 5 + .../tests/tasks/declarations/outputs.json | 3 + .../tests/tasks/declarations/source.wdl | 19 + wdl-engine/tests/tasks/declarations/stderr | 0 wdl-engine/tests/tasks/declarations/stdout | 0 .../tasks/default-option-task/files/result1 | 1 + .../tasks/default-option-task/files/result2 | 1 + .../tasks/default-option-task/files/result3 | 1 + .../tasks/default-option-task/inputs.json | 1 + .../tasks/default-option-task/outputs.json | 4 + .../tasks/default-option-task/source.wdl | 18 + .../tests/tasks/default-option-task/stderr | 0 .../tests/tasks/default-option-task/stdout | 0 .../tasks/expressions-task/files/hello.txt | 1 + .../tests/tasks/expressions-task/inputs.json | 3 + .../tests/tasks/expressions-task/outputs.json | 11 + .../tests/tasks/expressions-task/source.wdl | 23 + .../tests/tasks/expressions-task/stderr | 0 .../tests/tasks/expressions-task/stdout | 0 .../tasks/file-output-task/files/foo.goodbye | 1 + .../tasks/file-output-task/files/foo.hello | 1 + .../tests/tasks/file-output-task/inputs.json | 3 + .../tests/tasks/file-output-task/outputs.json | 6 + .../tests/tasks/file-output-task/source.wdl | 16 + .../tests/tasks/file-output-task/stderr | 0 .../tests/tasks/file-output-task/stdout | 0 .../tests/tasks/flags-task/greetings.txt | 3 + wdl-engine/tests/tasks/flags-task/inputs.json | 4 + .../tests/tasks/flags-task/outputs.json | 3 + wdl-engine/tests/tasks/flags-task/source.wdl | 24 + wdl-engine/tests/tasks/flags-task/stderr | 0 wdl-engine/tests/tasks/flags-task/stdout | 1 + .../tests/tasks/glob-task/files/file_1.txt | 1 + .../tests/tasks/glob-task/files/file_2.txt | 1 + .../tests/tasks/glob-task/files/file_3.txt | 1 + wdl-engine/tests/tasks/glob-task/inputs.json | 3 + wdl-engine/tests/tasks/glob-task/outputs.json | 8 + wdl-engine/tests/tasks/glob-task/source.wdl | 18 + wdl-engine/tests/tasks/glob-task/stderr | 0 wdl-engine/tests/tasks/glob-task/stdout | 0 wdl-engine/tests/tasks/hello/greetings.txt | 3 + wdl-engine/tests/tasks/hello/inputs.json | 4 + wdl-engine/tests/tasks/hello/outputs.json | 6 + wdl-engine/tests/tasks/hello/source.wdl | 20 + wdl-engine/tests/tasks/hello/stderr | 0 wdl-engine/tests/tasks/hello/stdout | 2 + .../tests/tasks/import-structs/inputs.json | 1 + .../tests/tasks/import-structs/outputs.json | 3 + .../tests/tasks/import-structs/source.wdl | 78 + wdl-engine/tests/tasks/import-structs/stderr | 0 wdl-engine/tests/tasks/import-structs/stdout | 1 + .../tasks/input-type-qualifiers/files/result | 3 + .../tasks/input-type-qualifiers/inputs.json | 10 + .../tasks/input-type-qualifiers/outputs.json | 7 + .../tasks/input-type-qualifiers/source.wdl | 33 + .../tests/tasks/input-type-qualifiers/stderr | 0 .../tests/tasks/input-type-qualifiers/stdout | 0 .../tests/tasks/member-access/inputs.json | 1 + .../tests/tasks/member-access/outputs.json | 4 + .../tests/tasks/member-access/source.wdl | 18 + wdl-engine/tests/tasks/member-access/stderr | 0 wdl-engine/tests/tasks/member-access/stdout | 1 + .../tests/tasks/missing-output-file/error.txt | 9 + .../tasks/missing-output-file/inputs.json | 1 + .../tasks/missing-output-file/source.wdl | 11 + .../tests/tasks/missing-output-file/stderr | 0 .../tests/tasks/missing-output-file/stdout | 1 + .../tasks/multiline-placeholders/inputs.json | 1 + .../tasks/multiline-placeholders/outputs.json | 6 + .../tasks/multiline-placeholders/source.wdl | 18 + .../tests/tasks/multiline-placeholders/stderr | 0 .../tests/tasks/multiline-placeholders/stdout | 0 .../tasks/multiline-strings1/inputs.json | 1 + .../tasks/multiline-strings1/outputs.json | 3 + .../tests/tasks/multiline-strings1/source.wdl | 12 + .../tests/tasks/multiline-strings1/stderr | 0 .../tests/tasks/multiline-strings1/stdout | 0 .../tasks/multiline-strings2/inputs.json | 1 + .../tasks/multiline-strings2/outputs.json | 10 + .../tests/tasks/multiline-strings2/source.wdl | 34 + .../tests/tasks/multiline-strings2/stderr | 0 .../tests/tasks/multiline-strings2/stdout | 0 .../tasks/multiline-strings3/inputs.json | 1 + .../tasks/multiline-strings3/outputs.json | 6 + .../tests/tasks/multiline-strings3/source.wdl | 39 + .../tests/tasks/multiline-strings3/stderr | 0 .../tests/tasks/multiline-strings3/stdout | 0 .../tasks/multiline-strings4/inputs.json | 1 + .../tasks/multiline-strings4/outputs.json | 3 + .../tests/tasks/multiline-strings4/source.wdl | 11 + .../tests/tasks/multiline-strings4/stderr | 0 .../tests/tasks/multiline-strings4/stdout | 0 .../tests/tasks/nested-access/inputs.json | 26 + .../tests/tasks/nested-access/outputs.json | 6 + .../tests/tasks/nested-access/source.wdl | 27 + wdl-engine/tests/tasks/nested-access/stderr | 0 wdl-engine/tests/tasks/nested-access/stdout | 0 .../tasks/nested-placeholders/inputs.json | 4 + .../tasks/nested-placeholders/outputs.json | 3 + .../tasks/nested-placeholders/source.wdl | 14 + .../tests/tasks/nested-placeholders/stderr | 0 .../tests/tasks/nested-placeholders/stdout | 0 .../tasks/non-empty-optional/inputs.json | 1 + .../tasks/non-empty-optional/outputs.json | 13 + .../tests/tasks/non-empty-optional/source.wdl | 15 + .../tests/tasks/non-empty-optional/stderr | 0 .../tests/tasks/non-empty-optional/stdout | 0 .../optional-output-task/files/example1.txt | 1 + .../tasks/optional-output-task/inputs.json | 3 + .../tasks/optional-output-task/outputs.json | 9 + .../tasks/optional-output-task/source.wdl | 21 + .../tests/tasks/optional-output-task/stderr | 0 .../tests/tasks/optional-output-task/stdout | 0 wdl-engine/tests/tasks/optionals/inputs.json | 1 + wdl-engine/tests/tasks/optionals/outputs.json | 7 + wdl-engine/tests/tasks/optionals/source.wdl | 22 + wdl-engine/tests/tasks/optionals/stderr | 0 wdl-engine/tests/tasks/optionals/stdout | 0 .../tests/tasks/outputs-task/files/a.csv | 0 .../tests/tasks/outputs-task/files/b.csv | 0 .../tasks/outputs-task/files/threshold.txt | 1 + .../tests/tasks/outputs-task/inputs.json | 3 + .../tests/tasks/outputs-task/outputs.json | 8 + .../tests/tasks/outputs-task/source.wdl | 18 + wdl-engine/tests/tasks/outputs-task/stderr | 0 wdl-engine/tests/tasks/outputs-task/stdout | 0 .../tasks/person-struct-task/inputs.json | 16 + .../tasks/person-struct-task/outputs.json | 3 + .../tests/tasks/person-struct-task/source.wdl | 53 + .../tests/tasks/person-struct-task/stderr | 0 .../tests/tasks/person-struct-task/stdout | 2 + .../tasks/placeholder-coercion/inputs.json | 1 + .../tasks/placeholder-coercion/outputs.json | 9 + .../tasks/placeholder-coercion/source.wdl | 18 + .../tests/tasks/placeholder-coercion/stderr | 0 .../tests/tasks/placeholder-coercion/stdout | 0 .../tests/tasks/placeholder-none/inputs.json | 1 + .../tests/tasks/placeholder-none/outputs.json | 4 + .../tests/tasks/placeholder-none/source.wdl | 13 + .../tests/tasks/placeholder-none/stderr | 0 .../tests/tasks/placeholder-none/stdout | 0 .../tests/tasks/placeholders/inputs.json | 5 + .../tests/tasks/placeholders/outputs.json | 4 + .../tests/tasks/placeholders/source.wdl | 17 + wdl-engine/tests/tasks/placeholders/stderr | 0 wdl-engine/tests/tasks/placeholders/stdout | 0 .../files/testdir/hello.txt | 1 + .../tasks/primitive-literals/inputs.json | 1 + .../tasks/primitive-literals/outputs.json | 8 + .../tests/tasks/primitive-literals/source.wdl | 17 + .../tests/tasks/primitive-literals/stderr | 0 .../tests/tasks/primitive-literals/stdout | 0 .../tasks/primitive-to-string/inputs.json | 3 + .../tasks/primitive-to-string/outputs.json | 3 + .../tasks/primitive-to-string/source.wdl | 13 + .../tests/tasks/primitive-to-string/stderr | 0 .../tests/tasks/primitive-to-string/stdout | 0 .../private-declaration-task/inputs.json | 8 + .../private-declaration-task/outputs.json | 7 + .../tasks/private-declaration-task/source.wdl | 18 + .../tasks/private-declaration-task/stderr | 0 .../tasks/private-declaration-task/stdout | 3 + .../files/my/path/to/something.txt | 1 + .../relative-and-absolute-task/inputs.json | 1 + .../relative-and-absolute-task/outputs.json | 3 + .../relative-and-absolute-task/source.wdl | 16 + .../tasks/relative-and-absolute-task/stderr | 0 .../tasks/relative-and-absolute-task/stdout | 0 .../tasks/seo-option-to-function/inputs.json | 12 + .../tasks/seo-option-to-function/outputs.json | 4 + .../tasks/seo-option-to-function/source.wdl | 15 + .../tests/tasks/seo-option-to-function/stderr | 0 .../tests/tasks/seo-option-to-function/stdout | 0 .../tests/tasks/string-to-file/inputs.json | 1 + .../tests/tasks/string-to-file/outputs.json | 3 + .../tests/tasks/string-to-file/source.wdl | 15 + wdl-engine/tests/tasks/string-to-file/stderr | 0 wdl-engine/tests/tasks/string-to-file/stdout | 0 .../tests/tasks/struct-to-struct/inputs.json | 1 + .../tests/tasks/struct-to-struct/outputs.json | 8 + .../tests/tasks/struct-to-struct/source.wdl | 36 + .../tests/tasks/struct-to-struct/stderr | 0 .../tests/tasks/struct-to-struct/stdout | 0 wdl-engine/tests/tasks/sum-task/inputs.json | 7 + wdl-engine/tests/tasks/sum-task/outputs.json | 3 + wdl-engine/tests/tasks/sum-task/source.wdl | 16 + wdl-engine/tests/tasks/sum-task/stderr | 0 wdl-engine/tests/tasks/sum-task/stdout | 1 + wdl-engine/tests/tasks/task-fail/error.txt | 1 + wdl-engine/tests/tasks/task-fail/inputs.json | 1 + wdl-engine/tests/tasks/task-fail/source.wdl | 8 + wdl-engine/tests/tasks/task-fail/stderr | 1 + wdl-engine/tests/tasks/task-fail/stdout | 0 .../tests/tasks/task-inputs-task/inputs.json | 3 + .../tests/tasks/task-inputs-task/outputs.json | 1 + .../tests/tasks/task-inputs-task/source.wdl | 19 + .../tests/tasks/task-inputs-task/stderr | 0 .../tests/tasks/task-inputs-task/stdout | 1 + .../tasks/task-with-comments/inputs.json | 3 + .../tasks/task-with-comments/outputs.json | 3 + .../tests/tasks/task-with-comments/source.wdl | 27 + .../tests/tasks/task-with-comments/stderr | 0 .../tests/tasks/task-with-comments/stdout | 1 + wdl-engine/tests/tasks/ternary/inputs.json | 3 + wdl-engine/tests/tasks/ternary/outputs.json | 3 + wdl-engine/tests/tasks/ternary/source.wdl | 24 + wdl-engine/tests/tasks/ternary/stderr | 0 wdl-engine/tests/tasks/ternary/stdout | 0 wdl-engine/tests/tasks/test-map/inputs.json | 1 + wdl-engine/tests/tasks/test-map/outputs.json | 9 + wdl-engine/tests/tasks/test-map/source.wdl | 18 + wdl-engine/tests/tasks/test-map/stderr | 0 wdl-engine/tests/tasks/test-map/stdout | 0 .../tests/tasks/test-object/inputs.json | 1 + .../tests/tasks/test-object/outputs.json | 7 + wdl-engine/tests/tasks/test-object/source.wdl | 13 + wdl-engine/tests/tasks/test-object/stderr | 0 wdl-engine/tests/tasks/test-object/stdout | 0 wdl-engine/tests/tasks/test-pairs/inputs.json | 1 + .../tests/tasks/test-pairs/outputs.json | 4 + wdl-engine/tests/tasks/test-pairs/source.wdl | 12 + wdl-engine/tests/tasks/test-pairs/stderr | 0 wdl-engine/tests/tasks/test-pairs/stdout | 0 .../test-placeholders-task/greetings.txt | 5 + .../tasks/test-placeholders-task/inputs.json | 3 + .../tasks/test-placeholders-task/outputs.json | 3 + .../tasks/test-placeholders-task/source.wdl | 20 + .../tests/tasks/test-placeholders-task/stderr | 0 .../tests/tasks/test-placeholders-task/stdout | 1 + .../tests/tasks/test-struct/inputs.json | 1 + .../tests/tasks/test-struct/outputs.json | 18 + wdl-engine/tests/tasks/test-struct/source.wdl | 32 + wdl-engine/tests/tasks/test-struct/stderr | 0 wdl-engine/tests/tasks/test-struct/stdout | 0 .../tasks/true-false-ternary/files/result1 | 1 + .../tasks/true-false-ternary/files/result2 | 1 + .../tasks/true-false-ternary/inputs.json | 4 + .../tasks/true-false-ternary/outputs.json | 3 + .../tests/tasks/true-false-ternary/source.wdl | 18 + .../tests/tasks/true-false-ternary/stderr | 0 .../tests/tasks/true-false-ternary/stdout | 0 wdl-format/tests/format.rs | 5 +- wdl-grammar/tests/parsing.rs | 5 +- wdl-lint/tests/lints.rs | 5 +- wdl/Cargo.toml | 9 +- wdl/src/bin/wdl.rs | 213 +- wdl/src/lib.rs | 3 + 354 files changed, 9278 insertions(+), 4109 deletions(-) create mode 100644 wdl-analysis/tests/analysis/import-same-struct/a/file.wdl create mode 100644 wdl-analysis/tests/analysis/import-same-struct/b/file.wdl create mode 100644 wdl-analysis/tests/analysis/import-same-struct/source.diagnostics create mode 100644 wdl-analysis/tests/analysis/import-same-struct/source.wdl create mode 100644 wdl-engine/src/backend.rs create mode 100644 wdl-engine/src/backend/local.rs create mode 100644 wdl-engine/src/eval/v1/expr.rs create mode 100644 wdl-engine/src/eval/v1/task.rs rename wdl-engine/tests/inputs/{unknown-call-1.2 => unknown-call-1_2}/error.txt (64%) rename wdl-engine/tests/inputs/{unknown-call-1.2 => unknown-call-1_2}/inputs.json (100%) rename wdl-engine/tests/inputs/{unknown-call-1.2 => unknown-call-1_2}/source.wdl (100%) create mode 100644 wdl-engine/tests/tasks.rs create mode 100644 wdl-engine/tests/tasks/array-access/inputs.json create mode 100644 wdl-engine/tests/tasks/array-access/outputs.json create mode 100644 wdl-engine/tests/tasks/array-access/source.wdl create mode 100644 wdl-engine/tests/tasks/array-access/stderr create mode 100644 wdl-engine/tests/tasks/array-access/stdout create mode 100644 wdl-engine/tests/tasks/array-map-equality/inputs.json create mode 100644 wdl-engine/tests/tasks/array-map-equality/outputs.json create mode 100644 wdl-engine/tests/tasks/array-map-equality/source.wdl create mode 100644 wdl-engine/tests/tasks/array-map-equality/stderr create mode 100644 wdl-engine/tests/tasks/array-map-equality/stdout create mode 100644 wdl-engine/tests/tasks/compare-coerced/inputs.json create mode 100644 wdl-engine/tests/tasks/compare-coerced/outputs.json create mode 100644 wdl-engine/tests/tasks/compare-coerced/source.wdl create mode 100644 wdl-engine/tests/tasks/compare-coerced/stderr create mode 100644 wdl-engine/tests/tasks/compare-coerced/stdout create mode 100644 wdl-engine/tests/tasks/compare-optionals/inputs.json create mode 100644 wdl-engine/tests/tasks/compare-optionals/outputs.json create mode 100644 wdl-engine/tests/tasks/compare-optionals/source.wdl create mode 100644 wdl-engine/tests/tasks/compare-optionals/stderr create mode 100644 wdl-engine/tests/tasks/compare-optionals/stdout create mode 100644 wdl-engine/tests/tasks/concat-optional/inputs.json create mode 100644 wdl-engine/tests/tasks/concat-optional/outputs.json create mode 100644 wdl-engine/tests/tasks/concat-optional/source.wdl create mode 100644 wdl-engine/tests/tasks/concat-optional/stderr create mode 100644 wdl-engine/tests/tasks/concat-optional/stdout create mode 100644 wdl-engine/tests/tasks/declarations/inputs.json create mode 100644 wdl-engine/tests/tasks/declarations/outputs.json create mode 100644 wdl-engine/tests/tasks/declarations/source.wdl create mode 100644 wdl-engine/tests/tasks/declarations/stderr create mode 100644 wdl-engine/tests/tasks/declarations/stdout create mode 100644 wdl-engine/tests/tasks/default-option-task/files/result1 create mode 100644 wdl-engine/tests/tasks/default-option-task/files/result2 create mode 100644 wdl-engine/tests/tasks/default-option-task/files/result3 create mode 100644 wdl-engine/tests/tasks/default-option-task/inputs.json create mode 100644 wdl-engine/tests/tasks/default-option-task/outputs.json create mode 100644 wdl-engine/tests/tasks/default-option-task/source.wdl create mode 100644 wdl-engine/tests/tasks/default-option-task/stderr create mode 100644 wdl-engine/tests/tasks/default-option-task/stdout create mode 100644 wdl-engine/tests/tasks/expressions-task/files/hello.txt create mode 100644 wdl-engine/tests/tasks/expressions-task/inputs.json create mode 100644 wdl-engine/tests/tasks/expressions-task/outputs.json create mode 100644 wdl-engine/tests/tasks/expressions-task/source.wdl create mode 100644 wdl-engine/tests/tasks/expressions-task/stderr create mode 100644 wdl-engine/tests/tasks/expressions-task/stdout create mode 100644 wdl-engine/tests/tasks/file-output-task/files/foo.goodbye create mode 100644 wdl-engine/tests/tasks/file-output-task/files/foo.hello create mode 100644 wdl-engine/tests/tasks/file-output-task/inputs.json create mode 100644 wdl-engine/tests/tasks/file-output-task/outputs.json create mode 100644 wdl-engine/tests/tasks/file-output-task/source.wdl create mode 100644 wdl-engine/tests/tasks/file-output-task/stderr create mode 100644 wdl-engine/tests/tasks/file-output-task/stdout create mode 100644 wdl-engine/tests/tasks/flags-task/greetings.txt create mode 100644 wdl-engine/tests/tasks/flags-task/inputs.json create mode 100644 wdl-engine/tests/tasks/flags-task/outputs.json create mode 100644 wdl-engine/tests/tasks/flags-task/source.wdl create mode 100644 wdl-engine/tests/tasks/flags-task/stderr create mode 100644 wdl-engine/tests/tasks/flags-task/stdout create mode 100644 wdl-engine/tests/tasks/glob-task/files/file_1.txt create mode 100644 wdl-engine/tests/tasks/glob-task/files/file_2.txt create mode 100644 wdl-engine/tests/tasks/glob-task/files/file_3.txt create mode 100644 wdl-engine/tests/tasks/glob-task/inputs.json create mode 100644 wdl-engine/tests/tasks/glob-task/outputs.json create mode 100644 wdl-engine/tests/tasks/glob-task/source.wdl create mode 100644 wdl-engine/tests/tasks/glob-task/stderr create mode 100644 wdl-engine/tests/tasks/glob-task/stdout create mode 100644 wdl-engine/tests/tasks/hello/greetings.txt create mode 100644 wdl-engine/tests/tasks/hello/inputs.json create mode 100644 wdl-engine/tests/tasks/hello/outputs.json create mode 100644 wdl-engine/tests/tasks/hello/source.wdl create mode 100644 wdl-engine/tests/tasks/hello/stderr create mode 100644 wdl-engine/tests/tasks/hello/stdout create mode 100644 wdl-engine/tests/tasks/import-structs/inputs.json create mode 100644 wdl-engine/tests/tasks/import-structs/outputs.json create mode 100644 wdl-engine/tests/tasks/import-structs/source.wdl create mode 100644 wdl-engine/tests/tasks/import-structs/stderr create mode 100644 wdl-engine/tests/tasks/import-structs/stdout create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/files/result create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/inputs.json create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/outputs.json create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/source.wdl create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/stderr create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/stdout create mode 100644 wdl-engine/tests/tasks/member-access/inputs.json create mode 100644 wdl-engine/tests/tasks/member-access/outputs.json create mode 100644 wdl-engine/tests/tasks/member-access/source.wdl create mode 100644 wdl-engine/tests/tasks/member-access/stderr create mode 100644 wdl-engine/tests/tasks/member-access/stdout create mode 100644 wdl-engine/tests/tasks/missing-output-file/error.txt create mode 100644 wdl-engine/tests/tasks/missing-output-file/inputs.json create mode 100644 wdl-engine/tests/tasks/missing-output-file/source.wdl create mode 100644 wdl-engine/tests/tasks/missing-output-file/stderr create mode 100644 wdl-engine/tests/tasks/missing-output-file/stdout create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/inputs.json create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/outputs.json create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/source.wdl create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/stderr create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/stdout create mode 100644 wdl-engine/tests/tasks/multiline-strings1/inputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings1/outputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings1/source.wdl create mode 100644 wdl-engine/tests/tasks/multiline-strings1/stderr create mode 100644 wdl-engine/tests/tasks/multiline-strings1/stdout create mode 100644 wdl-engine/tests/tasks/multiline-strings2/inputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings2/outputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings2/source.wdl create mode 100644 wdl-engine/tests/tasks/multiline-strings2/stderr create mode 100644 wdl-engine/tests/tasks/multiline-strings2/stdout create mode 100644 wdl-engine/tests/tasks/multiline-strings3/inputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings3/outputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings3/source.wdl create mode 100644 wdl-engine/tests/tasks/multiline-strings3/stderr create mode 100644 wdl-engine/tests/tasks/multiline-strings3/stdout create mode 100644 wdl-engine/tests/tasks/multiline-strings4/inputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings4/outputs.json create mode 100644 wdl-engine/tests/tasks/multiline-strings4/source.wdl create mode 100644 wdl-engine/tests/tasks/multiline-strings4/stderr create mode 100644 wdl-engine/tests/tasks/multiline-strings4/stdout create mode 100644 wdl-engine/tests/tasks/nested-access/inputs.json create mode 100644 wdl-engine/tests/tasks/nested-access/outputs.json create mode 100644 wdl-engine/tests/tasks/nested-access/source.wdl create mode 100644 wdl-engine/tests/tasks/nested-access/stderr create mode 100644 wdl-engine/tests/tasks/nested-access/stdout create mode 100644 wdl-engine/tests/tasks/nested-placeholders/inputs.json create mode 100644 wdl-engine/tests/tasks/nested-placeholders/outputs.json create mode 100644 wdl-engine/tests/tasks/nested-placeholders/source.wdl create mode 100644 wdl-engine/tests/tasks/nested-placeholders/stderr create mode 100644 wdl-engine/tests/tasks/nested-placeholders/stdout create mode 100644 wdl-engine/tests/tasks/non-empty-optional/inputs.json create mode 100644 wdl-engine/tests/tasks/non-empty-optional/outputs.json create mode 100644 wdl-engine/tests/tasks/non-empty-optional/source.wdl create mode 100644 wdl-engine/tests/tasks/non-empty-optional/stderr create mode 100644 wdl-engine/tests/tasks/non-empty-optional/stdout create mode 100644 wdl-engine/tests/tasks/optional-output-task/files/example1.txt create mode 100644 wdl-engine/tests/tasks/optional-output-task/inputs.json create mode 100644 wdl-engine/tests/tasks/optional-output-task/outputs.json create mode 100644 wdl-engine/tests/tasks/optional-output-task/source.wdl create mode 100644 wdl-engine/tests/tasks/optional-output-task/stderr create mode 100644 wdl-engine/tests/tasks/optional-output-task/stdout create mode 100644 wdl-engine/tests/tasks/optionals/inputs.json create mode 100644 wdl-engine/tests/tasks/optionals/outputs.json create mode 100644 wdl-engine/tests/tasks/optionals/source.wdl create mode 100644 wdl-engine/tests/tasks/optionals/stderr create mode 100644 wdl-engine/tests/tasks/optionals/stdout create mode 100644 wdl-engine/tests/tasks/outputs-task/files/a.csv create mode 100644 wdl-engine/tests/tasks/outputs-task/files/b.csv create mode 100644 wdl-engine/tests/tasks/outputs-task/files/threshold.txt create mode 100644 wdl-engine/tests/tasks/outputs-task/inputs.json create mode 100644 wdl-engine/tests/tasks/outputs-task/outputs.json create mode 100644 wdl-engine/tests/tasks/outputs-task/source.wdl create mode 100644 wdl-engine/tests/tasks/outputs-task/stderr create mode 100644 wdl-engine/tests/tasks/outputs-task/stdout create mode 100644 wdl-engine/tests/tasks/person-struct-task/inputs.json create mode 100644 wdl-engine/tests/tasks/person-struct-task/outputs.json create mode 100644 wdl-engine/tests/tasks/person-struct-task/source.wdl create mode 100644 wdl-engine/tests/tasks/person-struct-task/stderr create mode 100644 wdl-engine/tests/tasks/person-struct-task/stdout create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/inputs.json create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/outputs.json create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/source.wdl create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/stderr create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/stdout create mode 100644 wdl-engine/tests/tasks/placeholder-none/inputs.json create mode 100644 wdl-engine/tests/tasks/placeholder-none/outputs.json create mode 100644 wdl-engine/tests/tasks/placeholder-none/source.wdl create mode 100644 wdl-engine/tests/tasks/placeholder-none/stderr create mode 100644 wdl-engine/tests/tasks/placeholder-none/stdout create mode 100644 wdl-engine/tests/tasks/placeholders/inputs.json create mode 100644 wdl-engine/tests/tasks/placeholders/outputs.json create mode 100644 wdl-engine/tests/tasks/placeholders/source.wdl create mode 100644 wdl-engine/tests/tasks/placeholders/stderr create mode 100644 wdl-engine/tests/tasks/placeholders/stdout create mode 100644 wdl-engine/tests/tasks/primitive-literals/files/testdir/hello.txt create mode 100644 wdl-engine/tests/tasks/primitive-literals/inputs.json create mode 100644 wdl-engine/tests/tasks/primitive-literals/outputs.json create mode 100644 wdl-engine/tests/tasks/primitive-literals/source.wdl create mode 100644 wdl-engine/tests/tasks/primitive-literals/stderr create mode 100644 wdl-engine/tests/tasks/primitive-literals/stdout create mode 100644 wdl-engine/tests/tasks/primitive-to-string/inputs.json create mode 100644 wdl-engine/tests/tasks/primitive-to-string/outputs.json create mode 100644 wdl-engine/tests/tasks/primitive-to-string/source.wdl create mode 100644 wdl-engine/tests/tasks/primitive-to-string/stderr create mode 100644 wdl-engine/tests/tasks/primitive-to-string/stdout create mode 100644 wdl-engine/tests/tasks/private-declaration-task/inputs.json create mode 100644 wdl-engine/tests/tasks/private-declaration-task/outputs.json create mode 100644 wdl-engine/tests/tasks/private-declaration-task/source.wdl create mode 100644 wdl-engine/tests/tasks/private-declaration-task/stderr create mode 100644 wdl-engine/tests/tasks/private-declaration-task/stdout create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/files/my/path/to/something.txt create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/inputs.json create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/outputs.json create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/source.wdl create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/stderr create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/stdout create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/inputs.json create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/outputs.json create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/source.wdl create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/stderr create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/stdout create mode 100644 wdl-engine/tests/tasks/string-to-file/inputs.json create mode 100644 wdl-engine/tests/tasks/string-to-file/outputs.json create mode 100644 wdl-engine/tests/tasks/string-to-file/source.wdl create mode 100644 wdl-engine/tests/tasks/string-to-file/stderr create mode 100644 wdl-engine/tests/tasks/string-to-file/stdout create mode 100644 wdl-engine/tests/tasks/struct-to-struct/inputs.json create mode 100644 wdl-engine/tests/tasks/struct-to-struct/outputs.json create mode 100644 wdl-engine/tests/tasks/struct-to-struct/source.wdl create mode 100644 wdl-engine/tests/tasks/struct-to-struct/stderr create mode 100644 wdl-engine/tests/tasks/struct-to-struct/stdout create mode 100644 wdl-engine/tests/tasks/sum-task/inputs.json create mode 100644 wdl-engine/tests/tasks/sum-task/outputs.json create mode 100644 wdl-engine/tests/tasks/sum-task/source.wdl create mode 100644 wdl-engine/tests/tasks/sum-task/stderr create mode 100644 wdl-engine/tests/tasks/sum-task/stdout create mode 100644 wdl-engine/tests/tasks/task-fail/error.txt create mode 100644 wdl-engine/tests/tasks/task-fail/inputs.json create mode 100644 wdl-engine/tests/tasks/task-fail/source.wdl create mode 100644 wdl-engine/tests/tasks/task-fail/stderr create mode 100644 wdl-engine/tests/tasks/task-fail/stdout create mode 100644 wdl-engine/tests/tasks/task-inputs-task/inputs.json create mode 100644 wdl-engine/tests/tasks/task-inputs-task/outputs.json create mode 100644 wdl-engine/tests/tasks/task-inputs-task/source.wdl create mode 100644 wdl-engine/tests/tasks/task-inputs-task/stderr create mode 100644 wdl-engine/tests/tasks/task-inputs-task/stdout create mode 100644 wdl-engine/tests/tasks/task-with-comments/inputs.json create mode 100644 wdl-engine/tests/tasks/task-with-comments/outputs.json create mode 100644 wdl-engine/tests/tasks/task-with-comments/source.wdl create mode 100644 wdl-engine/tests/tasks/task-with-comments/stderr create mode 100644 wdl-engine/tests/tasks/task-with-comments/stdout create mode 100644 wdl-engine/tests/tasks/ternary/inputs.json create mode 100644 wdl-engine/tests/tasks/ternary/outputs.json create mode 100644 wdl-engine/tests/tasks/ternary/source.wdl create mode 100644 wdl-engine/tests/tasks/ternary/stderr create mode 100644 wdl-engine/tests/tasks/ternary/stdout create mode 100644 wdl-engine/tests/tasks/test-map/inputs.json create mode 100644 wdl-engine/tests/tasks/test-map/outputs.json create mode 100644 wdl-engine/tests/tasks/test-map/source.wdl create mode 100644 wdl-engine/tests/tasks/test-map/stderr create mode 100644 wdl-engine/tests/tasks/test-map/stdout create mode 100644 wdl-engine/tests/tasks/test-object/inputs.json create mode 100644 wdl-engine/tests/tasks/test-object/outputs.json create mode 100644 wdl-engine/tests/tasks/test-object/source.wdl create mode 100644 wdl-engine/tests/tasks/test-object/stderr create mode 100644 wdl-engine/tests/tasks/test-object/stdout create mode 100644 wdl-engine/tests/tasks/test-pairs/inputs.json create mode 100644 wdl-engine/tests/tasks/test-pairs/outputs.json create mode 100644 wdl-engine/tests/tasks/test-pairs/source.wdl create mode 100644 wdl-engine/tests/tasks/test-pairs/stderr create mode 100644 wdl-engine/tests/tasks/test-pairs/stdout create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/greetings.txt create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/inputs.json create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/outputs.json create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/source.wdl create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/stderr create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/stdout create mode 100644 wdl-engine/tests/tasks/test-struct/inputs.json create mode 100644 wdl-engine/tests/tasks/test-struct/outputs.json create mode 100644 wdl-engine/tests/tasks/test-struct/source.wdl create mode 100644 wdl-engine/tests/tasks/test-struct/stderr create mode 100644 wdl-engine/tests/tasks/test-struct/stdout create mode 100644 wdl-engine/tests/tasks/true-false-ternary/files/result1 create mode 100644 wdl-engine/tests/tasks/true-false-ternary/files/result2 create mode 100644 wdl-engine/tests/tasks/true-false-ternary/inputs.json create mode 100644 wdl-engine/tests/tasks/true-false-ternary/outputs.json create mode 100644 wdl-engine/tests/tasks/true-false-ternary/source.wdl create mode 100644 wdl-engine/tests/tasks/true-false-ternary/stderr create mode 100644 wdl-engine/tests/tasks/true-false-ternary/stdout diff --git a/Cargo.toml b/Cargo.toml index 73332b64..a72fa0ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ rowan = "0.15.15" serde = { version = "1", features = ["derive"] } serde_json = "1.0.120" serde_with = "3.8.1" +sysinfo = "0.32.1" tempfile = "3.10.1" tokio = { version = "1.38.0", features = ["full"] } toml = "0.8.14" diff --git a/gauntlet/src/lib.rs b/gauntlet/src/lib.rs index 17ee04bd..4c19b063 100644 --- a/gauntlet/src/lib.rs +++ b/gauntlet/src/lib.rs @@ -42,7 +42,6 @@ pub use repository::Repository; use wdl::analysis::Analyzer; use wdl::analysis::rules; use wdl::ast::Diagnostic; -use wdl::ast::SyntaxNode; use wdl::lint::LintVisitor; use wdl::lint::ast::Validator; @@ -220,11 +219,7 @@ pub async fn gauntlet(args: Args) -> Result<()> { let mut actual = IndexSet::new(); if !diagnostics.is_empty() { - let source = result - .document() - .root() - .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) - .unwrap_or(String::new()); + let source = result.document().node().syntax().text().to_string(); let file: SimpleFile<_, _> = SimpleFile::new( Path::new(document_identifier.path()) diff --git a/wdl-analysis/src/diagnostics.rs b/wdl-analysis/src/diagnostics.rs index 818ed55b..a6c9eed8 100644 --- a/wdl-analysis/src/diagnostics.rs +++ b/wdl-analysis/src/diagnostics.rs @@ -434,8 +434,8 @@ pub fn no_common_type( ) } -/// Creates a custom "type mismatch" diagnostic. -pub fn type_mismatch_custom( +/// Creates a "multiple type mismatch" diagnostic. +pub fn multiple_type_mismatch( types: &Types, expected: &[Type], expected_span: Span, diff --git a/wdl-analysis/src/document.rs b/wdl-analysis/src/document.rs index a9d23b77..ea257f3f 100644 --- a/wdl-analysis/src/document.rs +++ b/wdl-analysis/src/document.rs @@ -34,7 +34,7 @@ mod v1; /// The `task` variable name available in task command sections and outputs in /// WDL 1.2. -pub(crate) const TASK_VAR_NAME: &str = "task"; +pub const TASK_VAR_NAME: &str = "task"; /// Calculates the span of a scope given a braced node. fn braced_scope_span(parent: &impl AstNode) -> Span { @@ -371,9 +371,9 @@ pub struct Task { /// The scopes will be in sorted order by span start. scopes: Vec, /// The inputs of the task. - inputs: Arc>, + inputs: Arc>, /// The outputs of the task. - outputs: Arc>, + outputs: Arc>, } impl Task { @@ -388,12 +388,12 @@ impl Task { } /// Gets the inputs of the task. - pub fn inputs(&self) -> &HashMap { + pub fn inputs(&self) -> &IndexMap { &self.inputs } /// Gets the outputs of the task. - pub fn outputs(&self) -> &HashMap { + pub fn outputs(&self) -> &IndexMap { &self.outputs } } @@ -412,9 +412,9 @@ pub struct Workflow { /// The scopes will be in sorted order by span start. scopes: Vec, /// The inputs of the workflow. - inputs: Arc>, + inputs: Arc>, /// The outputs of the workflow. - outputs: Arc>, + outputs: Arc>, /// The calls made by the workflow. calls: HashMap, /// Whether or not nested inputs are allowed for the workflow. @@ -433,12 +433,12 @@ impl Workflow { } /// Gets the inputs of the workflow. - pub fn inputs(&self) -> &HashMap { + pub fn inputs(&self) -> &IndexMap { &self.inputs } /// Gets the outputs of the workflow. - pub fn outputs(&self) -> &HashMap { + pub fn outputs(&self) -> &IndexMap { &self.outputs } @@ -578,27 +578,12 @@ impl Document { } } - /// Gets the AST root for the document. - /// - /// Returns `None` if there was an error parsing the document. - pub fn root(&self) -> Option<&GreenNode> { - self.root.as_ref() - } - - /// Gets the AST of the document. - /// - /// Returns [`Ast::Unsupported`] when the document could not be parsed or - /// has an unsupported version. - pub fn ast(&self) -> Ast { - match &self.version { - Some(SupportedVersion::V1(_)) => Ast::V1( - wdl_ast::v1::Ast::cast(SyntaxNode::new_root( - self.root.clone().expect("should have a root"), - )) - .expect("should cast"), - ), - _ => Ast::Unsupported, - } + /// Gets the root AST document node. + pub fn node(&self) -> wdl_ast::Document { + wdl_ast::Document::cast(SyntaxNode::new_root( + self.root.clone().expect("should have a root"), + )) + .expect("should cast") } /// Gets the identifier of the document. diff --git a/wdl-analysis/src/document/v1.rs b/wdl-analysis/src/document/v1.rs index 646285b8..cdf4431c 100644 --- a/wdl-analysis/src/document/v1.rs +++ b/wdl-analysis/src/document/v1.rs @@ -99,6 +99,7 @@ use crate::types::Optional; use crate::types::PrimitiveTypeKind; use crate::types::PromotionKind; use crate::types::Type; +use crate::types::TypeNameResolver; use crate::types::Types; use crate::types::v1::AstTypeConverter; use crate::types::v1::ExprTypeEvaluator; @@ -325,23 +326,26 @@ fn add_namespace( .unwrap_or_else(|| (span, name, false)); match document.structs.get(aliased_name) { Some(prev) => { - // Import conflicts with a struct defined in this document - if prev.namespace.is_none() { - document.diagnostics.push(struct_conflicts_with_import( - aliased_name, - prev.span, - span, - )); - continue; - } - - if !are_structs_equal(prev, s) { - document.diagnostics.push(imported_struct_conflict( - aliased_name, - span, - prev.span, - !aliased, - )); + let a = StructDefinition::cast(SyntaxNode::new_root(prev.node.clone())) + .expect("node should cast"); + let b = StructDefinition::cast(SyntaxNode::new_root(s.node.clone())) + .expect("node should cast"); + if !are_structs_equal(&a, &b) { + // Import conflicts with a struct defined in this document + if prev.namespace.is_none() { + document.diagnostics.push(struct_conflicts_with_import( + aliased_name, + prev.span, + span, + )); + } else { + document.diagnostics.push(imported_struct_conflict( + aliased_name, + span, + prev.span, + !aliased, + )); + } continue; } } @@ -361,9 +365,7 @@ fn add_namespace( } /// Compares two structs for structural equality. -fn are_structs_equal(a: &Struct, b: &Struct) -> bool { - let a = StructDefinition::cast(SyntaxNode::new_root(a.node.clone())).expect("node should cast"); - let b = StructDefinition::cast(SyntaxNode::new_root(b.node.clone())).expect("node should cast"); +fn are_structs_equal(a: &StructDefinition, b: &StructDefinition) -> bool { for (a, b) in a.members().zip(b.members()) { if a.name().as_str() != b.name().as_str() { return false; @@ -382,11 +384,15 @@ fn add_struct(document: &mut Document, definition: &StructDefinition) { let name = definition.name(); if let Some(prev) = document.structs.get(name.as_str()) { if prev.namespace.is_some() { - document.diagnostics.push(struct_conflicts_with_import( - name.as_str(), - name.span(), - prev.span, - )) + let prev_def = StructDefinition::cast(SyntaxNode::new_root(prev.node.clone())) + .expect("node should cast"); + if !are_structs_equal(definition, &prev_def) { + document.diagnostics.push(struct_conflicts_with_import( + name.as_str(), + name.span(), + prev.span, + )) + } } else { document.diagnostics.push(name_conflict( name.as_str(), @@ -423,21 +429,31 @@ fn add_struct(document: &mut Document, definition: &StructDefinition) { /// Converts an AST type to an analysis type. fn convert_ast_type(document: &mut Document, ty: &wdl_ast::v1::Type) -> Type { - let mut converter = AstTypeConverter::new(&mut document.types, |name, span| { - document - .structs - .get(name) - .map(|s| { - // Mark the struct's namespace as used - if let Some(ns) = &s.namespace { - document.namespaces[ns].used = true; - } + /// Used to resolve a type name from a document. + struct Resolver<'a>(&'a mut Document); - s.ty().expect("struct should have type") - }) - .ok_or_else(|| unknown_type(name, span)) - }); + impl TypeNameResolver for Resolver<'_> { + fn types_mut(&mut self) -> &mut Types { + &mut self.0.types + } + + fn resolve_type_name(&mut self, name: &Ident) -> Result { + self.0 + .structs + .get(name.as_str()) + .map(|s| { + // Mark the struct's namespace as used + if let Some(ns) = &s.namespace { + self.0.namespaces[ns].used = true; + } + s.ty().expect("struct should have type") + }) + .ok_or_else(|| unknown_type(name.as_str(), name.span())) + } + } + + let mut converter = AstTypeConverter::new(Resolver(document)); match converter.convert_type(ty) { Ok(ty) => ty, Err(diagnostic) => { @@ -451,8 +467,8 @@ fn convert_ast_type(document: &mut Document, ty: &wdl_ast::v1::Type) -> Type { fn create_input_type_map( document: &mut Document, declarations: impl Iterator, -) -> Arc> { - let mut map = HashMap::new(); +) -> Arc> { + let mut map = IndexMap::new(); for decl in declarations { let name = decl.name(); if map.contains_key(name.as_str()) { @@ -474,8 +490,8 @@ fn create_input_type_map( fn create_output_type_map( document: &mut Document, declarations: impl Iterator, -) -> Arc> { - let mut map = HashMap::new(); +) -> Arc> { + let mut map = IndexMap::new(); for decl in declarations { let name = decl.name(); if map.contains_key(name.as_str()) { @@ -491,7 +507,7 @@ fn create_output_type_map( } /// Adds a task to the document. -fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefinition) { +fn add_task(config: DiagnosticsConfig, document: &mut Document, definition: &TaskDefinition) { /// Helper function for creating a scope for a task section. fn create_section_scope( version: Option, @@ -510,7 +526,7 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin } // Check for a name conflict with another task or workflow - let name = task.name(); + let name = definition.name(); if let Some(s) = document.tasks.get(name.as_str()) { document.diagnostics.push(name_conflict( name.as_str(), @@ -532,11 +548,15 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin // Populate type maps for the task's inputs and outputs let inputs = create_input_type_map( document, - task.input().into_iter().flat_map(|s| s.declarations()), + definition + .input() + .into_iter() + .flat_map(|s| s.declarations()), ); let outputs = create_output_type_map( document, - task.output() + definition + .output() .into_iter() .flat_map(|s| s.declarations().map(Decl::Bound)), ); @@ -544,10 +564,18 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin // Process the task in evaluation order let graph = TaskGraphBuilder::default().build( document.version.unwrap(), - task, + definition, &mut document.diagnostics, ); - let mut scopes = vec![Scope::new(None, braced_scope_span(task))]; + + let mut task = Task { + name_span: name.span(), + name: name.as_str().to_string(), + scopes: vec![Scope::new(None, braced_scope_span(definition))], + inputs, + outputs, + }; + let mut output_scope = None; let mut command_scope = None; @@ -557,9 +585,9 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin if !add_decl( config, document, - ScopeRefMut::new(&mut scopes, ScopeIndex(0)), + ScopeRefMut::new(&mut task.scopes, ScopeIndex(0)), &decl, - |_, n, _| inputs[n].ty, + |_, n, _| task.inputs[n].ty, ) { continue; } @@ -573,7 +601,7 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin .is_none() { // Determine if the input is really used based on its name and type - if is_input_used(document, name.as_str(), inputs[name.as_str()].ty) { + if is_input_used(document, name.as_str(), task.inputs[name.as_str()].ty) { continue; } @@ -589,7 +617,7 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin if !add_decl( config, document, - ScopeRefMut::new(&mut scopes, ScopeIndex(0)), + ScopeRefMut::new(&mut task.scopes, ScopeIndex(0)), &decl, |doc, _, decl| convert_ast_type(doc, &decl.ty()), ) { @@ -615,17 +643,19 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin let scope_index = *output_scope.get_or_insert_with(|| { create_section_scope( document.version(), - &mut scopes, + &mut task.scopes, &name, - braced_scope_span(&task.output().expect("should have output section")), + braced_scope_span( + &definition.output().expect("should have output section"), + ), ) }); add_decl( config, document, - ScopeRefMut::new(&mut scopes, scope_index), + ScopeRefMut::new(&mut task.scopes, scope_index), &decl, - |_, n, _| outputs[n].ty, + |_, n, _| task.outputs[n].ty, ); } TaskGraphNode::Command(section) => { @@ -636,11 +666,14 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin braced_scope_span(§ion) }; - create_section_scope(document.version(), &mut scopes, &name, span) + create_section_scope(document.version(), &mut task.scopes, &name, span) }); - let mut context = - EvaluationContext::new(document, ScopeRef::new(&scopes, scope_index), config); + let mut context = EvaluationContext::new( + document, + ScopeRef::new(&task.scopes, scope_index), + config, + ); let mut evaluator = ExprTypeEvaluator::new(&mut context); for part in section.parts() { if let CommandPart::Placeholder(p) = part { @@ -650,8 +683,11 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin } TaskGraphNode::Runtime(section) => { // Perform type checking on the runtime section's expressions - let mut context = - EvaluationContext::new(document, ScopeRef::new(&scopes, ScopeIndex(0)), config); + let mut context = EvaluationContext::new( + document, + ScopeRef::new(&task.scopes, ScopeIndex(0)), + config, + ); let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { evaluator.evaluate_runtime_item(&item.name(), &item.expr()); @@ -659,8 +695,11 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin } TaskGraphNode::Requirements(section) => { // Perform type checking on the requirements section's expressions - let mut context = - EvaluationContext::new(document, ScopeRef::new(&scopes, ScopeIndex(0)), config); + let mut context = EvaluationContext::new( + document, + ScopeRef::new(&task.scopes, ScopeIndex(0)), + config, + ); let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { evaluator.evaluate_requirements_item(&item.name(), &item.expr()); @@ -670,9 +709,9 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin // Perform type checking on the hints section's expressions let mut context = EvaluationContext::new_for_task( document, - ScopeRef::new(&scopes, ScopeIndex(0)), + ScopeRef::new(&task.scopes, ScopeIndex(0)), config, - TaskEvaluationContext::new(name.as_str(), &inputs, &outputs), + &task, ); let mut evaluator = ExprTypeEvaluator::new(&mut context); for item in section.items() { @@ -683,15 +722,8 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, task: &TaskDefin } // Sort the scopes - sort_scopes(&mut scopes); - - document.tasks.insert(name.as_str().to_string(), Task { - name_span: name.span(), - name: name.as_str().to_string(), - scopes, - inputs, - outputs, - }); + sort_scopes(&mut task.scopes); + document.tasks.insert(name.as_str().to_string(), task); } /// Adds a declaration to a scope. @@ -1357,6 +1389,38 @@ fn resolve_import( /// Sets the struct types in the document. fn set_struct_types(document: &mut Document) { + /// Used to resolve a type name from a document. + struct Resolver<'a> { + /// The document to resolve the type name from. + document: &'a mut Document, + /// The offset to use to adjust the start of diagnostics. + offset: usize, + } + + impl TypeNameResolver for Resolver<'_> { + fn types_mut(&mut self) -> &mut Types { + &mut self.document.types + } + + fn resolve_type_name(&mut self, name: &Ident) -> Result { + if let Some(s) = self.document.structs.get(name.as_str()) { + // Mark the struct's namespace as used + if let Some(ns) = &s.namespace { + self.document.namespaces[ns].used = true; + } + + Ok(s.ty().unwrap_or(Type::Union)) + } else { + let span = name.span(); + self.document.diagnostics.push(unknown_type( + name.as_str(), + Span::new(span.start() + self.offset, span.len()), + )); + Ok(Type::Union) + } + } + } + if document.structs.is_empty() { return; } @@ -1408,23 +1472,8 @@ fn set_struct_types(document: &mut Document) { StructDefinition::cast(SyntaxNode::new_root(document.structs[index].node.clone())) .expect("node should cast"); - let mut converter = AstTypeConverter::new(&mut document.types, |name, span| { - if let Some(s) = document.structs.get(name) { - // Mark the struct's namespace as used - if let Some(ns) = &s.namespace { - document.namespaces[ns].used = true; - } - - Ok(s.ty().unwrap_or(Type::Union)) - } else { - document.diagnostics.push(unknown_type( - name, - Span::new(span.start() + document.structs[index].offset, span.len()), - )); - Ok(Type::Union) - } - }); - + let offset = document.structs[index].offset; + let mut converter = AstTypeConverter::new(Resolver { document, offset }); let ty = converter .convert_struct_type(&definition) .expect("struct type conversion should not fail"); @@ -1435,34 +1484,6 @@ fn set_struct_types(document: &mut Document) { } } -/// Represents context of a task being evaluated for an expression type -/// evaluator. -#[derive(Clone, Copy, Debug)] -struct TaskEvaluationContext<'a> { - /// The name of the task. - name: &'a str, - /// The inputs of the task. - inputs: &'a HashMap, - /// The outputs of the task. - outputs: &'a HashMap, -} - -impl<'a> TaskEvaluationContext<'a> { - /// Constructs a new task evaluation context given the task name, inputs, - /// and outputs. - fn new( - name: &'a str, - inputs: &'a HashMap, - outputs: &'a HashMap, - ) -> Self { - Self { - name, - inputs, - outputs, - } - } -} - /// Represents context to an expression type evaluator. #[derive(Debug)] struct EvaluationContext<'a> { @@ -1475,7 +1496,7 @@ struct EvaluationContext<'a> { /// The context of the task being evaluated. /// /// This is only `Some` when evaluating a task's `hints` section.` - task: Option>, + task: Option<&'a Task>, } impl<'a> EvaluationContext<'a> { @@ -1489,8 +1510,7 @@ impl<'a> EvaluationContext<'a> { } } - /// Constructs a new expression type evaluation context with the given task - /// context. + /// Constructs a new expression type evaluation context with the given task. /// /// This is used to evaluated the type of expressions inside of a task's /// `hints` section. @@ -1498,7 +1518,7 @@ impl<'a> EvaluationContext<'a> { document: &'a mut Document, scope: ScopeRef<'a>, config: DiagnosticsConfig, - task: TaskEvaluationContext<'a>, + task: &'a Task, ) -> Self { Self { document, @@ -1543,28 +1563,8 @@ impl crate::types::v1::EvaluationContext for EvaluationContext<'_> { .ok_or_else(|| unknown_type(name.as_str(), name.span())) } - fn input(&self, name: &str) -> Option { - self.task.and_then(|task| task.inputs.get(name).copied()) - } - - fn output(&self, name: &str) -> Option { - self.task.and_then(|task| task.outputs.get(name).copied()) - } - - fn task_name(&self) -> Option<&str> { - self.task.map(|task| task.name) - } - - fn supports_hints_type(&self) -> bool { - self.task.is_some() - } - - fn supports_input_type(&self) -> bool { - self.task.is_some() - } - - fn supports_output_type(&self) -> bool { - self.task.is_some() + fn task(&self) -> Option<&Task> { + self.task } fn diagnostics_config(&self) -> DiagnosticsConfig { diff --git a/wdl-analysis/src/types.rs b/wdl-analysis/src/types.rs index e8fbb344..697f8497 100644 --- a/wdl-analysis/src/types.rs +++ b/wdl-analysis/src/types.rs @@ -1,6 +1,5 @@ //! Representation of the WDL type system. -use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::sync::Arc; @@ -10,6 +9,8 @@ use id_arena::ArenaBehavior; use id_arena::DefaultArenaBehavior; use id_arena::Id; use indexmap::IndexMap; +use wdl_ast::Diagnostic; +use wdl_ast::Ident; use crate::document::Input; use crate::document::Output; @@ -52,6 +53,15 @@ pub fn display_types<'a>(types: &'a Types, slice: &'a [Type]) -> impl fmt::Displ Display { types, slice } } +/// A trait implemented on type name resolvers. +pub trait TypeNameResolver { + /// Gets a reference mutable type names collection. + fn types_mut(&mut self) -> &mut Types; + + /// Resolves the given type name to a type. + fn resolve_type_name(&mut self, name: &Ident) -> Result; +} + /// A trait implemented on types that may be optional. pub trait Optional: Copy { /// Determines if the type is optional. @@ -1186,9 +1196,9 @@ pub struct CallType { /// The set of specified inputs in the call. specified: Arc>, /// The input types to the call. - inputs: Arc>, + inputs: Arc>, /// The output types from the call. - outputs: Arc>, + outputs: Arc>, } impl CallType { @@ -1197,8 +1207,8 @@ impl CallType { kind: CallKind, name: impl Into, specified: Arc>, - inputs: Arc>, - outputs: Arc>, + inputs: Arc>, + outputs: Arc>, ) -> Self { Self { kind, @@ -1217,8 +1227,8 @@ impl CallType { namespace: impl Into, name: impl Into, specified: Arc>, - inputs: Arc>, - outputs: Arc>, + inputs: Arc>, + outputs: Arc>, ) -> Self { Self { kind, @@ -1253,12 +1263,12 @@ impl CallType { } /// Gets the inputs of the called workflow or task. - pub fn inputs(&self) -> &HashMap { + pub fn inputs(&self) -> &IndexMap { &self.inputs } /// Gets the outputs of the called workflow or task. - pub fn outputs(&self) -> &HashMap { + pub fn outputs(&self) -> &IndexMap { &self.outputs } @@ -1388,6 +1398,51 @@ impl Types { .expect("invalid type identifier") } + /// Gets a pair type from the type collection. + /// + /// # Panics + /// + /// Panics if the given type is not a pair type. + pub fn pair_type(&self, ty: Type) -> &PairType { + if let Type::Compound(ty) = ty { + if let CompoundTypeDef::Pair(ty) = self.type_definition(ty.definition()) { + return ty; + } + } + + panic!("type `{ty}` is not a pair type", ty = ty.display(self)) + } + + /// Gets an array type from the type collection. + /// + /// # Panics + /// + /// Panics if the given type is not an array type. + pub fn array_type(&self, ty: Type) -> &ArrayType { + if let Type::Compound(ty) = ty { + if let CompoundTypeDef::Array(ty) = self.type_definition(ty.definition()) { + return ty; + } + } + + panic!("type `{ty}` is not an array type", ty = ty.display(self)) + } + + /// Gets a map type from the type collection. + /// + /// # Panics + /// + /// Panics if the given type is not a map type. + pub fn map_type(&self, ty: Type) -> &MapType { + if let Type::Compound(ty) = ty { + if let CompoundTypeDef::Map(ty) = self.type_definition(ty.definition()) { + return ty; + } + } + + panic!("type `{ty}` is not a map type", ty = ty.display(self)) + } + /// Gets a struct type from the type collection. /// /// # Panics @@ -1395,8 +1450,8 @@ impl Types { /// Panics if the given type is not a struct type. pub fn struct_type(&self, ty: Type) -> &StructType { if let Type::Compound(ty) = ty { - if let CompoundTypeDef::Struct(s) = &self.0[ty.definition()] { - return s; + if let CompoundTypeDef::Struct(ty) = self.type_definition(ty.definition()) { + return ty; } } diff --git a/wdl-analysis/src/types/v1.rs b/wdl-analysis/src/types/v1.rs index 495678ab..db951ddc 100644 --- a/wdl-analysis/src/types/v1.rs +++ b/wdl-analysis/src/types/v1.rs @@ -49,6 +49,7 @@ use super::PrimitiveTypeKind; use super::StructType; use super::Type; use super::TypeEq; +use super::TypeNameResolver; use super::Types; use crate::DiagnosticsConfig; use crate::UNNECESSARY_FUNCTION_CALL; @@ -66,6 +67,7 @@ use crate::diagnostics::logical_not_mismatch; use crate::diagnostics::logical_or_mismatch; use crate::diagnostics::map_key_not_primitive; use crate::diagnostics::missing_struct_members; +use crate::diagnostics::multiple_type_mismatch; use crate::diagnostics::negation_mismatch; use crate::diagnostics::no_common_type; use crate::diagnostics::not_a_pair_accessor; @@ -77,14 +79,12 @@ use crate::diagnostics::string_concat_mismatch; use crate::diagnostics::too_few_arguments; use crate::diagnostics::too_many_arguments; use crate::diagnostics::type_mismatch; -use crate::diagnostics::type_mismatch_custom; use crate::diagnostics::unknown_call_io; use crate::diagnostics::unknown_function; use crate::diagnostics::unknown_task_io; use crate::diagnostics::unnecessary_function_call; use crate::diagnostics::unsupported_function; -use crate::document::Input; -use crate::document::Output; +use crate::document::Task; use crate::stdlib::FunctionBindError; use crate::stdlib::MAX_PARAMETERS; use crate::stdlib::STDLIB; @@ -309,22 +309,15 @@ impl fmt::Display for NumericOperator { /// Used to convert AST types into diagnostic types. #[derive(Debug)] -pub struct AstTypeConverter<'a, L> { - /// The types collection to use for the conversion. - types: &'a mut Types, - /// A lookup function for looking up type names. - lookup: L, -} +pub struct AstTypeConverter(R); -impl<'a, L> AstTypeConverter<'a, L> +impl AstTypeConverter where - L: FnMut(&str, Span) -> Result, + R: TypeNameResolver, { /// Constructs a new AST type converter. - /// - /// The provided callback is used to look up type name references. - pub fn new(types: &'a mut Types, lookup: L) -> Self { - Self { types, lookup } + pub fn new(resolver: R) -> Self { + Self(resolver) } /// Converts a V1 AST type into an analysis type. @@ -337,21 +330,18 @@ where let ty = match ty { v1::Type::Map(ty) => { let ty = self.convert_map_type(ty)?; - self.types.add_map(ty) + self.0.types_mut().add_map(ty) } v1::Type::Array(ty) => { let ty = self.convert_array_type(ty)?; - self.types.add_array(ty) + self.0.types_mut().add_array(ty) } v1::Type::Pair(ty) => { let ty = self.convert_pair_type(ty)?; - self.types.add_pair(ty) + self.0.types_mut().add_pair(ty) } v1::Type::Object(_) => Type::Object, - v1::Type::Ref(r) => { - let name = r.name(); - (self.lookup)(name.as_str(), name.span())? - } + v1::Type::Ref(r) => self.0.resolve_type_name(&r.name())?, v1::Type::Primitive(ty) => Type::Primitive(ty.into()), }; @@ -454,31 +444,10 @@ pub trait EvaluationContext { /// Resolves a type name to a type. fn resolve_type_name(&mut self, name: &Ident) -> Result; - /// Gets an input for the given name. - /// - /// Returns `None` if `input` hidden types are not supported or if the - /// specified input isn't known. - fn input(&self, name: &str) -> Option; - - /// Gets an output for the given name. + /// Gets the task associated with the evaluation context. /// - /// Returns `None` if `output` hidden types are not supported or if the - /// specified output isn't known. - fn output(&self, name: &str) -> Option; - - /// The task name associated with the evaluation. - /// - /// Returns `None` if no task is visible in this context. - fn task_name(&self) -> Option<&str>; - - /// Whether or not `hints` hidden types are supported for the evaluation. - fn supports_hints_type(&self) -> bool; - - /// Whether or not `input` hidden types are supported for the evaluation. - fn supports_input_type(&self) -> bool; - - /// Whether or not `output` hidden types are supported for the evaluation. - fn supports_output_type(&self) -> bool; + /// This is only `Some` when evaluating a task `hints` section. + fn task(&self) -> Option<&Task>; /// Gets the diagnostics configuration for the evaluation. fn diagnostics_config(&self) -> DiagnosticsConfig; @@ -928,7 +897,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.context.add_diagnostic(type_mismatch_custom( + self.context.add_diagnostic(multiple_type_mismatch( self.context.types(), expected, name.span(), @@ -957,7 +926,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.context.add_diagnostic(type_mismatch_custom( + self.context.add_diagnostic(multiple_type_mismatch( self.context.types(), expected, name.span(), @@ -974,9 +943,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { /// Evaluates the type of a literal hints expression. fn evaluate_literal_hints(&mut self, expr: &LiteralHints) -> Option { - if !self.context.supports_hints_type() { - return None; - } + self.context.task()?; for item in expr.items() { self.evaluate_hints_item(&item.name(), &item.expr()) @@ -994,7 +961,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { .iter() .any(|target| expr_ty.is_coercible_to(self.context.types(), target)) { - self.context.add_diagnostic(type_mismatch_custom( + self.context.add_diagnostic(multiple_type_mismatch( self.context.types(), expected, name.span(), @@ -1008,9 +975,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { /// Evaluates the type of a literal input expression. fn evaluate_literal_input(&mut self, expr: &LiteralInput) -> Option { // Check to see if inputs literals are supported in the evaluation scope - if !self.context.supports_input_type() { - return None; - } + self.context.task()?; // Evaluate the items of the literal for item in expr.items() { @@ -1023,9 +988,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { /// Evaluates the type of a literal output expression. fn evaluate_literal_output(&mut self, expr: &LiteralOutput) -> Option { // Check to see if output literals are supported in the evaluation scope - if !self.context.supports_output_type() { - return None; - } + self.context.task()?; // Evaluate the items of the literal for item in expr.items() { @@ -1050,14 +1013,24 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { span = Some(name.span()); match if io == Io::Input { - self.context.input(name.as_str()).map(|i| i.ty()) + self.context + .task() + .expect("should have task") + .inputs() + .get(name.as_str()) + .map(|i| i.ty()) } else { - self.context.output(name.as_str()).map(|o| o.ty()) + self.context + .task() + .expect("should have task") + .outputs() + .get(name.as_str()) + .map(|o| o.ty()) } { Some(ty) => ty, None => { self.context.add_diagnostic(unknown_task_io( - self.context.task_name().expect("should have task name"), + self.context.task().expect("should have task").name(), &name, io, )); diff --git a/wdl-analysis/tests/analysis.rs b/wdl-analysis/tests/analysis.rs index 1d74d215..9f42dd49 100644 --- a/wdl-analysis/tests/analysis.rs +++ b/wdl-analysis/tests/analysis.rs @@ -36,7 +36,6 @@ use wdl_analysis::Analyzer; use wdl_analysis::path_to_uri; use wdl_analysis::rules; use wdl_ast::Diagnostic; -use wdl_ast::SyntaxNode; /// Finds tests to run as part of the analysis test suite. fn find_tests() -> Vec { @@ -103,8 +102,9 @@ fn compare_result(path: &Path, result: &str, is_error: bool) -> Result<()> { if expected != result { bail!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), ); } @@ -131,11 +131,7 @@ fn compare_results(test: &Path, results: Vec) -> Result<()> { }; if !diagnostics.is_empty() { - let source = result - .document() - .root() - .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) - .unwrap_or(String::new()); + let source = result.document().node().syntax().text().to_string(); let file = SimpleFile::new(path, &source); for diagnostic in diagnostics.as_ref() { term::emit( diff --git a/wdl-analysis/tests/analysis/conflicting-struct-names/bar.wdl b/wdl-analysis/tests/analysis/conflicting-struct-names/bar.wdl index 96081835..b869d436 100644 --- a/wdl-analysis/tests/analysis/conflicting-struct-names/bar.wdl +++ b/wdl-analysis/tests/analysis/conflicting-struct-names/bar.wdl @@ -1,5 +1,5 @@ version 1.1 struct Baz { - Int x + Int baz } diff --git a/wdl-analysis/tests/analysis/conflicting-struct-names/foo.wdl b/wdl-analysis/tests/analysis/conflicting-struct-names/foo.wdl index 04475e80..313e57be 100644 --- a/wdl-analysis/tests/analysis/conflicting-struct-names/foo.wdl +++ b/wdl-analysis/tests/analysis/conflicting-struct-names/foo.wdl @@ -1,5 +1,5 @@ version 1.1 struct Foo { - Int x + Int foo } diff --git a/wdl-analysis/tests/analysis/import-same-struct/a/file.wdl b/wdl-analysis/tests/analysis/import-same-struct/a/file.wdl new file mode 100644 index 00000000..efe6321c --- /dev/null +++ b/wdl-analysis/tests/analysis/import-same-struct/a/file.wdl @@ -0,0 +1,7 @@ +version 1.1 + +struct Foo { + String a + Int b + File c +} diff --git a/wdl-analysis/tests/analysis/import-same-struct/b/file.wdl b/wdl-analysis/tests/analysis/import-same-struct/b/file.wdl new file mode 100644 index 00000000..efe6321c --- /dev/null +++ b/wdl-analysis/tests/analysis/import-same-struct/b/file.wdl @@ -0,0 +1,7 @@ +version 1.1 + +struct Foo { + String a + Int b + File c +} diff --git a/wdl-analysis/tests/analysis/import-same-struct/source.diagnostics b/wdl-analysis/tests/analysis/import-same-struct/source.diagnostics new file mode 100644 index 00000000..e69de29b diff --git a/wdl-analysis/tests/analysis/import-same-struct/source.wdl b/wdl-analysis/tests/analysis/import-same-struct/source.wdl new file mode 100644 index 00000000..3429364f --- /dev/null +++ b/wdl-analysis/tests/analysis/import-same-struct/source.wdl @@ -0,0 +1,17 @@ +#@ except: UnusedImport +## This a test of importing an identical struct. +## There should be no diagnostics generated + +version 1.1 + +import "a/file.wdl" as a +import "b/file.wdl" as b + +struct Foo { + String a + Int b + File c +} + +workflow test { +} diff --git a/wdl-ast/src/v1/expr.rs b/wdl-ast/src/v1/expr.rs index 273ce4a1..39f0fe7c 100644 --- a/wdl-ast/src/v1/expr.rs +++ b/wdl-ast/src/v1/expr.rs @@ -1,5 +1,8 @@ //! V1 AST representation for expressions. +use wdl_grammar::lexer::v1::EscapeToken; +use wdl_grammar::lexer::v1::Logos; + use crate::AstChildren; use crate::AstNode; use crate::AstToken; @@ -2086,35 +2089,53 @@ pub enum StrippedStringPart { Placeholder(Placeholder), } -/// Removes line continuations from a string. +/// Unescapes a multiline string. /// -/// A line continuation is a backslash immediately preceding a newline -/// character. This function will remove the backslash and newline, and any -/// spaces or tabs that follow the newline character. -fn remove_line_continuations(s: &str) -> String { +/// This unescapes both line continuations and `\>` sequences. +fn unescape_multiline_string(s: &str) -> String { let mut result = String::new(); let mut chars = s.chars().peekable(); - let mut push_c; while let Some(c) = chars.next() { - push_c = true; - if c == '\\' { - if let Some(next) = chars.next() { - if next == '\n' { - push_c = false; - let inner_chars = chars.by_ref(); - while let Some(&c) = inner_chars.peek() { - if c == ' ' || c == '\t' { - inner_chars.next(); - } else { + match c { + '\\' => match chars.peek() { + Some('\r') => { + chars.next(); + if chars.peek() == Some(&'\n') { + chars.next(); + while let Some(&next) = chars.peek() { + if next == ' ' || next == '\t' { + chars.next(); + continue; + } + break; } + } else { + result.push_str("\\\r"); } } + Some('\n') => { + chars.next(); + while let Some(&next) = chars.peek() { + if next == ' ' || next == '\t' { + chars.next(); + continue; + } + + break; + } + } + Some('\\') | Some('>') | Some('~') | Some('$') => { + result.push(chars.next().unwrap()); + } + _ => { + result.push('\\'); + } + }, + _ => { + result.push(c); } } - if push_c { - result.push(c); - } } result } @@ -2172,36 +2193,23 @@ impl LiteralString { /// Strips leading whitespace from a multi-line string. /// - /// This function will remove leading whitespace from each line of a - /// multi-line string and parse line continuations. Single or double - /// quoted strings will return `None`. + /// This function will remove leading and trailing whitespace and handle + /// unescaping the string. + /// + /// Returns `None` if not a multi-line string. pub fn strip_whitespace(&self) -> Option> { if self.kind() != LiteralStringKind::Multiline { return None; } + // Unescape each line let mut result = Vec::new(); - - // Parse the string parts and remove line continuations. - // We also remove the first and last lines of the string. - // Placeholders are copied as-is. - for (i, part) in self.parts().enumerate() { + for part in self.parts() { match part { StringPart::Text(text) => { - let parsed = remove_line_continuations(text.as_str()); - let mut reconstructed = Vec::new(); - for (j, line) in parsed.lines().enumerate() { - if i == 0 && j == 0 { - let trimmed = line.trim_start(); - if !trimmed.is_empty() { - reconstructed.push(trimmed.to_string()); - } - continue; - } - reconstructed.push(line.to_string()); - } - - result.push(StrippedStringPart::Text(reconstructed.join("\n"))); + result.push(StrippedStringPart::Text(unescape_multiline_string( + text.as_str(), + ))); } StringPart::Placeholder(placeholder) => { result.push(StrippedStringPart::Placeholder(placeholder)); @@ -2209,28 +2217,40 @@ impl LiteralString { } } + // Trim the first line + if let Some(StrippedStringPart::Text(text)) = result.first_mut() { + let end = text.find('\n').map(|p| p + 1).unwrap_or(text.len()); + let line = &text[..end]; + let len = line.len() - line.trim_start().len(); + text.replace_range(..len, ""); + } + + // Trim the last line if let Some(StrippedStringPart::Text(text)) = result.last_mut() { if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) { text.truncate(index + 1); } else { text.clear(); } + if text.ends_with('\n') { text.pop(); } } - // Now that the string has had line continuations parsed and the first and last - // lines removed, we can detect any leading whitespace and trim it. + // Now that the string has been unescaped and the first and last lines trimmed, + // we can detect any leading whitespace and trim it. let mut leading_whitespace = usize::MAX; let mut parsing_leading_whitespace = true; - for part in &result { + let mut iter = result.iter().peekable(); + while let Some(part) = iter.next() { match part { StrippedStringPart::Text(text) => { for (i, line) in text.lines().enumerate() { if i > 0 { parsing_leading_whitespace = true; } + if parsing_leading_whitespace { let mut ws_count = 0; for c in line.chars() { @@ -2240,6 +2260,18 @@ impl LiteralString { break; } } + + // Don't include blank lines in determining leading whitespace, unless + // the next part is a placeholder + if ws_count == line.len() + && iter + .peek() + .map(|p| !matches!(p, StrippedStringPart::Placeholder(_))) + .unwrap_or(true) + { + continue; + } + leading_whitespace = leading_whitespace.min(ws_count); } } @@ -2250,29 +2282,42 @@ impl LiteralString { } } - let mut parsing_leading_whitespace = true; + // Finally, strip the leading whitespace on each line + // This is done in place using the `replace_range` method; the method will + // internally do moves without allocations + let mut strip_leading_whitespace = true; for part in &mut result { match part { StrippedStringPart::Text(text) => { - let mut lines = Vec::new(); - for (i, line) in text.lines().enumerate() { - if i > 0 { - parsing_leading_whitespace = true; + let mut offset = 0; + while let Some(next) = text[offset..].find('\n') { + let next = next + offset; + if offset > 0 { + strip_leading_whitespace = true; } - if !parsing_leading_whitespace { - lines.push(line); + + if !strip_leading_whitespace { + offset = next + 1; continue; } - if line.len() < leading_whitespace { - lines.push(""); - } else { - lines.push(&line[leading_whitespace..]); - } + + let line = &text[offset..next]; + let line = line.strip_suffix('\r').unwrap_or(line); + let len = line.len().min(leading_whitespace); + text.replace_range(offset..offset + len, ""); + offset = next + 1 - len; + } + + // Replace any remaining text + if strip_leading_whitespace || offset > 0 { + let line = &text[offset..]; + let line = line.strip_suffix('\r').unwrap_or(line); + let len = line.len().min(leading_whitespace); + text.replace_range(offset..offset + len, ""); } - *text = lines.join("\n"); } StrippedStringPart::Placeholder(_) => { - parsing_leading_whitespace = false; + strip_leading_whitespace = false; } } } @@ -2353,6 +2398,65 @@ impl StringPart { #[derive(Clone, Debug, PartialEq, Eq)] pub struct StringText(pub(crate) SyntaxToken); +impl StringText { + /// Unescapes the string text to the given buffer. + /// + /// If the string text contains invalid escape sequences, they are left + /// as-is. + pub fn unescape_to(&self, buffer: &mut String) { + let text = self.0.text(); + let lexer = EscapeToken::lexer(text).spanned(); + for (token, span) in lexer { + match token.expect("should lex") { + EscapeToken::Valid => { + match &text[span] { + r"\\" => buffer.push('\\'), + r"\n" => buffer.push('\n'), + r"\r" => buffer.push('\r'), + r"\t" => buffer.push('\t'), + r"\'" => buffer.push('\''), + r#"\""# => buffer.push('"'), + r"\~" => buffer.push('~'), + r"\$" => buffer.push('$'), + _ => unreachable!("unexpected escape token"), + } + continue; + } + EscapeToken::ValidOctal => { + if let Some(c) = char::from_u32( + u32::from_str_radix(&text[span.start + 1..span.end], 8) + .expect("should be a valid octal number"), + ) { + buffer.push(c); + continue; + } + } + EscapeToken::ValidHex => { + buffer.push( + u8::from_str_radix(&text[span.start + 2..span.end], 16) + .expect("should be a valid hex number") as char, + ); + continue; + } + EscapeToken::ValidUnicode => { + if let Some(c) = char::from_u32( + u32::from_str_radix(&text[span.start + 2..span.end], 16) + .expect("should be a valid hex number"), + ) { + buffer.push(c); + continue; + } + } + _ => { + // Write the token to the buffer below + } + } + + buffer.push_str(&text[span]); + } + } +} + impl AstToken for StringText { fn can_cast(kind: SyntaxKind) -> bool where diff --git a/wdl-ast/src/v1/task.rs b/wdl-ast/src/v1/task.rs index 3aa97d68..1c95accd 100644 --- a/wdl-ast/src/v1/task.rs +++ b/wdl-ast/src/v1/task.rs @@ -28,6 +28,32 @@ pub mod common; pub mod requirements; pub mod runtime; +/// Unescapes command text. +fn unescape_command_text(s: &str, heredoc: bool, buffer: &mut String) { + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + match c { + '\\' => match chars.peek() { + Some('\\') | Some('~') => { + buffer.push(chars.next().unwrap()); + } + Some('>') if heredoc => { + buffer.push(chars.next().unwrap()); + } + Some('$') | Some('}') if !heredoc => { + buffer.push(chars.next().unwrap()); + } + _ => { + buffer.push('\\'); + } + }, + _ => { + buffer.push(c); + } + } + } +} + /// Represents a task definition. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TaskDefinition(pub(crate) SyntaxNode); @@ -766,10 +792,10 @@ impl CommandSection { None } - /// Strips leading whitespace from the command. If the command has mixed - /// indentation, this will return `None`. + /// Strips leading whitespace from the command. + /// + /// If the command has mixed indentation, this will return `None`. pub fn strip_whitespace(&self) -> Option> { - let mut result = Vec::new(); let mut min_leading_spaces = usize::MAX; let mut min_leading_tabs = usize::MAX; let mut parsing_leading_whitespace = false; // init to false so that the first line is skipped @@ -820,46 +846,46 @@ impl CommandSection { } } - // Check for no indentation or all whitespace, in which cases the first and last - // line must be trimmed. - if (min_leading_spaces == 0 && min_leading_tabs == 0) - || (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX) - { - for (i, part) in self.parts().enumerate() { - match part { - CommandPart::Text(text) => { - let mut stripped_text = Vec::new(); - for (j, line) in text.as_str().lines().enumerate() { - if i == 0 && j == 0 { - let trimmed = line.trim_start(); - if !trimmed.is_empty() { - stripped_text.push(trimmed); - } - continue; - } - stripped_text.push(line); - } - let stripped_text = stripped_text.join("\n"); - - result.push(StrippedCommandPart::Text(stripped_text)); - } - CommandPart::Placeholder(p) => { - result.push(StrippedCommandPart::Placeholder(p)); - } + let mut result = Vec::new(); + let heredoc = self.is_heredoc(); + for part in self.parts() { + match part { + CommandPart::Text(text) => { + let mut s = String::new(); + unescape_command_text(text.as_str(), heredoc, &mut s); + result.push(StrippedCommandPart::Text(s)); + } + CommandPart::Placeholder(p) => { + result.push(StrippedCommandPart::Placeholder(p)); } } + } - if let Some(StrippedCommandPart::Text(text)) = result.last_mut() { - if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) { - text.truncate(index + 1); - } else { - text.clear(); - } - if text.ends_with('\n') { - text.pop(); - } + // Trim the first line + if let Some(StrippedCommandPart::Text(text)) = result.first_mut() { + let end = text.find('\n').map(|p| p + 1).unwrap_or(text.len()); + let line = &text[..end]; + let len = line.len() - line.trim_start().len(); + text.replace_range(..len, ""); + } + + // Trim the last line + if let Some(StrippedCommandPart::Text(text)) = result.last_mut() { + if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) { + text.truncate(index + 1); + } else { + text.clear(); } + if text.ends_with('\n') { + text.pop(); + } + } + + // Check for no indentation or all whitespace, in which case we're done + if (min_leading_spaces == 0 && min_leading_tabs == 0) + || (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX) + { return Some(result); } @@ -878,50 +904,43 @@ impl CommandSection { min_leading_tabs }; - for (i, part) in self.parts().enumerate() { + // Finally, strip the leading whitespace on each line + // This is done in place using the `replace_range` method; the method will + // internally do moves without allocations + let mut strip_leading_whitespace = true; + for part in &mut result { match part { - CommandPart::Text(text) => { - let mut stripped_text = Vec::new(); - for (j, line) in text.as_str().lines().enumerate() { - if i == 0 && j == 0 { - let trimmed = line.trim_start(); - if !trimmed.is_empty() { - stripped_text.push(trimmed); - } - continue; + StrippedCommandPart::Text(text) => { + let mut offset = 0; + while let Some(next) = text[offset..].find('\n') { + let next = next + offset; + if offset > 0 { + strip_leading_whitespace = true; } - if j == 0 { - stripped_text.push(line); + + if !strip_leading_whitespace { + offset = next + 1; continue; } - if line.len() >= num_stripped_chars { - stripped_text.push(&line[num_stripped_chars..]); - } else { - stripped_text.push(""); - } - } - let stripped_text = stripped_text.join("\n"); - result.push(StrippedCommandPart::Text(stripped_text)); - } - CommandPart::Placeholder(p) => { - result.push(StrippedCommandPart::Placeholder(p)); - } - } - } + let line = &text[offset..next]; + let line = line.strip_suffix('\r').unwrap_or(line); + let len = line.len().min(num_stripped_chars); + text.replace_range(offset..offset + len, ""); + offset = next + 1 - len; + } - if let Some(StrippedCommandPart::Text(text)) = result.last_mut() { - if text.ends_with('\n') { - text.pop(); - } - if text.lines().last().map_or(false, |l| l.trim().is_empty()) { - while let Some(last) = text.lines().last() { - if last.trim().is_empty() { - text.pop(); - } else { - break; + // Replace any remaining text + if strip_leading_whitespace || offset > 0 { + let line = &text[offset..]; + let line = line.strip_suffix('\r').unwrap_or(line); + let len = line.len().min(num_stripped_chars); + text.replace_range(offset..offset + len, ""); } } + StrippedCommandPart::Placeholder(_) => { + strip_leading_whitespace = false; + } } } @@ -964,6 +983,17 @@ impl AstNode for CommandSection { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CommandText(pub(crate) SyntaxToken); +impl CommandText { + /// Unescapes the command text to the given buffer. + /// + /// When `heredoc` is true, only heredoc escape sequences are allowed. + /// + /// Otherwise, brace command sequences are accepted. + pub fn unescape_to(&self, heredoc: bool, buffer: &mut String) { + unescape_command_text(self.0.text(), heredoc, buffer); + } +} + impl AstToken for CommandText { fn can_cast(kind: SyntaxKind) -> bool where @@ -1688,6 +1718,8 @@ impl AstNode for ParameterMetadataSection { #[cfg(test)] mod test { + use pretty_assertions::assert_eq; + use super::*; use crate::Document; use crate::SupportedVersion; @@ -2346,7 +2378,7 @@ task test { }; assert_eq!( text, - "echo \"hello\"\n echo \"world\"\necho \\\n \"goodbye\"\n" + "echo \"hello\"\n echo \"world\"\necho \\\n \"goodbye\"" ); } } diff --git a/wdl-ast/tests/validation.rs b/wdl-ast/tests/validation.rs index eb460ee5..d20847d2 100644 --- a/wdl-ast/tests/validation.rs +++ b/wdl-ast/tests/validation.rs @@ -111,8 +111,9 @@ fn compare_result(path: &Path, result: &str, is_error: bool) -> Result<(), Strin if expected != result { return Err(format!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), )); } diff --git a/wdl-doc/src/lib.rs b/wdl-doc/src/lib.rs index 5ac1f8ce..e17e89ce 100644 --- a/wdl-doc/src/lib.rs +++ b/wdl-doc/src/lib.rs @@ -147,6 +147,7 @@ pub async fn document_workspace(path: PathBuf) -> Result<()> { for result in results { let cur_path = result + .document() .uri() .to_file_path() .expect("URI should have a file path"); @@ -162,10 +163,7 @@ pub async fn document_workspace(path: PathBuf) -> Result<()> { .file_name() .expect("current directory should have a file name") .to_string_lossy(); - let ast_doc = result - .parse_result() - .document() - .expect("document should be parsable"); + let ast_doc = result.document().node(); let version = ast_doc .version_statement() .expect("document should have a version statement") diff --git a/wdl-engine/Cargo.toml b/wdl-engine/Cargo.toml index e47b5ba9..234a3427 100644 --- a/wdl-engine/Cargo.toml +++ b/wdl-engine/Cargo.toml @@ -23,14 +23,20 @@ glob = { workspace = true } tempfile = { workspace = true } itertools = { workspace = true } serde = { workspace = true } +tracing = { workspace = true } +petgraph = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +sysinfo = { workspace = true } [dev-dependencies] -tokio = { workspace = true } pretty_assertions = { workspace = true } codespan-reporting = { workspace = true } path-clean = { workspace = true } colored = { workspace = true } approx = { workspace = true } +walkdir = { workspace = true } +rayon = { workspace = true } [lints] workspace = true @@ -43,3 +49,8 @@ codespan = ["wdl-ast/codespan"] name = "inputs" required-features = ["codespan"] harness = false + +[[test]] +name = "tasks" +required-features = ["codespan"] +harness = false diff --git a/wdl-engine/src/backend.rs b/wdl-engine/src/backend.rs new file mode 100644 index 00000000..98aa4e00 --- /dev/null +++ b/wdl-engine/src/backend.rs @@ -0,0 +1,121 @@ +//! Implementation of task execution backends. + +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Result; +use futures::future::BoxFuture; +use indexmap::IndexMap; + +use crate::Engine; +use crate::Value; + +pub mod local; + +/// Represents constraints applied to a task's execution. +pub struct TaskExecutionConstraints { + /// The container the task will run in. + /// + /// A value of `None` indicates the task will run on the host. + pub container: Option, + /// The allocated number of CPUs; must be greater than 0. + pub cpu: f64, + /// The allocated memory in bytes; must be greater than 0. + pub memory: i64, + /// A list with one specification per allocated GPU. + /// + /// The specification is execution engine-specific. + /// + /// If no GPUs were allocated, then the value must be an empty list. + pub gpu: Vec, + /// A list with one specification per allocated FPGA. + /// + /// The specification is execution engine-specific. + /// + /// If no FPGAs were allocated, then the value must be an empty list. + pub fpga: Vec, + /// A map with one entry for each disk mount point. + /// + /// The key is the mount point and the value is the initial amount of disk + /// space allocated, in bytes. + /// + /// The execution engine must, at a minimum, provide one entry for each disk + /// mount point requested, but may provide more. + /// + /// The amount of disk space available for a given mount point may increase + /// during the lifetime of the task (e.g., autoscaling volumes provided by + /// some cloud services). + pub disks: IndexMap, +} + +/// Represents the execution of a particular task. +pub trait TaskExecution: Send { + /// Maps a host path to a guest path. + /// + /// Returns `None` if the execution directly uses host paths. + fn map_path(&mut self, path: &Path) -> Option; + + /// Gets the working directory path for the task's execution. + /// + /// The working directory will be created upon spawning the task. + fn work_dir(&self) -> &Path; + + /// Gets the temporary directory path for the task's execution. + /// + /// The temporary directory is created before spawning the task so that it + /// is available for task evaluation. + fn temp_dir(&self) -> &Path; + + /// Gets the command file path. + /// + /// The command file is created upon spawning the task. + fn command(&self) -> &Path; + + /// Gets the stdout file path. + /// + /// The stdout file is created upon spawning the task. + fn stdout(&self) -> &Path; + + /// Gets the stderr file path. + /// + /// The stderr file is created upon spawning the task. + fn stderr(&self) -> &Path; + + /// Gets the execution constraints for the task given the task's + /// requirements and hints. + /// + /// Returns an error if the task cannot be constrained for the execution + /// environment or if the task specifies invalid requirements. + fn constraints( + &self, + engine: &Engine, + requirements: &HashMap, + hints: &HashMap, + ) -> Result; + + /// Spawns the execution of a task given the task's command, requirements, + /// and hints. + /// + /// Upon success, returns a future that will complete when the task's + /// execution has finished; the future returns the exit status code of the + /// task's process. + fn spawn( + &self, + command: String, + requirements: &HashMap, + hints: &HashMap, + ) -> Result>>; +} + +/// Represents a task execution backend. +pub trait TaskExecutionBackend { + /// Creates a new task execution. + /// + /// The specified directory serves as the root location of where a task + /// execution may keep its files. + /// + /// Note that this does not spawn the task's execution; see + /// [TaskExecution::spawn](TaskExecution::spawn). + fn create_execution(&self, root: &Path) -> Result>; +} diff --git a/wdl-engine/src/backend/local.rs b/wdl-engine/src/backend/local.rs new file mode 100644 index 00000000..2be52b32 --- /dev/null +++ b/wdl-engine/src/backend/local.rs @@ -0,0 +1,285 @@ +//! Implementation of the local backend. + +use std::collections::HashMap; +use std::fs; +use std::fs::File; +use std::path::Path; +use std::path::PathBuf; +use std::path::absolute; +use std::process::Stdio; + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use futures::FutureExt; +use futures::future::BoxFuture; +use tokio::process::Command; +use tracing::info; +use wdl_analysis::types::PrimitiveTypeKind; + +use super::TaskExecution; +use super::TaskExecutionBackend; +use super::TaskExecutionConstraints; +use crate::Coercible; +use crate::Engine; +use crate::Value; +use crate::convert_unit_string; + +/// Represents a local task execution. +/// +/// Local executions directly execute processes on the host without a container. +#[derive(Debug)] +pub struct LocalTaskExecution { + /// The path to the working directory for the execution. + work_dir: PathBuf, + /// The path to the temp directory for the execution. + temp_dir: PathBuf, + /// The path to the command file. + command: PathBuf, + /// The path to the stdout file. + stdout: PathBuf, + /// The path to the stderr file. + stderr: PathBuf, +} + +impl LocalTaskExecution { + /// Creates a new local task execution with the given execution root + /// directory to use. + pub fn new(root: &Path) -> Result { + let root = absolute(root).with_context(|| { + format!( + "failed to determine absolute path of `{path}`", + path = root.display() + ) + })?; + + // Recreate the temp directory as it may be needed for task evaluation + let temp_dir = root.join("tmp"); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir).with_context(|| { + format!( + "failed to remove directory `{path}`", + path = temp_dir.display() + ) + })?; + } + fs::create_dir_all(&temp_dir).with_context(|| { + format!( + "failed to create directory `{path}`", + path = temp_dir.display() + ) + })?; + + Ok(Self { + work_dir: root.join("work"), + temp_dir, + command: root.join("command"), + stdout: root.join("stdout"), + stderr: root.join("stderr"), + }) + } +} + +impl TaskExecution for LocalTaskExecution { + fn map_path(&mut self, _: &Path) -> Option { + // Local execution doesn't use guest paths + None + } + + fn work_dir(&self) -> &Path { + &self.work_dir + } + + fn temp_dir(&self) -> &Path { + &self.temp_dir + } + + fn command(&self) -> &Path { + &self.command + } + + fn stdout(&self) -> &Path { + &self.stdout + } + + fn stderr(&self) -> &Path { + &self.stderr + } + + fn constraints( + &self, + engine: &Engine, + requirements: &HashMap, + _: &HashMap, + ) -> Result { + let num_cpus: f64 = engine.system().cpus().len() as f64; + let min_cpu = requirements + .get("cpu") + .map(|v| { + v.coerce(engine.types(), PrimitiveTypeKind::Float.into()) + .expect("type should coerce") + .unwrap_float() + }) + .unwrap_or(1.0); + if num_cpus < min_cpu { + bail!( + "task requires at least {min_cpu} CPU{s}, but only {num_cpus} CPU{s2} are \ + available", + s = if min_cpu == 1.0 { "" } else { "s" }, + s2 = if num_cpus == 1.0 { "" } else { "s" } + ); + } + + let memory: i64 = engine + .system() + .total_memory() + .try_into() + .context("system has too much memory to describe as a WDL value")?; + let min_memory = requirements + .get("memory") + .map(|v| { + if let Some(v) = v.as_integer() { + return Ok(v); + } + + if let Some(s) = v.as_string() { + return convert_unit_string(s) + .and_then(|v| v.try_into().ok()) + .with_context(|| { + format!("task specifies an invalid `memory` requirement `{s}`") + }); + } + + unreachable!("value should be an integer or string"); + }) + .transpose()? + .unwrap_or(1); + if memory < min_memory { + bail!( + "task requires at least {min_memory} byte{s} of memory, but only {memory} \ + byte{s2} are available", + s = if min_memory == 1 { "" } else { "s" }, + s2 = if memory == 1 { "" } else { "s" } + ); + } + + Ok(TaskExecutionConstraints { + container: None, + cpu: num_cpus, + memory, + gpu: Default::default(), + fpga: Default::default(), + disks: Default::default(), + }) + } + + fn spawn( + &self, + command: String, + _: &HashMap, + _: &HashMap, + ) -> Result>> { + // Recreate the working directory + if self.work_dir.exists() { + fs::remove_dir_all(&self.work_dir).with_context(|| { + format!( + "failed to remove directory `{path}`", + path = self.work_dir.display() + ) + })?; + } + + fs::create_dir_all(&self.work_dir).with_context(|| { + format!( + "failed to create directory `{path}`", + path = self.work_dir.display() + ) + })?; + + // Write the evaluated command to disk + fs::write(&self.command, command).with_context(|| { + format!( + "failed to write command contents to `{path}`", + path = self.command.display() + ) + })?; + + // Create a file for the stdout + let stdout = File::create(&self.stdout).with_context(|| { + format!( + "failed to create stdout file `{path}`", + path = self.stdout.display() + ) + })?; + + // Create a file for the stderr + let stderr = File::create(&self.stderr).with_context(|| { + format!( + "failed to create stderr file `{path}`", + path = self.stderr.display() + ) + })?; + + let mut command = Command::new("bash"); + command + .current_dir(&self.work_dir) + .arg("-C") + .arg(&self.command) + .stdin(Stdio::null()) + .stdout(stdout) + .stderr(stderr); + + // Set an environment variable on Windows to get consistent PATH searching + // See: https://github.com/rust-lang/rust/issues/122660 + #[cfg(windows)] + command.env("WDL_TASK_EVALUATION", "1"); + + #[cfg(unix)] + let stderr = self.stderr.clone(); + + let mut child = command.spawn().context("failed to spawn `bash`")?; + Ok(async move { + let id = child.id().expect("should have id"); + info!("spawning local `bash` process {id} for task execution"); + + let status = child.wait().await.with_context(|| { + format!("failed to wait for termination of task child process {id}") + })?; + + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + if let Some(signal) = status.signal() { + bail!( + "task child process {id} has terminated with signal {signal}; see stderr \ + file `{path}` for more details", + path = stderr.display() + ); + } + } + + let status_code = status.code().expect("process should have exited"); + Ok(status_code) + } + .boxed()) + } +} + +/// Represents a task execution backend that locally executes tasks. +/// +/// This backend will directly spawn processes without using a container. +#[derive(Debug, Default, Clone, Copy)] +pub struct LocalTaskExecutionBackend; + +impl LocalTaskExecutionBackend { + /// Constructs a new local task execution backend. + pub fn new() -> Self { + Self + } +} + +impl TaskExecutionBackend for LocalTaskExecutionBackend { + fn create_execution(&self, root: &Path) -> Result> { + Ok(Box::new(LocalTaskExecution::new(root)?)) + } +} diff --git a/wdl-engine/src/diagnostics.rs b/wdl-engine/src/diagnostics.rs index 42e25333..fc1e3635 100644 --- a/wdl-engine/src/diagnostics.rs +++ b/wdl-engine/src/diagnostics.rs @@ -81,16 +81,22 @@ pub fn call_failed(target: &Ident, error: &anyhow::Error) -> Diagnostic { .with_highlight(target.span()) } -/// Creates a "struct member coercion failed" diagnostic. -pub fn struct_member_coercion_failed( +/// Creates a "runtime type mismatch" diagnostic. +pub fn runtime_type_mismatch( types: &Types, - e: &anyhow::Error, + e: anyhow::Error, expected: Type, expected_span: Span, actual: Type, actual_span: Span, ) -> Diagnostic { - Diagnostic::error(format!("type mismatch: {e:?}")) + let e = e.context(format!( + "type mismatch: expected type `{expected}`, but found type `{actual}`", + expected = expected.display(types), + actual = actual.display(types) + )); + + Diagnostic::error(format!("{e:?}")) .with_label( format!("this is type `{actual}`", actual = actual.display(types)), actual_span, @@ -112,15 +118,16 @@ pub fn array_index_out_of_range( target_span: Span, ) -> Diagnostic { Diagnostic::error(format!("array index {index} is out of range")) + .with_highlight(span) .with_label( - format!("expected an index value between 0 and {count}"), - span, - ) - .with_label( - format!( - "this array has {count} element{s}", - s = if count == 1 { "" } else { "s" } - ), + if count == 0 { + "this array is empty".to_string() + } else { + format!( + "this array has only {count} element{s}", + s = if count == 1 { "" } else { "s" } + ) + }, target_span, ) } @@ -193,3 +200,13 @@ pub fn invalid_storage_unit(unit: &str, span: Span) -> Diagnostic { pub fn function_call_failed(name: &str, error: impl fmt::Display, span: Span) -> Diagnostic { Diagnostic::error(format!("call to function `{name}` failed: {error}")).with_highlight(span) } + +/// Creates a "missing task output" diagnostic. +pub fn missing_task_output(e: anyhow::Error, task: &str, output: &Ident) -> Diagnostic { + let e = e.context(format!( + "failed to evaluate output `{output}` for task `{task}`", + output = output.as_str(), + )); + + Diagnostic::error(format!("{e:?}")).with_highlight(output.span()) +} diff --git a/wdl-engine/src/engine.rs b/wdl-engine/src/engine.rs index 32ded0b7..92d26ac5 100644 --- a/wdl-engine/src/engine.rs +++ b/wdl-engine/src/engine.rs @@ -1,26 +1,58 @@ //! Implementation of the WDL evaluation engine. -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; +use std::collections::HashMap; +use std::sync::Arc; + +use sysinfo::CpuRefreshKind; +use sysinfo::MemoryRefreshKind; +use sysinfo::System; +use wdl_analysis::diagnostics::unknown_type; use wdl_analysis::document::Document; +use wdl_analysis::types::Type; use wdl_analysis::types::Types; +use wdl_ast::AstToken; +use wdl_ast::Diagnostic; +use wdl_ast::Ident; + +use crate::TaskExecutionBackend; -use crate::Outputs; -use crate::TaskInputs; -use crate::WorkflowInputs; +/// Represents a cache of imported types for a specific document. +/// +/// Maps a document-specific type name to a previously imported type. +#[derive(Debug, Default)] +struct DocumentTypeCache(HashMap); -/// Represents a WDL evaluation engine. +/// Represents a cache of imported types for all evaluated documents. +/// +/// Maps a document identifier to that document's type cache. #[derive(Debug, Default)] +struct TypeCache(HashMap, DocumentTypeCache>); + +/// Represents an evaluation engine. pub struct Engine { - /// The engine's type collection. - pub(crate) types: Types, + /// The types collection for evaluation. + types: Types, + /// The type cache for evaluation. + cache: TypeCache, + /// The task execution backend to use. + backend: Box, + /// Information about the current system. + system: System, } impl Engine { - /// Constructs a new WDL evaluation engine. - pub fn new() -> Self { - Self::default() + /// Constructs a new engine for the given task execution backend. + pub fn new(backend: B) -> Self { + let mut system = System::new(); + system.refresh_cpu_list(CpuRefreshKind::new()); + system.refresh_memory_specifics(MemoryRefreshKind::new().with_ram()); + + Self { + types: Default::default(), + cache: Default::default(), + backend: Box::new(backend), + system, + } } /// Gets the engine's type collection. @@ -33,50 +65,40 @@ impl Engine { &mut self.types } - /// Evaluates a workflow. - /// - /// Returns the workflow outputs upon success. - pub async fn evaluate_workflow( - &mut self, - document: &Document, - inputs: &WorkflowInputs, - ) -> Result { - let workflow = document - .workflow() - .ok_or_else(|| anyhow!("document does not contain a workflow"))?; - inputs - .validate(&mut self.types, document, workflow) - .with_context(|| { - format!( - "failed to validate the inputs to workflow `{workflow}`", - workflow = workflow.name() - ) - })?; + /// Gets a reference to the task execution backend. + pub fn backend(&self) -> &dyn TaskExecutionBackend { + self.backend.as_ref() + } - todo!("not yet implemented") + /// Gets information about the system the engine is running on. + pub fn system(&self) -> &System { + &self.system } - /// Evaluates a task with the given name. + /// Resolves a type name from a document. /// - /// Returns the task outputs upon success. - pub async fn evaluate_task( + /// This function will import the type into the engine's type collection if + /// not already cached. + pub(crate) fn resolve_type_name( &mut self, document: &Document, - name: &str, - inputs: &TaskInputs, - ) -> Result { - let task = document - .task_by_name(name) - .ok_or_else(|| anyhow!("document does not contain a task named `{name}`"))?; - inputs - .validate(&mut self.types, document, task) - .with_context(|| { - format!( - "failed to validate the inputs to task `{task}`", - task = task.name() - ) - })?; + name: &Ident, + ) -> Result { + let cache = self.cache.0.entry(document.id().clone()).or_default(); + + match cache.0.get(name.as_str()) { + Some(ty) => Ok(*ty), + None => { + let ty = document + .struct_by_name(name.as_str()) + .map(|s| s.ty().expect("struct should have type")) + .ok_or_else(|| unknown_type(name.as_str(), name.span()))?; - todo!("not yet implemented") + // Cache the imported type for future expression evaluations + let ty = self.types.import(document.types(), ty); + cache.0.insert(name.as_str().into(), ty); + Ok(ty) + } + } } } diff --git a/wdl-engine/src/eval.rs b/wdl-engine/src/eval.rs index 694a5ee2..c7c4b9bd 100644 --- a/wdl-engine/src/eval.rs +++ b/wdl-engine/src/eval.rs @@ -1,18 +1,51 @@ -//! Module for expression evaluation. +//! Module for evaluation. +use std::collections::HashMap; use std::path::Path; +use std::path::PathBuf; +use anyhow::Context; +use anyhow::bail; use indexmap::IndexMap; +use wdl_analysis::document::Task; use wdl_analysis::types::Type; use wdl_analysis::types::Types; use wdl_ast::Diagnostic; use wdl_ast::Ident; use wdl_ast::SupportedVersion; +use crate::CompoundValue; +use crate::Outputs; +use crate::PrimitiveValue; +use crate::TaskExecution; use crate::Value; pub mod v1; +/// Represents an error that may occur when evaluating a workflow or task. +#[derive(Debug)] +pub enum EvaluationError { + /// The error came from WDL source evaluation. + Source(Diagnostic), + /// The error came from another source. + Other(anyhow::Error), +} + +impl From for EvaluationError { + fn from(diagnostic: Diagnostic) -> Self { + Self::Source(diagnostic) + } +} + +impl From for EvaluationError { + fn from(e: anyhow::Error) -> Self { + Self::Other(e) + } +} + +/// Represents a result from evaluating a workflow or task. +pub type EvaluationResult = Result; + /// Represents context to an expression evaluator. pub trait EvaluationContext { /// Gets the supported version of the document being evaluated. @@ -28,23 +61,31 @@ pub trait EvaluationContext { fn resolve_name(&self, name: &Ident) -> Result; /// Resolves a type name to a type. - fn resolve_type_name(&self, name: &Ident) -> Result; + fn resolve_type_name(&mut self, name: &Ident) -> Result; - /// Gets the current working directory for the evaluation. - fn cwd(&self) -> &Path; + /// Gets the working directory for the evaluation. + fn work_dir(&self) -> &Path; /// Gets the temp directory for the evaluation. - fn tmp(&self) -> &Path; + fn temp_dir(&self) -> &Path; /// Gets the value to return for a call to the `stdout` function. /// /// This is `Some` only when evaluating task outputs. - fn stdout(&self) -> Option; + fn stdout(&self) -> Option<&Value>; /// Gets the value to return for a call to the `stderr` function. /// /// This is `Some` only when evaluating task outputs. - fn stderr(&self) -> Option; + fn stderr(&self) -> Option<&Value>; + + /// Gets the task associated with the evaluation context. + /// + /// This is only `Some` when evaluating task hints sections. + fn task(&self) -> Option<&Task>; + + /// Gets the types collection associated with the document being evaluated. + fn document_types(&self) -> &Types; } /// Represents an index of a scope in a collection of scopes. @@ -64,7 +105,7 @@ impl From for usize { } /// Represents an evaluation scope in a WDL document. -#[derive(Debug)] +#[derive(Default, Debug)] pub struct Scope { /// The index of the parent scope. /// @@ -87,6 +128,17 @@ impl Scope { pub fn insert(&mut self, name: impl Into, value: impl Into) { self.names.insert(name.into(), value.into()); } + + /// Gets a mutable reference to an existing name in scope. + pub(crate) fn get_mut(&mut self, name: &str) -> Option<&mut Value> { + self.names.get_mut(name) + } +} + +impl From for IndexMap { + fn from(scope: Scope) -> Self { + scope.names + } } /// Represents a reference to a scope. @@ -149,3 +201,155 @@ impl<'a> ScopeRef<'a> { None } } + +/// Represents an evaluated task. +pub struct EvaluatedTask { + /// The evaluated task's status code. + status_code: i32, + /// The working directory of the executed task. + work_dir: PathBuf, + /// The temp directory of the executed task. + temp_dir: PathBuf, + /// The command file of the executed task. + command: PathBuf, + /// The value to return from the `stdout` function. + stdout: Value, + /// The value to return from the `stderr` function. + stderr: Value, + /// The evaluated outputs of the task. + /// + /// This is `Ok` when the task executes successfully and all of the task's + /// outputs evaluated without error. + /// + /// Otherwise, this contains the error that occurred while attempting to + /// evaluate the task's outputs. + outputs: EvaluationResult, +} + +impl EvaluatedTask { + /// Constructs a new evaluated task. + /// + /// Returns an error if the stdout or stderr paths are not UTF-8. + fn new(execution: &dyn TaskExecution, status_code: i32) -> anyhow::Result { + let stdout = PrimitiveValue::new_file(execution.stdout().to_str().with_context(|| { + format!( + "path to stdout file `{path}` is not UTF-8", + path = execution.stdout().display() + ) + })?) + .into(); + let stderr = PrimitiveValue::new_file(execution.stderr().to_str().with_context(|| { + format!( + "path to stderr file `{path}` is not UTF-8", + path = execution.stderr().display() + ) + })?) + .into(); + + Ok(Self { + status_code, + work_dir: execution.work_dir().into(), + temp_dir: execution.temp_dir().into(), + command: execution.command().into(), + stdout, + stderr, + outputs: Ok(Default::default()), + }) + } + + /// Gets the status code of the evaluated task. + pub fn status_code(&self) -> i32 { + self.status_code + } + + /// Gets the working directory of the evaluated task. + pub fn work_dir(&self) -> &Path { + &self.work_dir + } + + /// Gets the temp directory of the evaluated task. + pub fn temp_dir(&self) -> &Path { + &self.temp_dir + } + + /// Gets the command file of the evaluated task. + pub fn command(&self) -> &Path { + &self.command + } + + /// Gets the stdout value of the evaluated task. + pub fn stdout(&self) -> &Value { + &self.stdout + } + + /// Gets the stderr value of the evaluated task. + pub fn stderr(&self) -> &Value { + &self.stderr + } + + /// Gets the outputs of the evaluated task. + /// + /// This is `Ok` when the task executes successfully and all of the task's + /// outputs evaluated without error. + /// + /// Otherwise, this contains the error that occurred while attempting to + /// evaluate the task's outputs. + pub fn outputs(&self) -> &EvaluationResult { + &self.outputs + } + + /// Converts the evaluated task into an evaluation result. + /// + /// Returns `Ok(_)` if the task outputs were evaluated. + /// + /// Returns `Err(_)` if the task outputs could not be evaluated. + pub fn into_result(self) -> EvaluationResult { + self.outputs + } + + /// Handles the exit of a task execution. + /// + /// Returns an error if the task failed. + fn handle_exit(&self, requirements: &HashMap) -> anyhow::Result<()> { + let mut error = true; + if let Some(return_codes) = requirements + .get("return_codes") + .or_else(|| requirements.get("returnCodes")) + { + match return_codes { + Value::Primitive(PrimitiveValue::String(s)) if s.as_ref() == "*" => { + error = false; + } + Value::Primitive(PrimitiveValue::String(s)) => { + bail!("invalid return code value `{s}`; only `*` is accepted"); + } + Value::Primitive(PrimitiveValue::Integer(ok)) => { + if self.status_code == i32::try_from(*ok).unwrap_or_default() { + error = false; + } + } + Value::Compound(CompoundValue::Array(codes)) => { + error = !codes.as_slice().iter().any(|v| { + v.as_integer() + .map(|i| i32::try_from(i).unwrap_or_default() == self.status_code) + .unwrap_or(false) + }); + } + _ => unreachable!("unexpected return codes value"), + } + } else { + error = self.status_code != 0; + } + + if error { + bail!( + "task process has terminated with status code {code}; see standard error output \ + `{stderr}`", + code = self.status_code, + stderr = self.stderr.as_file().unwrap().as_str(), + ); + } + + Ok(()) + } +} diff --git a/wdl-engine/src/eval/v1.rs b/wdl-engine/src/eval/v1.rs index e4ab7b8e..d0ceb7b8 100644 --- a/wdl-engine/src/eval/v1.rs +++ b/wdl-engine/src/eval/v1.rs @@ -1,2845 +1,7 @@ -//! Implementation of an expression evaluator for 1.x WDL documents. +//! Implementation of evaluation for V1 documents. -use std::cmp::Ordering; -use std::fmt::Write; -use std::iter::once; -use std::sync::Arc; +mod expr; +mod task; -use indexmap::IndexMap; -use ordered_float::Pow; -use wdl_analysis::DiagnosticsConfig; -use wdl_analysis::diagnostics::ambiguous_argument; -use wdl_analysis::diagnostics::argument_type_mismatch; -use wdl_analysis::diagnostics::cannot_access; -use wdl_analysis::diagnostics::cannot_coerce_to_string; -use wdl_analysis::diagnostics::cannot_index; -use wdl_analysis::diagnostics::comparison_mismatch; -use wdl_analysis::diagnostics::if_conditional_mismatch; -use wdl_analysis::diagnostics::index_type_mismatch; -use wdl_analysis::diagnostics::logical_and_mismatch; -use wdl_analysis::diagnostics::logical_not_mismatch; -use wdl_analysis::diagnostics::logical_or_mismatch; -use wdl_analysis::diagnostics::map_key_not_primitive; -use wdl_analysis::diagnostics::missing_struct_members; -use wdl_analysis::diagnostics::no_common_type; -use wdl_analysis::diagnostics::not_a_pair_accessor; -use wdl_analysis::diagnostics::not_a_struct_member; -use wdl_analysis::diagnostics::numeric_mismatch; -use wdl_analysis::diagnostics::too_few_arguments; -use wdl_analysis::diagnostics::too_many_arguments; -use wdl_analysis::diagnostics::type_mismatch_custom; -use wdl_analysis::diagnostics::unknown_function; -use wdl_analysis::diagnostics::unsupported_function; -use wdl_analysis::stdlib::FunctionBindError; -use wdl_analysis::stdlib::MAX_PARAMETERS; -use wdl_analysis::types::ArrayType; -use wdl_analysis::types::Coercible as _; -use wdl_analysis::types::CompoundTypeDef; -use wdl_analysis::types::MapType; -use wdl_analysis::types::Optional; -use wdl_analysis::types::PairType; -use wdl_analysis::types::PrimitiveTypeKind; -use wdl_analysis::types::Type; -use wdl_analysis::types::TypeEq; -use wdl_analysis::types::v1::ComparisonOperator; -use wdl_analysis::types::v1::ExprTypeEvaluator; -use wdl_analysis::types::v1::NumericOperator; -use wdl_ast::AstNode; -use wdl_ast::AstNodeExt; -use wdl_ast::AstToken; -use wdl_ast::Diagnostic; -use wdl_ast::Span; -use wdl_ast::SupportedVersion; -use wdl_ast::SyntaxKind; -use wdl_ast::v1::AccessExpr; -use wdl_ast::v1::CallExpr; -use wdl_ast::v1::Expr; -use wdl_ast::v1::IfExpr; -use wdl_ast::v1::IndexExpr; -use wdl_ast::v1::LiteralArray; -use wdl_ast::v1::LiteralExpr; -use wdl_ast::v1::LiteralMap; -use wdl_ast::v1::LiteralObject; -use wdl_ast::v1::LiteralPair; -use wdl_ast::v1::LiteralString; -use wdl_ast::v1::LiteralStringKind; -use wdl_ast::v1::LiteralStruct; -use wdl_ast::v1::LogicalAndExpr; -use wdl_ast::v1::LogicalNotExpr; -use wdl_ast::v1::LogicalOrExpr; -use wdl_ast::v1::NegationExpr; -use wdl_ast::v1::Placeholder; -use wdl_ast::v1::PlaceholderOption; -use wdl_ast::v1::StringPart; -use wdl_ast::v1::StrippedStringPart; -use wdl_ast::version::V1; - -use super::EvaluationContext; -use crate::Array; -use crate::Coercible; -use crate::CompoundValue; -use crate::Map; -use crate::Object; -use crate::Pair; -use crate::PrimitiveValue; -use crate::Struct; -use crate::Value; -use crate::diagnostics::array_index_out_of_range; -use crate::diagnostics::division_by_zero; -use crate::diagnostics::exponent_not_in_range; -use crate::diagnostics::exponentiation_requirement; -use crate::diagnostics::float_not_in_range; -use crate::diagnostics::integer_negation_not_in_range; -use crate::diagnostics::integer_not_in_range; -use crate::diagnostics::map_key_not_found; -use crate::diagnostics::multiline_string_requirement; -use crate::diagnostics::not_an_object_member; -use crate::diagnostics::numeric_overflow; -use crate::diagnostics::struct_member_coercion_failed; -use crate::stdlib::CallArgument; -use crate::stdlib::CallContext; -use crate::stdlib::STDLIB; - -/// Represents a WDL expression evaluator. -#[derive(Debug)] -pub struct ExprEvaluator<'a, C> { - /// The expression evaluation context. - context: &'a mut C, - /// The nested count of placeholder evaluation. - /// - /// This is incremented immediately before a placeholder expression is - /// evaluated and decremented immediately after. - /// - /// If the count is non-zero, special evaluation behavior is enabled for - /// string interpolation. - placeholders: usize, - /// Tracks whether or not a `None`-resulting expression was evaluated during - /// a placeholder evaluation. - evaluated_none: bool, -} - -impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { - /// Creates a new expression evaluator. - pub fn new(context: &'a mut C) -> Self { - Self { - context, - placeholders: 0, - evaluated_none: false, - } - } - - /// Evaluates the given expression. - pub fn evaluate_expr(&mut self, expr: &Expr) -> Result { - let value = match expr { - Expr::Literal(expr) => self.evaluate_literal_expr(expr), - Expr::Name(r) => self.context.resolve_name(&r.name()), - Expr::Parenthesized(expr) => self.evaluate_expr(&expr.inner()), - Expr::If(expr) => self.evaluate_if_expr(expr), - Expr::LogicalNot(expr) => self.evaluate_logical_not_expr(expr), - Expr::Negation(expr) => self.evaluate_negation_expr(expr), - Expr::LogicalOr(expr) => self.evaluate_logical_or_expr(expr), - Expr::LogicalAnd(expr) => self.evaluate_logical_and_expr(expr), - Expr::Equality(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr(ComparisonOperator::Equality, &lhs, &rhs, expr.span()) - } - Expr::Inequality(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr( - ComparisonOperator::Inequality, - &lhs, - &rhs, - expr.span(), - ) - } - Expr::Less(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr(ComparisonOperator::Less, &lhs, &rhs, expr.span()) - } - Expr::LessEqual(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr( - ComparisonOperator::LessEqual, - &lhs, - &rhs, - expr.span(), - ) - } - Expr::Greater(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr(ComparisonOperator::Greater, &lhs, &rhs, expr.span()) - } - Expr::GreaterEqual(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_comparison_expr( - ComparisonOperator::GreaterEqual, - &lhs, - &rhs, - expr.span(), - ) - } - Expr::Addition(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Addition, &lhs, &rhs, expr.span()) - } - Expr::Subtraction(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Subtraction, &lhs, &rhs, expr.span()) - } - Expr::Multiplication(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Multiplication, &lhs, &rhs, expr.span()) - } - Expr::Division(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Division, &lhs, &rhs, expr.span()) - } - Expr::Modulo(expr) => { - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Modulo, &lhs, &rhs, expr.span()) - } - Expr::Exponentiation(expr) => { - if self.context.version() < SupportedVersion::V1(V1::Two) { - return Err(exponentiation_requirement(expr.span())); - } - let (lhs, rhs) = expr.operands(); - self.evaluate_numeric_expr(NumericOperator::Exponentiation, &lhs, &rhs, expr.span()) - } - Expr::Call(expr) => self.evaluate_call_expr(expr), - Expr::Index(expr) => self.evaluate_index_expr(expr), - Expr::Access(expr) => self.evaluate_access_expr(expr), - }?; - - self.evaluated_none |= self.placeholders > 0 && value.is_none(); - Ok(value) - } - - /// Evaluates a literal expression. - fn evaluate_literal_expr(&mut self, expr: &LiteralExpr) -> Result { - match expr { - LiteralExpr::Boolean(lit) => Ok(lit.value().into()), - LiteralExpr::Integer(lit) => { - // Check to see if this literal is a direct child of a negation expression; if - // so, we want to negate the literal - let (value, span) = match lit.syntax().parent() { - Some(parent) if parent.kind() == SyntaxKind::NegationExprNode => { - let start = parent.text_range().start().into(); - (lit.negate(), Span::new(start, lit.span().end() - start)) - } - _ => (lit.value(), lit.span()), - }; - - Ok(value.ok_or_else(|| integer_not_in_range(span))?.into()) - } - LiteralExpr::Float(lit) => Ok(lit - .value() - .ok_or_else(|| float_not_in_range(lit.span()))? - .into()), - LiteralExpr::String(lit) => self.evaluate_literal_string(lit), - LiteralExpr::Array(lit) => self.evaluate_literal_array(lit), - LiteralExpr::Pair(lit) => self.evaluate_literal_pair(lit), - LiteralExpr::Map(lit) => self.evaluate_literal_map(lit), - LiteralExpr::Object(lit) => self.evaluate_literal_object(lit), - LiteralExpr::Struct(lit) => self.evaluate_literal_struct(lit), - LiteralExpr::None(_) => Ok(Value::None), - LiteralExpr::Hints(_) | LiteralExpr::Input(_) | LiteralExpr::Output(_) => { - todo!("implement for WDL 1.2 support") - } - } - } - - /// Evaluates a placeholder into the given string buffer. - fn evaluate_placeholder( - &mut self, - placeholder: &Placeholder, - buffer: &mut String, - ) -> Result<(), Diagnostic> { - let expr = placeholder.expr(); - match self.evaluate_expr(&expr)? { - Value::None => { - if let Some(o) = placeholder.option().as_ref().and_then(|o| o.as_default()) { - buffer.push_str(&self.evaluate_literal_string(&o.value())?.unwrap_string()) - } - } - Value::Primitive(PrimitiveValue::Boolean(v)) => { - match placeholder - .option() - .as_ref() - .and_then(|o| o.as_true_false()) - { - Some(o) => { - let (t, f) = o.values(); - if v { - buffer.push_str(&self.evaluate_literal_string(&t)?.unwrap_string()); - } else { - buffer.push_str(&self.evaluate_literal_string(&f)?.unwrap_string()); - } - } - None => { - if v { - buffer.push_str("true"); - } else { - buffer.push_str("false"); - } - } - } - } - Value::Primitive(v) => write!(buffer, "{v}", v = v.raw()).unwrap(), - Value::Compound(CompoundValue::Array(v)) - if matches!(placeholder.option(), Some(PlaceholderOption::Sep(_))) - && v.elements() - .first() - .map(|e| !matches!(e, Value::None | Value::Compound(_))) - .unwrap_or(false) => - { - let option = placeholder.option().unwrap().unwrap_sep(); - - let sep = self - .evaluate_literal_string(&option.separator())? - .unwrap_string(); - for (i, v) in v.elements().iter().enumerate() { - if i > 0 { - buffer.push_str(&sep); - } - - write!(buffer, "{v}").unwrap(); - } - } - Value::Compound(v) => { - return Err(cannot_coerce_to_string( - self.context.types(), - v.ty(), - expr.span(), - )); - } - } - - Ok(()) - } - - /// Evaluates a literal string expression. - fn evaluate_literal_string(&mut self, expr: &LiteralString) -> Result { - /// Helper for evaluating placeholders in a string. - /// This handles incrementing the nested placeholder count and handling - /// failures when a `None` expression was evaluated. - fn evaluate_placeholder( - evaluator: &mut ExprEvaluator<'_, C>, - placeholder: &Placeholder, - buffer: &mut String, - ) -> Result<(), Diagnostic> { - // Keep track of the start in case there is a `None` evaluated and an error - let start = buffer.len(); - - // Bump the placeholder count while evaluating the placeholder - evaluator.placeholders += 1; - let result = evaluator.evaluate_placeholder(placeholder, buffer); - evaluator.placeholders -= 1; - - // Reset the evaluated none flag - if evaluator.placeholders == 0 { - let evaluated_none = std::mem::replace(&mut evaluator.evaluated_none, false); - - // If a `None` was evaluated and an error occurred, truncate to the start of the - // placeholder evaluation - if evaluated_none && result.is_err() { - buffer.truncate(start); - return Ok(()); - } - } - - result - } - - if expr.kind() == LiteralStringKind::Multiline - && self.context.version() < SupportedVersion::V1(V1::Two) - { - return Err(multiline_string_requirement(expr.span())); - } - - let mut s = String::new(); - if let Some(parts) = expr.strip_whitespace() { - for part in parts { - match part { - StrippedStringPart::Text(t) => s.push_str(t.as_str()), - StrippedStringPart::Placeholder(placeholder) => { - evaluate_placeholder(self, &placeholder, &mut s)?; - } - } - } - } else { - for part in expr.parts() { - match part { - StringPart::Text(t) => s.push_str(t.as_str()), - StringPart::Placeholder(placeholder) => { - evaluate_placeholder(self, &placeholder, &mut s)?; - } - } - } - } - - Ok(PrimitiveValue::new_string(s).into()) - } - - /// Evaluates a literal array expression. - fn evaluate_literal_array(&mut self, expr: &LiteralArray) -> Result { - // Look at the first array element to determine the element type - // The remaining elements must have a common type - let mut elements = expr.elements(); - let (element_ty, values) = match elements.next() { - Some(expr) => { - let mut values = Vec::new(); - let value = self.evaluate_expr(&expr)?; - let mut expected: Type = value.ty(); - let mut expected_span = expr.span(); - values.push(value); - - // Ensure the remaining element types share a common type - for expr in elements { - let value = self.evaluate_expr(&expr)?; - let actual = value.ty(); - - if let Some(ty) = expected.common_type(self.context.types_mut(), actual) { - expected = ty; - expected_span = expr.span(); - } else { - return Err(no_common_type( - self.context.types(), - expected, - expected_span, - actual, - expr.span(), - )); - } - - values.push(value); - } - - (expected, values) - } - None => (Type::Union, Vec::new()), - }; - - let ty = self - .context - .types_mut() - .add_array(ArrayType::new(element_ty)); - Ok(Array::new(self.context.types(), ty, values) - .expect("array elements should coerce") - .into()) - } - - /// Evaluates a literal pair expression. - fn evaluate_literal_pair(&mut self, expr: &LiteralPair) -> Result { - let (left, right) = expr.exprs(); - let left = self.evaluate_expr(&left)?; - let right = self.evaluate_expr(&right)?; - let ty = self - .context - .types_mut() - .add_pair(PairType::new(left.ty(), right.ty())); - Ok(Pair::new(self.context.types(), ty, left, right) - .expect("types should coerce") - .into()) - } - - /// Evaluates a literal map expression. - fn evaluate_literal_map(&mut self, expr: &LiteralMap) -> Result { - let mut items = expr.items(); - let (key_ty, value_ty, elements) = match items.next() { - Some(item) => { - let mut elements = Vec::new(); - - // Evaluate the first key-value pair - let (key, value) = item.key_value(); - let expected_key = self.evaluate_expr(&key)?; - let mut expected_key_ty = expected_key.ty(); - let mut expected_key_span = key.span(); - let expected_value = self.evaluate_expr(&value)?; - let mut expected_value_ty = expected_value.ty(); - let mut expected_value_span = value.span(); - - // The key type must be primitive - let key = match expected_key { - Value::None => None, - Value::Primitive(key) => Some(key), - _ => { - return Err(map_key_not_primitive( - self.context.types(), - key.span(), - expected_key.ty(), - )); - } - }; - - elements.push((key, expected_value)); - - // Ensure the remaining items types share common types - for item in items { - let (key, value) = item.key_value(); - let actual_key = self.evaluate_expr(&key)?; - let actual_key_ty = actual_key.ty(); - let actual_value = self.evaluate_expr(&value)?; - let actual_value_ty = actual_value.ty(); - - if let Some(ty) = - expected_key_ty.common_type(self.context.types_mut(), actual_key_ty) - { - expected_key_ty = ty; - expected_key_span = key.span(); - } else { - // No common key type - return Err(no_common_type( - self.context.types(), - expected_key_ty, - expected_key_span, - actual_key_ty, - key.span(), - )); - } - - if let Some(ty) = - expected_value_ty.common_type(self.context.types_mut(), actual_value_ty) - { - expected_value_ty = ty; - expected_value_span = value.span(); - } else { - // No common value type - return Err(no_common_type( - self.context.types(), - expected_value_ty, - expected_value_span, - actual_value_ty, - value.span(), - )); - } - - let key = match actual_key { - Value::None => None, - Value::Primitive(key) => Some(key), - _ => panic!("the key type is not primitive, but had a common type"), - }; - - elements.push((key, actual_value)); - } - - (expected_key_ty, expected_value_ty, elements) - } - None => (Type::Union, Type::Union, Vec::new()), - }; - - let ty = self - .context - .types_mut() - .add_map(MapType::new(key_ty, value_ty)); - Ok(Map::new(self.context.types(), ty, elements) - .expect("map elements should coerce") - .into()) - } - - /// Evaluates a literal object expression. - fn evaluate_literal_object(&mut self, expr: &LiteralObject) -> Result { - Ok(Object::from( - expr.items() - .map(|item| { - let (name, value) = item.name_value(); - Ok((name.as_str().to_string(), self.evaluate_expr(&value)?)) - }) - .collect::, _>>()?, - ) - .into()) - } - - /// Evaluates a literal struct expression. - fn evaluate_literal_struct(&mut self, expr: &LiteralStruct) -> Result { - let name = expr.name(); - let ty = self.context.resolve_type_name(&name)?; - - // Evaluate the members - let mut members = - IndexMap::with_capacity(self.context.types().struct_type(ty).members().len()); - for item in expr.items() { - let (n, v) = item.name_value(); - if let Some(expected) = self - .context - .types() - .struct_type(ty) - .members() - .get(n.as_str()) - { - let expected = *expected; - let value = self.evaluate_expr(&v)?; - let value = value.coerce(self.context.types(), expected).map_err(|e| { - struct_member_coercion_failed( - self.context.types(), - &e, - expected, - n.span(), - value.ty(), - v.span(), - ) - })?; - - members.insert(n.as_str().to_string(), value); - } else { - // Not a struct member - return Err(not_a_struct_member(name.as_str(), &n)); - } - } - - let mut iter = self.context.types().struct_type(ty).members().iter(); - while let Some((n, ty)) = iter.next() { - // Check for optional members that should be set to `None` - if ty.is_optional() { - if !members.contains_key(n) { - members.insert(n.clone(), Value::None); - } - } else { - // Check for a missing required member - if !members.contains_key(n) { - let mut missing = once(n) - .chain(iter.filter_map(|(n, ty)| { - if ty.is_optional() && !members.contains_key(n.as_str()) { - Some(n) - } else { - None - } - })) - .peekable(); - let mut names: String = String::new(); - let mut count = 0; - while let Some(n) = missing.next() { - match (missing.peek().is_none(), count) { - (true, c) if c > 1 => names.push_str(", and "), - (true, 1) => names.push_str(" and "), - (false, c) if c > 0 => names.push_str(", "), - _ => {} - } - - write!(&mut names, "`{n}`").ok(); - count += 1; - } - - return Err(missing_struct_members(&name, count, &names)); - } - } - } - - Ok(Struct::new_unchecked( - ty, - self.context.types().struct_type(ty).name().clone(), - Arc::new(members), - ) - .into()) - } - - /// Evaluates an `if` expression. - fn evaluate_if_expr(&mut self, expr: &IfExpr) -> Result { - /// Used to translate an expression evaluation context to an expression - /// type evaluation context. - struct TypeContext<'a, C: EvaluationContext> { - /// The expression evaluation context. - context: &'a mut C, - /// The diagnostics from evaluating the type of an expression. - diagnostics: Vec, - } - - impl wdl_analysis::types::v1::EvaluationContext for TypeContext<'_, C> { - fn version(&self) -> SupportedVersion { - self.context.version() - } - - fn types(&self) -> &wdl_analysis::types::Types { - self.context.types() - } - - fn types_mut(&mut self) -> &mut wdl_analysis::types::Types { - self.context.types_mut() - } - - fn resolve_name(&self, name: &wdl_ast::Ident) -> Option { - self.context.resolve_name(name).map(|v| v.ty()).ok() - } - - fn resolve_type_name(&mut self, name: &wdl_ast::Ident) -> Result { - self.context.resolve_type_name(name) - } - - fn input(&self, _name: &str) -> Option { - todo!("implement for WDL 1.2 support") - } - - fn output(&self, _name: &str) -> Option { - todo!("implement for WDL 1.2 support") - } - - fn task_name(&self) -> Option<&str> { - todo!("implement for WDL 1.2 support") - } - - fn supports_hints_type(&self) -> bool { - todo!("implement for WDL 1.2 support") - } - - fn supports_input_type(&self) -> bool { - todo!("implement for WDL 1.2 support") - } - - fn supports_output_type(&self) -> bool { - todo!("implement for WDL 1.2 support") - } - - fn diagnostics_config(&self) -> DiagnosticsConfig { - DiagnosticsConfig::except_all() - } - - fn add_diagnostic(&mut self, diagnostic: Diagnostic) { - self.diagnostics.push(diagnostic); - } - } - - let (cond_expr, true_expr, false_expr) = expr.exprs(); - - // Evaluate the conditional expression and the true expression or the false - // expression, depending on the result of the conditional expression - let cond = self.evaluate_expr(&cond_expr)?; - let (value, true_ty, false_ty) = if cond - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| { - if_conditional_mismatch(self.context.types(), cond.ty(), cond_expr.span()) - })? - .unwrap_boolean() - { - // Evaluate the `true` expression and calculate the type of the `false` - // expression - let value = self.evaluate_expr(&true_expr)?; - let mut context = TypeContext { - context: self.context, - diagnostics: Vec::new(), - }; - let false_ty = ExprTypeEvaluator::new(&mut context) - .evaluate_expr(&false_expr) - .unwrap_or(Type::Union); - - if let Some(diagnostic) = context.diagnostics.pop() { - return Err(diagnostic); - } - - let true_ty = value.ty(); - (value, true_ty, false_ty) - } else { - // Evaluate the `false` expression and calculate the type of the `true` - // expression - let value = self.evaluate_expr(&false_expr)?; - let mut context = TypeContext { - context: self.context, - diagnostics: Vec::new(), - }; - let true_ty = ExprTypeEvaluator::new(&mut context) - .evaluate_expr(&true_expr) - .unwrap_or(Type::Union); - if let Some(diagnostic) = context.diagnostics.pop() { - return Err(diagnostic); - } - - let false_ty = value.ty(); - (value, true_ty, false_ty) - }; - - // Determine the common type of the true and false expressions - // The value must be coerced to that type - let ty = true_ty - .common_type(self.context.types_mut(), false_ty) - .ok_or_else(|| { - no_common_type( - self.context.types(), - true_ty, - true_expr.span(), - false_ty, - false_expr.span(), - ) - })?; - - Ok(value - .coerce(self.context.types(), ty) - .expect("coercion should not fail")) - } - - /// Evaluates a `logical not` expression. - fn evaluate_logical_not_expr(&mut self, expr: &LogicalNotExpr) -> Result { - // The operand should be a boolean - let operand = expr.operand(); - let value = self.evaluate_expr(&operand)?; - Ok((!value - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| logical_not_mismatch(self.context.types(), value.ty(), operand.span()))? - .unwrap_boolean()) - .into()) - } - - /// Evaluates a negation expression. - fn evaluate_negation_expr(&mut self, expr: &NegationExpr) -> Result { - let operand = expr.operand(); - let value = self.evaluate_expr(&operand)?; - let ty = value.ty(); - - // If the type is `Int`, treat it as `Int` - if ty.type_eq(self.context.types(), &PrimitiveTypeKind::Integer.into()) { - return match operand { - Expr::Literal(LiteralExpr::Integer(_)) => { - // Already negated during integer literal evaluation - Ok(value) - } - _ => { - let value = value.unwrap_integer(); - Ok(value - .checked_neg() - .ok_or_else(|| integer_negation_not_in_range(value, operand.span()))? - .into()) - } - }; - } - - // If the type is `Float`, treat it as `Float` - if ty.type_eq(self.context.types(), &PrimitiveTypeKind::Float.into()) { - let value = value.unwrap_float(); - return Ok((-value).into()); - } - - // Expected either `Int` or `Float` - Err(type_mismatch_custom( - self.context.types(), - &[ - PrimitiveTypeKind::Integer.into(), - PrimitiveTypeKind::Float.into(), - ], - operand.span(), - ty, - operand.span(), - )) - } - - /// Evaluates a `logical or` expression. - fn evaluate_logical_or_expr(&mut self, expr: &LogicalOrExpr) -> Result { - let (lhs, rhs) = expr.operands(); - - // Evaluate the left-hand side first - let left = self.evaluate_expr(&lhs)?; - if left - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| logical_or_mismatch(self.context.types(), left.ty(), lhs.span()))? - .unwrap_boolean() - { - // Short-circuit if the left-hand side is true - return Ok(true.into()); - } - - // Otherwise, evaluate the right-hand side - let right = self.evaluate_expr(&rhs)?; - right - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| logical_or_mismatch(self.context.types(), right.ty(), rhs.span())) - } - - /// Evaluates a `logical and` expression. - fn evaluate_logical_and_expr(&mut self, expr: &LogicalAndExpr) -> Result { - let (lhs, rhs) = expr.operands(); - - // Evaluate the left-hand side first - let left = self.evaluate_expr(&lhs)?; - if !left - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| logical_and_mismatch(self.context.types(), left.ty(), lhs.span()))? - .unwrap_boolean() - { - // Short-circuit if the left-hand side is false - return Ok(false.into()); - } - - // Otherwise, evaluate the right-hand side - let right = self.evaluate_expr(&rhs)?; - right - .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) - .map_err(|_| logical_and_mismatch(self.context.types(), right.ty(), rhs.span())) - } - - /// Evaluates a comparison expression. - fn evaluate_comparison_expr( - &mut self, - op: ComparisonOperator, - lhs: &Expr, - rhs: &Expr, - span: Span, - ) -> Result { - let left = self.evaluate_expr(lhs)?; - let right = self.evaluate_expr(rhs)?; - - match op { - ComparisonOperator::Equality => Value::equals(self.context.types(), &left, &right), - ComparisonOperator::Inequality => { - Value::equals(self.context.types(), &left, &right).map(|r| !r) - } - ComparisonOperator::Less - | ComparisonOperator::LessEqual - | ComparisonOperator::Greater - | ComparisonOperator::GreaterEqual => { - // Only primitive types support other comparisons - match (&left, &right) { - (Value::Primitive(left), Value::Primitive(right)) => { - PrimitiveValue::compare(left, right).map(|o| match o { - Ordering::Less => matches!( - op, - ComparisonOperator::Less | ComparisonOperator::LessEqual - ), - Ordering::Equal => matches!( - op, - ComparisonOperator::LessEqual | ComparisonOperator::GreaterEqual - ), - Ordering::Greater => matches!( - op, - ComparisonOperator::Greater | ComparisonOperator::GreaterEqual - ), - }) - } - _ => None, - } - } - } - .map(Into::into) - .ok_or_else(|| { - comparison_mismatch( - self.context.types(), - op, - span, - left.ty(), - lhs.span(), - right.ty(), - rhs.span(), - ) - }) - } - - /// Evaluates a numeric expression. - fn evaluate_numeric_expr( - &mut self, - op: NumericOperator, - lhs: &Expr, - rhs: &Expr, - span: Span, - ) -> Result { - /// Implements numeric operations on integer operands. - fn int_numeric_op( - op: NumericOperator, - left: i64, - right: i64, - span: Span, - rhs_span: Span, - ) -> Result { - match op { - NumericOperator::Addition => left - .checked_add(right) - .ok_or_else(|| numeric_overflow(span)), - NumericOperator::Subtraction => left - .checked_sub(right) - .ok_or_else(|| numeric_overflow(span)), - NumericOperator::Multiplication => left - .checked_mul(right) - .ok_or_else(|| numeric_overflow(span)), - NumericOperator::Division => { - if right == 0 { - return Err(division_by_zero(span, rhs_span)); - } - - left.checked_div(right) - .ok_or_else(|| numeric_overflow(span)) - } - NumericOperator::Modulo => { - if right == 0 { - return Err(division_by_zero(span, rhs_span)); - } - - left.checked_rem(right) - .ok_or_else(|| numeric_overflow(span)) - } - NumericOperator::Exponentiation => left - .checked_pow( - (right) - .try_into() - .map_err(|_| exponent_not_in_range(rhs_span))?, - ) - .ok_or_else(|| numeric_overflow(span)), - } - } - - /// Implements numeric operations on floating point operands. - fn float_numeric_op(op: NumericOperator, left: f64, right: f64) -> f64 { - match op { - NumericOperator::Addition => left + right, - NumericOperator::Subtraction => left - right, - NumericOperator::Multiplication => left * right, - NumericOperator::Division => left / right, - NumericOperator::Modulo => left % right, - NumericOperator::Exponentiation => left.pow(right), - } - } - - let left = self.evaluate_expr(lhs)?; - let right = self.evaluate_expr(rhs)?; - match (&left, &right) { - ( - Value::Primitive(PrimitiveValue::Integer(left)), - Value::Primitive(PrimitiveValue::Integer(right)), - ) => Some(int_numeric_op(op, *left, *right, span, rhs.span())?.into()), - ( - Value::Primitive(PrimitiveValue::Float(left)), - Value::Primitive(PrimitiveValue::Float(right)), - ) => Some(float_numeric_op(op, left.0, right.0).into()), - ( - Value::Primitive(PrimitiveValue::Integer(left)), - Value::Primitive(PrimitiveValue::Float(right)), - ) => Some(float_numeric_op(op, *left as f64, right.0).into()), - ( - Value::Primitive(PrimitiveValue::Float(left)), - Value::Primitive(PrimitiveValue::Integer(right)), - ) => Some(float_numeric_op(op, left.0, *right as f64).into()), - (Value::Primitive(PrimitiveValue::String(left)), Value::Primitive(right)) - if op == NumericOperator::Addition - && !matches!(right, PrimitiveValue::Boolean(_)) => - { - Some( - PrimitiveValue::new_string(format!("{left}{right}", right = right.raw())) - .into(), - ) - } - (Value::Primitive(left), Value::Primitive(PrimitiveValue::String(right))) - if op == NumericOperator::Addition - && !matches!(left, PrimitiveValue::Boolean(_)) => - { - Some(PrimitiveValue::new_string(format!("{left}{right}", left = left.raw())).into()) - } - (Value::Primitive(PrimitiveValue::String(_)), Value::None) - | (Value::None, Value::Primitive(PrimitiveValue::String(_))) - if op == NumericOperator::Addition && self.placeholders > 0 => - { - // Allow string concatenation with `None` in placeholders, which evaluates to - // `None` - Some(Value::None) - } - _ => None, - } - .ok_or_else(|| { - numeric_mismatch( - self.context.types(), - op, - span, - left.ty(), - lhs.span(), - right.ty(), - rhs.span(), - ) - }) - } - - /// Evaluates a call expression. - fn evaluate_call_expr(&mut self, expr: &CallExpr) -> Result { - let target = expr.target(); - match wdl_analysis::stdlib::STDLIB.function(target.as_str()) { - Some(f) => { - // Evaluate the argument expressions - let mut count = 0; - let mut types = [Type::Union; MAX_PARAMETERS]; - let mut arguments = [const { CallArgument::none() }; MAX_PARAMETERS]; - for arg in expr.arguments() { - if count < MAX_PARAMETERS { - let v = self.evaluate_expr(&arg)?; - types[count] = v.ty(); - arguments[count] = CallArgument::new(v, arg.span()); - } - - count += 1; - } - - // First bind the function based on the argument types, then dispatch the call - let types = &types[..count.min(MAX_PARAMETERS)]; - let arguments = &arguments[..count.min(MAX_PARAMETERS)]; - if count <= MAX_PARAMETERS { - match f.bind(self.context.version(), self.context.types_mut(), types) { - Ok(binding) => { - let context = CallContext::new( - self.context, - target.span(), - arguments, - binding.return_type(), - ); - - STDLIB - .get(target.as_str()) - .expect("should have implementation") - .call(binding, context) - } - Err(FunctionBindError::RequiresVersion(minimum)) => Err( - unsupported_function(minimum, target.as_str(), target.span()), - ), - Err(FunctionBindError::TooFewArguments(minimum)) => Err(too_few_arguments( - target.as_str(), - target.span(), - minimum, - arguments.len(), - )), - Err(FunctionBindError::TooManyArguments(maximum)) => { - Err(too_many_arguments( - target.as_str(), - target.span(), - maximum, - arguments.len(), - expr.arguments().skip(maximum).map(|e| e.span()), - )) - } - Err(FunctionBindError::ArgumentTypeMismatch { index, expected }) => { - Err(argument_type_mismatch( - self.context.types(), - target.as_str(), - &expected, - types[index], - expr.arguments() - .nth(index) - .map(|e| e.span()) - .expect("should have span"), - )) - } - Err(FunctionBindError::Ambiguous { first, second }) => Err( - ambiguous_argument(target.as_str(), target.span(), &first, &second), - ), - } - } else { - // Exceeded the maximum number of arguments to any function - match f.param_min_max(self.context.version()) { - Some((_, max)) => { - assert!(max <= MAX_PARAMETERS); - Err(too_many_arguments( - target.as_str(), - target.span(), - max, - count, - expr.arguments().skip(max).map(|e| e.span()), - )) - } - None => Err(unsupported_function( - f.minimum_version(), - target.as_str(), - target.span(), - )), - } - } - } - None => Err(unknown_function(target.as_str(), target.span())), - } - } - - /// Evaluates the type of an index expression. - fn evaluate_index_expr(&mut self, expr: &IndexExpr) -> Result { - let (target, index) = expr.operands(); - match self.evaluate_expr(&target)? { - Value::Compound(CompoundValue::Array(array)) => match self.evaluate_expr(&index)? { - Value::Primitive(PrimitiveValue::Integer(i)) => { - match i.try_into().map(|i: usize| array.elements().get(i)) { - Ok(Some(value)) => Ok(value.clone()), - _ => Err(array_index_out_of_range( - i, - array.len(), - index.span(), - target.span(), - )), - } - } - value => Err(index_type_mismatch( - self.context.types(), - PrimitiveTypeKind::Integer.into(), - value.ty(), - index.span(), - )), - }, - Value::Compound(CompoundValue::Map(map)) => { - let ty = map.ty().as_compound().expect("type should be compound"); - let key_type = match self.context.types().type_definition(ty.definition()) { - CompoundTypeDef::Map(ty) => ty - .key_type() - .as_primitive() - .expect("type should be primitive"), - _ => panic!("expected a map type"), - }; - - let i = match self.evaluate_expr(&index)? { - Value::None - if Type::None.is_coercible_to(self.context.types(), &key_type.into()) => - { - None - } - Value::Primitive(i) - if i.ty() - .is_coercible_to(self.context.types(), &key_type.into()) => - { - Some(i) - } - value => { - return Err(index_type_mismatch( - self.context.types(), - key_type.into(), - value.ty(), - index.span(), - )); - } - }; - - match map.elements().get(&i) { - Some(value) => Ok(value.clone()), - None => Err(map_key_not_found(index.span())), - } - } - value => Err(cannot_index( - self.context.types(), - value.ty(), - target.span(), - )), - } - } - - /// Evaluates the type of an access expression. - fn evaluate_access_expr(&mut self, expr: &AccessExpr) -> Result { - let (target, name) = expr.operands(); - - // TODO: implement support for task values (required for WDL 1.2 support) - // TODO: add support for access to call outputs - - match self.evaluate_expr(&target)? { - Value::Compound(CompoundValue::Pair(pair)) => match name.as_str() { - "left" => Ok(pair.left().clone()), - "right" => Ok(pair.right().clone()), - _ => Err(not_a_pair_accessor(&name)), - }, - Value::Compound(CompoundValue::Struct(s)) => match s.members().get(name.as_str()) { - Some(value) => Ok(value.clone()), - None => Err(not_a_struct_member( - self.context.types().struct_type(s.ty()).name(), - &name, - )), - }, - Value::Compound(CompoundValue::Object(object)) => { - match object.members().get(name.as_str()) { - Some(value) => Ok(value.clone()), - None => Err(not_an_object_member(&name)), - } - } - value => Err(cannot_access( - self.context.types(), - value.ty(), - target.span(), - )), - } - } -} - -#[cfg(test)] -pub(crate) mod test { - use std::collections::HashMap; - use std::fs; - use std::path::Path; - - use pretty_assertions::assert_eq; - use tempfile::TempDir; - use wdl_analysis::diagnostics::unknown_name; - use wdl_analysis::diagnostics::unknown_type; - use wdl_analysis::types::StructType; - use wdl_analysis::types::Types; - use wdl_ast::Ident; - use wdl_grammar::construct_tree; - use wdl_grammar::grammar::v1; - use wdl_grammar::lexer::Lexer; - - use super::*; - use crate::ScopeRef; - use crate::eval::Scope; - - /// Represents a test environment. - pub struct TestEnv { - /// The types collection for the test. - types: Types, - /// The scopes for the test. - scopes: Vec, - /// The structs for the test. - structs: HashMap<&'static str, Type>, - /// The temporary directory. - tmp: TempDir, - /// The current directory. - cwd: TempDir, - } - - impl TestEnv { - pub fn types(&self) -> &Types { - &self.types - } - - pub fn types_mut(&mut self) -> &mut Types { - &mut self.types - } - - pub fn scope(&self) -> ScopeRef<'_> { - ScopeRef::new(&self.scopes, 0) - } - - pub fn insert_name(&mut self, name: impl Into, value: impl Into) { - self.scopes[0].insert(name, value); - } - - pub fn insert_struct(&mut self, name: &'static str, ty: impl Into) { - self.structs.insert(name, ty.into()); - } - - pub fn cwd(&self) -> &Path { - self.cwd.path() - } - - pub fn tmp(&self) -> &Path { - self.tmp.path() - } - - pub fn write_file(&self, name: &str, bytes: impl AsRef<[u8]>) { - fs::write(self.cwd().join(name), bytes).expect("failed to create temp file"); - } - } - - impl Default for TestEnv { - fn default() -> Self { - Self { - types: Default::default(), - scopes: vec![Scope::new(None)], - structs: Default::default(), - tmp: TempDir::new().expect("failed to create temp directory"), - cwd: TempDir::new().expect("failed to create temp directory"), - } - } - } - - /// Represents test evaluation context to an expression evaluator. - pub struct TestEvaluationContext<'a> { - env: &'a mut TestEnv, - /// The supported version of WDL being evaluated. - version: SupportedVersion, - /// The stdout value from a task's execution. - stdout: Option, - /// The stderr value from a task's execution. - stderr: Option, - } - - impl<'a> TestEvaluationContext<'a> { - pub fn new(env: &'a mut TestEnv, version: SupportedVersion) -> Self { - Self { - env, - version, - stdout: None, - stderr: None, - } - } - - /// Sets the stdout to use for the evaluation context. - pub fn with_stdout(mut self, stdout: impl Into) -> Self { - self.stdout = Some(stdout.into()); - self - } - - /// Sets the stderr to use for the evaluation context. - pub fn with_stderr(mut self, stderr: impl Into) -> Self { - self.stderr = Some(stderr.into()); - self - } - } - - impl EvaluationContext for TestEvaluationContext<'_> { - fn version(&self) -> SupportedVersion { - self.version - } - - fn types(&self) -> &Types { - &self.env.types - } - - fn types_mut(&mut self) -> &mut Types { - &mut self.env.types - } - - fn resolve_name(&self, name: &Ident) -> Result { - self.env - .scope() - .lookup(name.as_str()) - .cloned() - .ok_or_else(|| unknown_name(name.as_str(), name.span())) - } - - fn resolve_type_name(&self, name: &Ident) -> Result { - self.env - .structs - .get(name.as_str()) - .copied() - .ok_or_else(|| unknown_type(name.as_str(), name.span())) - } - - fn cwd(&self) -> &Path { - self.env.cwd() - } - - fn tmp(&self) -> &Path { - self.env.tmp() - } - - fn stdout(&self) -> Option { - self.stdout.clone() - } - - fn stderr(&self) -> Option { - self.stderr.clone() - } - } - - pub fn eval_v1_expr(env: &mut TestEnv, version: V1, source: &str) -> Result { - eval_v1_expr_with_context( - TestEvaluationContext::new(env, SupportedVersion::V1(version)), - source, - ) - } - - pub fn eval_v1_expr_with_stdio( - env: &mut TestEnv, - version: V1, - source: &str, - stdout: impl Into, - stderr: impl Into, - ) -> Result { - eval_v1_expr_with_context( - TestEvaluationContext::new(env, SupportedVersion::V1(version)) - .with_stdout(stdout) - .with_stderr(stderr), - source, - ) - } - - fn eval_v1_expr_with_context( - mut context: TestEvaluationContext<'_>, - source: &str, - ) -> Result { - let source = source.trim(); - let mut parser = v1::Parser::new(Lexer::new(source)); - let marker = parser.start(); - match v1::expr(&mut parser, marker) { - Ok(()) => { - // This call to `next` is important as `next` adds any remaining buffered events - assert!( - parser.next().is_none(), - "parser is not finished; expected a single expression with no remaining tokens" - ); - let output = parser.finish(); - assert_eq!( - output.diagnostics.first(), - None, - "the provided WDL source failed to parse" - ); - let expr = Expr::cast(construct_tree(source, output.events)) - .expect("should be an expression"); - - let mut evaluator = ExprEvaluator::new(&mut context); - evaluator.evaluate_expr(&expr) - } - Err((marker, diagnostic)) => { - marker.abandon(&mut parser); - Err(diagnostic) - } - } - } - - #[test] - fn literal_none_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "None").unwrap(); - assert_eq!(value.to_string(), "None"); - } - - #[test] - fn literal_bool_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "true").unwrap(); - assert_eq!(value.unwrap_boolean(), true); - - let value = eval_v1_expr(&mut env, V1::Two, "false").unwrap(); - assert_eq!(value.unwrap_boolean(), false); - } - - #[test] - fn literal_int_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "12345").unwrap(); - assert_eq!(value.unwrap_integer(), 12345); - - let value = eval_v1_expr(&mut env, V1::Two, "-54321").unwrap(); - assert_eq!(value.unwrap_integer(), -54321); - - let value = eval_v1_expr(&mut env, V1::Two, "0xdeadbeef").unwrap(); - assert_eq!(value.unwrap_integer(), 0xDEADBEEF); - - let value = eval_v1_expr(&mut env, V1::Two, "0777").unwrap(); - assert_eq!(value.unwrap_integer(), 0o777); - - let value = eval_v1_expr(&mut env, V1::Two, "-9223372036854775808").unwrap(); - assert_eq!(value.unwrap_integer(), -9223372036854775808); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, "9223372036854775808").expect_err("should fail"); - assert_eq!( - diagnostic.message(), - "literal integer exceeds the range for a 64-bit signed integer \ - (-9223372036854775808..=9223372036854775807)" - ); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, "-9223372036854775809").expect_err("should fail"); - assert_eq!( - diagnostic.message(), - "literal integer exceeds the range for a 64-bit signed integer \ - (-9223372036854775808..=9223372036854775807)" - ); - } - - #[test] - fn literal_float_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "12345.6789").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 12345.6789); - - let value = eval_v1_expr(&mut env, V1::Two, "-12345.6789").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -12345.6789); - - let value = eval_v1_expr(&mut env, V1::Two, "1.7976931348623157E+308").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1.797_693_134_862_315_7E308); - - let value = eval_v1_expr(&mut env, V1::Two, "-1.7976931348623157E+308").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -1.797_693_134_862_315_7E308); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, "2.7976931348623157E+308").expect_err("should fail"); - assert_eq!( - diagnostic.message(), - "literal float exceeds the range for a 64-bit float \ - (-1.7976931348623157e308..=+1.7976931348623157e308)" - ); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, "-2.7976931348623157E+308").expect_err("should fail"); - assert_eq!( - diagnostic.message(), - "literal float exceeds the range for a 64-bit float \ - (-1.7976931348623157e308..=+1.7976931348623157e308)" - ); - } - - #[test] - fn literal_string_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "'hello\nworld'").unwrap(); - assert_eq!(value.unwrap_string().as_str(), "hello\nworld"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""hello world""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "hello world"); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#"<<< - hello \ - world - >>>"#, - ) - .unwrap(); - assert_eq!(value.unwrap_string().as_str(), "hello world"); - } - - #[test] - fn string_placeholders() { - let mut env = TestEnv::default(); - env.insert_name("str", PrimitiveValue::new_string("foo")); - env.insert_name("file", PrimitiveValue::new_file("bar")); - env.insert_name("dir", PrimitiveValue::new_directory("baz")); - env.insert_name("salutation", PrimitiveValue::new_string("hello")); - env.insert_name("name1", Value::None); - env.insert_name("name2", PrimitiveValue::new_string("Fred")); - env.insert_name("spaces", PrimitiveValue::new_string(" ")); - env.insert_name("name", PrimitiveValue::new_string("Henry")); - env.insert_name("company", PrimitiveValue::new_string("Acme")); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{None}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), ""); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{default="hi" None}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "hi"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{true}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "true"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{false}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "false"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{true="yes" false="no" false}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "no"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{12345}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "12345"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{12345.6789}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "12345.6789"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{str}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foo"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{file}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "bar"); - - let value = eval_v1_expr(&mut env, V1::Two, r#""~{dir}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "baz"); - - let value = - eval_v1_expr(&mut env, V1::Two, r#""~{sep="+" [1,2,3]} = ~{1 + 2 + 3}""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "1+2+3 = 6"); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, r#""~{[1, 2, 3]}""#).expect_err("should fail"); - assert_eq!( - diagnostic.message(), - "cannot coerce type `Array[Int]` to `String`" - ); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#""~{salutation + ' ' + name1 + ', '}nice to meet you!""#, - ) - .unwrap(); - assert_eq!(value.unwrap_string().as_str(), "nice to meet you!"); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#""${salutation + ' ' + name2 + ', '}nice to meet you!""#, - ) - .unwrap(); - assert_eq!( - value.unwrap_string().as_str(), - "hello Fred, nice to meet you!" - ); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#" - <<< - ~{spaces}Hello ~{name}, - ~{spaces}Welcome to ~{company}! - >>>"#, - ) - .unwrap(); - assert_eq!( - value.unwrap_string().as_str(), - " Hello Henry,\n Welcome to Acme!" - ); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#""~{1 + 2 + 3 + 4 * 10 * 10} ~{"~{<<<~{'!' + '='}>>>}"} ~{10**3}""#, - ) - .unwrap(); - assert_eq!(value.unwrap_string().as_str(), "406 != 1000"); - } - - #[test] - fn literal_array_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "[]").unwrap(); - assert_eq!(value.unwrap_array().to_string(), "[]"); - - let value = eval_v1_expr(&mut env, V1::Two, "[1, 2, 3]").unwrap(); - assert_eq!(value.unwrap_array().to_string(), "[1, 2, 3]"); - - let value = eval_v1_expr(&mut env, V1::Two, "[[1], [2], [3.0]]").unwrap(); - assert_eq!(value.unwrap_array().to_string(), "[[1.0], [2.0], [3.0]]"); - - let value = eval_v1_expr(&mut env, V1::Two, r#"["foo", "bar", "baz"]"#).unwrap(); - assert_eq!(value.unwrap_array().to_string(), r#"["foo", "bar", "baz"]"#); - } - - #[test] - fn literal_pair_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "(true, false)").unwrap(); - assert_eq!(value.unwrap_pair().to_string(), "(true, false)"); - - let value = eval_v1_expr(&mut env, V1::Two, "([1, 2, 3], [4, 5, 6])").unwrap(); - assert_eq!(value.unwrap_pair().to_string(), "([1, 2, 3], [4, 5, 6])"); - - let value = eval_v1_expr(&mut env, V1::Two, "([], {})").unwrap(); - assert_eq!(value.unwrap_pair().to_string(), "([], {})"); - - let value = eval_v1_expr(&mut env, V1::Two, r#"("foo", "bar")"#).unwrap(); - assert_eq!(value.unwrap_pair().to_string(), r#"("foo", "bar")"#); - } - - #[test] - fn literal_map_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "{}").unwrap(); - assert_eq!(value.unwrap_map().to_string(), "{}"); - - let value = eval_v1_expr(&mut env, V1::Two, "{ 1: 2, 3: 4, 5: 6 }").unwrap(); - assert_eq!(value.unwrap_map().to_string(), "{1: 2, 3: 4, 5: 6}"); - - let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": "bar", "baz": "qux"}"#).unwrap(); - assert_eq!( - value.unwrap_map().to_string(), - r#"{"foo": "bar", "baz": "qux"}"# - ); - - let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": { 1: 2 }, "baz": {}}"#).unwrap(); - assert_eq!( - value.unwrap_map().to_string(), - r#"{"foo": {1: 2}, "baz": {}}"# - ); - - let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": 100, "baz": 2.5}"#).unwrap(); - assert_eq!( - value.unwrap_map().to_string(), - r#"{"foo": 100.0, "baz": 2.5}"# - ); - } - - #[test] - fn literal_object_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Two, "object {}").unwrap(); - assert_eq!(value.unwrap_object().to_string(), "object {}"); - - let value = eval_v1_expr(&mut env, V1::Two, "object { foo: 2, bar: 4, baz: 6 }").unwrap(); - assert_eq!( - value.unwrap_object().to_string(), - "object {foo: 2, bar: 4, baz: 6}" - ); - - let value = eval_v1_expr(&mut env, V1::Two, r#"object {foo: "bar", baz: "qux"}"#).unwrap(); - assert_eq!( - value.unwrap_object().to_string(), - r#"object {foo: "bar", baz: "qux"}"# - ); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#"object {foo: { 1: 2 }, bar: [], qux: "jam"}"#, - ) - .unwrap(); - assert_eq!( - value.unwrap_object().to_string(), - r#"object {foo: {1: 2}, bar: [], qux: "jam"}"# - ); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#"object {foo: 1.0, bar: object { baz: "qux" }}"#, - ) - .unwrap(); - assert_eq!( - value.unwrap_object().to_string(), - r#"object {foo: 1.0, bar: object {baz: "qux"}}"# - ); - } - - #[test] - fn literal_struct_expr() { - let mut env = TestEnv::default(); - let bar_ty = env.types_mut().add_struct(StructType::new("Bar", [ - ("foo", PrimitiveTypeKind::File), - ("bar", PrimitiveTypeKind::Integer), - ])); - - let foo_ty = env.types_mut().add_struct(StructType::new("Foo", [ - ("foo", PrimitiveTypeKind::Float.into()), - ( - "bar", - Type::Compound(bar_ty.as_compound().expect("should be a compound type")), - ), - ])); - - env.insert_struct("Foo", foo_ty); - env.insert_struct("Bar", bar_ty); - - let value = eval_v1_expr( - &mut env, - V1::Two, - r#"Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#, - ) - .unwrap(); - assert_eq!( - value.unwrap_struct().to_string(), - r#"Foo {foo: 1.0, bar: Bar {foo: "baz", bar: 2}}"# - ); - - let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} == Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} == Foo { foo: 1.0, bar: Bar { foo: "jam", bar: 2 }}"#) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} != Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} != Foo { foo: 1.0, bar: Bar { foo: "jam", bar: 2 }}"#) - .unwrap(); - assert!(value.unwrap_boolean()); - } - - #[test] - fn name_ref_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", 1234); - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1234); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar"#).unwrap_err(); - assert_eq!(diagnostic.message(), "unknown name `bar`"); - } - - #[test] - fn parenthesized_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", 1234); - let value = eval_v1_expr(&mut env, V1::Zero, r#"(foo - foo) + (1234 - foo)"#).unwrap(); - assert_eq!(value.unwrap_integer(), 0); - } - - #[test] - fn if_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", true); - env.insert_name("bar", false); - env.insert_name("baz", PrimitiveValue::new_file("file")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then "foo" else "bar""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foo"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then "foo" else "bar""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "bar"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then 1234 else 0.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1234.0); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then 1234 else 0.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 0.5); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then baz else "str""#).unwrap(); - assert_eq!(value.unwrap_file().as_str(), "file"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then baz else "path""#).unwrap(); - assert_eq!(value.unwrap_file().as_str(), "path"); - } - - #[test] - fn logical_not_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"!true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"!false"#).unwrap(); - assert!(value.unwrap_boolean()); - } - - #[test] - fn negation_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"-1234"#).unwrap(); - assert_eq!(value.unwrap_integer(), -1234); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"-(1234)"#).unwrap(); - assert_eq!(value.unwrap_integer(), -1234); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"----1234"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1234); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"-1234.5678"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -1234.5678); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"-(1234.5678)"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -1234.5678); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"----1234.5678"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1234.5678); - } - - #[test] - fn logical_or_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"false || false"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false || true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true || false"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true || true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true || nope"#).unwrap(); - assert!(value.unwrap_boolean()); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"false || nope"#).unwrap_err(); - assert_eq!(diagnostic.message(), "unknown name `nope`"); - } - - #[test] - fn logical_and_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"false && false"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false && true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true && false"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true && true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false && nope"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"true && nope"#).unwrap_err(); - assert_eq!(diagnostic.message(), "unknown name `nope`"); - } - - #[test] - fn equality_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"None == None"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true == true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 1234"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 4321"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 1234.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"4321 == 1234.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 == 1234"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 == 4321"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 == 1234.5678"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 == 8765.4321"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == foo"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == bar"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo == "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo == "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar == "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar == "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") == (1234, "bar")"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") == (1234, "baz")"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1, 2, 3] == [1, 2, 3]"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] == [2, 3]"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] == [2]"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 1, "bar": 2, "baz": 3}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 1, "baz": 3}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 3, "bar": 2, "baz": 1}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 1, bar: 2, baz: "3"}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 1, baz: "3"}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 3, bar: 2, baz: "1"}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - // Note: struct equality is handled in the struct literal test - } - - #[test] - fn inequality_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"None != None"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true != true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 1234"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 4321"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 1234.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"4321 != 1234.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 != 1234"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 != 4321"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 != 1234.5678"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 != 8765.4321"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != foo"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != bar"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo != "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo != "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar != "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar != "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") != (1234, "bar")"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") != (1234, "baz")"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1, 2, 3] != [1, 2, 3]"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] != [2, 3]"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] != [2]"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 1, "bar": 2, "baz": 3}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 1, "baz": 3}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 3, "bar": 2, "baz": 1}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 1, bar: 2, baz: "3"}"#, - ) - .unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 1, baz: "3"}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr( - &mut env, - V1::Zero, - r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 3, bar: 2, baz: "1"}"#, - ) - .unwrap(); - assert!(value.unwrap_boolean()); - - // Note: struct inequality is handled in the struct literal test - } - - #[test] - fn less_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false < true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true < false"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true < true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 < 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 < 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 0.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 < 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 < 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 0.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" < "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" < "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" < "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar < "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar < bar"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo < "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo < foo"#).unwrap(); - assert!(!value.unwrap_boolean()); - } - - #[test] - fn less_equal_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false <= true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true <= false"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true <= true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 <= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 <= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 0.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 <= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 <= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 0.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" <= "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" <= "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" <= "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar <= "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar <= bar"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo <= "bar""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo <= foo"#).unwrap(); - assert!(value.unwrap_boolean()); - } - - #[test] - fn greater_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false > true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true > false"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true > true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 > 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 > 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 0.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 > 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 > 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 0.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" > "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" > "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" > "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar > "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar > bar"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo > "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo > foo"#).unwrap(); - assert!(!value.unwrap_boolean()); - } - - #[test] - fn greater_equal_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"false >= true"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true >= false"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"true >= true"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 >= 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0 >= 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 0.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 >= 1"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 1"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 >= 1.0"#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 0.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 1.0"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" >= "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" >= "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" >= "foo""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar >= "foo""#).unwrap(); - assert!(!value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar >= bar"#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo >= "bar""#).unwrap(); - assert!(value.unwrap_boolean()); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo >= foo"#).unwrap(); - assert!(value.unwrap_boolean()); - } - - #[test] - fn addition_expr() { - let mut env = TestEnv::default(); - env.insert_name("foo", PrimitiveValue::new_file("foo")); - env.insert_name("bar", PrimitiveValue::new_directory("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 + 2 + 3 + 4"#).unwrap(); - assert_eq!(value.unwrap_integer(), 10); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"10 + 20.0 + 30 + 40.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 100.0); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 + 200 + 300.0 + 400"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1000.0); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"1000.5 + 2000.5 + 3000.5 + 4000.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 10002.0); - - let diagnostic = - eval_v1_expr(&mut env, V1::Zero, &format!(r#"{max} + 1"#, max = i64::MAX)).unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + 1234"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foo1234"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 + "foo""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "1234foo"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + 1234.456"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foo1234.456"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.456 + "foo""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "1234.456foo"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + "bar""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foobar"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" + "foo""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "barfoo"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo + "bar""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foobar"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" + foo"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "barfoo"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + bar"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foobar"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar + "foo""#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "barfoo"); - } - - #[test] - fn subtraction_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"-1 - 2 - 3 - 4"#).unwrap(); - assert_eq!(value.unwrap_integer(), -10); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"-10 - 20.0 - 30 - 40.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -100.0); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"-100.0 - 200 - 300.0 - 400"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -1000.0); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"-1000.5 - 2000.5 - 3000.5 - 4000.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -10002.0); - - let diagnostic = - eval_v1_expr(&mut env, V1::Zero, &format!(r#"{min} - 1"#, min = i64::MIN)).unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - } - - #[test] - fn multiplication_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"1 * 2 * 3 * 4"#).unwrap(); - assert_eq!(value.unwrap_integer(), 24); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"10 * 20.0 * 30 * 40.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 240000.0); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 * 200 * 300.0 * 400"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 2400000000.0); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"1000.5 * 2000.5 * 3000.5 * 4000.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 24025008751250.063); - - let diagnostic = - eval_v1_expr(&mut env, V1::Zero, &format!(r#"{max} * 2"#, max = i64::MAX)).unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - } - - #[test] - fn division_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"5 / 2"#).unwrap(); - assert_eq!(value.unwrap_integer(), 2); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"10 / 20.0 / 30 / 40.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 0.00041666666666666664); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 / 200 / 300.0 / 400"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 4.166666666666667e-6); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"1000.5 / 2000.5 / 3000.5 / 4000.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 4.166492759125078e-8); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"10 / 0"#).unwrap_err(); - assert_eq!(diagnostic.message(), "attempt to divide by zero"); - - let diagnostic = eval_v1_expr( - &mut env, - V1::Zero, - &format!(r#"{min} / -1"#, min = i64::MIN), - ) - .unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - } - - #[test] - fn modulo_expr() { - let mut env = TestEnv::default(); - let value = eval_v1_expr(&mut env, V1::Zero, r#"5 % 2"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"5.5 % 2"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1.5); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"5 % 2.5"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 0.0); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"5.25 % 1.3"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 0.04999999999999982); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"5 % 0"#).unwrap_err(); - assert_eq!(diagnostic.message(), "attempt to divide by zero"); - - let diagnostic = eval_v1_expr( - &mut env, - V1::Zero, - &format!(r#"{min} % -1"#, min = i64::MIN), - ) - .unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - } - - #[test] - fn exponentiation_expr() { - let mut env = TestEnv::default(); - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"10 ** 0"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "use of the exponentiation operator requires WDL version 1.2" - ); - - let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2 ** 2"#).unwrap(); - assert_eq!(value.unwrap_integer(), 625); - - let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2.0 ** 2"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 625.0); - - let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2 ** 2.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 625.0); - - let value = eval_v1_expr(&mut env, V1::Two, r#"5.0 ** 2.0 ** 2.0"#).unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 625.0); - - let diagnostic = - eval_v1_expr(&mut env, V1::Two, &format!(r#"{max} ** 2"#, max = i64::MAX)).unwrap_err(); - assert_eq!( - diagnostic.message(), - "evaluation of arithmetic expression resulted in overflow" - ); - } - - #[test] - fn call_expr() { - // This test will just check for errors; testing of the function implementations - // is in `stdlib.rs` - let mut env = TestEnv::default(); - let diagnostic = eval_v1_expr(&mut env, V1::Zero, "min(1, 2)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "this use of function `min` requires a minimum WDL version of 1.1" - ); - - let diagnostic = - eval_v1_expr(&mut env, V1::Zero, "min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "this use of function `min` requires a minimum WDL version of 1.1" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::One, "min(1)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "function `min` requires at least 2 arguments but 1 was supplied" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::One, "min(1, 2, 3)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "function `min` requires no more than 2 arguments but 3 were supplied" - ); - - let diagnostic = - eval_v1_expr(&mut env, V1::One, "min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "function `min` requires no more than 2 arguments but 10 were supplied" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::One, "min('1', 2)").unwrap_err(); - assert_eq!( - diagnostic.message(), - "type mismatch: argument to function `min` expects type `Int` or `Float`, but found \ - type `String`" - ); - } - - #[test] - fn index_expr() { - let mut env = TestEnv::default(); - let array_ty = env - .types_mut() - .add_array(ArrayType::new(PrimitiveTypeKind::Integer)); - let map_ty = env.types_mut().add_map(MapType::new( - PrimitiveTypeKind::String, - PrimitiveTypeKind::Integer, - )); - - env.insert_name( - "foo", - Array::new(env.types(), array_ty, [1, 2, 3, 4, 5]).unwrap(), - ); - env.insert_name( - "bar", - Map::new(env.types(), map_ty, [ - (PrimitiveValue::new_string("foo"), 1), - (PrimitiveValue::new_string("bar"), 2), - ]) - .unwrap(), - ); - env.insert_name("baz", PrimitiveValue::new_file("bar")); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[1]"#).unwrap(); - assert_eq!(value.unwrap_integer(), 2); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[foo[[1, 2, 3][0]]]"#).unwrap(); - assert_eq!(value.unwrap_integer(), 3); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo[10]"#).unwrap_err(); - assert_eq!(diagnostic.message(), "array index 10 is out of range"); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo["10"]"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "type mismatch: expected index to be type `Int`, but found type `String`" - ); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar["foo"]"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar[baz]"#).unwrap(); - assert_eq!(value.unwrap_integer(), 2); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[bar["foo"]]"#).unwrap(); - assert_eq!(value.unwrap_integer(), 2); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar["does not exist"]"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "the map does not contain an entry for the specified key" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar[1]"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "type mismatch: expected index to be type `String`, but found type `Int`" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"1[0]"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "indexing is only allowed on `Array` and `Map` types" - ); - } - - #[test] - fn access_expr() { - let mut env = TestEnv::default(); - let pair_ty = env.types_mut().add_pair(PairType::new( - PrimitiveTypeKind::Integer, - PrimitiveTypeKind::String, - )); - let struct_ty = env.types_mut().add_struct(StructType::new("Foo", [ - ("foo", PrimitiveTypeKind::Integer), - ("bar", PrimitiveTypeKind::String), - ])); - - env.insert_name( - "foo", - Pair::new(env.types(), pair_ty, 1, PrimitiveValue::new_string("foo")).unwrap(), - ); - env.insert_name( - "bar", - Struct::new(env.types(), struct_ty, [ - ("foo", 1.into()), - ("bar", PrimitiveValue::new_string("bar")), - ]) - .unwrap(), - ); - env.insert_name("baz", 1); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo.left"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"foo.right"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "foo"); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo.bar"#).unwrap_err(); - assert_eq!(diagnostic.message(), "cannot access a pair with name `bar`"); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar.foo"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1); - - let value = eval_v1_expr(&mut env, V1::Zero, r#"bar.bar"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "bar"); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar.baz"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "struct `Foo` does not have a member named `baz`" - ); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.foo"#).unwrap(); - assert_eq!(value.unwrap_integer(), 1); - - let value = - eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.bar"#).unwrap(); - assert_eq!(value.unwrap_string().as_str(), "bar"); - - let diagnostic = - eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.baz"#).unwrap_err(); - assert_eq!( - diagnostic.message(), - "object does not have a member named `baz`" - ); - - let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"baz.foo"#).unwrap_err(); - assert_eq!(diagnostic.message(), "cannot access type `Int`"); - } -} +pub use expr::*; +pub use task::*; diff --git a/wdl-engine/src/eval/v1/expr.rs b/wdl-engine/src/eval/v1/expr.rs new file mode 100644 index 00000000..5648107b --- /dev/null +++ b/wdl-engine/src/eval/v1/expr.rs @@ -0,0 +1,3068 @@ +//! Implementation of an expression evaluator for 1.x WDL documents. + +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt::Write; +use std::iter::once; +use std::sync::Arc; + +use indexmap::IndexMap; +use ordered_float::Pow; +use wdl_analysis::DiagnosticsConfig; +use wdl_analysis::diagnostics::Io; +use wdl_analysis::diagnostics::ambiguous_argument; +use wdl_analysis::diagnostics::argument_type_mismatch; +use wdl_analysis::diagnostics::cannot_access; +use wdl_analysis::diagnostics::cannot_coerce_to_string; +use wdl_analysis::diagnostics::cannot_index; +use wdl_analysis::diagnostics::comparison_mismatch; +use wdl_analysis::diagnostics::if_conditional_mismatch; +use wdl_analysis::diagnostics::index_type_mismatch; +use wdl_analysis::diagnostics::logical_and_mismatch; +use wdl_analysis::diagnostics::logical_not_mismatch; +use wdl_analysis::diagnostics::logical_or_mismatch; +use wdl_analysis::diagnostics::map_key_not_primitive; +use wdl_analysis::diagnostics::missing_struct_members; +use wdl_analysis::diagnostics::multiple_type_mismatch; +use wdl_analysis::diagnostics::no_common_type; +use wdl_analysis::diagnostics::not_a_pair_accessor; +use wdl_analysis::diagnostics::not_a_struct; +use wdl_analysis::diagnostics::not_a_struct_member; +use wdl_analysis::diagnostics::not_a_task_member; +use wdl_analysis::diagnostics::numeric_mismatch; +use wdl_analysis::diagnostics::too_few_arguments; +use wdl_analysis::diagnostics::too_many_arguments; +use wdl_analysis::diagnostics::type_mismatch; +use wdl_analysis::diagnostics::unknown_function; +use wdl_analysis::diagnostics::unknown_task_io; +use wdl_analysis::diagnostics::unsupported_function; +use wdl_analysis::document::Task; +use wdl_analysis::stdlib::FunctionBindError; +use wdl_analysis::stdlib::MAX_PARAMETERS; +use wdl_analysis::types::ArrayType; +use wdl_analysis::types::Coercible as _; +use wdl_analysis::types::CompoundTypeDef; +use wdl_analysis::types::MapType; +use wdl_analysis::types::Optional; +use wdl_analysis::types::PairType; +use wdl_analysis::types::PrimitiveTypeKind; +use wdl_analysis::types::Type; +use wdl_analysis::types::TypeEq; +use wdl_analysis::types::v1::ComparisonOperator; +use wdl_analysis::types::v1::ExprTypeEvaluator; +use wdl_analysis::types::v1::NumericOperator; +use wdl_analysis::types::v1::task_hint_types; +use wdl_ast::AstNode; +use wdl_ast::AstNodeExt; +use wdl_ast::AstToken; +use wdl_ast::Diagnostic; +use wdl_ast::Ident; +use wdl_ast::Span; +use wdl_ast::SupportedVersion; +use wdl_ast::SyntaxKind; +use wdl_ast::v1::AccessExpr; +use wdl_ast::v1::CallExpr; +use wdl_ast::v1::Expr; +use wdl_ast::v1::IfExpr; +use wdl_ast::v1::IndexExpr; +use wdl_ast::v1::LiteralArray; +use wdl_ast::v1::LiteralExpr; +use wdl_ast::v1::LiteralHints; +use wdl_ast::v1::LiteralInput; +use wdl_ast::v1::LiteralMap; +use wdl_ast::v1::LiteralObject; +use wdl_ast::v1::LiteralOutput; +use wdl_ast::v1::LiteralPair; +use wdl_ast::v1::LiteralString; +use wdl_ast::v1::LiteralStringKind; +use wdl_ast::v1::LiteralStruct; +use wdl_ast::v1::LogicalAndExpr; +use wdl_ast::v1::LogicalNotExpr; +use wdl_ast::v1::LogicalOrExpr; +use wdl_ast::v1::NegationExpr; +use wdl_ast::v1::Placeholder; +use wdl_ast::v1::PlaceholderOption; +use wdl_ast::v1::StringPart; +use wdl_ast::v1::StrippedStringPart; +use wdl_ast::version::V1; + +use crate::Array; +use crate::Coercible; +use crate::CompoundValue; +use crate::EvaluationContext; +use crate::Map; +use crate::Object; +use crate::Pair; +use crate::PrimitiveValue; +use crate::Struct; +use crate::Value; +use crate::diagnostics::array_index_out_of_range; +use crate::diagnostics::division_by_zero; +use crate::diagnostics::exponent_not_in_range; +use crate::diagnostics::exponentiation_requirement; +use crate::diagnostics::float_not_in_range; +use crate::diagnostics::integer_negation_not_in_range; +use crate::diagnostics::integer_not_in_range; +use crate::diagnostics::map_key_not_found; +use crate::diagnostics::multiline_string_requirement; +use crate::diagnostics::not_an_object_member; +use crate::diagnostics::numeric_overflow; +use crate::diagnostics::runtime_type_mismatch; +use crate::stdlib::CallArgument; +use crate::stdlib::CallContext; +use crate::stdlib::STDLIB; + +/// Represents a WDL V1 expression evaluator. +#[derive(Debug)] +pub struct ExprEvaluator { + /// The expression evaluation context. + context: C, + /// The nested count of placeholder evaluation. + /// + /// This is incremented immediately before a placeholder expression is + /// evaluated and decremented immediately after. + /// + /// If the count is non-zero, special evaluation behavior is enabled for + /// string interpolation. + placeholders: usize, + /// Tracks whether or not a `None`-resulting expression was evaluated during + /// a placeholder evaluation. + evaluated_none: bool, +} + +impl ExprEvaluator { + /// Creates a new expression evaluator. + pub fn new(context: C) -> Self { + Self { + context, + placeholders: 0, + evaluated_none: false, + } + } + + /// Evaluates the given expression. + pub fn evaluate_expr(&mut self, expr: &Expr) -> Result { + let value = match expr { + Expr::Literal(expr) => self.evaluate_literal_expr(expr), + Expr::Name(r) => self.context.resolve_name(&r.name()), + Expr::Parenthesized(expr) => self.evaluate_expr(&expr.inner()), + Expr::If(expr) => self.evaluate_if_expr(expr), + Expr::LogicalNot(expr) => self.evaluate_logical_not_expr(expr), + Expr::Negation(expr) => self.evaluate_negation_expr(expr), + Expr::LogicalOr(expr) => self.evaluate_logical_or_expr(expr), + Expr::LogicalAnd(expr) => self.evaluate_logical_and_expr(expr), + Expr::Equality(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr(ComparisonOperator::Equality, &lhs, &rhs, expr.span()) + } + Expr::Inequality(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr( + ComparisonOperator::Inequality, + &lhs, + &rhs, + expr.span(), + ) + } + Expr::Less(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr(ComparisonOperator::Less, &lhs, &rhs, expr.span()) + } + Expr::LessEqual(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr( + ComparisonOperator::LessEqual, + &lhs, + &rhs, + expr.span(), + ) + } + Expr::Greater(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr(ComparisonOperator::Greater, &lhs, &rhs, expr.span()) + } + Expr::GreaterEqual(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_comparison_expr( + ComparisonOperator::GreaterEqual, + &lhs, + &rhs, + expr.span(), + ) + } + Expr::Addition(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Addition, &lhs, &rhs, expr.span()) + } + Expr::Subtraction(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Subtraction, &lhs, &rhs, expr.span()) + } + Expr::Multiplication(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Multiplication, &lhs, &rhs, expr.span()) + } + Expr::Division(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Division, &lhs, &rhs, expr.span()) + } + Expr::Modulo(expr) => { + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Modulo, &lhs, &rhs, expr.span()) + } + Expr::Exponentiation(expr) => { + if self.context.version() < SupportedVersion::V1(V1::Two) { + return Err(exponentiation_requirement(expr.span())); + } + let (lhs, rhs) = expr.operands(); + self.evaluate_numeric_expr(NumericOperator::Exponentiation, &lhs, &rhs, expr.span()) + } + Expr::Call(expr) => self.evaluate_call_expr(expr), + Expr::Index(expr) => self.evaluate_index_expr(expr), + Expr::Access(expr) => self.evaluate_access_expr(expr), + }?; + + self.evaluated_none |= self.placeholders > 0 && value.is_none(); + Ok(value) + } + + /// Evaluates a literal expression. + fn evaluate_literal_expr(&mut self, expr: &LiteralExpr) -> Result { + match expr { + LiteralExpr::Boolean(lit) => Ok(lit.value().into()), + LiteralExpr::Integer(lit) => { + // Check to see if this literal is a direct child of a negation expression; if + // so, we want to negate the literal + let (value, span) = match lit.syntax().parent() { + Some(parent) if parent.kind() == SyntaxKind::NegationExprNode => { + let start = parent.text_range().start().into(); + (lit.negate(), Span::new(start, lit.span().end() - start)) + } + _ => (lit.value(), lit.span()), + }; + + Ok(value.ok_or_else(|| integer_not_in_range(span))?.into()) + } + LiteralExpr::Float(lit) => Ok(lit + .value() + .ok_or_else(|| float_not_in_range(lit.span()))? + .into()), + LiteralExpr::String(lit) => self.evaluate_literal_string(lit), + LiteralExpr::Array(lit) => self.evaluate_literal_array(lit), + LiteralExpr::Pair(lit) => self.evaluate_literal_pair(lit), + LiteralExpr::Map(lit) => self.evaluate_literal_map(lit), + LiteralExpr::Object(lit) => self.evaluate_literal_object(lit), + LiteralExpr::Struct(lit) => self.evaluate_literal_struct(lit), + LiteralExpr::None(_) => Ok(Value::None), + LiteralExpr::Hints(lit) => self.evaluate_literal_hints(lit), + LiteralExpr::Input(lit) => self.evaluate_literal_input(lit), + LiteralExpr::Output(lit) => self.evaluate_literal_output(lit), + } + } + + /// Evaluates a placeholder into the given string buffer. + /// + /// `mapped_paths` is used to map from host paths to guest paths when + /// evaluating commands. + pub fn evaluate_placeholder( + &mut self, + placeholder: &Placeholder, + buffer: &mut String, + mapped_paths: &HashMap, + ) -> Result<(), Diagnostic> { + /// The actual implementation for evaluating placeholders + fn imp( + evaluator: &mut ExprEvaluator, + placeholder: &Placeholder, + buffer: &mut String, + mapped_paths: &HashMap, + ) -> Result<(), Diagnostic> { + let expr = placeholder.expr(); + match evaluator.evaluate_expr(&expr)? { + Value::None => { + if let Some(o) = placeholder.option().as_ref().and_then(|o| o.as_default()) { + buffer.push_str( + &evaluator + .evaluate_literal_string(&o.value())? + .unwrap_string(), + ) + } + } + Value::Primitive(PrimitiveValue::Boolean(v)) => { + match placeholder + .option() + .as_ref() + .and_then(|o| o.as_true_false()) + { + Some(o) => { + let (t, f) = o.values(); + if v { + buffer.push_str( + &evaluator.evaluate_literal_string(&t)?.unwrap_string(), + ); + } else { + buffer.push_str( + &evaluator.evaluate_literal_string(&f)?.unwrap_string(), + ); + } + } + None => { + if v { + buffer.push_str("true"); + } else { + buffer.push_str("false"); + } + } + } + } + Value::Primitive(PrimitiveValue::File(path)) + | Value::Primitive(PrimitiveValue::Directory(path)) + if mapped_paths.contains_key(path.as_str()) => + { + write!(buffer, "{path}", path = mapped_paths[path.as_str()]).unwrap() + } + Value::Primitive(v) => write!(buffer, "{v}", v = v.raw()).unwrap(), + Value::Compound(CompoundValue::Array(v)) + if matches!(placeholder.option(), Some(PlaceholderOption::Sep(_))) + && v.as_slice() + .first() + .map(|e| !matches!(e, Value::None | Value::Compound(_))) + .unwrap_or(false) => + { + let option = placeholder.option().unwrap().unwrap_sep(); + + let sep = evaluator + .evaluate_literal_string(&option.separator())? + .unwrap_string(); + for (i, e) in v.as_slice().iter().enumerate() { + if i > 0 { + buffer.push_str(&sep); + } + + match e { + Value::None => {} + Value::Primitive(v) => write!(buffer, "{v}", v = v.raw()).unwrap(), + _ => { + return Err(cannot_coerce_to_string( + evaluator.context.types(), + v.ty(), + expr.span(), + )); + } + } + } + } + v => { + return Err(cannot_coerce_to_string( + evaluator.context.types(), + v.ty(), + expr.span(), + )); + } + } + + Ok(()) + } + + // Keep track of the start in case there is a `None` evaluated and an error + let start = buffer.len(); + + // Bump the placeholder count while evaluating the placeholder + self.placeholders += 1; + let result = imp(self, placeholder, buffer, mapped_paths); + self.placeholders -= 1; + + // Reset the evaluated none flag + if self.placeholders == 0 { + let evaluated_none = std::mem::replace(&mut self.evaluated_none, false); + + // If a `None` was evaluated and an error occurred, truncate to the start of the + // placeholder evaluation + if evaluated_none && result.is_err() { + buffer.truncate(start); + return Ok(()); + } + } + + result + } + + /// Evaluates a literal string expression. + fn evaluate_literal_string(&mut self, expr: &LiteralString) -> Result { + if expr.kind() == LiteralStringKind::Multiline + && self.context.version() < SupportedVersion::V1(V1::Two) + { + return Err(multiline_string_requirement(expr.span())); + } + + let mut s = String::new(); + if let Some(parts) = expr.strip_whitespace() { + for part in parts { + match part { + StrippedStringPart::Text(t) => { + s.push_str(&t); + } + StrippedStringPart::Placeholder(placeholder) => { + self.evaluate_placeholder(&placeholder, &mut s, &Default::default())?; + } + } + } + } else { + for part in expr.parts() { + match part { + StringPart::Text(t) => { + t.unescape_to(&mut s); + } + StringPart::Placeholder(placeholder) => { + self.evaluate_placeholder(&placeholder, &mut s, &Default::default())?; + } + } + } + } + + Ok(PrimitiveValue::new_string(s).into()) + } + + /// Evaluates a literal array expression. + fn evaluate_literal_array(&mut self, expr: &LiteralArray) -> Result { + // Look at the first array element to determine the element type + // The remaining elements must have a common type + let mut elements = expr.elements(); + let (element_ty, values) = match elements.next() { + Some(expr) => { + let mut values = Vec::new(); + let value = self.evaluate_expr(&expr)?; + let mut expected: Type = value.ty(); + let mut expected_span = expr.span(); + values.push(value); + + // Ensure the remaining element types share a common type + for expr in elements { + let value = self.evaluate_expr(&expr)?; + let actual = value.ty(); + + if let Some(ty) = expected.common_type(self.context.types_mut(), actual) { + expected = ty; + expected_span = expr.span(); + } else { + return Err(no_common_type( + self.context.types(), + expected, + expected_span, + actual, + expr.span(), + )); + } + + values.push(value); + } + + (expected, values) + } + None => (Type::Union, Vec::new()), + }; + + let ty = self + .context + .types_mut() + .add_array(ArrayType::new(element_ty)); + Ok(Array::new(self.context.types(), ty, values) + .expect("array elements should coerce") + .into()) + } + + /// Evaluates a literal pair expression. + fn evaluate_literal_pair(&mut self, expr: &LiteralPair) -> Result { + let (left, right) = expr.exprs(); + let left = self.evaluate_expr(&left)?; + let right = self.evaluate_expr(&right)?; + let ty = self + .context + .types_mut() + .add_pair(PairType::new(left.ty(), right.ty())); + Ok(Pair::new(self.context.types(), ty, left, right) + .expect("types should coerce") + .into()) + } + + /// Evaluates a literal map expression. + fn evaluate_literal_map(&mut self, expr: &LiteralMap) -> Result { + let mut items = expr.items(); + let (key_ty, value_ty, elements) = match items.next() { + Some(item) => { + let mut elements = Vec::new(); + + // Evaluate the first key-value pair + let (key, value) = item.key_value(); + let expected_key = self.evaluate_expr(&key)?; + let mut expected_key_ty = expected_key.ty(); + let mut expected_key_span = key.span(); + let expected_value = self.evaluate_expr(&value)?; + let mut expected_value_ty = expected_value.ty(); + let mut expected_value_span = value.span(); + + // The key type must be primitive + let key = match expected_key { + Value::None => None, + Value::Primitive(key) => Some(key), + _ => { + return Err(map_key_not_primitive( + self.context.types(), + key.span(), + expected_key.ty(), + )); + } + }; + + elements.push((key, expected_value)); + + // Ensure the remaining items types share common types + for item in items { + let (key, value) = item.key_value(); + let actual_key = self.evaluate_expr(&key)?; + let actual_key_ty = actual_key.ty(); + let actual_value = self.evaluate_expr(&value)?; + let actual_value_ty = actual_value.ty(); + + if let Some(ty) = + expected_key_ty.common_type(self.context.types_mut(), actual_key_ty) + { + expected_key_ty = ty; + expected_key_span = key.span(); + } else { + // No common key type + return Err(no_common_type( + self.context.types(), + expected_key_ty, + expected_key_span, + actual_key_ty, + key.span(), + )); + } + + if let Some(ty) = + expected_value_ty.common_type(self.context.types_mut(), actual_value_ty) + { + expected_value_ty = ty; + expected_value_span = value.span(); + } else { + // No common value type + return Err(no_common_type( + self.context.types(), + expected_value_ty, + expected_value_span, + actual_value_ty, + value.span(), + )); + } + + let key = match actual_key { + Value::None => None, + Value::Primitive(key) => Some(key), + _ => panic!("the key type is not primitive, but had a common type"), + }; + + elements.push((key, actual_value)); + } + + (expected_key_ty, expected_value_ty, elements) + } + None => (Type::Union, Type::Union, Vec::new()), + }; + + let ty = self + .context + .types_mut() + .add_map(MapType::new(key_ty, value_ty)); + Ok(Map::new(self.context.types(), ty, elements) + .expect("map elements should coerce") + .into()) + } + + /// Evaluates a literal object expression. + fn evaluate_literal_object(&mut self, expr: &LiteralObject) -> Result { + Ok(Object::from( + expr.items() + .map(|item| { + let (name, value) = item.name_value(); + Ok((name.as_str().to_string(), self.evaluate_expr(&value)?)) + }) + .collect::, _>>()?, + ) + .into()) + } + + /// Evaluates a literal struct expression. + fn evaluate_literal_struct(&mut self, expr: &LiteralStruct) -> Result { + let name = expr.name(); + let ty = self.context.resolve_type_name(&name)?; + + // Evaluate the members + let mut members = + IndexMap::with_capacity(self.context.types().struct_type(ty).members().len()); + for item in expr.items() { + let (n, v) = item.name_value(); + if let Some(expected) = self + .context + .types() + .struct_type(ty) + .members() + .get(n.as_str()) + { + let expected = *expected; + let value = self.evaluate_expr(&v)?; + let value = value.coerce(self.context.types(), expected).map_err(|e| { + runtime_type_mismatch( + self.context.types(), + e, + expected, + n.span(), + value.ty(), + v.span(), + ) + })?; + + members.insert(n.as_str().to_string(), value); + } else { + // Not a struct member + return Err(not_a_struct_member(name.as_str(), &n)); + } + } + + let mut iter = self.context.types().struct_type(ty).members().iter(); + while let Some((n, ty)) = iter.next() { + // Check for optional members that should be set to `None` + if ty.is_optional() { + if !members.contains_key(n) { + members.insert(n.clone(), Value::None); + } + } else { + // Check for a missing required member + if !members.contains_key(n) { + let mut missing = once(n) + .chain(iter.filter_map(|(n, ty)| { + if ty.is_optional() && !members.contains_key(n.as_str()) { + Some(n) + } else { + None + } + })) + .peekable(); + let mut names: String = String::new(); + let mut count = 0; + while let Some(n) = missing.next() { + match (missing.peek().is_none(), count) { + (true, c) if c > 1 => names.push_str(", and "), + (true, 1) => names.push_str(" and "), + (false, c) if c > 0 => names.push_str(", "), + _ => {} + } + + write!(&mut names, "`{n}`").ok(); + count += 1; + } + + return Err(missing_struct_members(&name, count, &names)); + } + } + } + + Ok(Struct::new_unchecked( + ty, + self.context.types().struct_type(ty).name().clone(), + Arc::new(members), + ) + .into()) + } + + /// Evaluates a literal hints expression. + fn evaluate_literal_hints(&mut self, expr: &LiteralHints) -> Result { + let object: Object = expr + .items() + .map(|item| { + let name = item.name(); + let expr = item.expr(); + Ok(( + name.as_str().to_string(), + self.evaluate_hints_item(&name, &expr)?, + )) + }) + .collect::, _>>()? + .into(); + + Ok(Value::Hints(object.into())) + } + + /// Evaluates a hints item, whether in task `hints` section or a `hints` + /// literal expression. + pub(crate) fn evaluate_hints_item( + &mut self, + name: &Ident, + expr: &Expr, + ) -> Result { + let value = self.evaluate_expr(expr)?; + if let Some(expected) = task_hint_types(self.context.version(), name.as_str(), true) { + if let Some(value) = expected + .iter() + .find_map(|ty| value.coerce(self.context.types(), *ty).ok()) + { + return Ok(value); + } else { + return Err(multiple_type_mismatch( + self.context.types(), + expected, + name.span(), + value.ty(), + expr.span(), + )); + } + } + + Ok(value) + } + + /// Evaluates a literal input expression. + fn evaluate_literal_input(&mut self, expr: &LiteralInput) -> Result { + let object: Object = expr + .items() + .map(|item| self.evaluate_literal_io_item(item.names(), item.expr(), Io::Input)) + .collect::, _>>()? + .into(); + + Ok(Value::Input(object.into())) + } + + /// Evaluates a literal output expression. + fn evaluate_literal_output(&mut self, expr: &LiteralOutput) -> Result { + let object: Object = expr + .items() + .map(|item| self.evaluate_literal_io_item(item.names(), item.expr(), Io::Output)) + .collect::, _>>()? + .into(); + + Ok(Value::Output(object.into())) + } + + /// Evaluates a literal input/output item. + fn evaluate_literal_io_item( + &mut self, + segments: impl Iterator, + expr: Expr, + io: Io, + ) -> Result<(String, Value), Diagnostic> { + let mut segments = segments.enumerate().peekable(); + + let mut name = String::new(); + let value = self.evaluate_expr(&expr)?; + + // The first name should be an input/output and then the remainder should be a + // struct member + let mut span = None; + let mut struct_ty: Option = None; + while let Some((i, segment)) = segments.next() { + if !name.is_empty() { + name.push('.'); + } + + name.push_str(segment.as_str()); + + // The first name is an input or an output + let ty = if i == 0 { + span = Some(segment.span()); + + match if io == Io::Input { + self.context + .task() + .expect("should have task") + .inputs() + .get(segment.as_str()) + .map(|i| i.ty()) + } else { + self.context + .task() + .expect("should have task") + .outputs() + .get(segment.as_str()) + .map(|o| o.ty()) + } { + Some(ty) => ty, + None => { + return Err(unknown_task_io( + self.context.task().expect("should have task").name(), + &segment, + io, + )); + } + } + } else { + // Every other name is a struct member + let start = span.unwrap().start(); + span = Some(Span::new(start, segment.span().end() - start)); + let s = self + .context + .document_types() + .struct_type(struct_ty.unwrap()); + match s.members().get(segment.as_str()) { + Some(ty) => *ty, + None => { + return Err(not_a_struct_member(s.name(), &segment)); + } + } + }; + + match ty { + Type::Compound(compound_ty) + if matches!( + self.context + .document_types() + .type_definition(compound_ty.definition()), + CompoundTypeDef::Struct(_) + ) => + { + struct_ty = Some(ty); + } + _ if segments.peek().is_some() => { + return Err(not_a_struct(&segment, i == 0)); + } + _ => { + // It's ok for the last one to not name a struct + } + } + } + + // The type of every item should be `hints` + if !matches!(value.ty(), Type::Hints) { + return Err(type_mismatch( + self.context.types(), + Type::Hints, + span.expect("should have span"), + value.ty(), + expr.span(), + )); + } + + Ok((name, value)) + } + + /// Evaluates an `if` expression. + fn evaluate_if_expr(&mut self, expr: &IfExpr) -> Result { + /// Used to translate an expression evaluation context to an expression + /// type evaluation context. + struct TypeContext<'a, C: EvaluationContext> { + /// The expression evaluation context. + context: &'a mut C, + /// The diagnostics from evaluating the type of an expression. + diagnostics: Vec, + } + + impl wdl_analysis::types::v1::EvaluationContext for TypeContext<'_, C> { + fn version(&self) -> SupportedVersion { + self.context.version() + } + + fn types(&self) -> &wdl_analysis::types::Types { + self.context.types() + } + + fn types_mut(&mut self) -> &mut wdl_analysis::types::Types { + self.context.types_mut() + } + + fn resolve_name(&self, name: &wdl_ast::Ident) -> Option { + self.context.resolve_name(name).map(|v| v.ty()).ok() + } + + fn resolve_type_name(&mut self, name: &wdl_ast::Ident) -> Result { + self.context.resolve_type_name(name) + } + + fn task(&self) -> Option<&Task> { + self.context.task() + } + + fn diagnostics_config(&self) -> DiagnosticsConfig { + DiagnosticsConfig::except_all() + } + + fn add_diagnostic(&mut self, diagnostic: Diagnostic) { + self.diagnostics.push(diagnostic); + } + } + + let (cond_expr, true_expr, false_expr) = expr.exprs(); + + // Evaluate the conditional expression and the true expression or the false + // expression, depending on the result of the conditional expression + let cond = self.evaluate_expr(&cond_expr)?; + let (value, true_ty, false_ty) = if cond + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| { + if_conditional_mismatch(self.context.types(), cond.ty(), cond_expr.span()) + })? + .unwrap_boolean() + { + // Evaluate the `true` expression and calculate the type of the `false` + // expression + let value = self.evaluate_expr(&true_expr)?; + let mut context = TypeContext { + context: &mut self.context, + diagnostics: Vec::new(), + }; + let false_ty = ExprTypeEvaluator::new(&mut context) + .evaluate_expr(&false_expr) + .unwrap_or(Type::Union); + + if let Some(diagnostic) = context.diagnostics.pop() { + return Err(diagnostic); + } + + let true_ty = value.ty(); + (value, true_ty, false_ty) + } else { + // Evaluate the `false` expression and calculate the type of the `true` + // expression + let value = self.evaluate_expr(&false_expr)?; + let mut context = TypeContext { + context: &mut self.context, + diagnostics: Vec::new(), + }; + let true_ty = ExprTypeEvaluator::new(&mut context) + .evaluate_expr(&true_expr) + .unwrap_or(Type::Union); + if let Some(diagnostic) = context.diagnostics.pop() { + return Err(diagnostic); + } + + let false_ty = value.ty(); + (value, true_ty, false_ty) + }; + + // Determine the common type of the true and false expressions + // The value must be coerced to that type + let ty = true_ty + .common_type(self.context.types_mut(), false_ty) + .ok_or_else(|| { + no_common_type( + self.context.types(), + true_ty, + true_expr.span(), + false_ty, + false_expr.span(), + ) + })?; + + Ok(value + .coerce(self.context.types(), ty) + .expect("coercion should not fail")) + } + + /// Evaluates a `logical not` expression. + fn evaluate_logical_not_expr(&mut self, expr: &LogicalNotExpr) -> Result { + // The operand should be a boolean + let operand = expr.operand(); + let value = self.evaluate_expr(&operand)?; + Ok((!value + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| logical_not_mismatch(self.context.types(), value.ty(), operand.span()))? + .unwrap_boolean()) + .into()) + } + + /// Evaluates a negation expression. + fn evaluate_negation_expr(&mut self, expr: &NegationExpr) -> Result { + let operand = expr.operand(); + let value = self.evaluate_expr(&operand)?; + let ty = value.ty(); + + // If the type is `Int`, treat it as `Int` + if ty.type_eq(self.context.types(), &PrimitiveTypeKind::Integer.into()) { + return match operand { + Expr::Literal(LiteralExpr::Integer(_)) => { + // Already negated during integer literal evaluation + Ok(value) + } + _ => { + let value = value.unwrap_integer(); + Ok(value + .checked_neg() + .ok_or_else(|| integer_negation_not_in_range(value, operand.span()))? + .into()) + } + }; + } + + // If the type is `Float`, treat it as `Float` + if ty.type_eq(self.context.types(), &PrimitiveTypeKind::Float.into()) { + let value = value.unwrap_float(); + return Ok((-value).into()); + } + + // Expected either `Int` or `Float` + Err(multiple_type_mismatch( + self.context.types(), + &[ + PrimitiveTypeKind::Integer.into(), + PrimitiveTypeKind::Float.into(), + ], + operand.span(), + ty, + operand.span(), + )) + } + + /// Evaluates a `logical or` expression. + fn evaluate_logical_or_expr(&mut self, expr: &LogicalOrExpr) -> Result { + let (lhs, rhs) = expr.operands(); + + // Evaluate the left-hand side first + let left = self.evaluate_expr(&lhs)?; + if left + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| logical_or_mismatch(self.context.types(), left.ty(), lhs.span()))? + .unwrap_boolean() + { + // Short-circuit if the left-hand side is true + return Ok(true.into()); + } + + // Otherwise, evaluate the right-hand side + let right = self.evaluate_expr(&rhs)?; + right + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| logical_or_mismatch(self.context.types(), right.ty(), rhs.span())) + } + + /// Evaluates a `logical and` expression. + fn evaluate_logical_and_expr(&mut self, expr: &LogicalAndExpr) -> Result { + let (lhs, rhs) = expr.operands(); + + // Evaluate the left-hand side first + let left = self.evaluate_expr(&lhs)?; + if !left + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| logical_and_mismatch(self.context.types(), left.ty(), lhs.span()))? + .unwrap_boolean() + { + // Short-circuit if the left-hand side is false + return Ok(false.into()); + } + + // Otherwise, evaluate the right-hand side + let right = self.evaluate_expr(&rhs)?; + right + .coerce(self.context.types(), PrimitiveTypeKind::Boolean.into()) + .map_err(|_| logical_and_mismatch(self.context.types(), right.ty(), rhs.span())) + } + + /// Evaluates a comparison expression. + fn evaluate_comparison_expr( + &mut self, + op: ComparisonOperator, + lhs: &Expr, + rhs: &Expr, + span: Span, + ) -> Result { + let left = self.evaluate_expr(lhs)?; + let right = self.evaluate_expr(rhs)?; + + match op { + ComparisonOperator::Equality => Value::equals(self.context.types(), &left, &right), + ComparisonOperator::Inequality => { + Value::equals(self.context.types(), &left, &right).map(|r| !r) + } + ComparisonOperator::Less + | ComparisonOperator::LessEqual + | ComparisonOperator::Greater + | ComparisonOperator::GreaterEqual => { + // Only primitive types support other comparisons + match (&left, &right) { + (Value::Primitive(left), Value::Primitive(right)) => { + PrimitiveValue::compare(left, right).map(|o| match o { + Ordering::Less => matches!( + op, + ComparisonOperator::Less | ComparisonOperator::LessEqual + ), + Ordering::Equal => matches!( + op, + ComparisonOperator::LessEqual | ComparisonOperator::GreaterEqual + ), + Ordering::Greater => matches!( + op, + ComparisonOperator::Greater | ComparisonOperator::GreaterEqual + ), + }) + } + _ => None, + } + } + } + .map(Into::into) + .ok_or_else(|| { + comparison_mismatch( + self.context.types(), + op, + span, + left.ty(), + lhs.span(), + right.ty(), + rhs.span(), + ) + }) + } + + /// Evaluates a numeric expression. + fn evaluate_numeric_expr( + &mut self, + op: NumericOperator, + lhs: &Expr, + rhs: &Expr, + span: Span, + ) -> Result { + /// Implements numeric operations on integer operands. + fn int_numeric_op( + op: NumericOperator, + left: i64, + right: i64, + span: Span, + rhs_span: Span, + ) -> Result { + match op { + NumericOperator::Addition => left + .checked_add(right) + .ok_or_else(|| numeric_overflow(span)), + NumericOperator::Subtraction => left + .checked_sub(right) + .ok_or_else(|| numeric_overflow(span)), + NumericOperator::Multiplication => left + .checked_mul(right) + .ok_or_else(|| numeric_overflow(span)), + NumericOperator::Division => { + if right == 0 { + return Err(division_by_zero(span, rhs_span)); + } + + left.checked_div(right) + .ok_or_else(|| numeric_overflow(span)) + } + NumericOperator::Modulo => { + if right == 0 { + return Err(division_by_zero(span, rhs_span)); + } + + left.checked_rem(right) + .ok_or_else(|| numeric_overflow(span)) + } + NumericOperator::Exponentiation => left + .checked_pow( + (right) + .try_into() + .map_err(|_| exponent_not_in_range(rhs_span))?, + ) + .ok_or_else(|| numeric_overflow(span)), + } + } + + /// Implements numeric operations on floating point operands. + fn float_numeric_op(op: NumericOperator, left: f64, right: f64) -> f64 { + match op { + NumericOperator::Addition => left + right, + NumericOperator::Subtraction => left - right, + NumericOperator::Multiplication => left * right, + NumericOperator::Division => left / right, + NumericOperator::Modulo => left % right, + NumericOperator::Exponentiation => left.pow(right), + } + } + + let left = self.evaluate_expr(lhs)?; + let right = self.evaluate_expr(rhs)?; + match (&left, &right) { + ( + Value::Primitive(PrimitiveValue::Integer(left)), + Value::Primitive(PrimitiveValue::Integer(right)), + ) => Some(int_numeric_op(op, *left, *right, span, rhs.span())?.into()), + ( + Value::Primitive(PrimitiveValue::Float(left)), + Value::Primitive(PrimitiveValue::Float(right)), + ) => Some(float_numeric_op(op, left.0, right.0).into()), + ( + Value::Primitive(PrimitiveValue::Integer(left)), + Value::Primitive(PrimitiveValue::Float(right)), + ) => Some(float_numeric_op(op, *left as f64, right.0).into()), + ( + Value::Primitive(PrimitiveValue::Float(left)), + Value::Primitive(PrimitiveValue::Integer(right)), + ) => Some(float_numeric_op(op, left.0, *right as f64).into()), + (Value::Primitive(PrimitiveValue::String(left)), Value::Primitive(right)) + if op == NumericOperator::Addition + && !matches!(right, PrimitiveValue::Boolean(_)) => + { + Some( + PrimitiveValue::new_string(format!("{left}{right}", right = right.raw())) + .into(), + ) + } + (Value::Primitive(left), Value::Primitive(PrimitiveValue::String(right))) + if op == NumericOperator::Addition + && !matches!(left, PrimitiveValue::Boolean(_)) => + { + Some(PrimitiveValue::new_string(format!("{left}{right}", left = left.raw())).into()) + } + (Value::Primitive(PrimitiveValue::String(_)), Value::None) + | (Value::None, Value::Primitive(PrimitiveValue::String(_))) + if op == NumericOperator::Addition && self.placeholders > 0 => + { + // Allow string concatenation with `None` in placeholders, which evaluates to + // `None` + Some(Value::None) + } + _ => None, + } + .ok_or_else(|| { + numeric_mismatch( + self.context.types(), + op, + span, + left.ty(), + lhs.span(), + right.ty(), + rhs.span(), + ) + }) + } + + /// Evaluates a call expression. + fn evaluate_call_expr(&mut self, expr: &CallExpr) -> Result { + let target = expr.target(); + match wdl_analysis::stdlib::STDLIB.function(target.as_str()) { + Some(f) => { + // Evaluate the argument expressions + let mut count = 0; + let mut types = [Type::Union; MAX_PARAMETERS]; + let mut arguments = [const { CallArgument::none() }; MAX_PARAMETERS]; + for arg in expr.arguments() { + if count < MAX_PARAMETERS { + let v = self.evaluate_expr(&arg)?; + types[count] = v.ty(); + arguments[count] = CallArgument::new(v, arg.span()); + } + + count += 1; + } + + // First bind the function based on the argument types, then dispatch the call + let types = &types[..count.min(MAX_PARAMETERS)]; + let arguments = &arguments[..count.min(MAX_PARAMETERS)]; + if count <= MAX_PARAMETERS { + match f.bind(self.context.version(), self.context.types_mut(), types) { + Ok(binding) => { + let context = CallContext::new( + &mut self.context, + target.span(), + arguments, + binding.return_type(), + ); + + STDLIB + .get(target.as_str()) + .expect("should have implementation") + .call(binding, context) + } + Err(FunctionBindError::RequiresVersion(minimum)) => Err( + unsupported_function(minimum, target.as_str(), target.span()), + ), + Err(FunctionBindError::TooFewArguments(minimum)) => Err(too_few_arguments( + target.as_str(), + target.span(), + minimum, + arguments.len(), + )), + Err(FunctionBindError::TooManyArguments(maximum)) => { + Err(too_many_arguments( + target.as_str(), + target.span(), + maximum, + arguments.len(), + expr.arguments().skip(maximum).map(|e| e.span()), + )) + } + Err(FunctionBindError::ArgumentTypeMismatch { index, expected }) => { + Err(argument_type_mismatch( + self.context.types(), + target.as_str(), + &expected, + types[index], + expr.arguments() + .nth(index) + .map(|e| e.span()) + .expect("should have span"), + )) + } + Err(FunctionBindError::Ambiguous { first, second }) => Err( + ambiguous_argument(target.as_str(), target.span(), &first, &second), + ), + } + } else { + // Exceeded the maximum number of arguments to any function + match f.param_min_max(self.context.version()) { + Some((_, max)) => { + assert!(max <= MAX_PARAMETERS); + Err(too_many_arguments( + target.as_str(), + target.span(), + max, + count, + expr.arguments().skip(max).map(|e| e.span()), + )) + } + None => Err(unsupported_function( + f.minimum_version(), + target.as_str(), + target.span(), + )), + } + } + } + None => Err(unknown_function(target.as_str(), target.span())), + } + } + + /// Evaluates the type of an index expression. + fn evaluate_index_expr(&mut self, expr: &IndexExpr) -> Result { + let (target, index) = expr.operands(); + match self.evaluate_expr(&target)? { + Value::Compound(CompoundValue::Array(array)) => match self.evaluate_expr(&index)? { + Value::Primitive(PrimitiveValue::Integer(i)) => { + match i.try_into().map(|i: usize| array.as_slice().get(i)) { + Ok(Some(value)) => Ok(value.clone()), + _ => Err(array_index_out_of_range( + i, + array.len(), + index.span(), + target.span(), + )), + } + } + value => Err(index_type_mismatch( + self.context.types(), + PrimitiveTypeKind::Integer.into(), + value.ty(), + index.span(), + )), + }, + Value::Compound(CompoundValue::Map(map)) => { + let ty = map.ty().as_compound().expect("type should be compound"); + let key_type = match self.context.types().type_definition(ty.definition()) { + CompoundTypeDef::Map(ty) => ty + .key_type() + .as_primitive() + .expect("type should be primitive"), + _ => panic!("expected a map type"), + }; + + let i = match self.evaluate_expr(&index)? { + Value::None + if Type::None.is_coercible_to(self.context.types(), &key_type.into()) => + { + None + } + Value::Primitive(i) + if i.ty() + .is_coercible_to(self.context.types(), &key_type.into()) => + { + Some(i) + } + value => { + return Err(index_type_mismatch( + self.context.types(), + key_type.into(), + value.ty(), + index.span(), + )); + } + }; + + match map.get(&i) { + Some(value) => Ok(value.clone()), + None => Err(map_key_not_found(index.span())), + } + } + value => Err(cannot_index( + self.context.types(), + value.ty(), + target.span(), + )), + } + } + + /// Evaluates the type of an access expression. + fn evaluate_access_expr(&mut self, expr: &AccessExpr) -> Result { + let (target, name) = expr.operands(); + + // TODO: add support for access to call outputs + + match self.evaluate_expr(&target)? { + Value::Compound(CompoundValue::Pair(pair)) => match name.as_str() { + "left" => Ok(pair.left().clone()), + "right" => Ok(pair.right().clone()), + _ => Err(not_a_pair_accessor(&name)), + }, + Value::Compound(CompoundValue::Struct(s)) => match s.get(name.as_str()) { + Some(value) => Ok(value.clone()), + None => Err(not_a_struct_member( + self.context.types().struct_type(s.ty()).name(), + &name, + )), + }, + Value::Compound(CompoundValue::Object(object)) => match object.get(name.as_str()) { + Some(value) => Ok(value.clone()), + None => Err(not_an_object_member(&name)), + }, + Value::Task(task) => match task.field(name.as_str()) { + Some(value) => Ok(value.clone()), + None => Err(not_a_task_member(&name)), + }, + value => Err(cannot_access( + self.context.types(), + value.ty(), + target.span(), + )), + } + } +} + +#[cfg(test)] +pub(crate) mod test { + use std::collections::HashMap; + use std::fs; + use std::path::Path; + + use pretty_assertions::assert_eq; + use tempfile::TempDir; + use wdl_analysis::diagnostics::unknown_name; + use wdl_analysis::diagnostics::unknown_type; + use wdl_analysis::types::StructType; + use wdl_analysis::types::Types; + use wdl_ast::Ident; + use wdl_grammar::construct_tree; + use wdl_grammar::grammar::v1; + use wdl_grammar::lexer::Lexer; + + use super::*; + use crate::ScopeRef; + use crate::eval::Scope; + + /// Represents a test environment. + pub struct TestEnv { + /// The types collection for the test. + types: Types, + /// The scopes for the test. + scopes: Vec, + /// The structs for the test. + structs: HashMap<&'static str, Type>, + /// The working directory. + work_dir: TempDir, + /// The current directory. + temp_dir: TempDir, + } + + impl TestEnv { + pub fn types(&self) -> &Types { + &self.types + } + + pub fn types_mut(&mut self) -> &mut Types { + &mut self.types + } + + pub fn scope(&self) -> ScopeRef<'_> { + ScopeRef::new(&self.scopes, 0) + } + + pub fn insert_name(&mut self, name: impl Into, value: impl Into) { + self.scopes[0].insert(name, value); + } + + pub fn insert_struct(&mut self, name: &'static str, ty: impl Into) { + self.structs.insert(name, ty.into()); + } + + pub fn work_dir(&self) -> &Path { + self.work_dir.path() + } + + pub fn temp_dir(&self) -> &Path { + self.temp_dir.path() + } + + pub fn write_file(&self, name: &str, bytes: impl AsRef<[u8]>) { + fs::write(self.work_dir().join(name), bytes).expect("failed to create temp file"); + } + } + + impl Default for TestEnv { + fn default() -> Self { + Self { + types: Default::default(), + scopes: vec![Scope::new(None)], + structs: Default::default(), + temp_dir: TempDir::new().expect("failed to create temp directory"), + work_dir: TempDir::new().expect("failed to create work directory"), + } + } + } + + /// Represents test evaluation context to an expression evaluator. + pub struct TestEvaluationContext<'a> { + env: &'a mut TestEnv, + /// The supported version of WDL being evaluated. + version: SupportedVersion, + /// The stdout value from a task's execution. + stdout: Option, + /// The stderr value from a task's execution. + stderr: Option, + } + + impl<'a> TestEvaluationContext<'a> { + pub fn new(env: &'a mut TestEnv, version: SupportedVersion) -> Self { + Self { + env, + version, + stdout: None, + stderr: None, + } + } + + /// Sets the stdout to use for the evaluation context. + pub fn with_stdout(mut self, stdout: impl Into) -> Self { + self.stdout = Some(stdout.into()); + self + } + + /// Sets the stderr to use for the evaluation context. + pub fn with_stderr(mut self, stderr: impl Into) -> Self { + self.stderr = Some(stderr.into()); + self + } + } + + impl EvaluationContext for TestEvaluationContext<'_> { + fn version(&self) -> SupportedVersion { + self.version + } + + fn types(&self) -> &Types { + &self.env.types + } + + fn types_mut(&mut self) -> &mut Types { + &mut self.env.types + } + + fn resolve_name(&self, name: &Ident) -> Result { + self.env + .scope() + .lookup(name.as_str()) + .map(|v| v.clone()) + .ok_or_else(|| unknown_name(name.as_str(), name.span())) + } + + fn resolve_type_name(&mut self, name: &Ident) -> Result { + self.env + .structs + .get(name.as_str()) + .copied() + .ok_or_else(|| unknown_type(name.as_str(), name.span())) + } + + fn work_dir(&self) -> &Path { + self.env.work_dir() + } + + fn temp_dir(&self) -> &Path { + self.env.temp_dir() + } + + fn stdout(&self) -> Option<&Value> { + self.stdout.as_ref() + } + + fn stderr(&self) -> Option<&Value> { + self.stderr.as_ref() + } + + fn task(&self) -> Option<&Task> { + None + } + + fn document_types(&self) -> &Types { + unimplemented!() + } + } + + pub fn eval_v1_expr(env: &mut TestEnv, version: V1, source: &str) -> Result { + eval_v1_expr_with_context( + TestEvaluationContext::new(env, SupportedVersion::V1(version)), + source, + ) + } + + pub fn eval_v1_expr_with_stdio( + env: &mut TestEnv, + version: V1, + source: &str, + stdout: impl Into, + stderr: impl Into, + ) -> Result { + eval_v1_expr_with_context( + TestEvaluationContext::new(env, SupportedVersion::V1(version)) + .with_stdout(stdout) + .with_stderr(stderr), + source, + ) + } + + fn eval_v1_expr_with_context( + context: TestEvaluationContext<'_>, + source: &str, + ) -> Result { + let source = source.trim(); + let mut parser = v1::Parser::new(Lexer::new(source)); + let marker = parser.start(); + match v1::expr(&mut parser, marker) { + Ok(()) => { + // This call to `next` is important as `next` adds any remaining buffered events + assert!( + parser.next().is_none(), + "parser is not finished; expected a single expression with no remaining tokens" + ); + let output = parser.finish(); + assert_eq!( + output.diagnostics.iter().next(), + None, + "the provided WDL source failed to parse" + ); + let expr = Expr::cast(construct_tree(source, output.events)) + .expect("should be an expression"); + + let mut evaluator = ExprEvaluator::new(context); + evaluator.evaluate_expr(&expr) + } + Err((marker, diagnostic)) => { + marker.abandon(&mut parser); + Err(diagnostic) + } + } + } + + #[test] + fn literal_none_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "None").unwrap(); + assert_eq!(value.to_string(), "None"); + } + + #[test] + fn literal_bool_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "true").unwrap(); + assert_eq!(value.unwrap_boolean(), true); + + let value = eval_v1_expr(&mut env, V1::Two, "false").unwrap(); + assert_eq!(value.unwrap_boolean(), false); + } + + #[test] + fn literal_int_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "12345").unwrap(); + assert_eq!(value.unwrap_integer(), 12345); + + let value = eval_v1_expr(&mut env, V1::Two, "-54321").unwrap(); + assert_eq!(value.unwrap_integer(), -54321); + + let value = eval_v1_expr(&mut env, V1::Two, "0xdeadbeef").unwrap(); + assert_eq!(value.unwrap_integer(), 0xDEADBEEF); + + let value = eval_v1_expr(&mut env, V1::Two, "0777").unwrap(); + assert_eq!(value.unwrap_integer(), 0o777); + + let value = eval_v1_expr(&mut env, V1::Two, "-9223372036854775808").unwrap(); + assert_eq!(value.unwrap_integer(), -9223372036854775808); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, "9223372036854775808").expect_err("should fail"); + assert_eq!( + diagnostic.message(), + "literal integer exceeds the range for a 64-bit signed integer \ + (-9223372036854775808..=9223372036854775807)" + ); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, "-9223372036854775809").expect_err("should fail"); + assert_eq!( + diagnostic.message(), + "literal integer exceeds the range for a 64-bit signed integer \ + (-9223372036854775808..=9223372036854775807)" + ); + } + + #[test] + fn literal_float_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "12345.6789").unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 12345.6789); + + let value = eval_v1_expr(&mut env, V1::Two, "-12345.6789").unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -12345.6789); + + let value = eval_v1_expr(&mut env, V1::Two, "1.7976931348623157E+308").unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 1.7976931348623157E+308); + + let value = eval_v1_expr(&mut env, V1::Two, "-1.7976931348623157E+308").unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -1.7976931348623157E+308); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, "2.7976931348623157E+308").expect_err("should fail"); + assert_eq!( + diagnostic.message(), + "literal float exceeds the range for a 64-bit float \ + (-1.7976931348623157e308..=+1.7976931348623157e308)" + ); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, "-2.7976931348623157E+308").expect_err("should fail"); + assert_eq!( + diagnostic.message(), + "literal float exceeds the range for a 64-bit float \ + (-1.7976931348623157e308..=+1.7976931348623157e308)" + ); + } + + #[test] + fn literal_string_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "'hello\nworld'").unwrap(); + assert_eq!(value.unwrap_string().as_str(), "hello\nworld"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""hello world""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "hello world"); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#"<<< + <<< hello \\ \${foo} \~{bar} \ + world \>\>\> + >>>"#, + ) + .unwrap(); + assert_eq!( + value.unwrap_string().as_str(), + "<<< hello \\ ${foo} ~{bar} world >>>" + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#""\\\n\r\t\'\"\~\$\101\x41\u0041\U00000041\?""#, + ) + .unwrap(); + assert_eq!(value.unwrap_string().as_str(), "\\\n\r\t'\"~$AAAA\\?"); + } + + #[test] + fn string_placeholders() { + let mut env = TestEnv::default(); + env.insert_name("str", PrimitiveValue::new_string("foo")); + env.insert_name("file", PrimitiveValue::new_file("bar")); + env.insert_name("dir", PrimitiveValue::new_directory("baz")); + env.insert_name("salutation", PrimitiveValue::new_string("hello")); + env.insert_name("name1", Value::None); + env.insert_name("name2", PrimitiveValue::new_string("Fred")); + env.insert_name("spaces", PrimitiveValue::new_string(" ")); + env.insert_name("name", PrimitiveValue::new_string("Henry")); + env.insert_name("company", PrimitiveValue::new_string("Acme")); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{None}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), ""); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{default="hi" None}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "hi"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{true}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "true"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{false}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "false"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{true="yes" false="no" false}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "no"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{12345}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "12345"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{12345.6789}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "12345.678900"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{str}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foo"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{file}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "bar"); + + let value = eval_v1_expr(&mut env, V1::Two, r#""~{dir}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "baz"); + + let value = + eval_v1_expr(&mut env, V1::Two, r#""~{sep="+" [1,2,3]} = ~{1 + 2 + 3}""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "1+2+3 = 6"); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, r#""~{[1, 2, 3]}""#).expect_err("should fail"); + assert_eq!( + diagnostic.message(), + "cannot coerce type `Array[Int]` to `String`" + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#""~{salutation + ' ' + name1 + ', '}nice to meet you!""#, + ) + .unwrap(); + assert_eq!(value.unwrap_string().as_str(), "nice to meet you!"); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#""${salutation + ' ' + name2 + ', '}nice to meet you!""#, + ) + .unwrap(); + assert_eq!( + value.unwrap_string().as_str(), + "hello Fred, nice to meet you!" + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#" + <<< + ~{spaces}Hello ~{name}, + ~{spaces}Welcome to ~{company}! + >>>"#, + ) + .unwrap(); + assert_eq!( + value.unwrap_string().as_str(), + " Hello Henry,\n Welcome to Acme!" + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#""~{1 + 2 + 3 + 4 * 10 * 10} ~{"~{<<<~{'!' + '='}>>>}"} ~{10**3}""#, + ) + .unwrap(); + assert_eq!(value.unwrap_string().as_str(), "406 != 1000"); + } + + #[test] + fn literal_array_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "[]").unwrap(); + assert_eq!(value.unwrap_array().to_string(), "[]"); + + let value = eval_v1_expr(&mut env, V1::Two, "[1, 2, 3]").unwrap(); + assert_eq!(value.unwrap_array().to_string(), "[1, 2, 3]"); + + let value = eval_v1_expr(&mut env, V1::Two, "[[1], [2], [3.0]]").unwrap(); + assert_eq!( + value.unwrap_array().to_string(), + "[[1.000000], [2.000000], [3.000000]]" + ); + + let value = eval_v1_expr(&mut env, V1::Two, r#"["foo", "bar", "baz"]"#).unwrap(); + assert_eq!(value.unwrap_array().to_string(), r#"["foo", "bar", "baz"]"#); + } + + #[test] + fn literal_pair_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "(true, false)").unwrap(); + assert_eq!(value.unwrap_pair().to_string(), "(true, false)"); + + let value = eval_v1_expr(&mut env, V1::Two, "([1, 2, 3], [4, 5, 6])").unwrap(); + assert_eq!(value.unwrap_pair().to_string(), "([1, 2, 3], [4, 5, 6])"); + + let value = eval_v1_expr(&mut env, V1::Two, "([], {})").unwrap(); + assert_eq!(value.unwrap_pair().to_string(), "([], {})"); + + let value = eval_v1_expr(&mut env, V1::Two, r#"("foo", "bar")"#).unwrap(); + assert_eq!(value.unwrap_pair().to_string(), r#"("foo", "bar")"#); + } + + #[test] + fn literal_map_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "{}").unwrap(); + assert_eq!(value.unwrap_map().to_string(), "{}"); + + let value = eval_v1_expr(&mut env, V1::Two, "{ 1: 2, 3: 4, 5: 6 }").unwrap(); + assert_eq!(value.unwrap_map().to_string(), "{1: 2, 3: 4, 5: 6}"); + + let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": "bar", "baz": "qux"}"#).unwrap(); + assert_eq!( + value.unwrap_map().to_string(), + r#"{"foo": "bar", "baz": "qux"}"# + ); + + let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": { 1: 2 }, "baz": {}}"#).unwrap(); + assert_eq!( + value.unwrap_map().to_string(), + r#"{"foo": {1: 2}, "baz": {}}"# + ); + + let value = eval_v1_expr(&mut env, V1::Two, r#"{"foo": 100, "baz": 2.5}"#).unwrap(); + assert_eq!( + value.unwrap_map().to_string(), + r#"{"foo": 100.000000, "baz": 2.500000}"# + ); + } + + #[test] + fn literal_object_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Two, "object {}").unwrap(); + assert_eq!(value.unwrap_object().to_string(), "object {}"); + + let value = eval_v1_expr(&mut env, V1::Two, "object { foo: 2, bar: 4, baz: 6 }").unwrap(); + assert_eq!( + value.unwrap_object().to_string(), + "object {foo: 2, bar: 4, baz: 6}" + ); + + let value = eval_v1_expr(&mut env, V1::Two, r#"object {foo: "bar", baz: "qux"}"#).unwrap(); + assert_eq!( + value.unwrap_object().to_string(), + r#"object {foo: "bar", baz: "qux"}"# + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#"object {foo: { 1: 2 }, bar: [], qux: "jam"}"#, + ) + .unwrap(); + assert_eq!( + value.unwrap_object().to_string(), + r#"object {foo: {1: 2}, bar: [], qux: "jam"}"# + ); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#"object {foo: 1.0, bar: object { baz: "qux" }}"#, + ) + .unwrap(); + assert_eq!( + value.unwrap_object().to_string(), + r#"object {foo: 1.000000, bar: object {baz: "qux"}}"# + ); + } + + #[test] + fn literal_struct_expr() { + let mut env = TestEnv::default(); + let bar_ty = env.types_mut().add_struct(StructType::new("Bar", [ + ("foo", PrimitiveTypeKind::File), + ("bar", PrimitiveTypeKind::Integer), + ])); + + let foo_ty = env.types_mut().add_struct(StructType::new("Foo", [ + ("foo", PrimitiveTypeKind::Float.into()), + ( + "bar", + Type::Compound(bar_ty.as_compound().expect("should be a compound type")), + ), + ])); + + env.insert_struct("Foo", foo_ty); + env.insert_struct("Bar", bar_ty); + + let value = eval_v1_expr( + &mut env, + V1::Two, + r#"Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#, + ) + .unwrap(); + assert_eq!( + value.unwrap_struct().to_string(), + r#"Foo {foo: 1.000000, bar: Bar {foo: "baz", bar: 2}}"# + ); + + let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} == Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} == Foo { foo: 1.0, bar: Bar { foo: "jam", bar: 2 }}"#) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} != Foo { foo: 1.0, bar: Bar { foo: "baz", bar: 2 }}"#) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Two,r#"Foo { foo: 1, bar: Bar { foo: "baz", bar: 2 }} != Foo { foo: 1.0, bar: Bar { foo: "jam", bar: 2 }}"#) + .unwrap(); + assert!(value.unwrap_boolean()); + } + + #[test] + fn name_ref_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", 1234); + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1234); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar"#).unwrap_err(); + assert_eq!(diagnostic.message(), "unknown name `bar`"); + } + + #[test] + fn parenthesized_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", 1234); + let value = eval_v1_expr(&mut env, V1::Zero, r#"(foo - foo) + (1234 - foo)"#).unwrap(); + assert_eq!(value.unwrap_integer(), 0); + } + + #[test] + fn if_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", true); + env.insert_name("bar", false); + env.insert_name("baz", PrimitiveValue::new_file("file")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then "foo" else "bar""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foo"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then "foo" else "bar""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "bar"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then 1234 else 0.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 1234.0); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then 1234 else 0.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 0.5); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (foo) then baz else "str""#).unwrap(); + assert_eq!(value.unwrap_file().as_str(), "file"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"if (bar) then baz else "path""#).unwrap(); + assert_eq!(value.unwrap_file().as_str(), "path"); + } + + #[test] + fn logical_not_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"!true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"!false"#).unwrap(); + assert!(value.unwrap_boolean()); + } + + #[test] + fn negation_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"-1234"#).unwrap(); + assert_eq!(value.unwrap_integer(), -1234); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"-(1234)"#).unwrap(); + assert_eq!(value.unwrap_integer(), -1234); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"----1234"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1234); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"-1234.5678"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -1234.5678); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"-(1234.5678)"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -1234.5678); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"----1234.5678"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 1234.5678); + } + + #[test] + fn logical_or_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"false || false"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false || true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true || false"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true || true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true || nope"#).unwrap(); + assert!(value.unwrap_boolean()); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"false || nope"#).unwrap_err(); + assert_eq!(diagnostic.message(), "unknown name `nope`"); + } + + #[test] + fn logical_and_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"false && false"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false && true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true && false"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true && true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false && nope"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"true && nope"#).unwrap_err(); + assert_eq!(diagnostic.message(), "unknown name `nope`"); + } + + #[test] + fn equality_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"None == None"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true == true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 1234"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 4321"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 == 1234.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"4321 == 1234.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 == 1234"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 == 4321"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 == 1234.5678"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 == 8765.4321"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == foo"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" == bar"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo == "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo == "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar == "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar == "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") == (1234, "bar")"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") == (1234, "baz")"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1, 2, 3] == [1, 2, 3]"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] == [2, 3]"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] == [2]"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 1, "bar": 2, "baz": 3}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 1, "baz": 3, "bar": 2}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 1, "baz": 3}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} == {"foo": 3, "bar": 2, "baz": 1}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 1, bar: 2, baz: "3"}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 1, baz: "3"}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} == object {foo: 3, bar: 2, baz: "1"}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + // Note: struct equality is handled in the struct literal test + } + + #[test] + fn inequality_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"None != None"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true != true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 1234"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 4321"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 != 1234.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"4321 != 1234.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 != 1234"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.0 != 4321"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 != 1234.5678"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.5678 != 8765.4321"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != foo"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" != bar"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo != "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo != "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar != "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar != "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") != (1234, "bar")"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"(1234, "bar") != (1234, "baz")"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1, 2, 3] != [1, 2, 3]"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] != [2, 3]"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"[1] != [2]"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 1, "bar": 2, "baz": 3}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 1, "baz": 3}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"{"foo": 1, "bar": 2, "baz": 3} != {"foo": 3, "bar": 2, "baz": 1}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 1, bar: 2, baz: "3"}"#, + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 1, baz: "3"}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Zero, + r#"object {foo: 1, bar: 2, baz: "3"} != object {foo: 3, bar: 2, baz: "1"}"#, + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + // Note: struct inequality is handled in the struct literal test + } + + #[test] + fn less_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false < true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true < false"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true < true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 < 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 < 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 0.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 < 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 < 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 < 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 0.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 < 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" < "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" < "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" < "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar < "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar < bar"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo < "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo < foo"#).unwrap(); + assert!(!value.unwrap_boolean()); + } + + #[test] + fn less_equal_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false <= true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true <= false"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true <= true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 <= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 <= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 0.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 <= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 <= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 <= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 0.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 <= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" <= "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" <= "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" <= "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar <= "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar <= bar"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo <= "bar""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo <= foo"#).unwrap(); + assert!(value.unwrap_boolean()); + } + + #[test] + fn greater_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false > true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true > false"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true > true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 > 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 > 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 0.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 > 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 > 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 > 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 0.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 > 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" > "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" > "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" > "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar > "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar > bar"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo > "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo > foo"#).unwrap(); + assert!(!value.unwrap_boolean()); + } + + #[test] + fn greater_equal_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"false >= true"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true >= false"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"true >= true"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 >= 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0 >= 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 0.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 >= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 >= 1"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 1"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"0.0 >= 1.0"#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 0.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1.0 >= 1.0"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" >= "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" >= "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" >= "foo""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar >= "foo""#).unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar >= bar"#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo >= "bar""#).unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo >= foo"#).unwrap(); + assert!(value.unwrap_boolean()); + } + + #[test] + fn addition_expr() { + let mut env = TestEnv::default(); + env.insert_name("foo", PrimitiveValue::new_file("foo")); + env.insert_name("bar", PrimitiveValue::new_directory("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 + 2 + 3 + 4"#).unwrap(); + assert_eq!(value.unwrap_integer(), 10); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"10 + 20.0 + 30 + 40.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 100.0); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 + 200 + 300.0 + 400"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 1000.0); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"1000.5 + 2000.5 + 3000.5 + 4000.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 10002.0); + + let diagnostic = + eval_v1_expr(&mut env, V1::Zero, &format!(r#"{max} + 1"#, max = i64::MAX)).unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + 1234"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foo1234"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234 + "foo""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "1234foo"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + 1234.456"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foo1234.456000"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"1234.456 + "foo""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "1234.456000foo"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + "bar""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foobar"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" + "foo""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "barfoo"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo + "bar""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foobar"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""bar" + foo"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "barfoo"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#""foo" + bar"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foobar"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar + "foo""#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "barfoo"); + } + + #[test] + fn subtraction_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"-1 - 2 - 3 - 4"#).unwrap(); + assert_eq!(value.unwrap_integer(), -10); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"-10 - 20.0 - 30 - 40.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -100.0); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"-100.0 - 200 - 300.0 - 400"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -1000.0); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"-1000.5 - 2000.5 - 3000.5 - 4000.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), -10002.0); + + let diagnostic = + eval_v1_expr(&mut env, V1::Zero, &format!(r#"{min} - 1"#, min = i64::MIN)).unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + } + + #[test] + fn multiplication_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"1 * 2 * 3 * 4"#).unwrap(); + assert_eq!(value.unwrap_integer(), 24); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"10 * 20.0 * 30 * 40.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 240000.0); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 * 200 * 300.0 * 400"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 2400000000.0); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"1000.5 * 2000.5 * 3000.5 * 4000.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 24025008751250.063); + + let diagnostic = + eval_v1_expr(&mut env, V1::Zero, &format!(r#"{max} * 2"#, max = i64::MAX)).unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + } + + #[test] + fn division_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"5 / 2"#).unwrap(); + assert_eq!(value.unwrap_integer(), 2); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"10 / 20.0 / 30 / 40.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 0.00041666666666666664); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"100.0 / 200 / 300.0 / 400"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 4.166666666666667e-6); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"1000.5 / 2000.5 / 3000.5 / 4000.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 4.166492759125078e-8); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"10 / 0"#).unwrap_err(); + assert_eq!(diagnostic.message(), "attempt to divide by zero"); + + let diagnostic = eval_v1_expr( + &mut env, + V1::Zero, + &format!(r#"{min} / -1"#, min = i64::MIN), + ) + .unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + } + + #[test] + fn modulo_expr() { + let mut env = TestEnv::default(); + let value = eval_v1_expr(&mut env, V1::Zero, r#"5 % 2"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"5.5 % 2"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 1.5); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"5 % 2.5"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 0.0); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"5.25 % 1.3"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 0.04999999999999982); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"5 % 0"#).unwrap_err(); + assert_eq!(diagnostic.message(), "attempt to divide by zero"); + + let diagnostic = eval_v1_expr( + &mut env, + V1::Zero, + &format!(r#"{min} % -1"#, min = i64::MIN), + ) + .unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + } + + #[test] + fn exponentiation_expr() { + let mut env = TestEnv::default(); + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"10 ** 0"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "use of the exponentiation operator requires WDL version 1.2" + ); + + let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2 ** 2"#).unwrap(); + assert_eq!(value.unwrap_integer(), 625); + + let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2.0 ** 2"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 625.0); + + let value = eval_v1_expr(&mut env, V1::Two, r#"5 ** 2 ** 2.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 625.0); + + let value = eval_v1_expr(&mut env, V1::Two, r#"5.0 ** 2.0 ** 2.0"#).unwrap(); + approx::assert_relative_eq!(value.unwrap_float(), 625.0); + + let diagnostic = + eval_v1_expr(&mut env, V1::Two, &format!(r#"{max} ** 2"#, max = i64::MAX)).unwrap_err(); + assert_eq!( + diagnostic.message(), + "evaluation of arithmetic expression resulted in overflow" + ); + } + + #[test] + fn call_expr() { + // This test will just check for errors; testing of the function implementations + // is in `stdlib.rs` + let mut env = TestEnv::default(); + let diagnostic = eval_v1_expr(&mut env, V1::Zero, "min(1, 2)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "this use of function `min` requires a minimum WDL version of 1.1" + ); + + let diagnostic = + eval_v1_expr(&mut env, V1::Zero, "min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "this use of function `min` requires a minimum WDL version of 1.1" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::One, "min(1)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "function `min` requires at least 2 arguments but 1 was supplied" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::One, "min(1, 2, 3)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "function `min` requires no more than 2 arguments but 3 were supplied" + ); + + let diagnostic = + eval_v1_expr(&mut env, V1::One, "min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "function `min` requires no more than 2 arguments but 10 were supplied" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::One, "min('1', 2)").unwrap_err(); + assert_eq!( + diagnostic.message(), + "type mismatch: argument to function `min` expects type `Int` or `Float`, but found \ + type `String`" + ); + } + + #[test] + fn index_expr() { + let mut env = TestEnv::default(); + let array_ty = env + .types_mut() + .add_array(ArrayType::new(PrimitiveTypeKind::Integer)); + let map_ty = env.types_mut().add_map(MapType::new( + PrimitiveTypeKind::String, + PrimitiveTypeKind::Integer, + )); + + env.insert_name( + "foo", + Array::new(env.types(), array_ty, [1, 2, 3, 4, 5]).unwrap(), + ); + env.insert_name( + "bar", + Map::new(env.types(), map_ty, [ + (PrimitiveValue::new_string("foo"), 1), + (PrimitiveValue::new_string("bar"), 2), + ]) + .unwrap(), + ); + env.insert_name("baz", PrimitiveValue::new_file("bar")); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[1]"#).unwrap(); + assert_eq!(value.unwrap_integer(), 2); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[foo[[1, 2, 3][0]]]"#).unwrap(); + assert_eq!(value.unwrap_integer(), 3); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo[10]"#).unwrap_err(); + assert_eq!(diagnostic.message(), "array index 10 is out of range"); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo["10"]"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "type mismatch: expected index to be type `Int`, but found type `String`" + ); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar["foo"]"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar[baz]"#).unwrap(); + assert_eq!(value.unwrap_integer(), 2); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo[bar["foo"]]"#).unwrap(); + assert_eq!(value.unwrap_integer(), 2); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar["does not exist"]"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "the map does not contain an entry for the specified key" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar[1]"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "type mismatch: expected index to be type `String`, but found type `Int`" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"1[0]"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "indexing is only allowed on `Array` and `Map` types" + ); + } + + #[test] + fn access_expr() { + let mut env = TestEnv::default(); + let pair_ty = env.types_mut().add_pair(PairType::new( + PrimitiveTypeKind::Integer, + PrimitiveTypeKind::String, + )); + let struct_ty = env.types_mut().add_struct(StructType::new("Foo", [ + ("foo", PrimitiveTypeKind::Integer), + ("bar", PrimitiveTypeKind::String), + ])); + + env.insert_name( + "foo", + Pair::new(env.types(), pair_ty, 1, PrimitiveValue::new_string("foo")).unwrap(), + ); + env.insert_name( + "bar", + Struct::new(env.types(), struct_ty, [ + ("foo", 1.into()), + ("bar", PrimitiveValue::new_string("bar")), + ]) + .unwrap(), + ); + env.insert_name("baz", 1); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo.left"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"foo.right"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "foo"); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"foo.bar"#).unwrap_err(); + assert_eq!(diagnostic.message(), "cannot access a pair with name `bar`"); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar.foo"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1); + + let value = eval_v1_expr(&mut env, V1::Zero, r#"bar.bar"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "bar"); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"bar.baz"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "struct `Foo` does not have a member named `baz`" + ); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.foo"#).unwrap(); + assert_eq!(value.unwrap_integer(), 1); + + let value = + eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.bar"#).unwrap(); + assert_eq!(value.unwrap_string().as_str(), "bar"); + + let diagnostic = + eval_v1_expr(&mut env, V1::Zero, r#"object { foo: 1, bar: "bar" }.baz"#).unwrap_err(); + assert_eq!( + diagnostic.message(), + "object does not have a member named `baz`" + ); + + let diagnostic = eval_v1_expr(&mut env, V1::Zero, r#"baz.foo"#).unwrap_err(); + assert_eq!(diagnostic.message(), "cannot access type `Int`"); + } +} diff --git a/wdl-engine/src/eval/v1/task.rs b/wdl-engine/src/eval/v1/task.rs new file mode 100644 index 00000000..2ee9944a --- /dev/null +++ b/wdl-engine/src/eval/v1/task.rs @@ -0,0 +1,929 @@ +//! Implementation of evaluation for V1 tasks. + +use std::collections::HashMap; +use std::mem; +use std::path::Path; + +use anyhow::Context; +use anyhow::anyhow; +use petgraph::Direction; +use petgraph::Graph; +use petgraph::algo::toposort; +use petgraph::graph::NodeIndex; +use petgraph::visit::EdgeRef; +use tracing::debug; +use tracing::info; +use tracing::warn; +use wdl_analysis::diagnostics::multiple_type_mismatch; +use wdl_analysis::diagnostics::unknown_name; +use wdl_analysis::document::Document; +use wdl_analysis::document::TASK_VAR_NAME; +use wdl_analysis::document::Task; +use wdl_analysis::eval::v1::TaskGraphBuilder; +use wdl_analysis::eval::v1::TaskGraphNode; +use wdl_analysis::types::Optional; +use wdl_analysis::types::Type; +use wdl_analysis::types::TypeNameResolver; +use wdl_analysis::types::Types; +use wdl_analysis::types::v1::AstTypeConverter; +use wdl_analysis::types::v1::task_hint_types; +use wdl_analysis::types::v1::task_requirement_types; +use wdl_ast::Ast; +use wdl_ast::AstNodeExt; +use wdl_ast::AstToken; +use wdl_ast::Diagnostic; +use wdl_ast::Ident; +use wdl_ast::Severity; +use wdl_ast::SupportedVersion; +use wdl_ast::ToSpan; +use wdl_ast::TokenStrHash; +use wdl_ast::v1::CommandPart; +use wdl_ast::v1::CommandSection; +use wdl_ast::v1::Decl; +use wdl_ast::v1::RequirementsSection; +use wdl_ast::v1::RuntimeSection; +use wdl_ast::v1::StrippedCommandPart; +use wdl_ast::v1::TaskHintsSection; +use wdl_ast::version::V1; + +use crate::Coercible; +use crate::Engine; +use crate::EvaluationContext; +use crate::EvaluationResult; +use crate::Outputs; +use crate::Scope; +use crate::ScopeRef; +use crate::TaskExecution; +use crate::TaskInputs; +use crate::TaskValue; +use crate::Value; +use crate::diagnostics::missing_task_output; +use crate::diagnostics::runtime_type_mismatch; +use crate::eval::EvaluatedTask; +use crate::v1::ExprEvaluator; + +/// The index of a task's root scope. +const ROOT_SCOPE_INDEX: usize = 0; +/// The index of a task's output scope. +const OUTPUT_SCOPE_INDEX: usize = 1; +/// The index of the evaluation scope where the WDL 1.2 `task` variable is +/// visible. +const TASK_SCOPE_INDEX: usize = 2; + +/// Used to evaluate expressions in tasks. +struct TaskEvaluationContext<'a> { + /// The associated evaluation engine. + engine: &'a mut Engine, + /// The document being evaluated. + document: &'a Document, + /// The working directory for the evaluation. + work_dir: &'a Path, + /// The temp directory for the evaluation. + temp_dir: &'a Path, + /// The current evaluation scope. + scope: ScopeRef<'a>, + /// The standard out value to use. + stdout: Option<&'a Value>, + /// The standard error value to use. + stderr: Option<&'a Value>, + /// The task associated with the evaluation. + /// + /// This is only `Some` when evaluating task hints sections. + task: Option<&'a Task>, +} + +impl EvaluationContext for TaskEvaluationContext<'_> { + fn version(&self) -> SupportedVersion { + self.document + .version() + .expect("document should have a version") + } + + fn types(&self) -> &Types { + self.engine.types() + } + + fn types_mut(&mut self) -> &mut Types { + self.engine.types_mut() + } + + fn resolve_name(&self, name: &Ident) -> Result { + self.scope + .lookup(name.as_str()) + .cloned() + .ok_or_else(|| unknown_name(name.as_str(), name.span())) + } + + fn resolve_type_name(&mut self, name: &Ident) -> Result { + self.engine.resolve_type_name(self.document, name) + } + + fn work_dir(&self) -> &Path { + self.work_dir + } + + fn temp_dir(&self) -> &Path { + self.temp_dir + } + + fn stdout(&self) -> Option<&Value> { + self.stdout + } + + fn stderr(&self) -> Option<&Value> { + self.stderr + } + + fn task(&self) -> Option<&Task> { + self.task + } + + fn document_types(&self) -> &Types { + self.document.types() + } +} + +impl<'a> TaskEvaluationContext<'a> { + /// Constructs a new expression evaluation context. + pub fn new( + engine: &'a mut Engine, + document: &'a Document, + work_dir: &'a Path, + temp_dir: &'a Path, + scope: ScopeRef<'a>, + ) -> Self { + Self { + engine, + document, + work_dir, + temp_dir, + scope, + stdout: None, + stderr: None, + task: None, + } + } + + /// Sets the stdout value to use for the evaluation context. + pub fn with_stdout(mut self, stdout: &'a Value) -> Self { + self.stdout = Some(stdout); + self + } + + /// Sets the stderr value to use for the evaluation context. + pub fn with_stderr(mut self, stderr: &'a Value) -> Self { + self.stderr = Some(stderr); + self + } + + /// Sets the associated task for evaluation. + /// + /// This is used in evaluating hints sections. + pub fn with_task(mut self, task: &'a Task) -> Self { + self.task = Some(task); + self + } +} + +/// Represents a WDL V1 task evaluator. +pub struct TaskEvaluator<'a> { + /// The associated evaluation engine. + engine: &'a mut Engine, +} + +impl<'a> TaskEvaluator<'a> { + /// Constructs a new task evaluator. + pub fn new(engine: &'a mut Engine) -> Self { + Self { engine } + } + + /// Evaluates the given task. + /// + /// Upon success, returns the evaluated task. + #[allow(clippy::redundant_closure_call)] + pub async fn evaluate( + &mut self, + document: &'a Document, + task: &Task, + inputs: &TaskInputs, + root: &Path, + ) -> EvaluationResult { + // Return the first error analysis diagnostic if there was one + // With this check, we can assume certain correctness properties of the document + if let Some(diagnostic) = document + .diagnostics() + .iter() + .find(|d| d.severity() == Severity::Error) + { + return Err(diagnostic.clone().into()); + } + + inputs + .validate(self.engine.types_mut(), document, task) + .with_context(|| { + format!( + "failed to validate the inputs to task `{task}`", + task = task.name() + ) + })?; + + let mut execution = self.engine.backend().create_execution(root)?; + match document.node().ast() { + Ast::V1(ast) => { + // Find the task in the AST + let definition = ast + .tasks() + .find(|t| t.name().as_str() == task.name()) + .expect("task should exist in the AST"); + + let version = document.version().expect("document should have version"); + + // Build an evaluation graph for the task + let mut diagnostics = Vec::new(); + let graph = + TaskGraphBuilder::default().build(version, &definition, &mut diagnostics); + + if let Some(diagnostic) = diagnostics.pop() { + return Err(diagnostic.into()); + } + + info!( + "evaluating task `{task}` in `{uri}`", + task = task.name(), + uri = document.uri() + ); + + // Tasks only have a root scope (0), an output scope (1), and a `task` variable + // scope (2) + let mut scopes = [ + Scope::new(None), + Scope::new(Some(ROOT_SCOPE_INDEX.into())), + Scope::new(Some(OUTPUT_SCOPE_INDEX.into())), + ]; + let mut command = String::new(); + let mut requirements = None; + let mut hints = None; + + let nodes = toposort(&graph, None).expect("graph should be acyclic"); + let mut current = 0; + while current < nodes.len() { + match &graph[nodes[current]] { + TaskGraphNode::Input(decl) => { + self.evaluate_input( + document, + execution.as_ref(), + &mut scopes, + task, + decl, + inputs, + )?; + } + TaskGraphNode::Decl(decl) => { + self.evaluate_decl( + document, + execution.as_ref(), + &mut scopes, + task, + decl, + )?; + } + TaskGraphNode::Output(_) => { + // Stop at the first output; at this point the task can be executed + break; + } + TaskGraphNode::Command(section) => { + assert!(command.is_empty()); + + // Get the execution constraints + let empty = Default::default(); + let constraints = execution + .constraints( + self.engine, + requirements.as_ref().unwrap_or(&empty), + hints.as_ref().unwrap_or(&empty), + ) + .with_context(|| { + format!("failed to execute task `{task}`", task = task.name()) + })?; + + // Introduce the task variable at this point; valid for both the command + // section and the outputs section + if version >= SupportedVersion::V1(V1::Two) { + let task = + TaskValue::new_v1(task.name(), "bar", &definition, constraints); + scopes[TASK_SCOPE_INDEX].insert(TASK_VAR_NAME, Value::Task(task)); + } + + // Map any paths needed for command evaluation + let mapped_paths = Self::map_command_paths( + &graph, + nodes[current], + ScopeRef::new(&scopes, TASK_SCOPE_INDEX), + &mut execution, + ); + + command = self.evaluate_command( + document, + execution.as_mut(), + &scopes, + task, + section, + &mapped_paths, + )?; + } + TaskGraphNode::Runtime(section) => { + assert!( + requirements.is_none(), + "requirements should not have been evaluated" + ); + assert!(hints.is_none(), "hints should not have been evaluated"); + + let (r, h) = self.evaluate_runtime_section( + document, + execution.as_ref(), + &scopes, + task, + section, + inputs, + )?; + + requirements = Some(r); + hints = Some(h); + } + TaskGraphNode::Requirements(section) => { + assert!( + requirements.is_none(), + "requirements should not have been evaluated" + ); + requirements = Some(self.evaluate_requirements_section( + document, + execution.as_ref(), + &scopes, + task, + section, + inputs, + )?); + } + TaskGraphNode::Hints(section) => { + assert!(hints.is_none(), "hints should not have been evaluated"); + hints = Some(self.evaluate_hints_section( + document, + execution.as_ref(), + &scopes, + task, + section, + inputs, + )?); + } + } + + current += 1; + } + + let requirements = requirements.unwrap_or_default(); + let hints = hints.unwrap_or_default(); + + // TODO: check call cache for a hit. if so, skip task execution and use cache + // paths for output evaluation + + let status_code = execution.spawn(command, &requirements, &hints)?.await?; + + // TODO: support retrying the task if it fails + + let mut evaluated = EvaluatedTask::new(execution.as_ref(), status_code)?; + + // Update the task variable's return code + if version >= SupportedVersion::V1(V1::Two) { + let task = scopes[TASK_SCOPE_INDEX] + .get_mut(TASK_VAR_NAME) + .unwrap() + .as_task_mut() + .unwrap(); + task.set_return_code(evaluated.status_code); + } + + // Use a closure that returns an evaluation result for evaluating the outputs + let mut outputs = || -> EvaluationResult { + evaluated.handle_exit(&requirements)?; + + for index in &nodes[current..] { + match &graph[*index] { + TaskGraphNode::Output(decl) => { + self.evaluate_output( + document, + &mut scopes, + task, + decl, + &evaluated, + )?; + } + TaskGraphNode::Input(decl) => { + self.evaluate_input( + document, + execution.as_ref(), + &mut scopes, + task, + decl, + inputs, + )?; + } + TaskGraphNode::Decl(decl) => { + self.evaluate_decl( + document, + execution.as_ref(), + &mut scopes, + task, + decl, + )?; + } + _ => { + unreachable!( + "only declarations should be evaluated after the command" + ) + } + } + } + + let mut outputs: Outputs = mem::take(&mut scopes[OUTPUT_SCOPE_INDEX]).into(); + if let Some(section) = definition.output() { + let indexes: HashMap<_, _> = section + .declarations() + .enumerate() + .map(|(i, d)| (TokenStrHash::new(d.name()), i)) + .collect(); + outputs.sort_by(move |a, b| indexes[a].cmp(&indexes[b])) + } + + Ok(outputs) + }; + + evaluated.outputs = outputs(); + Ok(evaluated) + } + _ => Err(anyhow!("document is not a 1.x document").into()), + } + } + + /// Evaluates a task input. + fn evaluate_input( + &mut self, + document: &Document, + execution: &dyn TaskExecution, + scopes: &mut [Scope], + task: &Task, + decl: &Decl, + inputs: &TaskInputs, + ) -> EvaluationResult<()> { + let name = decl.name(); + let decl_ty = decl.ty(); + let ty = self.convert_ast_type(document, &decl_ty)?; + + let (value, span) = match inputs.get(name.as_str()) { + Some(input) => (input.clone(), name.span()), + None => { + if let Some(expr) = decl.expr() { + debug!( + "evaluating input `{name}` for task `{task}` in `{uri}`", + name = name.as_str(), + task = task.name(), + uri = document.uri(), + ); + + let mut evaluator = ExprEvaluator::new(TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, ROOT_SCOPE_INDEX), + )); + let value = evaluator.evaluate_expr(&expr)?; + (value, expr.span()) + } else { + assert!(decl.ty().is_optional(), "type should be optional"); + (Value::None, name.span()) + } + } + }; + + let value = value.coerce(self.engine.types_mut(), ty).map_err(|e| { + runtime_type_mismatch( + self.engine.types(), + e, + ty, + decl_ty.syntax().text_range().to_span(), + value.ty(), + span, + ) + })?; + scopes[ROOT_SCOPE_INDEX].insert(name.as_str(), value); + Ok(()) + } + + /// Evaluates a task private declaration. + fn evaluate_decl( + &mut self, + document: &Document, + execution: &dyn TaskExecution, + scopes: &mut [Scope], + task: &Task, + decl: &Decl, + ) -> EvaluationResult<()> { + let name = decl.name(); + debug!( + "evaluating private declaration `{name}` for task `{task}` in `{uri}`", + name = name.as_str(), + task = task.name(), + uri = document.uri(), + ); + + let decl_ty = decl.ty(); + let ty = self.convert_ast_type(document, &decl_ty)?; + + let mut evaluator = ExprEvaluator::new(TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, ROOT_SCOPE_INDEX), + )); + + let expr = decl.expr().expect("private decls should have expressions"); + let value = evaluator.evaluate_expr(&expr)?; + let value = value.coerce(self.engine.types_mut(), ty).map_err(|e| { + runtime_type_mismatch( + self.engine.types(), + e, + ty, + decl_ty.syntax().text_range().to_span(), + value.ty(), + expr.span(), + ) + })?; + scopes[ROOT_SCOPE_INDEX].insert(name.as_str(), value); + Ok(()) + } + + /// Evaluates the runtime section. + fn evaluate_runtime_section( + &mut self, + document: &Document, + execution: &dyn TaskExecution, + scopes: &[Scope], + task: &Task, + section: &RuntimeSection, + inputs: &TaskInputs, + ) -> EvaluationResult<(HashMap, HashMap)> { + debug!( + "evaluating runtimes section for task `{task}` in `{uri}`", + task = task.name(), + uri = document.uri() + ); + + let mut requirements = HashMap::new(); + let mut hints = HashMap::new(); + let version = document.version().expect("document should have version"); + for item in section.items() { + let name = item.name(); + if let Some(value) = inputs.requirement(name.as_str()) { + requirements.insert(name.as_str().to_string(), value.clone()); + continue; + } else if let Some(value) = inputs.hint(name.as_str()) { + hints.insert(name.as_str().to_string(), value.clone()); + continue; + } + + let mut evaluator = ExprEvaluator::new(TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, ROOT_SCOPE_INDEX), + )); + + let (types, requirement) = match task_requirement_types(version, name.as_str()) { + Some(types) => (Some(types), true), + None => match task_hint_types(version, name.as_str(), false) { + Some(types) => (Some(types), false), + None => (None, false), + }, + }; + + // Evaluate and coerce to the expected type + let expr = item.expr(); + let mut value = evaluator.evaluate_expr(&expr)?; + if let Some(types) = types { + value = types + .iter() + .find_map(|ty| value.coerce(self.engine.types_mut(), *ty).ok()) + .ok_or_else(|| { + multiple_type_mismatch( + self.engine.types(), + types, + name.span(), + value.ty(), + expr.span(), + ) + })?; + } + + if requirement { + requirements.insert(name.as_str().to_string(), value); + } else { + hints.insert(name.as_str().to_string(), value); + } + } + + Ok((requirements, hints)) + } + + /// Evaluates the requirements section. + fn evaluate_requirements_section( + &mut self, + document: &Document, + execution: &dyn TaskExecution, + scopes: &[Scope], + task: &Task, + section: &RequirementsSection, + inputs: &TaskInputs, + ) -> EvaluationResult> { + debug!( + "evaluating requirements section for task `{task}` in `{uri}`", + task = task.name(), + uri = document.uri() + ); + + let mut requirements = HashMap::new(); + let version = document.version().expect("document should have version"); + for item in section.items() { + let name = item.name(); + if let Some(value) = inputs.requirement(name.as_str()) { + requirements.insert(name.as_str().to_string(), value.clone()); + continue; + } + + let mut evaluator = ExprEvaluator::new(TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, ROOT_SCOPE_INDEX), + )); + + let types = task_requirement_types(version, name.as_str()) + .expect("requirement should be known"); + + // Evaluate and coerce to the expected type + let expr = item.expr(); + let value = evaluator.evaluate_expr(&expr)?; + let value = types + .iter() + .find_map(|ty| value.coerce(self.engine.types_mut(), *ty).ok()) + .ok_or_else(|| { + multiple_type_mismatch( + self.engine.types(), + types, + name.span(), + value.ty(), + expr.span(), + ) + })?; + + requirements.insert(name.as_str().to_string(), value); + } + + Ok(requirements) + } + + /// Evaluates the hints section. + fn evaluate_hints_section( + &mut self, + document: &Document, + execution: &dyn TaskExecution, + scopes: &[Scope], + task: &Task, + section: &TaskHintsSection, + inputs: &TaskInputs, + ) -> EvaluationResult> { + debug!( + "evaluating hints section for task `{task}` in `{uri}`", + task = task.name(), + uri = document.uri() + ); + + let mut hints = HashMap::new(); + for item in section.items() { + let name = item.name(); + if let Some(value) = inputs.hint(name.as_str()) { + hints.insert(name.as_str().to_string(), value.clone()); + continue; + } + + let mut evaluator = ExprEvaluator::new( + TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, ROOT_SCOPE_INDEX), + ) + .with_task(task), + ); + + let value = evaluator.evaluate_hints_item(&name, &item.expr())?; + hints.insert(name.as_str().to_string(), value); + } + + Ok(hints) + } + + /// Evaluates the command of a task. + fn evaluate_command( + &mut self, + document: &Document, + execution: &mut dyn TaskExecution, + scopes: &[Scope], + task: &Task, + section: &CommandSection, + mapped_paths: &HashMap, + ) -> EvaluationResult { + debug!( + "evaluating command section for task `{task}` in `{uri}`", + task = task.name(), + uri = document.uri() + ); + + let mut evaluator = ExprEvaluator::new(TaskEvaluationContext::new( + self.engine, + document, + execution.work_dir(), + execution.temp_dir(), + ScopeRef::new(scopes, OUTPUT_SCOPE_INDEX), + )); + + let mut command = String::new(); + if let Some(parts) = section.strip_whitespace() { + for part in parts { + match part { + StrippedCommandPart::Text(t) => { + command.push_str(t.as_str()); + } + StrippedCommandPart::Placeholder(placeholder) => { + evaluator.evaluate_placeholder(&placeholder, &mut command, mapped_paths)?; + } + } + } + } else { + warn!( + "command for task `{task}` in `{uri}` has mixed indentation; whitespace stripping \ + was skipped", + task = task.name(), + uri = document.uri(), + ); + + let heredoc = section.is_heredoc(); + for part in section.parts() { + match part { + CommandPart::Text(t) => { + t.unescape_to(heredoc, &mut command); + } + CommandPart::Placeholder(placeholder) => { + evaluator.evaluate_placeholder(&placeholder, &mut command, mapped_paths)?; + } + } + } + } + + Ok(command) + } + + /// Evaluates a task output. + fn evaluate_output( + &mut self, + document: &Document, + scopes: &mut [Scope], + task: &Task, + decl: &Decl, + evaluated: &EvaluatedTask, + ) -> EvaluationResult<()> { + let name = decl.name(); + debug!( + "evaluating output `{name}` for task `{task}`", + name = name.as_str(), + task = task.name(), + ); + + let decl_ty = decl.ty(); + let ty = self.convert_ast_type(document, &decl_ty)?; + let mut evaluator = ExprEvaluator::new( + TaskEvaluationContext::new( + self.engine, + document, + &evaluated.work_dir, + &evaluated.temp_dir, + ScopeRef::new(scopes, TASK_SCOPE_INDEX), + ) + .with_stdout(&evaluated.stdout) + .with_stderr(&evaluated.stderr), + ); + + let expr = decl.expr().expect("outputs should have expressions"); + let value = evaluator.evaluate_expr(&expr)?; + + // First coerce the output value to the expected type + let mut value = value.coerce(self.engine.types(), ty).map_err(|e| { + runtime_type_mismatch( + self.engine.types(), + e, + ty, + decl_ty.syntax().text_range().to_span(), + value.ty(), + expr.span(), + ) + })?; + + // Finally, join the path with the working directory, checking for existence + value + .join_paths( + self.engine.types(), + &evaluated.work_dir, + true, + ty.is_optional(), + ) + .map_err(|e| missing_task_output(e, task.name(), &name))?; + + scopes[OUTPUT_SCOPE_INDEX].insert(name.as_str(), value); + Ok(()) + } + + /// Converts an AST type to an analysis type. + fn convert_ast_type( + &mut self, + document: &Document, + ty: &wdl_ast::v1::Type, + ) -> Result { + /// Used to resolve a type name from a document. + struct Resolver<'a> { + /// The engine that we'll resolve type names with. + engine: &'a mut Engine, + /// The document containing the type name to resolve. + document: &'a Document, + } + + impl TypeNameResolver for Resolver<'_> { + fn types_mut(&mut self) -> &mut Types { + self.engine.types_mut() + } + + fn resolve_type_name(&mut self, name: &Ident) -> Result { + self.engine.resolve_type_name(self.document, name) + } + } + + AstTypeConverter::new(Resolver { + engine: self.engine, + document, + }) + .convert_type(ty) + } + + /// Maps any host paths referenced by a command to a corresponding guest + /// path. + fn map_command_paths( + graph: &Graph, + index: NodeIndex, + scope: ScopeRef<'_>, + execution: &mut Box, + ) -> HashMap { + let mut mapped_paths = HashMap::new(); + for edge in graph.edges_directed(index, Direction::Incoming) { + match &graph[edge.source()] { + TaskGraphNode::Input(decl) | TaskGraphNode::Decl(decl) => { + scope + .lookup(decl.name().as_str()) + .expect("declaration should be in scope") + .visit_paths(&mut |path| { + if !mapped_paths.contains_key(path) { + if let Some(guest) = execution.map_path(Path::new(path)) { + debug!( + "host path `{path}` mapped to guest path `{guest}`", + guest = guest.display() + ); + + mapped_paths.insert( + path.to_string(), + guest + .into_os_string() + .into_string() + .expect("mapped path should be UTF-8"), + ); + } + } + }); + } + _ => continue, + } + } + + mapped_paths + } +} diff --git a/wdl-engine/src/inputs.rs b/wdl-engine/src/inputs.rs index 913c9250..30c3eee0 100644 --- a/wdl-engine/src/inputs.rs +++ b/wdl-engine/src/inputs.rs @@ -8,24 +8,55 @@ use std::path::Path; use anyhow::Context; use anyhow::Result; -use anyhow::anyhow; use anyhow::bail; use serde_json::Value as JsonValue; use wdl_analysis::document::Document; use wdl_analysis::document::Task; use wdl_analysis::document::Workflow; use wdl_analysis::types::CallKind; -use wdl_analysis::types::Coercible; +use wdl_analysis::types::Coercible as _; +use wdl_analysis::types::Type; use wdl_analysis::types::Types; use wdl_analysis::types::display_types; use wdl_analysis::types::v1::task_hint_types; use wdl_analysis::types::v1::task_requirement_types; +use crate::Coercible; use crate::Value; /// A type alias to a JSON map (object). type JsonMap = serde_json::Map; +/// Helper for replacing input paths with a path derived from joining the +/// specified path with the input path. +fn join_paths( + inputs: &mut HashMap, + types: &mut Types, + path: &Path, + ty: impl Fn(&mut Types, &str) -> Option, +) { + for (name, value) in inputs.iter_mut() { + let ty = if let Some(ty) = ty(types, name) { + ty + } else { + continue; + }; + + // Replace the value with `None` temporarily + // This is useful when this value is the only reference to shared data as this + // would prevent internal cloning + let mut replacement = std::mem::replace(value, Value::None); + if let Ok(mut v) = replacement.coerce(types, ty) { + drop(replacement); + v.join_paths(types, path, false, false) + .expect("joining should not fail"); + replacement = v; + } + + *value = replacement; + } +} + /// Represents inputs to a task. #[derive(Default, Debug, Clone)] pub struct TaskInputs { @@ -38,48 +69,66 @@ pub struct TaskInputs { } impl TaskInputs { - /// Gets the inputs to the task. - pub fn inputs(&self) -> &HashMap { - &self.inputs - } - - /// Gets the overridden requirements. - pub fn requirements(&self) -> &HashMap { - &self.requirements + /// Iterates the inputs to the task. + pub fn iter(&self) -> impl Iterator + use<'_> { + self.inputs.iter().map(|(k, v)| (k.as_str(), v)) } - /// Gets the overridden hints. - pub fn hints(&self) -> &HashMap { - &self.hints + /// Gets an input by name. + pub fn get(&self, name: &str) -> Option<&Value> { + self.inputs.get(name) } /// Sets a task input. - pub fn set_input(&mut self, name: impl Into, value: impl Into) { + pub fn set(&mut self, name: impl Into, value: impl Into) { self.inputs.insert(name.into(), value.into()); } - /// Overrides a task requirement. + /// Gets an overridden requirement by name. + pub fn requirement(&self, name: &str) -> Option<&Value> { + self.requirements.get(name) + } + + /// Overrides a requirement by name. pub fn override_requirement(&mut self, name: impl Into, value: impl Into) { self.requirements.insert(name.into(), value.into()); } - /// Overrides a task hint. + /// Gets an overridden hint by name. + pub fn hint(&self, name: &str) -> Option<&Value> { + self.hints.get(name) + } + + /// Overrides a hint by name. pub fn override_hint(&mut self, name: impl Into, value: impl Into) { self.hints.insert(name.into(), value.into()); } + /// Replaces any `File` or `Directory` input values with joining the + /// specified path with the value. + /// + /// This method will attempt to coerce matching input values to their + /// expected types. + pub fn join_paths(&mut self, types: &mut Types, document: &Document, task: &Task, path: &Path) { + join_paths(&mut self.inputs, types, path, |types, name| { + task.inputs() + .get(name) + .map(|input| types.import(document.types(), input.ty())) + }); + } + /// Validates the inputs for the given task. + /// + /// Note that this alters the inputs pub fn validate(&self, types: &mut Types, document: &Document, task: &Task) -> Result<()> { - let version = document - .version() - .ok_or_else(|| anyhow!("missing document version"))?; + let version = document.version().context("missing document version")?; // Start by validating all the specified inputs and their types for (name, value) in &self.inputs { let input = task .inputs() .get(name) - .ok_or_else(|| anyhow!("unknown input `{name}`"))?; + .with_context(|| format!("unknown input `{name}`"))?; let expected_ty = types.import(document.types(), input.ty()); let ty = value.ty(); if !ty.is_coercible_to(types, &expected_ty) { @@ -204,8 +253,8 @@ impl TaskInputs { } // The path is to an input None => { - let input = task.inputs().get(path).ok_or_else(|| { - anyhow!( + let input = task.inputs().get(path).with_context(|| { + format!( "task `{name}` does not have an input named `{path}`", name = task.name() ) @@ -253,9 +302,14 @@ pub struct WorkflowInputs { } impl WorkflowInputs { - /// Gets the inputs to the workflow. - pub fn inputs(&self) -> &HashMap { - &self.inputs + /// Iterates the inputs to the workflow. + pub fn iter(&self) -> impl Iterator + use<'_> { + self.inputs.iter().map(|(k, v)| (k.as_str(), v)) + } + + /// Gets an input by name. + pub fn get(&self, name: &str) -> Option<&Value> { + self.inputs.get(name) } /// Gets the nested call inputs. @@ -263,14 +317,34 @@ impl WorkflowInputs { &self.calls } + /// Gets the nested call inputs. + pub fn calls_mut(&mut self) -> &mut HashMap { + &mut self.calls + } + /// Sets a workflow input. - pub fn set_input(&mut self, name: impl Into, value: impl Into) { + pub fn set(&mut self, name: impl Into, value: impl Into) { self.inputs.insert(name.into(), value.into()); } - /// Sets a nested call inputs. - pub fn set_call_inputs(&mut self, name: impl Into, inputs: impl Into) { - self.calls.insert(name.into(), inputs.into()); + /// Replaces any `File` or `Directory` input values with joining the + /// specified path with the value. + /// + /// This method will attempt to coerce matching input values to their + /// expected types. + pub fn join_paths( + &mut self, + types: &mut Types, + document: &Document, + workflow: &Workflow, + path: &Path, + ) { + join_paths(&mut self.inputs, types, path, |types, name| { + workflow + .inputs() + .get(name) + .map(|input| types.import(document.types(), input.ty())) + }); } /// Validates the inputs for the given workflow. @@ -285,7 +359,7 @@ impl WorkflowInputs { let input = workflow .inputs() .get(name) - .ok_or_else(|| anyhow!("unknown input `{name}`"))?; + .with_context(|| format!("unknown input `{name}`"))?; let expected_ty = types.import(document.types(), input.ty()); let ty = value.ty(); if !ty.is_coercible_to(types, &expected_ty) { @@ -318,7 +392,7 @@ impl WorkflowInputs { let call = workflow .calls() .get(name) - .ok_or_else(|| anyhow!("unknown call `{name}`"))?; + .with_context(|| format!("unknown call `{name}`"))?; // Resolve the target document; the namespace is guaranteed to be present in the // document. @@ -339,12 +413,12 @@ impl WorkflowInputs { .task_by_name(call.name()) .expect("task should be present"); - let task_inputs = inputs.as_task_inputs().ok_or_else(|| { - anyhow!("`{name}` is a call to a task, but workflow inputs were supplied") + let task_inputs = inputs.as_task_inputs().with_context(|| { + format!("`{name}` is a call to a task, but workflow inputs were supplied") })?; task_inputs.validate(types, document, task)?; - task_inputs.inputs() + &task_inputs.inputs } CallKind::Workflow => { let workflow = document.workflow().expect("should have a workflow"); @@ -353,12 +427,12 @@ impl WorkflowInputs { call.name(), "call name does not match workflow name" ); - let workflow_inputs = inputs.as_workflow_inputs().ok_or_else(|| { - anyhow!("`{name}` is a call to a workflow, but task inputs were supplied") + let workflow_inputs = inputs.as_workflow_inputs().with_context(|| { + format!("`{name}` is a call to a workflow, but task inputs were supplied") })?; workflow_inputs.validate(types, document, workflow)?; - workflow_inputs.inputs() + &workflow_inputs.inputs } }; @@ -383,10 +457,7 @@ impl WorkflowInputs { .iter() .filter(|(n, i)| i.required() && !ty.specified().contains(*n)) { - if !inputs - .map(|i| i.inputs().contains_key(input)) - .unwrap_or(false) - { + if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) { bail!("missing required input `{input}` for call `{call}`"); } } @@ -417,8 +488,8 @@ impl WorkflowInputs { } // Resolve the call by name - let call = workflow.calls().get(name).ok_or_else(|| { - anyhow!( + let call = workflow.calls().get(name).with_context(|| { + format!( "workflow `{workflow}` does not have a call named `{name}`", workflow = workflow.name() ) @@ -482,8 +553,8 @@ impl WorkflowInputs { } } None => { - let input = workflow.inputs().get(path).ok_or_else(|| { - anyhow!( + let input = workflow.inputs().get(path).with_context(|| { + format!( "workflow `{workflow}` does not have an input named `{path}`", workflow = workflow.name() ) @@ -530,11 +601,49 @@ pub enum Inputs { } impl Inputs { - /// Gets the input values. - pub fn inputs(&self) -> &HashMap { + /// Parses a JSON inputs file from the given file path. + /// + /// The parse uses the provided document to validate the input keys within + /// the file. + /// + /// Returns `Ok(Some(_))` if the file is a non-empty inputs. + /// + /// Returns `Ok(None)` if the file contains an empty input. + pub fn parse( + types: &mut Types, + document: &Document, + path: impl AsRef, + ) -> Result> { + let path = path.as_ref(); + let file = File::open(path).with_context(|| { + format!("failed to open input file `{path}`", path = path.display()) + })?; + + // Parse the JSON (should be an object) + let reader = BufReader::new(file); + let object = mem::take( + serde_json::from_reader::<_, JsonValue>(reader) + .with_context(|| { + format!("failed to parse input file `{path}`", path = path.display()) + })? + .as_object_mut() + .with_context(|| { + format!( + "expected input file `{path}` to contain a JSON object", + path = path.display() + ) + })?, + ); + + Self::parse_object(types, document, object) + .with_context(|| format!("failed to parse input file `{path}`", path = path.display())) + } + + /// Gets an input value. + pub fn get(&self, name: &str) -> Option<&Value> { match self { - Self::Task(t) => &t.inputs, - Self::Workflow(w) => &w.inputs, + Self::Task(t) => t.inputs.get(name), + Self::Workflow(w) => w.inputs.get(name), } } @@ -577,89 +686,13 @@ impl Inputs { Self::Workflow(inputs) => Some(inputs), } } -} - -impl From for Inputs { - fn from(inputs: TaskInputs) -> Self { - Self::Task(inputs) - } -} - -impl From for Inputs { - fn from(inputs: WorkflowInputs) -> Self { - Self::Workflow(inputs) - } -} - -/// Represents a WDL JSON inputs file. -/// -/// The expected file format is described in the [WDL specification][1]. -/// -/// [1]: https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#json-input-format -pub struct InputsFile { - /// The name of the task to evaluate. - /// - /// This is `None` for workflows. - task: Option, - /// The inputs to the workflow or task. - inputs: Inputs, -} - -impl InputsFile { - /// Parses a JSON inputs file from the given file path. - /// - /// The parse uses the provided document to validate the input keys within - /// the file. - pub fn parse(types: &mut Types, document: &Document, path: impl AsRef) -> Result { - let path = path.as_ref(); - let file = File::open(path).with_context(|| { - format!("failed to open input file `{path}`", path = path.display()) - })?; - - // Parse the JSON (should be an object) - let reader = BufReader::new(file); - let object = mem::take( - serde_json::from_reader::<_, JsonValue>(reader) - .with_context(|| { - format!("failed to parse input file `{path}`", path = path.display()) - })? - .as_object_mut() - .ok_or_else(|| { - anyhow!( - "expected input file `{path}` to contain a JSON object", - path = path.display() - ) - })?, - ); - - Self::parse_object(types, document, object) - .with_context(|| format!("failed to parse input file `{path}`", path = path.display())) - } - - /// Gets the file as inputs to a task. - /// - /// Returns `None` if the inputs are to a workflow. - pub fn as_task_inputs(&self) -> Option<(&str, &TaskInputs)> { - match &self.inputs { - Inputs::Task(inputs) => { - Some((self.task.as_deref().expect("should have task name"), inputs)) - } - Inputs::Workflow(_) => None, - } - } - - /// Gets the file as inputs to a workflow. - /// - /// Returns `None` if the inputs are to a task. - pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> { - match &self.inputs { - Inputs::Task(_) => None, - Inputs::Workflow(inputs) => Some(inputs), - } - } /// Parses the root object in an input file. - fn parse_object(types: &mut Types, document: &Document, object: JsonMap) -> Result { + fn parse_object( + types: &mut Types, + document: &Document, + object: JsonMap, + ) -> Result> { // Determine the root workflow or task name let (key, name) = match object.iter().next() { Some((key, _)) => match key.split_once('.') { @@ -673,18 +706,17 @@ impl InputsFile { }, // If the object is empty, treat it as a workflow evaluation without any inputs None => { - return Ok(Self { - task: None, - inputs: Inputs::Workflow(Default::default()), - }); + return Ok(None); } }; match (document.task_by_name(name), document.workflow()) { - (Some(task), _) => Self::parse_task_inputs(types, document, task, object), - (None, Some(workflow)) if workflow.name() == name => { - Self::parse_workflow_inputs(types, document, workflow, object) - } + (Some(task), _) => Ok(Some(Self::parse_task_inputs( + types, document, task, object, + )?)), + (None, Some(workflow)) if workflow.name() == name => Ok(Some( + Self::parse_workflow_inputs(types, document, workflow, object)?, + )), _ => bail!( "invalid input key `{key}`: a task or workflow named `{name}` does not exist in \ the document" @@ -698,7 +730,7 @@ impl InputsFile { document: &Document, task: &Task, object: JsonMap, - ) -> Result { + ) -> Result<(String, Self)> { let mut inputs = TaskInputs::default(); for (key, value) in object { let value = Value::deserialize(types, value) @@ -719,10 +751,7 @@ impl InputsFile { } } - Ok(Self { - task: Some(task.name().to_string()), - inputs: Inputs::Task(inputs), - }) + Ok((task.name().to_string(), Inputs::Task(inputs))) } /// Parses the inputs for a workflow. @@ -731,7 +760,7 @@ impl InputsFile { document: &Document, workflow: &Workflow, object: JsonMap, - ) -> Result { + ) -> Result<(String, Self)> { let mut inputs = WorkflowInputs::default(); for (key, value) in object { let value = Value::deserialize(types, value) @@ -752,9 +781,18 @@ impl InputsFile { } } - Ok(Self { - task: None, - inputs: Inputs::Workflow(inputs), - }) + Ok((workflow.name().to_string(), Inputs::Workflow(inputs))) + } +} + +impl From for Inputs { + fn from(inputs: TaskInputs) -> Self { + Self::Task(inputs) + } +} + +impl From for Inputs { + fn from(inputs: WorkflowInputs) -> Self { + Self::Workflow(inputs) } } diff --git a/wdl-engine/src/lib.rs b/wdl-engine/src/lib.rs index 1e0d5e79..0e041ece 100644 --- a/wdl-engine/src/lib.rs +++ b/wdl-engine/src/lib.rs @@ -1,5 +1,6 @@ //! Execution engine for Workflow Description Language (WDL) documents. +mod backend; pub mod diagnostics; mod engine; mod eval; @@ -9,6 +10,7 @@ mod stdlib; mod units; mod value; +pub use backend::*; pub use engine::*; pub use eval::*; pub use inputs::*; diff --git a/wdl-engine/src/outputs.rs b/wdl-engine/src/outputs.rs index 1e0ff589..de7d078a 100644 --- a/wdl-engine/src/outputs.rs +++ b/wdl-engine/src/outputs.rs @@ -1,5 +1,105 @@ //! Implementation of workflow and task outputs. +use std::cmp::Ordering; + +use indexmap::IndexMap; +use wdl_analysis::types::Types; + +use crate::Scope; +use crate::Value; + /// Represents outputs of a WDL workflow or task. #[derive(Default, Debug, Clone)] -pub struct Outputs; +pub struct Outputs { + /// The name of the outputs. + /// + /// This may be set to the name of the call in a workflow or the task name + /// for a direct task execution. + name: Option, + /// The map of output name to value. + values: IndexMap, +} + +impl Outputs { + /// Constructs a new outputs collection. + pub fn new() -> Self { + Self::default() + } + + /// Sets the name of the outputs collection. + /// + /// Typically this is the name of the call in a workflow. + pub fn with_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + /// Iterates over the outputs in the collection. + pub fn iter(&self) -> impl Iterator + use<'_> { + self.values.iter().map(|(k, v)| (k.as_str(), v)) + } + + /// Gets an output of the collection by name. + /// + /// Returns `None` if an output with the given name doesn't exist. + pub fn get(&self, name: &str) -> Option<&Value> { + self.values.get(name) + } + + /// Serializes the value to the given serializer. + pub fn serialize(&self, types: &Types, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + /// Helper `Serialize` implementation for serializing element values. + struct Serialize<'a> { + /// The types collection. + types: &'a Types, + /// The value being serialized. + value: &'a Value, + } + + impl serde::Serialize for Serialize<'_> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + self.value.serialize(self.types, serializer) + } + } + + let mut s = serializer.serialize_map(Some(self.values.len()))?; + for (k, v) in &self.values { + match &self.name { + Some(prefix) => { + s.serialize_entry(&format!("{prefix}.{k}"), &Serialize { types, value: v })? + } + None => s.serialize_entry(k, &Serialize { types, value: v })?, + } + } + + s.end() + } + + /// Sorts the outputs according to a callback. + pub(crate) fn sort_by(&mut self, mut cmp: impl FnMut(&str, &str) -> Ordering) { + // We can sort unstable as none of the keys are equivalent in ordering; thus the + // resulting sort is still considered to be stable + self.values.sort_unstable_by(move |a, _, b, _| { + let ordering = cmp(a, b); + assert!(ordering != Ordering::Equal); + ordering + }); + } +} + +impl From for Outputs { + fn from(scope: Scope) -> Self { + Self { + name: None, + values: scope.into(), + } + } +} diff --git a/wdl-engine/src/stdlib.rs b/wdl-engine/src/stdlib.rs index 9716ad6b..371c299a 100644 --- a/wdl-engine/src/stdlib.rs +++ b/wdl-engine/src/stdlib.rs @@ -130,23 +130,23 @@ impl<'a> CallContext<'a> { self.context.types_mut() } - /// Gets the current working directory for the call. - pub fn cwd(&self) -> &Path { - self.context.cwd() + /// Gets the working directory for the call. + pub fn work_dir(&self) -> &Path { + self.context.work_dir() } /// Gets the temp directory for the call. - pub fn tmp(&self) -> &Path { - self.context.tmp() + pub fn temp_dir(&self) -> &Path { + self.context.temp_dir() } /// Gets the stdout value for the call. - pub fn stdout(&self) -> Option { + pub fn stdout(&self) -> Option<&Value> { self.context.stdout() } /// Gets the stderr value for the call. - pub fn stderr(&self) -> Option { + pub fn stderr(&self) -> Option<&Value> { self.context.stderr() } @@ -167,7 +167,7 @@ impl<'a> CallContext<'a> { /// Checks to see if the calculated return type equals the given type. /// /// This is only used in assertions made by the function implementations. - #[cfg(debug_assertions)] + #[allow(unused)] fn return_type_eq(&self, ty: impl Into) -> bool { self.return_type.type_eq(self.context.types(), &ty.into()) } diff --git a/wdl-engine/src/stdlib/as_map.rs b/wdl-engine/src/stdlib/as_map.rs index cd499275..75819771 100644 --- a/wdl-engine/src/stdlib/as_map.rs +++ b/wdl-engine/src/stdlib/as_map.rs @@ -1,7 +1,6 @@ //! Implements the `as_map` function from the WDL standard library. use std::fmt; -use std::sync::Arc; use indexmap::IndexMap; use wdl_ast::Diagnostic; @@ -62,7 +61,7 @@ fn as_map(context: CallContext<'_>) -> Result { .expect("argument should be an array"); let mut elements = IndexMap::with_capacity(array.len()); - for e in array.elements() { + for e in array.as_slice() { let pair = e.as_pair().expect("element should be a pair"); let key = match pair.left() { Value::None => None, @@ -79,7 +78,7 @@ fn as_map(context: CallContext<'_>) -> Result { } } - Ok(Map::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Map::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `as_map`. @@ -118,7 +117,6 @@ mod test { let elements: Vec<_> = value .as_map() .unwrap() - .elements() .iter() .map(|(k, v)| { ( @@ -134,7 +132,6 @@ mod test { let elements: Vec<_> = value .as_map() .unwrap() - .elements() .iter() .map(|(k, v)| { ( @@ -150,7 +147,6 @@ mod test { let elements: Vec<_> = value .as_map() .unwrap() - .elements() .iter() .map(|(k, v)| { ( @@ -170,7 +166,6 @@ mod test { let elements: Vec<_> = value .as_map() .unwrap() - .elements() .iter() .map(|(k, v)| { ( diff --git a/wdl-engine/src/stdlib/as_pairs.rs b/wdl-engine/src/stdlib/as_pairs.rs index bf598322..b97205af 100644 --- a/wdl-engine/src/stdlib/as_pairs.rs +++ b/wdl-engine/src/stdlib/as_pairs.rs @@ -1,7 +1,5 @@ //! Implements the `as_pairs` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -39,14 +37,11 @@ fn as_pairs(context: CallContext<'_>) -> Result { .element_type(); let elements = map - .elements() .iter() - .map(|(k, v)| { - Pair::new_unchecked(element_ty, Arc::new(k.clone().into()), Arc::new(v.clone())).into() - }) + .map(|(k, v)| Pair::new_unchecked(element_ty, k.clone().into(), v.clone()).into()) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `as_pairs`. @@ -87,7 +82,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { let pair = v.as_pair().unwrap(); @@ -103,7 +98,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { let pair = v.as_pair().unwrap(); @@ -119,7 +114,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { let pair = v.as_pair().unwrap(); diff --git a/wdl-engine/src/stdlib/chunk.rs b/wdl-engine/src/stdlib/chunk.rs index 083ae087..350b0aff 100644 --- a/wdl-engine/src/stdlib/chunk.rs +++ b/wdl-engine/src/stdlib/chunk.rs @@ -1,7 +1,5 @@ //! Implements the `chunk` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -53,14 +51,12 @@ fn chunk(context: CallContext<'_>) -> Result { .element_type(); let elements = array - .elements() + .as_slice() .chunks(size as usize) - .map(|chunk| { - Array::new_unchecked(element_ty, Arc::new(Vec::from_iter(chunk.iter().cloned()))).into() - }) + .map(|chunk| Array::new_unchecked(element_ty, Vec::from_iter(chunk.iter().cloned())).into()) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `chunk`. @@ -87,12 +83,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -104,12 +100,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -121,12 +117,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -138,12 +134,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -155,12 +151,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -172,12 +168,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() diff --git a/wdl-engine/src/stdlib/collect_by_key.rs b/wdl-engine/src/stdlib/collect_by_key.rs index da0f49ec..79fbf11a 100644 --- a/wdl-engine/src/stdlib/collect_by_key.rs +++ b/wdl-engine/src/stdlib/collect_by_key.rs @@ -1,7 +1,5 @@ //! Implements the `collect_by_key` function from the WDL standard library. -use std::sync::Arc; - use indexmap::IndexMap; use wdl_ast::Diagnostic; @@ -63,7 +61,7 @@ fn collect_by_key(context: CallContext<'_>) -> Result { // Start by collecting duplicate keys into a `Vec` let mut map: IndexMap<_, Vec<_>> = IndexMap::new(); - for v in array.elements() { + for v in array.as_slice() { let pair = v.as_pair().expect("value should be a pair"); map.entry(match pair.left() { Value::None => None, @@ -77,15 +75,10 @@ fn collect_by_key(context: CallContext<'_>) -> Result { // Transform each `Vec` into an array value let elements = map .into_iter() - .map(|(k, v)| { - ( - k, - Array::new_unchecked(map_ty.value_type(), Arc::new(v)).into(), - ) - }) + .map(|(k, v)| (k, Array::new_unchecked(map_ty.value_type(), v).into())) .collect(); - Ok(Map::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Map::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `collect_by_key`. diff --git a/wdl-engine/src/stdlib/contains.rs b/wdl-engine/src/stdlib/contains.rs index 7e286994..3b6b851c 100644 --- a/wdl-engine/src/stdlib/contains.rs +++ b/wdl-engine/src/stdlib/contains.rs @@ -24,7 +24,7 @@ fn contains(context: CallContext<'_>) -> Result { let item = &context.arguments[1].value; Ok(array - .elements() + .as_slice() .iter() .any(|e| Value::equals(context.types(), e, item).unwrap_or(false)) .into()) diff --git a/wdl-engine/src/stdlib/contains_key.rs b/wdl-engine/src/stdlib/contains_key.rs index 3cc639e8..7afa39fa 100644 --- a/wdl-engine/src/stdlib/contains_key.rs +++ b/wdl-engine/src/stdlib/contains_key.rs @@ -11,7 +11,6 @@ use super::CallContext; use super::Function; use super::Signature; use crate::CompoundValue; -use crate::Object; use crate::PrimitiveValue; use crate::Struct; use crate::Value; @@ -39,7 +38,7 @@ fn contains_key_map(context: CallContext<'_>) -> Result { _ => unreachable!("expected a primitive value for second argument"), }; - Ok(map.elements().contains_key(&key).into()) + Ok(map.contains_key(&key).into()) } /// Given an object and a key, tests whether the object contains an entry with @@ -61,10 +60,7 @@ fn contains_key_object(context: CallContext<'_>) -> Result { let object = context.coerce_argument(0, Type::Object).unwrap_object(); let key = context.coerce_argument(1, PrimitiveTypeKind::String); - Ok(object - .members() - .contains_key(key.unwrap_string().as_str()) - .into()) + Ok(object.contains_key(key.unwrap_string().as_str()).into()) } /// Given a key-value type collection (Map, Struct, or Object) and a key, tests @@ -82,12 +78,11 @@ fn contains_key_recursive(context: CallContext<'_>) -> Result /// key. fn get(value: &Value, key: &Arc) -> Option { match value { - Value::Compound(CompoundValue::Map(map)) => map - .elements() - .get(&Some(PrimitiveValue::String(key.clone()))) - .cloned(), - Value::Compound(CompoundValue::Object(Object { members, .. })) - | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => { + Value::Compound(CompoundValue::Map(map)) => { + map.get(&Some(PrimitiveValue::String(key.clone()))).cloned() + } + Value::Compound(CompoundValue::Object(object)) => object.get(key.as_str()).cloned(), + Value::Compound(CompoundValue::Struct(Struct { members, .. })) => { members.get(key.as_str()).cloned() } _ => None, @@ -100,7 +95,7 @@ fn contains_key_recursive(context: CallContext<'_>) -> Result .unwrap_array(); for key in keys - .elements() + .as_slice() .iter() .map(|v| v.as_string().expect("element should be a string")) { diff --git a/wdl-engine/src/stdlib/cross.rs b/wdl-engine/src/stdlib/cross.rs index 80361326..cd76b31a 100644 --- a/wdl-engine/src/stdlib/cross.rs +++ b/wdl-engine/src/stdlib/cross.rs @@ -1,7 +1,5 @@ //! Implements the `cross` function from the WDL standard library. -use std::sync::Arc; - use itertools::Itertools; use wdl_ast::Diagnostic; @@ -64,14 +62,12 @@ fn cross(context: CallContext<'_>) -> Result { .element_type(); let elements = left - .elements() + .as_slice() .iter() - .cartesian_product(right.elements().iter()) - .map(|(l, r)| { - Pair::new_unchecked(element_ty, Arc::new(l.clone()), Arc::new(r.clone())).into() - }) + .cartesian_product(right.as_slice().iter()) + .map(|(l, r)| Pair::new_unchecked(element_ty, l.clone(), r.clone()).into()) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `cross`. @@ -111,7 +107,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { let p = v.as_pair().unwrap(); diff --git a/wdl-engine/src/stdlib/flatten.rs b/wdl-engine/src/stdlib/flatten.rs index 7343a71b..45931372 100644 --- a/wdl-engine/src/stdlib/flatten.rs +++ b/wdl-engine/src/stdlib/flatten.rs @@ -1,7 +1,5 @@ //! Implements the `flatten` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -42,18 +40,18 @@ fn flatten(context: CallContext<'_>) -> Result { .expect("argument should be an array"); let elements = array - .elements() + .as_slice() .iter() .flat_map(|v| { v.as_array() .expect("array element should be an array") - .elements() + .as_slice() .iter() .cloned() }) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `flatten`. @@ -88,7 +86,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -103,7 +101,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); diff --git a/wdl-engine/src/stdlib/glob.rs b/wdl-engine/src/stdlib/glob.rs index 2fcc8533..2bfb28d7 100644 --- a/wdl-engine/src/stdlib/glob.rs +++ b/wdl-engine/src/stdlib/glob.rs @@ -1,7 +1,6 @@ //! Implements the `glob` function from the WDL standard library. use std::path::Path; -use std::sync::Arc; use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_analysis::types::PrimitiveTypeKind; @@ -28,8 +27,9 @@ fn glob(context: CallContext<'_>) -> Result { .coerce_argument(0, PrimitiveTypeKind::String) .unwrap_string(); + // TODO: replace glob with walkpath and globmatch let mut elements: Vec = Vec::new(); - for path in glob::glob(&context.cwd().join(path.as_str()).to_string_lossy()) + for path in glob::glob(&context.work_dir().join(path.as_str()).to_string_lossy()) .map_err(|e| invalid_glob_pattern(&e, context.arguments[0].span))? { let path = path.map_err(|e| function_call_failed("glob", &e, context.call_site))?; @@ -40,7 +40,7 @@ fn glob(context: CallContext<'_>) -> Result { } // Strip the CWD prefix if there is one - let path = match path.strip_prefix(context.cwd()) { + let path = match path.strip_prefix(context.work_dir()) { Ok(path) => { // Create a string from the stripped path path.to_str() @@ -74,7 +74,7 @@ fn glob(context: CallContext<'_>) -> Result { elements.push(PrimitiveValue::new_file(path).into()); } - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `glob`. @@ -105,7 +105,7 @@ mod test { env.write_file("baz", "baz"); env.write_file("foo", "foo"); env.write_file("bar", "bar"); - fs::create_dir_all(env.cwd().join("nested")).expect("failed to create directory"); + fs::create_dir_all(env.work_dir().join("nested")).expect("failed to create directory"); env.write_file("nested/bar", "bar"); env.write_file("nested/baz", "baz"); @@ -113,7 +113,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_file().unwrap().as_str()) .collect(); @@ -123,7 +123,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_file().unwrap().as_str()) .collect(); @@ -133,7 +133,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_file().unwrap().as_str()) .collect(); @@ -143,7 +143,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_file().unwrap().as_str()) .collect(); @@ -153,7 +153,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_file().unwrap().as_str().replace('\\', "/")) .collect(); diff --git a/wdl-engine/src/stdlib/join_paths.rs b/wdl-engine/src/stdlib/join_paths.rs index 824ab147..52dd6389 100644 --- a/wdl-engine/src/stdlib/join_paths.rs +++ b/wdl-engine/src/stdlib/join_paths.rs @@ -80,7 +80,7 @@ fn join_paths(context: CallContext<'_>) -> Result { .unwrap_array(); ( - array.elements()[0].clone().unwrap_string(), + array.as_slice()[0].clone().unwrap_string(), array, true, context.arguments[0].span, @@ -100,7 +100,7 @@ fn join_paths(context: CallContext<'_>) -> Result { let mut path = PathBuf::from(Arc::unwrap_or_clone(first)); for (i, element) in array - .elements() + .as_slice() .iter() .enumerate() .skip(if skip { 1 } else { 0 }) diff --git a/wdl-engine/src/stdlib/keys.rs b/wdl-engine/src/stdlib/keys.rs index f2f2ecd2..2beb50fb 100644 --- a/wdl-engine/src/stdlib/keys.rs +++ b/wdl-engine/src/stdlib/keys.rs @@ -1,7 +1,5 @@ //! Implements the `keys` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -9,7 +7,6 @@ use super::Function; use super::Signature; use crate::Array; use crate::CompoundValue; -use crate::Object; use crate::PrimitiveValue; use crate::Struct; use crate::Value; @@ -44,18 +41,19 @@ fn keys(context: CallContext<'_>) -> Result { ); let elements = match &context.arguments[0].value { - Value::Compound(CompoundValue::Map(map)) => { - map.elements().keys().map(|k| k.clone().into()).collect() - } - Value::Compound(CompoundValue::Object(Object { members, .. })) - | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => members + Value::Compound(CompoundValue::Map(map)) => map.keys().map(|k| k.clone().into()).collect(), + Value::Compound(CompoundValue::Object(object)) => object + .keys() + .map(|k| PrimitiveValue::new_string(k).into()) + .collect(), + Value::Compound(CompoundValue::Struct(Struct { members, .. })) => members .keys() .map(|k| PrimitiveValue::new_string(k).into()) .collect(), _ => unreachable!("expected a map, object, or struct"), }; - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `keys`. @@ -105,7 +103,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -115,7 +113,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => None, @@ -130,7 +128,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -141,7 +139,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/length.rs b/wdl-engine/src/stdlib/length.rs index 208ad83c..67e3d7fc 100644 --- a/wdl-engine/src/stdlib/length.rs +++ b/wdl-engine/src/stdlib/length.rs @@ -70,7 +70,7 @@ fn object_length(context: CallContext<'_>) -> Result { debug_assert!(context.return_type_eq(PrimitiveTypeKind::Integer)); let object = context.coerce_argument(0, Type::Object).unwrap_object(); - Ok(i64::try_from(object.members.len()) + Ok(i64::try_from(object.len()) .map_err(|_| { function_call_failed( "length", diff --git a/wdl-engine/src/stdlib/prefix.rs b/wdl-engine/src/stdlib/prefix.rs index cde7c985..d033c214 100644 --- a/wdl-engine/src/stdlib/prefix.rs +++ b/wdl-engine/src/stdlib/prefix.rs @@ -1,7 +1,5 @@ //! Implements the `prefix` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_analysis::types::PrimitiveTypeKind; use wdl_ast::Diagnostic; @@ -33,7 +31,7 @@ fn prefix(context: CallContext<'_>) -> Result { .expect("value should be an array"); let elements = array - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => PrimitiveValue::String(prefix.clone()).into(), @@ -44,7 +42,7 @@ fn prefix(context: CallContext<'_>) -> Result { }) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `prefix`. @@ -74,7 +72,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -84,18 +82,18 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); - assert_eq!(elements, ["foo1.0", "foo1.1", "foo1.2"]); + assert_eq!(elements, ["foo1.000000", "foo1.100000", "foo1.200000"]); let value = eval_v1_expr(&mut env, V1::Zero, "prefix('foo', ['bar', 'baz', 'qux'])").unwrap(); let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -105,7 +103,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/quote.rs b/wdl-engine/src/stdlib/quote.rs index c9ffd16f..b6d7810b 100644 --- a/wdl-engine/src/stdlib/quote.rs +++ b/wdl-engine/src/stdlib/quote.rs @@ -1,7 +1,5 @@ //! Implements the `quote` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_ast::Diagnostic; @@ -28,7 +26,7 @@ fn quote(context: CallContext<'_>) -> Result { .expect("value should be an array"); let elements = array - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => PrimitiveValue::new_string("\"\"").into(), @@ -39,7 +37,7 @@ fn quote(context: CallContext<'_>) -> Result { }) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `quote`. @@ -69,7 +67,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -79,17 +77,21 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); - assert_eq!(elements, [r#""1.0""#, r#""1.1""#, r#""1.2""#]); + assert_eq!(elements, [ + r#""1.000000""#, + r#""1.100000""#, + r#""1.200000""# + ]); let value = eval_v1_expr(&mut env, V1::One, "quote(['bar', 'baz', 'qux'])").unwrap(); let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -99,7 +101,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/range.rs b/wdl-engine/src/stdlib/range.rs index c628acab..df6babc0 100644 --- a/wdl-engine/src/stdlib/range.rs +++ b/wdl-engine/src/stdlib/range.rs @@ -1,7 +1,5 @@ //! Implements the `range` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_analysis::types::PrimitiveTypeKind; use wdl_ast::Diagnostic; @@ -35,11 +33,7 @@ fn range(context: CallContext<'_>) -> Result { )); } - Ok(Array::new_unchecked( - context.return_type, - Arc::new((0..n).map(Into::into).collect()), - ) - .into()) + Ok(Array::new_unchecked(context.return_type, (0..n).map(Into::into).collect()).into()) } /// Gets the function describing `range`. @@ -65,7 +59,7 @@ mod test { assert_eq!( value .unwrap_array() - .elements() + .as_slice() .iter() .cloned() .map(|v| v.unwrap_integer()) diff --git a/wdl-engine/src/stdlib/read_boolean.rs b/wdl-engine/src/stdlib/read_boolean.rs index e7c75d5d..828a712a 100644 --- a/wdl-engine/src/stdlib/read_boolean.rs +++ b/wdl-engine/src/stdlib/read_boolean.rs @@ -25,7 +25,7 @@ fn read_boolean(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(PrimitiveTypeKind::Boolean)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() diff --git a/wdl-engine/src/stdlib/read_float.rs b/wdl-engine/src/stdlib/read_float.rs index 4d984079..031efdf2 100644 --- a/wdl-engine/src/stdlib/read_float.rs +++ b/wdl-engine/src/stdlib/read_float.rs @@ -24,7 +24,7 @@ fn read_float(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(PrimitiveTypeKind::Float)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() diff --git a/wdl-engine/src/stdlib/read_int.rs b/wdl-engine/src/stdlib/read_int.rs index ea13d3bb..cec76a72 100644 --- a/wdl-engine/src/stdlib/read_int.rs +++ b/wdl-engine/src/stdlib/read_int.rs @@ -24,7 +24,7 @@ fn read_int(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(PrimitiveTypeKind::Integer)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() diff --git a/wdl-engine/src/stdlib/read_json.rs b/wdl-engine/src/stdlib/read_json.rs index 48f30f94..c2174ef8 100644 --- a/wdl-engine/src/stdlib/read_json.rs +++ b/wdl-engine/src/stdlib/read_json.rs @@ -22,7 +22,7 @@ fn read_json(mut context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(Type::Union)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -124,7 +124,7 @@ mod test { assert_eq!( value .unwrap_array() - .elements() + .as_slice() .iter() .cloned() .map(Value::unwrap_integer) @@ -148,13 +148,18 @@ mod test { let value = eval_v1_expr(&mut env, V1::One, "read_json('object.json')") .unwrap() .unwrap_object(); - assert_eq!(value.members()["foo"].as_string().unwrap().as_str(), "bar"); - assert_eq!(value.members()["bar"].as_integer().unwrap(), 12345); assert_eq!( - value.members()["baz"] + value.get("foo").unwrap().as_string().unwrap().as_str(), + "bar" + ); + assert_eq!(value.get("bar").unwrap().as_integer().unwrap(), 12345); + assert_eq!( + value + .get("baz") + .unwrap() .as_array() .unwrap() - .elements() + .as_slice() .iter() .cloned() .map(Value::unwrap_integer) diff --git a/wdl-engine/src/stdlib/read_lines.rs b/wdl-engine/src/stdlib/read_lines.rs index 6542052d..d46bb29f 100644 --- a/wdl-engine/src/stdlib/read_lines.rs +++ b/wdl-engine/src/stdlib/read_lines.rs @@ -3,7 +3,6 @@ use std::fs; use std::io::BufRead; use std::io::BufReader; -use std::sync::Arc; use anyhow::Context; use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; @@ -33,7 +32,7 @@ fn read_lines(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(ANALYSIS_STDLIB.array_string_type())); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -59,7 +58,7 @@ fn read_lines(context: CallContext<'_>) -> Result { }) .collect::, _>>()?; - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `read_lines`. @@ -95,7 +94,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -105,7 +104,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/read_map.rs b/wdl-engine/src/stdlib/read_map.rs index b48b140c..685ef5e1 100644 --- a/wdl-engine/src/stdlib/read_map.rs +++ b/wdl-engine/src/stdlib/read_map.rs @@ -3,7 +3,6 @@ use std::fs; use std::io::BufRead; use std::io::BufReader; -use std::sync::Arc; use anyhow::Context; use indexmap::IndexMap; @@ -37,7 +36,7 @@ fn read_map(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(ANALYSIS_STDLIB.map_string_string_type())); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -87,7 +86,7 @@ fn read_map(context: CallContext<'_>) -> Result { } } - Ok(Map::new_unchecked(ANALYSIS_STDLIB.map_string_string_type(), Arc::new(map)).into()) + Ok(Map::new_unchecked(ANALYSIS_STDLIB.map_string_string_type(), map).into()) } /// Gets the function describing `read_map`. diff --git a/wdl-engine/src/stdlib/read_object.rs b/wdl-engine/src/stdlib/read_object.rs index b075e000..ed586aaf 100644 --- a/wdl-engine/src/stdlib/read_object.rs +++ b/wdl-engine/src/stdlib/read_object.rs @@ -40,7 +40,7 @@ fn read_object(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(Type::Object)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() diff --git a/wdl-engine/src/stdlib/read_objects.rs b/wdl-engine/src/stdlib/read_objects.rs index 7707df1e..67e3416e 100644 --- a/wdl-engine/src/stdlib/read_objects.rs +++ b/wdl-engine/src/stdlib/read_objects.rs @@ -3,7 +3,6 @@ use std::fs; use std::io::BufRead; use std::io::BufReader; -use std::sync::Arc; use anyhow::Context; use indexmap::IndexMap; @@ -45,7 +44,7 @@ fn read_objects(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(ANALYSIS_STDLIB.array_object_type())); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -64,11 +63,9 @@ fn read_objects(context: CallContext<'_>) -> Result { function_call_failed("read_objects", format!("{e:?}"), context.call_site) })?, None => { - return Ok(Array::new_unchecked( - ANALYSIS_STDLIB.array_object_type(), - Arc::new(Vec::new()), - ) - .into()); + return Ok( + Array::new_unchecked(ANALYSIS_STDLIB.array_object_type(), Vec::new()).into(), + ); } }; @@ -130,7 +127,7 @@ fn read_objects(context: CallContext<'_>) -> Result { objects.push(Object::from(members).into()); } - Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_object_type(), Arc::new(objects)).into()) + Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_object_type(), objects).into()) } /// Gets the function describing `read_objects`. diff --git a/wdl-engine/src/stdlib/read_string.rs b/wdl-engine/src/stdlib/read_string.rs index 55034be5..cb56490d 100644 --- a/wdl-engine/src/stdlib/read_string.rs +++ b/wdl-engine/src/stdlib/read_string.rs @@ -23,7 +23,7 @@ fn read_string(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(PrimitiveTypeKind::String)); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -58,7 +58,12 @@ mod test { env.write_file("foo", "hello\nworld!\n\r\n"); env.insert_name( "file", - PrimitiveValue::new_file(env.cwd().join("foo").to_str().expect("should be UTF-8")), + PrimitiveValue::new_file( + env.work_dir() + .join("foo") + .to_str() + .expect("should be UTF-8"), + ), ); let diagnostic = diff --git a/wdl-engine/src/stdlib/read_tsv.rs b/wdl-engine/src/stdlib/read_tsv.rs index 23697103..115c7cc4 100644 --- a/wdl-engine/src/stdlib/read_tsv.rs +++ b/wdl-engine/src/stdlib/read_tsv.rs @@ -3,7 +3,6 @@ use std::fs; use std::io::BufRead; use std::io::BufReader; -use std::sync::Arc; use anyhow::Context; use indexmap::IndexMap; @@ -40,7 +39,7 @@ impl TsvHeader { /// Panics if a specified header contains a value that is not a string. pub fn columns(&self) -> impl Iterator { match self { - Self::Specified(array) => Either::Left(array.elements().iter().map(|v| { + Self::Specified(array) => Either::Left(array.as_slice().iter().map(|v| { v.as_string() .expect("header value must be a string") .as_str() @@ -64,7 +63,7 @@ fn read_tsv_simple(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() == 1); debug_assert!(context.return_type_eq(ANALYSIS_STDLIB.array_array_string_type())); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -84,12 +83,10 @@ fn read_tsv_simple(context: CallContext<'_>) -> Result { .split('\t') .map(|s| PrimitiveValue::new_string(s).into()) .collect::>(); - rows.push( - Array::new_unchecked(ANALYSIS_STDLIB.array_string_type(), Arc::new(values)).into(), - ); + rows.push(Array::new_unchecked(ANALYSIS_STDLIB.array_string_type(), values).into()); } - Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_array_string_type(), Arc::new(rows)).into()) + Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_array_string_type(), rows).into()) } /// Reads a tab-separated value (TSV) file as an Array[Object] representing a @@ -116,7 +113,7 @@ fn read_tsv(context: CallContext<'_>) -> Result { debug_assert!(context.arguments.len() >= 2 && context.arguments.len() <= 3); debug_assert!(context.return_type_eq(ANALYSIS_STDLIB.array_object_type())); - let path = context.cwd().join( + let path = context.work_dir().join( context .coerce_argument(0, PrimitiveTypeKind::File) .unwrap_file() @@ -228,7 +225,7 @@ fn read_tsv(context: CallContext<'_>) -> Result { rows.push(CompoundValue::Object(members.into()).into()); } - Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_object_type(), Arc::new(rows)).into()) + Ok(Array::new_unchecked(ANALYSIS_STDLIB.array_object_type(), rows).into()) } /// Gets the function describing `read_tsv`. @@ -299,12 +296,12 @@ mod test { let elements = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect::>() @@ -320,14 +317,13 @@ mod test { let elements = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_object() .unwrap() - .members() .iter() - .map(|(k, v)| (k.as_str(), v.as_string().unwrap().as_str())) + .map(|(k, v)| (k, v.as_string().unwrap().as_str())) .collect::>() }) .collect::>(); @@ -346,14 +342,13 @@ mod test { let elements = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_object() .unwrap() - .members() .iter() - .map(|(k, v)| (k.as_str(), v.as_string().unwrap().as_str())) + .map(|(k, v)| (k, v.as_string().unwrap().as_str())) .collect::>() }) .collect::>(); @@ -398,14 +393,13 @@ mod test { let elements = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_object() .unwrap() - .members() .iter() - .map(|(k, v)| (k.as_str(), v.as_string().unwrap().as_str())) + .map(|(k, v)| (k, v.as_string().unwrap().as_str())) .collect::>() }) .collect::>(); diff --git a/wdl-engine/src/stdlib/select_all.rs b/wdl-engine/src/stdlib/select_all.rs index e1643ea8..a3494941 100644 --- a/wdl-engine/src/stdlib/select_all.rs +++ b/wdl-engine/src/stdlib/select_all.rs @@ -1,7 +1,5 @@ //! Implements the `select_all` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -40,12 +38,12 @@ fn select_all(context: CallContext<'_>) -> Result { .expect("argument should be an array"); let elements = array - .elements() + .as_slice() .iter() .filter(|v| !v.is_none()) .cloned() .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `select_all`. @@ -75,7 +73,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -85,7 +83,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -95,7 +93,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -105,7 +103,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); diff --git a/wdl-engine/src/stdlib/select_first.rs b/wdl-engine/src/stdlib/select_first.rs index 20891367..395edbd9 100644 --- a/wdl-engine/src/stdlib/select_first.rs +++ b/wdl-engine/src/stdlib/select_first.rs @@ -34,8 +34,8 @@ fn select_first(context: CallContext<'_>) -> Result { )); } - match array.elements().iter().find(|v| !v.is_none()) { - Some(v) => Ok(v.clone()), + match array.as_slice().iter().find(|v| !v.is_none()) { + Some(v) => Ok(v.clone_as_required()), None => { if context.arguments.len() < 2 { return Err(function_call_failed( @@ -45,7 +45,7 @@ fn select_first(context: CallContext<'_>) -> Result { )); } - Ok(context.arguments[1].value.clone()) + Ok(context.arguments[1].value.clone_as_required()) } } } diff --git a/wdl-engine/src/stdlib/sep.rs b/wdl-engine/src/stdlib/sep.rs index 8dadac11..2397df0f 100644 --- a/wdl-engine/src/stdlib/sep.rs +++ b/wdl-engine/src/stdlib/sep.rs @@ -36,7 +36,7 @@ fn sep(context: CallContext<'_>) -> Result { .expect("value should be an array"); let s = array - .elements() + .as_slice() .iter() .enumerate() .fold(String::new(), |mut s, (i, v)| { diff --git a/wdl-engine/src/stdlib/size.rs b/wdl-engine/src/stdlib/size.rs index 3be75862..79af3e7a 100644 --- a/wdl-engine/src/stdlib/size.rs +++ b/wdl-engine/src/stdlib/size.rs @@ -45,7 +45,7 @@ fn size(context: CallContext<'_>) -> Result { // If the first argument is a string, we need to check if it's a file or // directory and treat it as such. let value = if let Some(s) = context.arguments[0].value.as_string() { - let path = context.cwd().join(s.as_str()); + let path = context.work_dir().join(s.as_str()); let metadata = path .metadata() .with_context(|| { @@ -64,7 +64,7 @@ fn size(context: CallContext<'_>) -> Result { context.arguments[0].value.clone() }; - calculate_disk_size(&value, unit, context.cwd()) + calculate_disk_size(&value, unit, context.work_dir()) .map_err(|e| function_call_failed("size", format!("{e:?}"), context.call_site)) .map(Into::into) } @@ -81,6 +81,10 @@ fn calculate_disk_size(value: &Value, unit: StorageUnit, cwd: &Path) -> Result Ok(0.0), Value::Primitive(v) => primitive_disk_size(v, unit, cwd), Value::Compound(v) => compound_disk_size(v, unit, cwd), + Value::Task(_) => bail!("the size of a task variable cannot be calculated"), + Value::Hints(_) => bail!("the size of a hints value cannot be calculated"), + Value::Input(_) => bail!("the size of an input value cannot be calculated"), + Value::Output(_) => bail!("the size of an output value cannot be calculated"), } } @@ -100,7 +104,7 @@ fn primitive_disk_size(value: &PrimitiveValue, unit: StorageUnit, cwd: &Path) -> bail!("path `{path}` is not a file", path = path.display()); } - Ok(unit.convert(metadata.len())) + Ok(unit.units(metadata.len())) } PrimitiveValue::Directory(path) => calculate_directory_size(&cwd.join(path.as_str()), unit), _ => Ok(0.0), @@ -112,10 +116,10 @@ fn compound_disk_size(value: &CompoundValue, unit: StorageUnit, cwd: &Path) -> R match value { CompoundValue::Pair(pair) => Ok(calculate_disk_size(pair.left(), unit, cwd)? + calculate_disk_size(pair.right(), unit, cwd)?), - CompoundValue::Array(array) => Ok(array.elements().iter().try_fold(0.0, |t, e| { + CompoundValue::Array(array) => Ok(array.as_slice().iter().try_fold(0.0, |t, e| { anyhow::Ok(t + calculate_disk_size(e, unit, cwd)?) })?), - CompoundValue::Map(map) => Ok(map.elements().iter().try_fold(0.0, |t, (k, v)| { + CompoundValue::Map(map) => Ok(map.iter().try_fold(0.0, |t, (k, v)| { anyhow::Ok( t + match k { Some(k) => primitive_disk_size(k, unit, cwd)?, @@ -123,12 +127,10 @@ fn compound_disk_size(value: &CompoundValue, unit: StorageUnit, cwd: &Path) -> R } + calculate_disk_size(v, unit, cwd)?, ) })?), - CompoundValue::Object(object) => { - Ok(object.members().iter().try_fold(0.0, |t, (_, v)| { - anyhow::Ok(t + calculate_disk_size(v, unit, cwd)?) - })?) - } - CompoundValue::Struct(s) => Ok(s.members().iter().try_fold(0.0, |t, (_, v)| { + CompoundValue::Object(object) => Ok(object.iter().try_fold(0.0, |t, (_, v)| { + anyhow::Ok(t + calculate_disk_size(v, unit, cwd)?) + })?), + CompoundValue::Struct(s) => Ok(s.iter().try_fold(0.0, |t, (_, v)| { anyhow::Ok(t + calculate_disk_size(v, unit, cwd)?) })?), } @@ -173,7 +175,7 @@ fn calculate_directory_size(path: &Path, unit: StorageUnit) -> Result { if metadata.is_dir() { queue.push(entry.path().into()); } else { - size += unit.convert(metadata.len()); + size += unit.units(metadata.len()); } } } @@ -222,11 +224,16 @@ mod test { env.insert_name( "file", - PrimitiveValue::new_file(env.cwd().join("bar").to_str().expect("should be UTF-8")), + PrimitiveValue::new_file( + env.work_dir() + .join("bar") + .to_str() + .expect("should be UTF-8"), + ), ); env.insert_name( "dir", - PrimitiveValue::new_directory(env.cwd().to_str().expect("should be UTF-8")), + PrimitiveValue::new_directory(env.work_dir().to_str().expect("should be UTF-8")), ); let diagnostic = eval_v1_expr(&mut env, V1::Two, "size('foo', 'invalid')").unwrap_err(); @@ -244,7 +251,7 @@ mod test { .starts_with("call to function `size` failed: failed to read metadata for file") ); - let source = format!("size('{path}', 'B')", path = env.cwd().display()); + let source = format!("size('{path}', 'B')", path = env.work_dir().display()); let value = eval_v1_expr(&mut env, V1::Two, &source).unwrap(); approx::assert_relative_eq!(value.unwrap_float(), 60.0); diff --git a/wdl-engine/src/stdlib/squote.rs b/wdl-engine/src/stdlib/squote.rs index 07c8fa74..30bbcaf4 100644 --- a/wdl-engine/src/stdlib/squote.rs +++ b/wdl-engine/src/stdlib/squote.rs @@ -1,7 +1,5 @@ //! Implements the `squote` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_ast::Diagnostic; @@ -28,7 +26,7 @@ fn squote(context: CallContext<'_>) -> Result { .expect("value should be an array"); let elements = array - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => PrimitiveValue::new_string("''").into(), @@ -37,7 +35,7 @@ fn squote(context: CallContext<'_>) -> Result { }) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `squote`. @@ -67,7 +65,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -77,17 +75,17 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); - assert_eq!(elements, ["'1.0'", "'1.1'", "'1.2'"]); + assert_eq!(elements, ["'1.000000'", "'1.100000'", "'1.200000'"]); let value = eval_v1_expr(&mut env, V1::One, "squote(['bar', 'baz', 'qux'])").unwrap(); let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -97,7 +95,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/stderr.rs b/wdl-engine/src/stdlib/stderr.rs index 09fa8ce3..3e823201 100644 --- a/wdl-engine/src/stdlib/stderr.rs +++ b/wdl-engine/src/stdlib/stderr.rs @@ -23,7 +23,7 @@ fn stderr(context: CallContext<'_>) -> Result { stderr.as_file().is_some(), "expected the value to be a file" ); - Ok(stderr) + Ok(stderr.clone()) } None => Err(function_call_failed( "stderr", diff --git a/wdl-engine/src/stdlib/stdout.rs b/wdl-engine/src/stdlib/stdout.rs index 27afc403..040c503e 100644 --- a/wdl-engine/src/stdlib/stdout.rs +++ b/wdl-engine/src/stdlib/stdout.rs @@ -23,7 +23,7 @@ fn stdout(context: CallContext<'_>) -> Result { stdout.as_file().is_some(), "expected the value to be a file" ); - Ok(stdout) + Ok(stdout.clone()) } None => Err(function_call_failed( "stdout", diff --git a/wdl-engine/src/stdlib/suffix.rs b/wdl-engine/src/stdlib/suffix.rs index 46b0ee54..92ea3df4 100644 --- a/wdl-engine/src/stdlib/suffix.rs +++ b/wdl-engine/src/stdlib/suffix.rs @@ -1,7 +1,5 @@ //! Implements the `suffix` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_analysis::types::PrimitiveTypeKind; use wdl_ast::Diagnostic; @@ -33,7 +31,7 @@ fn suffix(context: CallContext<'_>) -> Result { .expect("value should be an array"); let elements = array - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => PrimitiveValue::String(suffix.clone()).into(), @@ -44,7 +42,7 @@ fn suffix(context: CallContext<'_>) -> Result { }) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `suffix`. @@ -74,7 +72,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -84,18 +82,18 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); - assert_eq!(elements, ["1.0foo", "1.1foo", "1.2foo"]); + assert_eq!(elements, ["1.000000foo", "1.100000foo", "1.200000foo"]); let value = eval_v1_expr(&mut env, V1::One, "suffix('foo', ['bar', 'baz', 'qux'])").unwrap(); let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); @@ -105,7 +103,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/transpose.rs b/wdl-engine/src/stdlib/transpose.rs index 64f64b05..b1dc230e 100644 --- a/wdl-engine/src/stdlib/transpose.rs +++ b/wdl-engine/src/stdlib/transpose.rs @@ -1,7 +1,5 @@ //! Implements the `transpose` function from the WDL standard library. -use std::sync::Arc; - use wdl_analysis::types::Type; use wdl_ast::Diagnostic; @@ -46,13 +44,13 @@ fn transpose(context: CallContext<'_>) -> Result { let rows = outer.len(); let (columns, ty) = outer - .elements() + .as_slice() .first() .map(|v| { ( v.as_array() .expect("element should be an array") - .elements() + .as_slice() .len(), v.ty(), ) @@ -63,7 +61,7 @@ fn transpose(context: CallContext<'_>) -> Result { for i in 0..columns { let mut transposed_inner: Vec = Vec::with_capacity(rows); for j in 0..rows { - let inner = outer.elements()[j] + let inner = outer.as_slice()[j] .as_array() .expect("element should be an array"); if inner.len() != columns { @@ -74,13 +72,13 @@ fn transpose(context: CallContext<'_>) -> Result { )); } - transposed_inner.push(inner.elements()[i].clone()) + transposed_inner.push(inner.as_slice()[i].clone()) } - transposed_outer.push(Array::new_unchecked(ty, Arc::new(transposed_inner)).into()); + transposed_outer.push(Array::new_unchecked(ty, transposed_inner).into()); } - Ok(Array::new_unchecked(context.return_type, Arc::new(transposed_outer)).into()) + Ok(Array::new_unchecked(context.return_type, transposed_outer).into()) } /// Gets the function describing `transpose`. @@ -117,12 +115,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect::>() @@ -139,12 +137,12 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { v.as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect::>() diff --git a/wdl-engine/src/stdlib/unzip.rs b/wdl-engine/src/stdlib/unzip.rs index 30b76ea5..cea8b4c1 100644 --- a/wdl-engine/src/stdlib/unzip.rs +++ b/wdl-engine/src/stdlib/unzip.rs @@ -1,7 +1,5 @@ //! Implements the `unzip` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -71,7 +69,7 @@ fn unzip(context: CallContext<'_>) -> Result { let mut left = Vec::with_capacity(array.len()); let mut right = Vec::with_capacity(array.len()); - for v in array.elements() { + for v in array.as_slice() { let p = v.as_pair().expect("element should be a pair"); left.push(p.left().clone()); right.push(p.right().clone()); @@ -79,8 +77,8 @@ fn unzip(context: CallContext<'_>) -> Result { Ok(Pair::new_unchecked( context.return_type, - Arc::new(Array::new_unchecked(left_ty, Arc::new(left)).into()), - Arc::new(Array::new_unchecked(right_ty, Arc::new(right)).into()), + Array::new_unchecked(left_ty, left).into(), + Array::new_unchecked(right_ty, right).into(), ) .into()) } @@ -122,7 +120,7 @@ mod test { .left() .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -131,7 +129,7 @@ mod test { .right() .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_string().unwrap().as_str()) .collect(); diff --git a/wdl-engine/src/stdlib/values.rs b/wdl-engine/src/stdlib/values.rs index e18958ea..acd700a2 100644 --- a/wdl-engine/src/stdlib/values.rs +++ b/wdl-engine/src/stdlib/values.rs @@ -1,7 +1,5 @@ //! Implements the `values` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -37,11 +35,10 @@ fn values(context: CallContext<'_>) -> Result { .value .as_map() .expect("value should be a map") - .elements() .values() .cloned() .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `values`. @@ -87,7 +84,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| v.as_integer().unwrap()) .collect(); @@ -102,7 +99,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| match v { Value::None => None, diff --git a/wdl-engine/src/stdlib/write_json.rs b/wdl-engine/src/stdlib/write_json.rs index 5a3c14e4..3f017992 100644 --- a/wdl-engine/src/stdlib/write_json.rs +++ b/wdl-engine/src/stdlib/write_json.rs @@ -31,7 +31,7 @@ fn write_json(context: CallContext<'_>) -> Result { }; // Create a temporary file that will be persisted after writing the lines - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_json", format!("failed to create temporary file: {e}"), @@ -113,7 +113,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); } diff --git a/wdl-engine/src/stdlib/write_lines.rs b/wdl-engine/src/stdlib/write_lines.rs index d2cde6ea..e9494953 100644 --- a/wdl-engine/src/stdlib/write_lines.rs +++ b/wdl-engine/src/stdlib/write_lines.rs @@ -42,7 +42,7 @@ fn write_lines(context: CallContext<'_>) -> Result { .unwrap_array(); // Create a temporary file that will be persisted after writing the lines - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_lines", format!("failed to create temporary file: {e}"), @@ -52,7 +52,7 @@ fn write_lines(context: CallContext<'_>) -> Result { // Write the lines let mut writer = BufWriter::new(file.as_file_mut()); - for line in lines.elements() { + for line in lines.as_slice() { writer .write(line.as_string().unwrap().as_bytes()) .map_err(write_error)?; @@ -112,7 +112,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -131,7 +131,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( diff --git a/wdl-engine/src/stdlib/write_map.rs b/wdl-engine/src/stdlib/write_map.rs index d9aa70ae..a7b9dfa9 100644 --- a/wdl-engine/src/stdlib/write_map.rs +++ b/wdl-engine/src/stdlib/write_map.rs @@ -45,7 +45,7 @@ fn write_map(context: CallContext<'_>) -> Result { .unwrap_map(); // Create a temporary file that will be persisted after writing the map - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_map", format!("failed to create temporary file: {e}"), @@ -55,7 +55,7 @@ fn write_map(context: CallContext<'_>) -> Result { // Write the lines let mut writer = BufWriter::new(file.as_file_mut()); - for (key, value) in map.elements() { + for (key, value) in map.iter() { writeln!( &mut writer, "{key}\t{value}", @@ -122,7 +122,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -141,7 +141,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( diff --git a/wdl-engine/src/stdlib/write_object.rs b/wdl-engine/src/stdlib/write_object.rs index 95f31f17..2ac2f58a 100644 --- a/wdl-engine/src/stdlib/write_object.rs +++ b/wdl-engine/src/stdlib/write_object.rs @@ -45,7 +45,7 @@ fn write_object(context: CallContext<'_>) -> Result { let object = context.coerce_argument(0, Type::Object).unwrap_object(); // Create a temporary file that will be persisted after writing the map - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_object", format!("failed to create temporary file: {e}"), @@ -54,9 +54,9 @@ fn write_object(context: CallContext<'_>) -> Result { })?; let mut writer = BufWriter::new(file.as_file_mut()); - if !object.members().is_empty() { + if !object.is_empty() { // Write the header first - for (i, key) in object.members().keys().enumerate() { + for (i, key) in object.keys().enumerate() { if i > 0 { writer.write(b"\t").map_err(write_error)?; } @@ -66,7 +66,7 @@ fn write_object(context: CallContext<'_>) -> Result { writeln!(&mut writer).map_err(write_error)?; - for (i, (key, value)) in object.members().iter().enumerate() { + for (i, (key, value)) in object.iter().enumerate() { if i > 0 { writer.write(b"\t").map_err(write_error)?; } @@ -168,7 +168,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -187,12 +187,12 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), - "foo\tbar\tbaz\nbar\t1\t3.5\n", + "foo\tbar\tbaz\nbar\t1\t3.500000\n", ); let value = eval_v1_expr( @@ -206,7 +206,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -225,7 +225,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( diff --git a/wdl-engine/src/stdlib/write_objects.rs b/wdl-engine/src/stdlib/write_objects.rs index c4b0eb73..8a92d9b6 100644 --- a/wdl-engine/src/stdlib/write_objects.rs +++ b/wdl-engine/src/stdlib/write_objects.rs @@ -4,6 +4,7 @@ use std::io::BufWriter; use std::io::Write; use std::path::Path; +use itertools::Either; use tempfile::NamedTempFile; use wdl_analysis::types::CompoundTypeDef; use wdl_analysis::types::PrimitiveTypeKind; @@ -14,9 +15,7 @@ use super::CallContext; use super::Function; use super::Signature; use crate::CompoundValue; -use crate::Object; use crate::PrimitiveValue; -use crate::Struct; use crate::Value; use crate::diagnostics::function_call_failed; use crate::stdlib::write_tsv::write_tsv_value; @@ -60,7 +59,7 @@ fn write_objects(context: CallContext<'_>) -> Result { .expect("argument should be an array"); // Create a temporary file that will be persisted after writing the map - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_objects", format!("failed to create temporary file: {e}"), @@ -80,20 +79,17 @@ fn write_objects(context: CallContext<'_>) -> Result { // member names let mut empty = array.is_empty(); if matches!(element_type, Type::Object) { - let mut iter = array.elements().iter(); + let mut iter = array.as_slice().iter(); let expected = iter .next() .expect("should be non-empty") .as_object() - .expect("should be object") - .members(); + .expect("should be object"); empty = expected.is_empty(); for v in iter { - let next = v - .as_object() - .expect("element should be an object") - .members(); + let next = v.as_object().expect("element should be an object"); + if next.len() != expected.len() || next.keys().any(|k| !expected.contains_key(k)) { return Err(function_call_failed( "write_objects", @@ -107,9 +103,9 @@ fn write_objects(context: CallContext<'_>) -> Result { let mut writer = BufWriter::new(file.as_file_mut()); if !empty { // Write the header first - let keys = match array.elements().first().expect("array should not be empty") { - Value::Compound(CompoundValue::Object(Object { members, .. })) - | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => members.keys(), + let keys = match array.as_slice().first().expect("array should not be empty") { + Value::Compound(CompoundValue::Object(object)) => Either::Left(object.keys()), + Value::Compound(CompoundValue::Struct(s)) => Either::Right(s.keys()), _ => unreachable!("value should either be an object or struct"), }; @@ -124,14 +120,14 @@ fn write_objects(context: CallContext<'_>) -> Result { writeln!(&mut writer).map_err(write_error)?; // Next, write a row for each object/struct - for v in array.elements().iter() { - let members = match v { - Value::Compound(CompoundValue::Object(Object { members, .. })) - | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => members, + for v in array.as_slice().iter() { + let iter = match v { + Value::Compound(CompoundValue::Object(object)) => Either::Left(object.iter()), + Value::Compound(CompoundValue::Struct(s)) => Either::Right(s.iter()), _ => unreachable!("value should either be an object or struct"), }; - for (i, (k, v)) in members.iter().enumerate() { + for (i, (k, v)) in iter.enumerate() { if i > 0 { writer.write(b"\t").map_err(write_error)?; } @@ -234,7 +230,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -254,12 +250,12 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), - "foo\tbar\tbaz\nbar\t1\t3.5\nfoo\t101\t1234\n", + "foo\tbar\tbaz\nbar\t1\t3.500000\nfoo\t101\t1234\n", ); let value = eval_v1_expr( @@ -274,12 +270,12 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), - "foo\tbar\tbaz\nbar\t1\t3.5\nfoo\t\t1234\n", + "foo\tbar\tbaz\nbar\t1\t3.500000\nfoo\t\t1234\n", ); let value = eval_v1_expr( @@ -294,7 +290,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( diff --git a/wdl-engine/src/stdlib/write_tsv.rs b/wdl-engine/src/stdlib/write_tsv.rs index 1f469847..5f0ce6cc 100644 --- a/wdl-engine/src/stdlib/write_tsv.rs +++ b/wdl-engine/src/stdlib/write_tsv.rs @@ -60,7 +60,7 @@ fn write_array_tsv_file( }; // Create a temporary file that will be persisted after writing - let mut file = NamedTempFile::new_in(tmp).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", tmp).map_err(|e| { function_call_failed( "write_tsv", format!("failed to create temporary file: {e}"), @@ -72,7 +72,7 @@ fn write_array_tsv_file( // Start by writing the header, if one was provided let column_count = if let Some(header) = header { - for (i, name) in header.elements().iter().enumerate() { + for (i, name) in header.as_slice().iter().enumerate() { let name = name.as_string().unwrap(); if name.contains('\t') { return Err(function_call_failed( @@ -96,7 +96,7 @@ fn write_array_tsv_file( }; // Write the rows - for (index, row) in rows.elements().iter().enumerate() { + for (index, row) in rows.as_slice().iter().enumerate() { let row = row.as_array().unwrap(); if let Some(column_count) = column_count { if row.len() != column_count { @@ -113,7 +113,7 @@ fn write_array_tsv_file( } } - for (i, column) in row.elements().iter().enumerate() { + for (i, column) in row.as_slice().iter().enumerate() { let column = column.as_string().unwrap(); if column.contains('\t') { return Err(function_call_failed( @@ -177,7 +177,7 @@ fn write_tsv(context: CallContext<'_>) -> Result { .coerce_argument(0, ANALYSIS_STDLIB.array_array_string_type()) .unwrap_array(); - write_array_tsv_file(context.tmp(), rows, None, context.call_site) + write_array_tsv_file(context.temp_dir(), rows, None, context.call_site) } /// Given an Array of elements, writes a tab-separated value (TSV) file with one @@ -205,7 +205,7 @@ fn write_tsv_with_header(context: CallContext<'_>) -> Result .unwrap_array(); write_array_tsv_file( - context.tmp(), + context.temp_dir(), rows, if write_header { Some(header) } else { None }, context.call_site, @@ -255,7 +255,7 @@ fn write_tsv_struct(context: CallContext<'_>) -> Result { }; // Create a temporary file that will be persisted after writing - let mut file = NamedTempFile::new_in(context.tmp()).map_err(|e| { + let mut file = NamedTempFile::with_prefix_in("tmp", context.temp_dir()).map_err(|e| { function_call_failed( "write_tsv", format!("failed to create temporary file: {e}"), @@ -295,7 +295,7 @@ fn write_tsv_struct(context: CallContext<'_>) -> Result { } // Header was explicitly specified, write out the values - for (i, name) in header.elements().iter().enumerate() { + for (i, name) in header.as_slice().iter().enumerate() { let name = name.as_string().unwrap(); if name.contains('\t') { return Err(function_call_failed( @@ -326,9 +326,9 @@ fn write_tsv_struct(context: CallContext<'_>) -> Result { } // Write the rows - for row in rows.elements() { + for row in rows.as_slice() { let row = row.as_struct().unwrap(); - for (i, (name, column)) in row.members().iter().enumerate() { + for (i, (name, column)) in row.iter().enumerate() { if i > 0 { writer.write(b"\t").map_err(write_error)?; } @@ -434,7 +434,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -453,7 +453,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -472,7 +472,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -505,7 +505,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -525,7 +525,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -545,7 +545,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -565,7 +565,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( @@ -585,7 +585,7 @@ mod test { .as_file() .expect("should be file") .as_str() - .starts_with(env.tmp().to_str().expect("should be UTF-8")), + .starts_with(env.temp_dir().to_str().expect("should be UTF-8")), "file should be in temp directory" ); assert_eq!( diff --git a/wdl-engine/src/stdlib/zip.rs b/wdl-engine/src/stdlib/zip.rs index 81d3e317..993dce7b 100644 --- a/wdl-engine/src/stdlib/zip.rs +++ b/wdl-engine/src/stdlib/zip.rs @@ -1,7 +1,5 @@ //! Implements the `zip` function from the WDL standard library. -use std::sync::Arc; - use wdl_ast::Diagnostic; use super::CallContext; @@ -74,15 +72,13 @@ fn zip(context: CallContext<'_>) -> Result { ); let elements = left - .elements() + .as_slice() .iter() - .zip(right.elements().iter()) - .map(|(l, r)| { - Pair::new_unchecked(element_ty, Arc::new(l.clone()), Arc::new(r.clone())).into() - }) + .zip(right.as_slice()) + .map(|(l, r)| Pair::new_unchecked(element_ty, l.clone(), r.clone()).into()) .collect(); - Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) + Ok(Array::new_unchecked(context.return_type, elements).into()) } /// Gets the function describing `zip`. @@ -116,7 +112,7 @@ mod test { let elements: Vec<_> = value .as_array() .unwrap() - .elements() + .as_slice() .iter() .map(|v| { let p = v.as_pair().unwrap(); diff --git a/wdl-engine/src/units.rs b/wdl-engine/src/units.rs index b8896513..e0c42c0a 100644 --- a/wdl-engine/src/units.rs +++ b/wdl-engine/src/units.rs @@ -29,7 +29,7 @@ pub enum StorageUnit { impl StorageUnit { /// Converts the given number of bytes into a float representing the number /// of units. - pub fn convert(&self, bytes: u64) -> f64 { + pub fn units(&self, bytes: u64) -> f64 { let bytes = bytes as f64; match self { Self::Bytes => bytes, @@ -43,6 +43,22 @@ impl StorageUnit { Self::Tebibytes => bytes / 1099511627776.0, } } + + /// Converts the given number of bytes into the corresponding number of + /// bytes based on the unit. + pub fn bytes(&self, bytes: u64) -> Option { + match self { + Self::Bytes => Some(bytes), + Self::Kilobytes => bytes.checked_mul(1000), + Self::Megabytes => bytes.checked_mul(1000000), + Self::Gigabytes => bytes.checked_mul(1000000000), + Self::Terabytes => bytes.checked_mul(1000000000000), + Self::Kibibytes => bytes.checked_mul(1024), + Self::Mebibytes => bytes.checked_mul(1048576), + Self::Gibibytes => bytes.checked_mul(1073741824), + Self::Tebibytes => bytes.checked_mul(1099511627776), + } + } } impl FromStr for StorageUnit { @@ -63,3 +79,25 @@ impl FromStr for StorageUnit { } } } + +/// Converts a unit string (e.g. `2 GiB`) to bytes. +/// +/// The string is expected to contain a single integer followed by the unit. +/// +/// Returns `None` if the string is not a valid unit string or if the resulting +/// byte count exceeds an unsigned 64-bit integer. +pub fn convert_unit_string(s: &str) -> Option { + // No space, so try splitting on first alpha + let (n, unit) = match s.chars().position(|c| c.is_ascii_alphabetic()) { + Some(index) => { + let (n, unit) = s.split_at(index); + ( + n.trim().parse::().ok()?, + unit.trim().parse::().ok()?, + ) + } + None => return None, + }; + + unit.bytes(n) +} diff --git a/wdl-engine/src/value.rs b/wdl-engine/src/value.rs index 180c64e1..ffcdfafc 100644 --- a/wdl-engine/src/value.rs +++ b/wdl-engine/src/value.rs @@ -4,6 +4,7 @@ use std::cmp::Ordering; use std::fmt; use std::hash::Hash; use std::hash::Hasher; +use std::path::Path; use std::sync::Arc; use anyhow::Context; @@ -11,9 +12,11 @@ use anyhow::Result; use anyhow::anyhow; use anyhow::bail; use indexmap::IndexMap; +use itertools::Either; use ordered_float::OrderedFloat; use serde::ser::SerializeMap; use serde::ser::SerializeSeq; +use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; use wdl_analysis::types::ArrayType; use wdl_analysis::types::Coercible as _; use wdl_analysis::types::CompoundTypeDef; @@ -22,8 +25,12 @@ use wdl_analysis::types::PrimitiveTypeKind; use wdl_analysis::types::Type; use wdl_analysis::types::TypeEq; use wdl_analysis::types::Types; +use wdl_ast::AstToken; +use wdl_ast::v1; use wdl_grammar::lexer::v1::is_ident; +use crate::TaskExecutionConstraints; + /// Implemented on coercible values. pub trait Coercible: Sized { /// Coerces the value into the given type. @@ -38,6 +45,8 @@ pub trait Coercible: Sized { } /// Represents a WDL runtime value. +/// +/// Values are cheap to clone. #[derive(Debug, Clone)] pub enum Value { /// The value is a literal `None` value. @@ -46,15 +55,62 @@ pub enum Value { Primitive(PrimitiveValue), /// The value is a compound value. Compound(CompoundValue), + /// The value is a task variable. + /// + /// This value occurs only during command and output section evaluation in + /// WDL 1.2 tasks. + Task(TaskValue), + /// The value is a hints value. + /// + /// Hints values only appear in a task hints section in WDL 1.2. + Hints(HintsValue), + /// The value is an input value. + /// + /// Input values only appear in a task hints section in WDL 1.2. + Input(InputValue), + /// The value is an output value. + /// + /// Output values only appear in a task hints section in WDL 1.2. + Output(OutputValue), } impl Value { + /// Creates an object from an iterator of V1 AST metadata items. + /// + /// # Panics + /// + /// Panics if the metadata value contains an invalid numeric value. + pub fn from_v1_metadata(value: &v1::MetadataValue) -> Self { + match value { + v1::MetadataValue::Boolean(v) => v.value().into(), + v1::MetadataValue::Integer(v) => v.value().expect("number should be in range").into(), + v1::MetadataValue::Float(v) => v.value().expect("number should be in range").into(), + v1::MetadataValue::String(v) => PrimitiveValue::new_string( + v.text() + .expect("metadata strings shouldn't have placeholders") + .as_str(), + ) + .into(), + v1::MetadataValue::Null(_) => Self::None, + v1::MetadataValue::Object(o) => Object::from_v1_metadata(o.items()).into(), + v1::MetadataValue::Array(a) => Array::new_unchecked( + ANALYSIS_STDLIB.array_object_type(), + a.elements().map(|v| Value::from_v1_metadata(&v)).collect(), + ) + .into(), + } + } + /// Gets the type of the value. pub fn ty(&self) -> Type { match self { Self::None => Type::None, Self::Primitive(v) => v.ty(), Self::Compound(v) => v.ty(), + Self::Task(_) => Type::Task, + Self::Hints(_) => Type::Hints, + Self::Input(_) => Type::Input, + Self::Output(_) => Type::Output, } } @@ -305,6 +361,105 @@ impl Value { } } + /// Gets the value as a task. + /// + /// Returns `None` if the value is not a task. + pub fn as_task(&self) -> Option<&TaskValue> { + match self { + Self::Task(v) => Some(v), + _ => None, + } + } + + /// Gets a mutable reference to the value as a task. + /// + /// Returns `None` if the value is not a task. + pub(crate) fn as_task_mut(&mut self) -> Option<&mut TaskValue> { + match self { + Self::Task(v) => Some(v), + _ => None, + } + } + + /// Unwraps the value into a task. + /// + /// # Panics + /// + /// Panics if the value is not a task. + pub fn unwrap_task(self) -> TaskValue { + match self { + Self::Task(v) => v, + _ => panic!("value is not a task"), + } + } + + /// Gets the value as a hints value. + /// + /// Returns `None` if the value is not a hints value. + pub fn as_hints(&self) -> Option<&HintsValue> { + match self { + Self::Hints(v) => Some(v), + _ => None, + } + } + + /// Unwraps the value into a hints value. + /// + /// # Panics + /// + /// Panics if the value is not a hints value. + pub fn unwrap_hints(self) -> HintsValue { + match self { + Self::Hints(v) => v, + _ => panic!("value is not a hints value"), + } + } + + /// Visits each file or directory path contained in the value. + pub(crate) fn visit_paths(&self, cb: &mut impl FnMut(&str)) { + match self { + Self::Primitive(v) => v.visit_paths(cb), + Self::Compound(v) => v.visit_paths(cb), + _ => {} + } + } + + /// Replaces any inner path values by joining the specified path with the + /// path value. + /// + /// If `check_existence` is `true` and a required path does not exist, an + /// error is returned. An optional path that does not exist is replaced with + /// `None`. + pub(crate) fn join_paths( + &mut self, + types: &Types, + path: &Path, + check_existence: bool, + optional: bool, + ) -> Result<()> { + match self { + Self::Primitive(v) => { + if !v.join_paths(path, check_existence, optional)? { + *self = Value::None; + } + } + Self::Compound(v) => v.join_paths(types, path, check_existence)?, + _ => {} + } + + Ok(()) + } + + /// Creates a clone of the value, but makes the type required. + /// + /// This only affects compound values that internally store their type. + pub(crate) fn clone_as_required(&self) -> Self { + match self { + Self::Compound(v) => Self::Compound(v.clone_as_required()), + _ => self.clone(), + } + } + /// Determines if two values have equality according to the WDL /// specification. /// @@ -329,11 +484,15 @@ impl Value { S: serde::Serializer, { use serde::Serialize; + use serde::ser::Error; match self { Self::None => serializer.serialize_none(), Self::Primitive(v) => v.serialize(serializer), Self::Compound(v) => v.serialize(types, serializer), + Self::Task(_) | Self::Hints(_) | Self::Input(_) | Self::Output(_) => { + Err(S::Error::custom("value cannot be serialized")) + } } } @@ -487,9 +646,7 @@ impl Value { members.insert(key, map.next_value_seed(Deserialize(self.0))?); } - Ok(Value::Compound(CompoundValue::Object(Object { - members: Arc::new(members), - }))) + Ok(Value::Compound(CompoundValue::Object(members.into()))) } fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -507,6 +664,10 @@ impl fmt::Display for Value { Value::None => write!(f, "None"), Value::Primitive(v) => v.fmt(f), Value::Compound(v) => v.fmt(f), + Value::Task(_) => write!(f, "task"), + Value::Hints(v) => v.fmt(f), + Value::Input(v) => v.fmt(f), + Value::Output(v) => v.fmt(f), } } } @@ -530,6 +691,34 @@ impl Coercible for Value { } Self::Primitive(v) => v.coerce(types, target).map(Self::Primitive), Self::Compound(v) => v.coerce(types, target).map(Self::Compound), + Self::Task(_) => { + if matches!(target, Type::Task) { + return Ok(self.clone()); + } + + bail!("task variables cannot be coerced to any other type"); + } + Self::Hints(_) => { + if matches!(target, Type::Hints) { + return Ok(self.clone()); + } + + bail!("hints values cannot be coerced to any other type"); + } + Self::Input(_) => { + if matches!(target, Type::Input) { + return Ok(self.clone()); + } + + bail!("input values cannot be coerced to any other type"); + } + Self::Output(_) => { + if matches!(target, Type::Output) { + return Ok(self.clone()); + } + + bail!("output values cannot be coerced to any other type"); + } } } } @@ -597,6 +786,18 @@ impl From for Value { } } +impl From for Value { + fn from(value: TaskValue) -> Self { + Self::Task(value) + } +} + +impl From for Value { + fn from(value: HintsValue) -> Self { + Self::Hints(value) + } +} + impl From for Value { fn from(value: CompoundValue) -> Self { Self::Compound(value) @@ -604,6 +805,8 @@ impl From for Value { } /// Represents a primitive WDL value. +/// +/// Primitive values are cheap to clone. #[derive(Debug, Clone)] pub enum PrimitiveValue { /// The value is a `Boolean`. @@ -821,7 +1024,7 @@ impl PrimitiveValue { match self.0 { PrimitiveValue::Boolean(v) => write!(f, "{v}"), PrimitiveValue::Integer(v) => write!(f, "{v}"), - PrimitiveValue::Float(v) => write!(f, "{v:?}"), + PrimitiveValue::Float(v) => write!(f, "{v:.6?}"), PrimitiveValue::String(v) | PrimitiveValue::File(v) | PrimitiveValue::Directory(v) => { @@ -833,6 +1036,54 @@ impl PrimitiveValue { Display(self) } + + /// Visits each file or directory path contained in the value. + fn visit_paths(&self, cb: &mut impl FnMut(&str)) { + match self { + Self::File(path) | Self::Directory(path) => cb(path.as_str()), + _ => {} + } + } + + /// Replaces any inner path values by joining the specified path with the + /// path value. + /// + /// If `check_existence` is `true` and a required path does not exist, an + /// error is returned. An optional path that does not exist is replaced with + /// `None`. + fn join_paths(&mut self, path: &Path, check_existence: bool, optional: bool) -> Result { + match self { + PrimitiveValue::File(p) => { + if let Ok(joined) = path.join(p.as_str()).into_os_string().into_string() { + *Arc::make_mut(p) = joined; + } + + if check_existence && !Path::new(p.as_str()).is_file() { + if optional { + return Ok(false); + } else { + bail!("file `{p}` does not exist"); + } + } + } + PrimitiveValue::Directory(p) => { + if let Ok(joined) = path.join(p.as_str()).into_os_string().into_string() { + *Arc::make_mut(p) = joined; + } + + if check_existence && !Path::new(p.as_str()).is_dir() { + if optional { + return Ok(false); + } else { + bail!("directory `{p}` does not exist"); + } + } + } + _ => {} + } + + Ok(true) + } } impl fmt::Display for PrimitiveValue { @@ -840,7 +1091,7 @@ impl fmt::Display for PrimitiveValue { match self { Self::Boolean(v) => write!(f, "{v}"), Self::Integer(v) => write!(f, "{v}"), - Self::Float(v) => write!(f, "{v:?}"), + Self::Float(v) => write!(f, "{v:.6?}"), Self::String(s) | Self::File(s) | Self::Directory(s) => { // TODO: handle necessary escape sequences write!(f, "\"{s}\"") @@ -917,8 +1168,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::Boolean => Some(Self::Boolean(*v)), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `Boolean` to type `{target}`", target = target.display(types) ) @@ -934,8 +1185,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::Float => Some(Self::Float((*v as f64).into())), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `Int` to type `{target}`", target = target.display(types) ) @@ -949,8 +1200,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::Float => Some(Self::Float(*v)), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `Float` to type `{target}`", target = target.display(types) ) @@ -968,8 +1219,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::Directory => Some(Self::Directory(s.clone())), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `String` to type `{target}`", target = target.display(types) ) @@ -985,8 +1236,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::String => Some(Self::String(s.clone())), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `File` to type `{target}`", target = target.display(types) ) @@ -1002,8 +1253,8 @@ impl Coercible for PrimitiveValue { PrimitiveTypeKind::String => Some(Self::String(s.clone())), _ => None, }) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce type `Directory` to type `{target}`", target = target.display(types) ) @@ -1028,6 +1279,8 @@ impl serde::Serialize for PrimitiveValue { } /// Represents a `Pair` value. +/// +/// Pairs are cheap to clone. #[derive(Debug, Clone)] pub struct Pair { /// The type of the pair. @@ -1059,19 +1312,16 @@ impl Pair { { let left_ty = pair_ty.left_type(); let right_ty = pair_ty.right_type(); - return Ok(Self { + return Ok(Self::new_unchecked( ty, - left: left - .into() + left.into() .coerce(types, left_ty) - .context("failed to coerce pair's left value")? - .into(), - right: right + .context("failed to coerce pair's left value")?, + right .into() .coerce(types, right_ty) - .context("failed to coerce pair's right value")? - .into(), - }); + .context("failed to coerce pair's right value")?, + )); } } @@ -1080,8 +1330,12 @@ impl Pair { /// Constructs a new pair without checking the given left and right conform /// to the given type. - pub(crate) fn new_unchecked(ty: Type, left: Arc, right: Arc) -> Self { - Self { ty, left, right } + pub(crate) fn new_unchecked(ty: Type, left: Value, right: Value) -> Self { + Self { + ty, + left: left.into(), + right: right.into(), + } } /// Gets the type of the `Pair`. @@ -1107,12 +1361,16 @@ impl fmt::Display for Pair { } /// Represents an `Array` value. +/// +/// Arrays are cheap to clone. #[derive(Debug, Clone)] pub struct Array { /// The type of the array. ty: Type, /// The array's elements. - elements: Arc>, + /// + /// A value of `None` indicates an empty array. + elements: Option>>, } impl Array { @@ -1133,21 +1391,17 @@ impl Array { types.type_definition(compound_ty.definition()) { let element_type = array_ty.element_type(); - return Ok(Self { - ty, - elements: Arc::new( - elements - .into_iter() - .enumerate() - .map(|(i, v)| { - let v = v.into(); - v.coerce(types, element_type).with_context(|| { - format!("failed to coerce array element at index {i}") - }) - }) - .collect::>>()?, - ), - }); + let elements = elements + .into_iter() + .enumerate() + .map(|(i, v)| { + let v = v.into(); + v.coerce(types, element_type) + .with_context(|| format!("failed to coerce array element at index {i}")) + }) + .collect::>>()?; + + return Ok(Self::new_unchecked(ty, elements)); } } @@ -1156,8 +1410,15 @@ impl Array { /// Constructs a new array without checking the given elements conform to /// the given type. - pub(crate) fn new_unchecked(ty: Type, elements: Arc>) -> Self { - Self { ty, elements } + pub(crate) fn new_unchecked(ty: Type, elements: Vec) -> Self { + Self { + ty, + elements: if elements.is_empty() { + None + } else { + Some(Arc::new(elements)) + }, + } } /// Gets the type of the `Array` value. @@ -1165,19 +1426,19 @@ impl Array { self.ty } - /// Gets the elements of the `Array` value. - pub fn elements(&self) -> &[Value] { - &self.elements + /// Converts the array value to a slice of values. + pub fn as_slice(&self) -> &[Value] { + self.elements.as_ref().map(|v| v.as_slice()).unwrap_or(&[]) } /// Returns the number of elements in the array. pub fn len(&self) -> usize { - self.elements.len() + self.elements.as_ref().map(|v| v.len()).unwrap_or(0) } /// Returns `true` if the array has no elements. pub fn is_empty(&self) -> bool { - self.elements.is_empty() + self.len() == 0 } } @@ -1185,12 +1446,14 @@ impl fmt::Display for Array { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[")?; - for (i, element) in self.elements.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } + if let Some(elements) = &self.elements { + for (i, element) in elements.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } - write!(f, "{element}")?; + write!(f, "{element}")?; + } } write!(f, "]") @@ -1198,12 +1461,16 @@ impl fmt::Display for Array { } /// Represents a `Map` value. +/// +/// Maps are cheap to clone. #[derive(Debug, Clone)] pub struct Map { /// The type of the map value. ty: Type, /// The elements of the map value. - elements: Arc, Value>>, + /// + /// A value of `None` indicates an empty map. + elements: Option, Value>>>, } impl Map { @@ -1230,41 +1497,34 @@ impl Map { let key_type = map_ty.key_type(); let value_type = map_ty.value_type(); - return Ok(Self { - ty, - elements: Arc::new( - elements - .into_iter() - .enumerate() - .map(|(i, (k, v))| { - let k = k.into(); - let v = v.into(); - Ok(( - if k.is_none() { - None - } else { - match k.coerce(types, key_type).with_context(|| { - format!( - "failed to coerce map key for element at index {i}" - ) - })? { - Value::None => None, - Value::Primitive(v) => Some(v), - Value::Compound(_) => { - bail!("not all key values are primitive") - } - } - }, - v.coerce(types, value_type).with_context(|| { - format!( - "failed to coerce map value for element at index {i}" - ) - })?, - )) - }) - .collect::>()?, - ), - }); + let elements = elements + .into_iter() + .enumerate() + .map(|(i, (k, v))| { + let k = k.into(); + let v = v.into(); + Ok(( + if k.is_none() { + None + } else { + match k.coerce(types, key_type).with_context(|| { + format!("failed to coerce map key for element at index {i}") + })? { + Value::None => None, + Value::Primitive(v) => Some(v), + _ => { + bail!("not all key values are primitive") + } + } + }, + v.coerce(types, value_type).with_context(|| { + format!("failed to coerce map value for element at index {i}") + })?, + )) + }) + .collect::>()?; + + return Ok(Self::new_unchecked(ty, elements)); } } @@ -1275,9 +1535,16 @@ impl Map { /// given type. pub(crate) fn new_unchecked( ty: Type, - elements: Arc, Value>>, + elements: IndexMap, Value>, ) -> Self { - Self { ty, elements } + Self { + ty, + elements: if elements.is_empty() { + None + } else { + Some(Arc::new(elements)) + }, + } } /// Gets the type of the `Map` value. @@ -1285,19 +1552,51 @@ impl Map { self.ty } - /// Gets the elements of the `Map` value. - pub fn elements(&self) -> &IndexMap, Value> { - &self.elements + /// Iterates the elements of the map. + pub fn iter(&self) -> impl Iterator, &Value)> { + self.elements + .as_ref() + .map(|m| Either::Left(m.iter())) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Iterates the keys of the map. + pub fn keys(&self) -> impl Iterator> { + self.elements + .as_ref() + .map(|m| Either::Left(m.keys())) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Iterates the values of the map. + pub fn values(&self) -> impl Iterator { + self.elements + .as_ref() + .map(|m| Either::Left(m.values())) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Determines if the map contains the given key. + pub fn contains_key(&self, key: &Option) -> bool { + self.elements + .as_ref() + .map(|m| m.contains_key(key)) + .unwrap_or(false) + } + + /// Gets a value from the map by key. + pub fn get(&self, key: &Option) -> Option<&Value> { + self.elements.as_ref().and_then(|m| m.get(key)) } /// Returns the number of elements in the map. pub fn len(&self) -> usize { - self.elements.len() + self.elements.as_ref().map(|m| m.len()).unwrap_or(0) } /// Returns `true` if the map has no elements. pub fn is_empty(&self) -> bool { - self.elements.is_empty() + self.len() == 0 } } @@ -1305,7 +1604,7 @@ impl fmt::Display for Map { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{")?; - for (i, (k, v)) in self.elements.iter().enumerate() { + for (i, (k, v)) in self.iter().enumerate() { if i > 0 { write!(f, ", ")?; } @@ -1321,10 +1620,14 @@ impl fmt::Display for Map { } /// Represents an `Object` value. +/// +/// Objects are cheap to clone. #[derive(Debug, Clone)] pub struct Object { /// The members of the object. - pub(crate) members: Arc>, + /// + /// A value of `None` indicates an empty object. + pub(crate) members: Option>>, } impl Object { @@ -1334,18 +1637,33 @@ impl Object { S: Into, V: Into, { - Self { - members: Arc::new( - items - .into_iter() - .map(|(n, v)| { - let n = n.into(); - let v = v.into(); - (n, v) - }) - .collect(), - ), - } + items + .into_iter() + .map(|(n, v)| { + let n = n.into(); + let v = v.into(); + (n, v) + }) + .collect::>() + .into() + } + + /// Returns an empty object. + pub fn empty() -> Self { + IndexMap::default().into() + } + + /// Creates an object from an iterator of V1 AST metadata items. + pub fn from_v1_metadata(items: impl Iterator) -> Self { + items + .map(|i| { + ( + i.name().as_str().to_string(), + Value::from_v1_metadata(&i.value()), + ) + }) + .collect::>() + .into() } /// Gets the type of the `Object` value. @@ -1353,9 +1671,51 @@ impl Object { Type::Object } - /// Gets the members of the `Object` value. - pub fn members(&self) -> &IndexMap { - &self.members + /// Iterates the members of the object. + pub fn iter(&self) -> impl Iterator { + self.members + .as_ref() + .map(|m| Either::Left(m.iter().map(|(k, v)| (k.as_str(), v)))) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Iterates the keys of the object. + pub fn keys(&self) -> impl Iterator { + self.members + .as_ref() + .map(|m| Either::Left(m.keys().map(|k| k.as_str()))) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Iterates the values of the object. + pub fn values(&self) -> impl Iterator { + self.members + .as_ref() + .map(|m| Either::Left(m.values())) + .unwrap_or(Either::Right(std::iter::empty())) + } + + /// Determines if the object contains the given key. + pub fn contains_key(&self, key: &str) -> bool { + self.members + .as_ref() + .map(|m| m.contains_key(key)) + .unwrap_or(false) + } + + /// Gets a value from the object by key. + pub fn get(&self, key: &str) -> Option<&Value> { + self.members.as_ref().and_then(|m| m.get(key)) + } + + /// Returns the number of members in the object. + pub fn len(&self) -> usize { + self.members.as_ref().map(|m| m.len()).unwrap_or(0) + } + + /// Returns `true` if the object has no members. + pub fn is_empty(&self) -> bool { + self.len() == 0 } } @@ -1363,7 +1723,7 @@ impl fmt::Display for Object { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "object {{")?; - for (i, (k, v)) in self.members.iter().enumerate() { + for (i, (k, v)) in self.iter().enumerate() { if i > 0 { write!(f, ", ")?; } @@ -1378,12 +1738,18 @@ impl fmt::Display for Object { impl From> for Object { fn from(members: IndexMap) -> Self { Self { - members: Arc::new(members), + members: if members.is_empty() { + None + } else { + Some(Arc::new(members)) + }, } } } /// Represents a `Struct` value. +/// +/// Structs are cheap to clone. #[derive(Debug, Clone)] pub struct Struct { /// The type of the struct value. @@ -1471,9 +1837,29 @@ impl Struct { &self.name } - /// Gets the members of the `Struct` value. - pub fn members(&self) -> &IndexMap { - &self.members + /// Iterates the members of the struct. + pub fn iter(&self) -> impl Iterator { + self.members.iter().map(|(k, v)| (k.as_str(), v)) + } + + /// Iterates the keys of the struct. + pub fn keys(&self) -> impl Iterator { + self.members.keys().map(|k| k.as_str()) + } + + /// Iterates the values of the struct. + pub fn values(&self) -> impl Iterator { + self.members.values() + } + + /// Determines if the struct contains the given member name. + pub fn contains_key(&self, key: &str) -> bool { + self.members.contains_key(key) + } + + /// Gets a value from the struct by member name. + pub fn get(&self, key: &str) -> Option<&Value> { + self.members.get(key) } } @@ -1495,7 +1881,7 @@ impl fmt::Display for Struct { /// Represents a compound value. /// -/// Compound values may be trivially cloned. +/// Compound values are cheap to clone. #[derive(Debug, Clone)] pub enum CompoundValue { /// The value is a `Pair` of values. @@ -1504,7 +1890,7 @@ pub enum CompoundValue { Array(Array), /// The value is a `Map` of values. Map(Map), - /// The value is an `Object.` + /// The value is an `Object`. Object(Object), /// The value is a struct. Struct(Struct), @@ -1650,28 +2036,34 @@ impl CompoundValue { && Value::equals(types, &left.right, &right.right)?, ), (CompoundValue::Array(left), CompoundValue::Array(right)) => Some( - left.elements.len() == right.elements.len() + left.len() == right.len() && left - .elements + .as_slice() .iter() - .zip(right.elements.iter()) + .zip(right.as_slice()) .all(|(l, r)| Value::equals(types, l, r).unwrap_or(false)), ), (CompoundValue::Map(left), CompoundValue::Map(right)) => Some( - left.elements.len() == right.elements.len() - && left - .elements - .iter() - .all(|(k, left)| match right.elements.get(k) { - Some(right) => Value::equals(types, left, right).unwrap_or(false), - None => false, - }), + left.len() == right.len() + // Maps are ordered, so compare via iteration + && left.iter().zip(right.iter()).all(|((lk, lv), (rk, rv))| { + match (lk, rk) { + (None, None) => {}, + (Some(lk), Some(rk)) if lk == rk => {}, + _ => return false + } + + Value::equals(types, lv, rv).unwrap_or(false) + }), + ), + (CompoundValue::Object(left), CompoundValue::Object(right)) => Some( + left.len() == right.len() + && left.iter().all(|(k, left)| match right.get(k) { + Some(right) => Value::equals(types, left, right).unwrap_or(false), + None => false, + }), ), ( - CompoundValue::Object(Object { members: left }), - CompoundValue::Object(Object { members: right }), - ) - | ( CompoundValue::Struct(Struct { members: left, .. }), CompoundValue::Struct(Struct { members: right, .. }), ) => Some( @@ -1712,8 +2104,8 @@ impl CompoundValue { match self { Self::Pair(_) => Err(S::Error::custom("a pair cannot be serialized")), Self::Array(v) => { - let mut s = serializer.serialize_seq(Some(v.elements.len()))?; - for e in v.elements.iter() { + let mut s = serializer.serialize_seq(Some(v.len()))?; + for e in v.as_slice() { s.serialize_element(&Serialize { types, value: e })?; } @@ -1737,14 +2129,22 @@ impl CompoundValue { )); } - let mut s = serializer.serialize_map(Some(v.elements.len()))?; - for (k, v) in v.elements.iter() { + let mut s = serializer.serialize_map(Some(v.len()))?; + for (k, v) in v.iter() { + s.serialize_entry(k, &Serialize { types, value: v })?; + } + + s.end() + } + Self::Object(object) => { + let mut s = serializer.serialize_map(Some(object.len()))?; + for (k, v) in object.iter() { s.serialize_entry(k, &Serialize { types, value: v })?; } s.end() } - Self::Object(Object { members, .. }) | Self::Struct(Struct { members, .. }) => { + Self::Struct(Struct { members, .. }) => { let mut s = serializer.serialize_map(Some(members.len()))?; for (k, v) in members.iter() { s.serialize_entry(k, &Serialize { types, value: v })?; @@ -1754,6 +2154,161 @@ impl CompoundValue { } } } + + /// Visits each file or directory path contained in the value. + fn visit_paths(&self, cb: &mut impl FnMut(&str)) { + match self { + Self::Pair(pair) => { + pair.left().visit_paths(cb); + pair.right().visit_paths(cb); + } + Self::Array(array) => { + for v in array.as_slice() { + v.visit_paths(cb); + } + } + Self::Map(map) => { + for (k, v) in map.iter() { + if let Some(k) = k { + k.visit_paths(cb); + } + + v.visit_paths(cb); + } + } + Self::Object(object) => { + for v in object.values() { + v.visit_paths(cb); + } + } + Self::Struct(Struct { members, .. }) => { + for v in members.values() { + v.visit_paths(cb); + } + } + } + } + + /// Replaces any inner path values by joining the specified path with the + /// path value. + /// + /// If `check_existence` is `true` and a required path does not exist, an + /// error is returned. An optional path that does not exist is replaced with + /// `None`. + pub(crate) fn join_paths( + &mut self, + types: &Types, + path: &Path, + check_existence: bool, + ) -> Result<()> { + match self { + Self::Pair(pair) => { + let ty = types.pair_type(pair.ty); + let (left_optional, right_optional) = + (ty.left_type().is_optional(), ty.right_type().is_optional()); + Arc::make_mut(&mut pair.left).join_paths( + types, + path, + check_existence, + left_optional, + )?; + Arc::make_mut(&mut pair.right).join_paths( + types, + path, + check_existence, + right_optional, + )?; + } + Self::Array(array) => { + let ty = types.array_type(array.ty); + let optional = ty.element_type().is_optional(); + if let Some(elements) = &mut array.elements { + for v in Arc::make_mut(elements) { + v.join_paths(types, path, check_existence, optional)?; + } + } + } + Self::Map(map) => { + let ty = types.map_type(map.ty); + let (key_optional, value_optional) = + (ty.key_type().is_optional(), ty.value_type().is_optional()); + if let Some(elements) = &mut map.elements { + if elements + .iter() + .find_map(|(k, _)| { + k.as_ref().map(|v| { + matches!(v, PrimitiveValue::File(_) | PrimitiveValue::Directory(_)) + }) + }) + .unwrap_or(false) + { + // The key type contains a path, we need to rebuild the map to alter the + // keys + let elements = Arc::make_mut(elements); + let new = elements + .drain(..) + .map(|(mut k, mut v)| { + if let Some(v) = &mut k { + if !v.join_paths(path, check_existence, key_optional)? { + k = None; + } + } + + v.join_paths(types, path, check_existence, value_optional)?; + Ok((k, v)) + }) + .collect::>>()?; + elements.extend(new); + } else { + // Otherwise, we can just mutable the values in place + for v in Arc::make_mut(elements).values_mut() { + v.join_paths(types, path, check_existence, value_optional)?; + } + } + } + } + Self::Object(object) => { + if let Some(members) = &mut object.members { + for v in Arc::make_mut(members).values_mut() { + v.join_paths(types, path, check_existence, false)?; + } + } + } + Self::Struct(s) => { + let ty = types.struct_type(s.ty); + for (n, v) in Arc::make_mut(&mut s.members).iter_mut() { + v.join_paths(types, path, check_existence, ty.members()[n].is_optional())?; + } + } + } + + Ok(()) + } + + /// Creates a clone of the value, but makes the type required. + fn clone_as_required(&self) -> Self { + match self { + Self::Pair(v) => Self::Pair(Pair { + ty: v.ty.require(), + left: v.left.clone(), + right: v.right.clone(), + }), + Self::Array(v) => Self::Array(Array { + ty: v.ty.require(), + elements: v.elements.clone(), + }), + Self::Map(v) => Self::Map(Map { + ty: v.ty.require(), + elements: v.elements.clone(), + }), + Self::Object(_) => self.clone(), + Self::Struct(v) => Self::Struct(Struct { + ty: v.ty.require(), + name: v.name.clone(), + members: v.members.clone(), + }), + } + } } impl fmt::Display for CompoundValue { @@ -1780,7 +2335,7 @@ impl Coercible for CompoundValue { (Self::Array(v), CompoundTypeDef::Array(array_ty)) => { // Don't allow coercion when the source is empty but the target has the // non-empty qualifier - if v.elements.is_empty() && array_ty.is_non_empty() { + if v.is_empty() && array_ty.is_non_empty() { bail!( "cannot coerce empty array value to non-empty array type `{ty}`", ty = array_ty.display(types) @@ -1790,7 +2345,7 @@ impl Coercible for CompoundValue { return Ok(Self::Array(Array::new( types, target, - v.elements.iter().cloned(), + v.as_slice().iter().cloned(), )?)); } // Map[W, Y] -> Map[X, Z] where W -> X and Y -> Z @@ -1798,7 +2353,7 @@ impl Coercible for CompoundValue { return Ok(Self::Map(Map::new( types, target, - v.elements.iter().map(|(k, v)| { + v.iter().map(|(k, v)| { (k.clone().map(Into::into).unwrap_or(Value::None), v.clone()) }), )?)); @@ -1814,7 +2369,7 @@ impl Coercible for CompoundValue { } // Map[String, Y] -> Struct (Self::Map(v), CompoundTypeDef::Struct(struct_ty)) => { - let len = v.elements.len(); + let len = v.len(); let expected_len = types.struct_type(target).members().len(); if len != expected_len { @@ -1831,14 +2386,13 @@ impl Coercible for CompoundValue { ty: target, name: struct_ty.name().clone(), members: Arc::new( - v.elements - .iter() + v.iter() .map(|(k, v)| { let k: String = k .as_ref() .and_then(|k| k.as_string()) - .ok_or_else(|| { - anyhow!( + .with_context(|| { + format!( "cannot coerce a map with a non-string key type \ to struct type `{ty}`", ty = compound_ty.display(types) @@ -1846,9 +2400,9 @@ impl Coercible for CompoundValue { })? .to_string(); let ty = - *types.struct_type(target).members().get(&k).ok_or_else( + *types.struct_type(target).members().get(&k).with_context( || { - anyhow!( + format!( "cannot coerce a map with key `{k}` to struct \ type `{ty}` as the struct does not contain a \ member with that name", @@ -1867,8 +2421,7 @@ impl Coercible for CompoundValue { } // Struct -> Map[String, Y] // Object -> Map[String, Y] - (Self::Struct(Struct { members, .. }), CompoundTypeDef::Map(map_ty)) - | (Self::Object(Object { members }), CompoundTypeDef::Map(map_ty)) => { + (Self::Struct(Struct { members, .. }), CompoundTypeDef::Map(map_ty)) => { if map_ty.key_type().as_primitive() != Some(PrimitiveTypeKind::String.into()) { bail!( "cannot coerce a struct or object to type `{ty}` as it requires a \ @@ -1878,60 +2431,49 @@ impl Coercible for CompoundValue { } let value_ty = map_ty.value_type(); - return Ok(Self::Map(Map { - ty: target, - elements: Arc::new( - members - .iter() - .map(|(n, v)| { - let v = v.coerce(types, value_ty).with_context(|| { - format!("failed to coerce member `{n}`") - })?; - Ok((PrimitiveValue::new_string(n).into(), v)) - }) - .collect::>()?, - ), - })); + return Ok(Self::Map(Map::new_unchecked( + target, + members + .iter() + .map(|(n, v)| { + let v = v + .coerce(types, value_ty) + .with_context(|| format!("failed to coerce member `{n}`"))?; + Ok((PrimitiveValue::new_string(n).into(), v)) + }) + .collect::>()?, + ))); } - // Object -> Struct - (Self::Object(v), CompoundTypeDef::Struct(struct_ty)) => { - let len = v.members.len(); - let expected_len = struct_ty.members().len(); - - if len != expected_len { + (Self::Object(object), CompoundTypeDef::Map(map_ty)) => { + if map_ty.key_type().as_primitive() != Some(PrimitiveTypeKind::String.into()) { bail!( - "cannot coerce an object of {len} members{s1} to struct type `{ty}` \ - as the target struct has {expected_len} member{s2}", - s1 = if len == 1 { "" } else { "s" }, - ty = compound_ty.display(types), - s2 = if expected_len == 1 { "" } else { "s" } + "cannot coerce a struct or object to type `{ty}` as it requires a \ + `String` key type", + ty = compound_ty.display(types) ); } - return Ok(Self::Struct(Struct { - ty: target, - name: struct_ty.name().clone(), - members: Arc::new( - v.members - .iter() - .map(|(k, v)| { - let ty = - types.struct_type(target).members().get(k).ok_or_else( - || { - anyhow!( - "cannot coerce an object with member `{k}` to \ - struct type `{ty}` as the struct does not \ - contain a member with that name", - ty = compound_ty.display(types) - ) - }, - )?; - let v = v.coerce(types, *ty)?; - Ok((k.clone(), v)) - }) - .collect::>()?, - ), - })); + let value_ty = map_ty.value_type(); + return Ok(Self::Map(Map::new_unchecked( + target, + object + .iter() + .map(|(n, v)| { + let v = v + .coerce(types, value_ty) + .with_context(|| format!("failed to coerce member `{n}`"))?; + Ok((PrimitiveValue::new_string(n).into(), v)) + }) + .collect::>()?, + ))); + } + // Object -> Struct + (Self::Object(v), CompoundTypeDef::Struct(_)) => { + return Ok(Self::Struct(Struct::new( + types, + target, + v.iter().map(|(k, v)| (k, v.clone())), + )?)); } // Struct -> Struct (Self::Struct(v), CompoundTypeDef::Struct(struct_ty)) => { @@ -1983,31 +2525,27 @@ impl Coercible for CompoundValue { match self { // Map[String, Y] -> Object Self::Map(v) => { - return Ok(Self::Object(Object { - members: Arc::new( - v.elements - .iter() - .map(|(k, v)| { - let k = k - .as_ref() - .and_then(|k| k.as_string()) - .ok_or_else(|| { - anyhow!( - "cannot coerce a map with a non-string key type \ - to type `Object`" - ) - })? - .to_string(); - Ok((k, v.clone())) - }) - .collect::>()?, - ), - })); + return Ok(Self::Object( + v.iter() + .map(|(k, v)| { + let k = k + .as_ref() + .and_then(|k| k.as_string()) + .context( + "cannot coerce a map with a non-string key type to type \ + `Object`", + )? + .to_string(); + Ok((k, v.clone())) + }) + .collect::>>()? + .into(), + )); } // Struct -> Object Self::Struct(v) => { return Ok(Self::Object(Object { - members: v.members.clone(), + members: Some(v.members.clone()), })); } _ => {} @@ -2052,6 +2590,339 @@ impl From for CompoundValue { } } +/// Represents a value for `task` variables in WDL 1.2. +/// +/// Task values are cheap to clone. +#[derive(Debug, Clone)] +pub struct TaskValue { + /// The name of the task. + name: Arc, + /// The id of the task. + id: Arc, + /// The container of the task. + container: Option>, + /// The allocated number of cpus for the task. + cpu: f64, + /// The allocated memory (in bytes) for the task. + memory: i64, + /// The GPU allocations for the task. + /// + /// An array with one specification per allocated GPU; the specification is + /// execution engine-specific. + gpu: Array, + /// The FPGA allocations for the task. + /// + /// An array with one specification per allocated FPGA; the specification is + /// execution engine-specific. + fpga: Array, + /// The disk allocations for the task. + /// + /// A map with one entry for each disk mount point. + /// + /// The key is the mount point and the value is the initial amount of disk + /// space allocated, in bytes. + disks: Map, + /// The current task attempt count. + /// + /// The value must be 0 the first time the task is executed and incremented + /// by 1 each time the task is retried (if any). + attempt: i64, + /// The time by which the task must be completed, as a Unix time stamp. + /// + /// A value of `None` indicates there is no deadline. + end_time: Option, + /// The task's return code. + /// + /// Initially set to `None`, but set after task execution completes. + return_code: Option, + /// The task's `meta` section as an object. + meta: Object, + /// The tasks's `parameter_meta` section as an object. + parameter_meta: Object, + /// The task's extension metadata. + ext: Object, +} + +impl TaskValue { + /// Constructs a new task value with the given name and identifier. + pub(crate) fn new_v1( + name: impl Into, + id: impl Into, + definition: &v1::TaskDefinition, + constraints: TaskExecutionConstraints, + ) -> Self { + Self { + name: Arc::new(name.into()), + id: Arc::new(id.into()), + container: constraints.container.map(Into::into), + cpu: constraints.cpu, + memory: constraints.memory, + gpu: Array::new_unchecked( + ANALYSIS_STDLIB.array_string_type(), + constraints + .gpu + .into_iter() + .map(|v| PrimitiveValue::new_string(v).into()) + .collect(), + ), + fpga: Array::new_unchecked( + ANALYSIS_STDLIB.array_string_type(), + constraints + .fpga + .into_iter() + .map(|v| PrimitiveValue::new_string(v).into()) + .collect(), + ), + disks: Map::new_unchecked( + ANALYSIS_STDLIB.map_string_int_type(), + constraints + .disks + .into_iter() + .map(|(k, v)| (Some(PrimitiveValue::new_string(k)), v.into())) + .collect(), + ), + attempt: 1, + end_time: None, + return_code: None, + meta: definition + .metadata() + .map(|s| Object::from_v1_metadata(s.items())) + .unwrap_or_else(Object::empty), + parameter_meta: definition + .parameter_metadata() + .map(|s| Object::from_v1_metadata(s.items())) + .unwrap_or_else(Object::empty), + ext: Object::empty(), + } + } + + /// Gets the task name. + pub fn name(&self) -> &Arc { + &self.name + } + + /// Gets the unique ID of the task. + pub fn id(&self) -> &Arc { + &self.id + } + + /// Gets the container in which the task is executing. + pub fn container(&self) -> Option<&Arc> { + self.container.as_ref() + } + + /// Gets the allocated number of cpus for the task. + pub fn cpu(&self) -> f64 { + self.cpu + } + + /// Gets the allocated memory (in bytes) for the task. + pub fn memory(&self) -> i64 { + self.memory + } + + /// Gets the GPU allocations for the task. + /// + /// An array with one specification per allocated GPU; the specification is + /// execution engine-specific. + pub fn gpu(&self) -> &Array { + &self.gpu + } + + /// Gets the FPGA allocations for the task. + /// + /// An array with one specification per allocated FPGA; the specification is + /// execution engine-specific. + pub fn fpga(&self) -> &Array { + &self.fpga + } + + /// Gets the disk allocations for the task. + /// + /// A map with one entry for each disk mount point. + /// + /// The key is the mount point and the value is the initial amount of disk + /// space allocated, in bytes. + pub fn disks(&self) -> &Map { + &self.disks + } + + /// Gets current task attempt count. + /// + /// The value must be 0 the first time the task is executed and incremented + /// by 1 each time the task is retried (if any). + pub fn attempt(&self) -> i64 { + self.attempt + } + + /// Gets the time by which the task must be completed, as a Unix time stamp. + /// + /// A value of `None` indicates there is no deadline. + pub fn end_time(&self) -> Option { + self.end_time + } + + /// Gets the task's return code. + /// + /// Initially set to `None`, but set after task execution completes. + pub fn return_code(&self) -> Option { + self.return_code + } + + /// Gets the task's `meta` section as an object. + pub fn meta(&self) -> &Object { + &self.meta + } + + /// Gets the tasks's `parameter_meta` section as an object. + pub fn parameter_meta(&self) -> &Object { + &self.parameter_meta + } + + /// Gets the task's extension metadata. + pub fn ext(&self) -> &Object { + &self.ext + } + + /// Sets the return code after the task execution has completed. + pub(crate) fn set_return_code(&mut self, code: i32) { + self.return_code = Some(code as i64); + } + + /// Accesses a field of the task value by name. + /// + /// Returns `None` if the name is not a known field name. + pub fn field(&self, name: &str) -> Option { + match name { + "name" => Some(PrimitiveValue::String(self.name.clone()).into()), + "id" => Some(PrimitiveValue::String(self.id.clone()).into()), + "container" => Some( + self.container + .clone() + .map(|c| PrimitiveValue::String(c).into()) + .unwrap_or(Value::None), + ), + "cpu" => Some(self.cpu.into()), + "memory" => Some(self.memory.into()), + "gpu" => Some(self.gpu.clone().into()), + "fpga" => Some(self.fpga.clone().into()), + "disks" => Some(self.disks.clone().into()), + "attempt" => Some(self.attempt.into()), + "end_time" => Some(self.end_time.map(Into::into).unwrap_or(Value::None)), + "return_code" => Some(self.return_code.map(Into::into).unwrap_or(Value::None)), + "meta" => Some(self.meta.clone().into()), + "parameter_meta" => Some(self.parameter_meta.clone().into()), + "ext" => Some(self.ext.clone().into()), + _ => None, + } + } +} + +/// Represents a hints value from a WDL 1.2 hints section. +/// +/// Hints values are cheap to clone. +#[derive(Debug, Clone)] +pub struct HintsValue(Object); + +impl HintsValue { + /// Converts the hints value to an object. + pub fn as_object(&self) -> &Object { + &self.0 + } +} + +impl fmt::Display for HintsValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "hints {{")?; + + for (i, (k, v)) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{k}: {v}")?; + } + + write!(f, "}}") + } +} + +impl From for HintsValue { + fn from(value: Object) -> Self { + Self(value) + } +} + +/// Represents an input value from a WDL 1.2 hints section. +/// +/// Input values are cheap to clone. +#[derive(Debug, Clone)] +pub struct InputValue(Object); + +impl InputValue { + /// Converts the input value to an object. + pub fn as_object(&self) -> &Object { + &self.0 + } +} + +impl fmt::Display for InputValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "input {{")?; + + for (i, (k, v)) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{k}: {v}")?; + } + + write!(f, "}}") + } +} + +impl From for InputValue { + fn from(value: Object) -> Self { + Self(value) + } +} + +/// Represents an output value from a WDL 1.2 hints section. +/// +/// Output values are cheap to clone. +#[derive(Debug, Clone)] +pub struct OutputValue(Object); + +impl OutputValue { + /// Converts the output value to an object. + pub fn as_object(&self) -> &Object { + &self.0 + } +} + +impl fmt::Display for OutputValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "output {{")?; + + for (i, (k, v)) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + + write!(f, "{k}: {v}")?; + } + + write!(f, "}}") + } +} + +impl From for OutputValue { + fn from(value: Object) -> Self { + Self(value) + } +} + #[cfg(test)] mod test { use approx::assert_relative_eq; @@ -2157,8 +3028,8 @@ mod test { #[test] fn float_display() { - assert_eq!(Value::from(12345.12345).to_string(), "12345.12345"); - assert_eq!(Value::from(-12345.12345).to_string(), "-12345.12345"); + assert_eq!(Value::from(12345.12345).to_string(), "12345.123450"); + assert_eq!(Value::from(-12345.12345).to_string(), "-12345.123450"); } #[test] @@ -2321,7 +3192,10 @@ mod test { .expect("should create array value") .into(); let target = src.coerce(&types, target_ty).expect("should coerce"); - assert_eq!(target.unwrap_array().to_string(), "[1.0, 2.0, 3.0]"); + assert_eq!( + target.unwrap_array().to_string(), + "[1.000000, 2.000000, 3.000000]" + ); // Array[Int] -> Array[String] (invalid) let target_ty = types.add_array(ArrayType::new(PrimitiveTypeKind::String)); @@ -2538,7 +3412,7 @@ Caused by: let map_value = value.coerce(&types, ty).expect("value should coerce"); assert_eq!( map_value.to_string(), - r#"{"foo": 1.0, "bar": 2.0, "baz": 3.0}"# + r#"{"foo": 1.000000, "bar": 2.000000, "baz": 3.000000}"# ); // Struct -> Struct @@ -2550,7 +3424,7 @@ Caused by: let struct_value = value.coerce(&types, ty).expect("value should coerce"); assert_eq!( struct_value.to_string(), - r#"Bar {foo: 1.0, bar: 2.0, baz: 3.0}"# + r#"Bar {foo: 1.000000, bar: 2.000000, baz: 3.000000}"# ); // Struct -> Object @@ -2559,7 +3433,7 @@ Caused by: .expect("value should coerce"); assert_eq!( object_value.to_string(), - r#"object {foo: 1.0, bar: 2.0, baz: 3.0}"# + r#"object {foo: 1.000000, bar: 2.000000, baz: 3.000000}"# ); } diff --git a/wdl-engine/tests/inputs.rs b/wdl-engine/tests/inputs.rs index e0e77ab7..34144de2 100644 --- a/wdl-engine/tests/inputs.rs +++ b/wdl-engine/tests/inputs.rs @@ -21,6 +21,8 @@ use std::path::Path; use std::path::PathBuf; use std::path::absolute; use std::process::exit; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use anyhow::Context; use anyhow::Result; @@ -32,14 +34,14 @@ use codespan_reporting::term::termcolor::Buffer; use colored::Colorize; use path_clean::clean; use pretty_assertions::StrComparison; +use rayon::prelude::*; use wdl_analysis::AnalysisResult; use wdl_analysis::Analyzer; use wdl_analysis::rules; +use wdl_analysis::types::Types; use wdl_ast::Diagnostic; use wdl_ast::Severity; -use wdl_ast::SyntaxNode; -use wdl_engine::Engine; -use wdl_engine::InputsFile; +use wdl_engine::Inputs; /// Finds tests to run as part of the analysis test suite. fn find_tests() -> Vec { @@ -100,8 +102,9 @@ fn compare_result(path: &Path, result: &str) -> Result<()> { if expected != result { bail!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), ); } @@ -109,7 +112,7 @@ fn compare_result(path: &Path, result: &str) -> Result<()> { } /// Runts the test given the provided analysis result. -fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { +fn run_test(test: &Path, result: AnalysisResult, ntests: &AtomicUsize) -> Result<()> { let cwd = std::env::current_dir().expect("must have a CWD"); let mut buffer = Buffer::no_color(); @@ -128,11 +131,7 @@ fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { }; if let Some(diagnostic) = diagnostics.iter().find(|d| d.severity() == Severity::Error) { - let source = result - .document() - .root() - .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) - .unwrap_or_default(); + let source = result.document().node().syntax().text().to_string(); let file = SimpleFile::new(&path, &source); term::emit( @@ -147,26 +146,29 @@ fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { bail!("document `{path}` contains at least one diagnostic error:\n{diagnostic}"); } - let mut engine = Engine::default(); + let mut types = Types::default(); let document = result.document(); - let result = match InputsFile::parse(engine.types_mut(), document, test.join("inputs.json")) { - Ok(inputs) => { - if let Some((task, inputs)) = inputs.as_task_inputs() { + let result = match Inputs::parse(&mut types, document, test.join("inputs.json")) { + Ok(Some((name, inputs))) => match inputs { + Inputs::Task(inputs) => { match inputs .validate( - engine.types_mut(), + &mut types, document, - document.task_by_name(task).expect("task should be present"), + document + .task_by_name(&name) + .expect("task should be present"), ) - .with_context(|| format!("failed to validate the inputs to task `{task}`")) + .with_context(|| format!("failed to validate the inputs to task `{name}`")) { Ok(()) => String::new(), Err(e) => format!("{e:?}"), } - } else if let Some(inputs) = inputs.as_workflow_inputs() { + } + Inputs::Workflow(inputs) => { let workflow = document.workflow().expect("workflow should be present"); match inputs - .validate(engine.types_mut(), document, workflow) + .validate(&mut types, document, workflow) .with_context(|| { format!( "failed to validate the inputs to workflow `{workflow}`", @@ -176,15 +178,17 @@ fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { Ok(()) => String::new(), Err(e) => format!("{e:?}"), } - } else { - panic!("expected either a task input or a workflow input"); } - } + }, + Ok(None) => String::new(), Err(e) => format!("{e:?}"), }; let output = test.join("error.txt"); - compare_result(&output, &result) + compare_result(&output, &result)?; + + ntests.fetch_add(1, Ordering::SeqCst); + Ok(()) } #[tokio::main] @@ -205,41 +209,77 @@ async fn main() { .await .expect("failed to analyze documents"); - let mut errors = Vec::new(); - for test in &tests { - let test_name = test.file_stem().and_then(OsStr::to_str).unwrap(); + let ntests = AtomicUsize::new(0); + let errors = tests + .par_iter() + .filter_map(|test| { + let test_name = test.file_stem().and_then(OsStr::to_str).unwrap(); - // Discover the results that are relevant only to this test - let base = clean(absolute(test).expect("should be made absolute")); + // Discover the results that are relevant only to this test + let base = clean(absolute(test).expect("should be made absolute")); - let mut results = results.iter().filter_map(|r| { - if r.document().uri().to_file_path().ok()?.starts_with(&base) { - Some(r.clone()) - } else { - None - } - }); - - let result = results.next().expect("should have a result"); - if results.next().is_some() { - println!("test {test_name} ... {failed}", failed = "failed".red()); - errors.push(( - test_name, - "more than one WDL file was in the test directory".to_string(), - )); - continue; - } + let mut results = results.iter().filter_map(|r| { + if r.document().uri().to_file_path().ok()?.starts_with(&base) { + Some(r.clone()) + } else { + None + } + }); - match run_test(test, result) { - Ok(_) => { - println!("test {test_name} ... {ok}", ok = "ok".green()); - } - Err(e) => { - println!("test {test_name} ... {failed}", failed = "failed".red()); - errors.push((test_name, e.to_string())); + let result = match results.find_map(|r| { + let path = r.document().uri().to_file_path().ok()?; + if path.parent()?.file_name()?.to_str()? == test_name { + Some(r.clone()) + } else { + None + } + }) { + Some(document) => document, + None => { + return Some(( + test_name, + format!("failed to find analysis result for test `{test_name}`"), + )); + } + }; + + let test_name = test.file_stem().and_then(OsStr::to_str).unwrap(); + match std::panic::catch_unwind(|| { + match run_test(test, result, &ntests) + .map_err(|e| format!("failed to run test `{path}`: {e}", path = test.display())) + .err() + { + Some(e) => { + println!("test {test_name} ... {failed}", failed = "failed".red()); + Some((test_name, e)) + } + None => { + println!("test {test_name} ... {ok}", ok = "ok".green()); + None + } + } + }) { + Ok(result) => result, + Err(e) => { + println!( + "test {test_name} ... {panicked}", + panicked = "panicked".red() + ); + Some(( + test_name, + format!( + "test panicked: {e:?}", + e = e + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| e.downcast_ref::<&str>().copied()) + .unwrap_or("no panic message") + ), + )) + } } - } - } + }) + .collect::>(); if !errors.is_empty() { eprintln!( @@ -255,5 +295,8 @@ async fn main() { exit(1); } - println!("\ntest result: ok. {count} passed\n", count = tests.len()); + println!( + "\ntest result: ok. {} passed\n", + ntests.load(Ordering::SeqCst) + ); } diff --git a/wdl-engine/tests/inputs/mising-call-input/error.txt b/wdl-engine/tests/inputs/mising-call-input/error.txt index 5883043c..e69de29b 100644 --- a/wdl-engine/tests/inputs/mising-call-input/error.txt +++ b/wdl-engine/tests/inputs/mising-call-input/error.txt @@ -1,4 +0,0 @@ -failed to validate the inputs to workflow `test` - -Caused by: - missing required input `x` for call `foo` \ No newline at end of file diff --git a/wdl-engine/tests/inputs/unknown-call-1.2/error.txt b/wdl-engine/tests/inputs/unknown-call-1_2/error.txt similarity index 64% rename from wdl-engine/tests/inputs/unknown-call-1.2/error.txt rename to wdl-engine/tests/inputs/unknown-call-1_2/error.txt index 2bc07672..ea71409b 100644 --- a/wdl-engine/tests/inputs/unknown-call-1.2/error.txt +++ b/wdl-engine/tests/inputs/unknown-call-1_2/error.txt @@ -1,4 +1,4 @@ -failed to parse input file `tests/inputs/unknown-call-1.2/inputs.json` +failed to parse input file `tests/inputs/unknown-call-1_2/inputs.json` Caused by: 0: invalid input key `test.foo.bar` diff --git a/wdl-engine/tests/inputs/unknown-call-1.2/inputs.json b/wdl-engine/tests/inputs/unknown-call-1_2/inputs.json similarity index 100% rename from wdl-engine/tests/inputs/unknown-call-1.2/inputs.json rename to wdl-engine/tests/inputs/unknown-call-1_2/inputs.json diff --git a/wdl-engine/tests/inputs/unknown-call-1.2/source.wdl b/wdl-engine/tests/inputs/unknown-call-1_2/source.wdl similarity index 100% rename from wdl-engine/tests/inputs/unknown-call-1.2/source.wdl rename to wdl-engine/tests/inputs/unknown-call-1_2/source.wdl diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs new file mode 100644 index 00000000..ed80e531 --- /dev/null +++ b/wdl-engine/tests/tasks.rs @@ -0,0 +1,438 @@ +//! The WDL task file tests. +//! +//! This test looks for directories in `tests/tasks`. +//! +//! Each directory is expected to contain: +//! +//! * `source.wdl` - the test input source to evaluate; the file is expected to +//! contain no static analysis errors, but may fail at evaluation time. +//! * `error.txt` - the expected evaluation error, if any. +//! * `inputs.json` - the inputs to the task. +//! * `outputs.json` - the expected outputs from the task, if the task run +//! successfully. +//! * `stdout` - the expected stdout from the task. +//! * `stderr` - the expected stderr from the task. +//! * `files` - a directory containing any expected files written by the task. +//! +//! The expected files may be automatically generated or updated by setting the +//! `BLESS` environment variable when running this test. + +use std::borrow::Cow; +use std::collections::HashSet; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::path::MAIN_SEPARATOR; +use std::path::Path; +use std::path::PathBuf; +use std::path::absolute; +use std::process::exit; +use std::thread::available_parallelism; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use anyhow::bail; +use codespan_reporting::files::SimpleFile; +use codespan_reporting::term; +use codespan_reporting::term::Config; +use codespan_reporting::term::termcolor::Buffer; +use colored::Colorize; +use futures::StreamExt; +use futures::stream; +use path_clean::clean; +use pretty_assertions::StrComparison; +use tempfile::TempDir; +use walkdir::WalkDir; +use wdl_analysis::AnalysisResult; +use wdl_analysis::Analyzer; +use wdl_analysis::document::Document; +use wdl_analysis::rules; +use wdl_ast::Diagnostic; +use wdl_ast::Severity; +use wdl_engine::Engine; +use wdl_engine::EvaluatedTask; +use wdl_engine::EvaluationError; +use wdl_engine::Inputs; +use wdl_engine::local::LocalTaskExecutionBackend; +use wdl_engine::v1::TaskEvaluator; + +/// Finds tests to run as part of the analysis test suite. +fn find_tests() -> Vec { + // Check for filter arguments consisting of test names + let mut filter = HashSet::new(); + for arg in std::env::args().skip_while(|a| a != "--").skip(1) { + if !arg.starts_with('-') { + filter.insert(arg); + } + } + + let mut tests: Vec = Vec::new(); + for entry in Path::new("tests/tasks").read_dir().unwrap() { + let entry = entry.expect("failed to read directory"); + let path = entry.path(); + if !path.is_dir() + || (!filter.is_empty() + && !filter.contains(entry.file_name().to_str().expect("name should be UTF-8"))) + { + continue; + } + + tests.push(path); + } + + tests.sort(); + tests +} + +/// Normalizes a result. +fn normalize(root: &Path, s: &str) -> String { + // Strip any paths that start with the root directory + let s: Cow<'_, str> = if let Some(mut root) = root.to_str().map(str::to_string) { + if !root.ends_with(MAIN_SEPARATOR) { + root.push(MAIN_SEPARATOR); + } + + s.replace(&root, "").into() + } else { + s.into() + }; + + // Normalize paths separation characters + s.replace('\\', "/").replace("\r\n", "\n") +} + +/// Compares a single result. +fn compare_result(root: &Path, path: &Path, result: &str) -> Result<()> { + let result = normalize(root, result); + if env::var_os("BLESS").is_some() { + fs::write(path, &result).with_context(|| { + format!( + "failed to write result file `{path}`", + path = path.display() + ) + })?; + return Ok(()); + } + + let expected = fs::read_to_string(path) + .with_context(|| format!("failed to read result file `{path}`", path = path.display()))? + .replace("\r\n", "\n"); + + if expected != result { + bail!( + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), + ); + } + + Ok(()) +} + +/// Runts the test given the provided analysis result. +async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { + let cwd = std::env::current_dir().expect("must have a CWD"); + // Attempt to strip the CWD from the result path + let path = result.document().uri().to_file_path(); + let path: Cow<'_, str> = match &path { + // Strip the CWD from the path + Ok(path) => path.strip_prefix(&cwd).unwrap_or(path).to_string_lossy(), + // Use the id itself if there is no path + Err(_) => result.document().uri().as_str().into(), + }; + + let diagnostics: Cow<'_, [Diagnostic]> = match result.error() { + Some(e) => vec![Diagnostic::error(format!("failed to read `{path}`: {e:#}"))].into(), + None => result.document().diagnostics().into(), + }; + + if let Some(diagnostic) = diagnostics.iter().find(|d| d.severity() == Severity::Error) { + bail!(diagnostic_to_string(result.document(), &path, &diagnostic)); + } + + let mut engine = Engine::new(LocalTaskExecutionBackend::new()); + let (name, mut inputs) = match Inputs::parse( + engine.types_mut(), + result.document(), + test.join("inputs.json"), + )? { + Some((name, Inputs::Task(inputs))) => (name, inputs), + Some((_, Inputs::Workflow(_))) => { + bail!("`inputs.json` contains inputs for a workflow, not a task") + } + None => { + let mut iter = result.document().tasks(); + let name = iter + .next() + .context("inputs file is empty and the WDL document contains no tasks")? + .name() + .to_string(); + if iter.next().is_some() { + bail!("inputs file is empty and the WDL document contains more than one task"); + } + + (name, Default::default()) + } + }; + + // Make any paths specified in the inputs file relative to the test directory + let task = result + .document() + .task_by_name(&name) + .ok_or_else(|| anyhow!("document does not contain a task named `{name}`"))?; + inputs.join_paths( + engine.types_mut(), + result.document(), + task, + &absolute(test).expect("failed to get absolute directory"), + ); + + let dir = TempDir::new().context("failed to create temporary directory")?; + let mut evaluator = TaskEvaluator::new(&mut engine); + match evaluator + .evaluate(result.document(), task, &inputs, dir.path()) + .await + { + Ok(evaluated) => { + compare_evaluation_results(test, dir.path(), &evaluated)?; + + match evaluated.into_result() { + Ok(outputs) => { + let outputs = outputs.with_name(name); + let mut buffer = Vec::new(); + let mut serializer = serde_json::Serializer::pretty(&mut buffer); + outputs.serialize(engine.types(), &mut serializer)?; + let outputs = String::from_utf8(buffer).expect("output should be UTF-8"); + let outputs_path = test.join("outputs.json"); + compare_result(dir.path(), &outputs_path, &outputs)?; + } + Err(e) => { + let error = match e { + EvaluationError::Source(diagnostic) => { + diagnostic_to_string(result.document(), &path, &diagnostic) + } + EvaluationError::Other(e) => format!("{e:?}"), + }; + + let error_path = test.join("error.txt"); + compare_result(dir.path(), &error_path, &error)?; + } + } + } + Err(e) => { + let error = match e { + EvaluationError::Source(diagnostic) => { + diagnostic_to_string(result.document(), &path, &diagnostic) + } + EvaluationError::Other(e) => format!("{e:?}"), + }; + + let error_path = test.join("error.txt"); + compare_result(dir.path(), &error_path, &error)?; + } + } + + Ok(()) +} + +fn compare_evaluation_results(test: &Path, dir: &Path, evaluated: &EvaluatedTask) -> Result<()> { + let stdout = + fs::read_to_string(evaluated.stdout().as_file().unwrap().as_str()).with_context(|| { + format!( + "failed to read task stdout file `{path}`", + path = evaluated.stdout().as_file().unwrap() + ) + })?; + let stderr = + fs::read_to_string(evaluated.stderr().as_file().unwrap().as_str()).with_context(|| { + format!( + "failed to read task stderr file `{path}`", + path = evaluated.stderr().as_file().unwrap() + ) + })?; + + let stdout_path = test.join("stdout"); + compare_result(dir, &stdout_path, &stdout)?; + + let stderr_path = test.join("stderr"); + compare_result(dir, &stderr_path, &stderr)?; + + // Compare expected output files + let mut had_files = false; + let files_dir = test.join("files"); + for entry in WalkDir::new(evaluated.work_dir()) { + let entry = entry.with_context(|| { + format!( + "failed to read directory `{path}`", + path = evaluated.work_dir().display() + ) + })?; + let metadata = entry.metadata().with_context(|| { + format!( + "failed to read metadata of `{path}`", + path = entry.path().display() + ) + })?; + if !metadata.is_file() { + continue; + } + + had_files = true; + + let contents = fs::read_to_string(entry.path()).with_context(|| { + format!( + "failed to read file `{path}`", + path = entry.path().display() + ) + })?; + let expected_path = files_dir.join( + entry + .path() + .strip_prefix(evaluated.work_dir()) + .unwrap_or(entry.path()), + ); + fs::create_dir_all( + expected_path + .parent() + .expect("should have parent directory"), + ) + .context("failed to create output file directory")?; + compare_result(dir, &expected_path, &contents)?; + } + + // Look for missing output files + if files_dir.exists() { + for entry in WalkDir::new(&files_dir) { + let entry = entry.with_context(|| { + format!( + "failed to read directory `{path}`", + path = files_dir.display() + ) + })?; + let metadata = entry.metadata().with_context(|| { + format!( + "failed to read metadata of `{path}`", + path = entry.path().display() + ) + })?; + if !metadata.is_file() { + continue; + } + + let relative_path = entry + .path() + .strip_prefix(&files_dir) + .unwrap_or(entry.path()); + let expected_path = evaluated.work_dir().join(relative_path); + if !expected_path.is_file() { + bail!( + "task did not produce expected output file `{path}`", + path = relative_path.display() + ); + } + } + } else if had_files { + bail!( + "task generated files in the working directory that are not present in a `files` \ + subdirectory" + ); + } + + Ok(()) +} + +/// Creates a string from the given diagnostic. +fn diagnostic_to_string(document: &Document, path: &str, diagnostic: &Diagnostic) -> String { + let source = document.node().syntax().text().to_string(); + let file = SimpleFile::new(path, &source); + + let mut buffer = Buffer::no_color(); + term::emit( + &mut buffer, + &Config::default(), + &file, + &diagnostic.to_codespan(), + ) + .expect("should emit"); + + String::from_utf8(buffer.into_inner()).expect("should be UTF-8") +} + +#[tokio::main] +async fn main() { + let tests = find_tests(); + println!("\nrunning {} tests\n", tests.len()); + + // Start with a single analysis pass over all the test files + let analyzer = Analyzer::new(rules(), |_, _, _, _| async {}); + for test in &tests { + analyzer + .add_directory(test.clone()) + .await + .expect("should add directory"); + } + let results = analyzer + .analyze(()) + .await + .expect("failed to analyze documents"); + + let mut futures = Vec::new(); + let mut errors = Vec::new(); + for test in &tests { + let test_name = test.file_stem().and_then(OsStr::to_str).unwrap(); + + // Discover the results that are relevant only to this test + let base = clean(absolute(test).expect("should be made absolute")); + + let mut results = results.iter().filter_map(|r| { + if r.document().uri().to_file_path().ok()?.starts_with(&base) { + Some(r.clone()) + } else { + None + } + }); + + let result = results.next().expect("should have a result"); + if results.next().is_some() { + println!("test {test_name} ... {failed}", failed = "failed".red()); + errors.push(( + test_name.to_string(), + "more than one WDL file was in the test directory".to_string(), + )); + continue; + } + + futures.push(async { (test_name.to_string(), run_test(test, result).await) }); + } + + let mut stream = stream::iter(futures) + .buffer_unordered(available_parallelism().map(Into::into).unwrap_or(1)); + while let Some((test_name, result)) = stream.next().await { + match result { + Ok(_) => { + println!("test {test_name} ... {ok}", ok = "ok".green()); + } + Err(e) => { + println!("test {test_name} ... {failed}", failed = "failed".red()); + errors.push((test_name, format!("{e:?}"))); + } + } + } + + if !errors.is_empty() { + eprintln!( + "\n{count} test(s) {failed}:", + count = errors.len(), + failed = "failed".red() + ); + + for (name, msg) in errors.iter() { + eprintln!("{name}: {msg}", msg = msg.red()); + } + + exit(1); + } + + println!("\ntest result: ok. {count} passed\n", count = tests.len()); +} diff --git a/wdl-engine/tests/tasks/array-access/inputs.json b/wdl-engine/tests/tasks/array-access/inputs.json new file mode 100644 index 00000000..09666dce --- /dev/null +++ b/wdl-engine/tests/tasks/array-access/inputs.json @@ -0,0 +1,4 @@ +{ + "array_access.strings": ["hello", "world"], + "array_access.index": 0 +} diff --git a/wdl-engine/tests/tasks/array-access/outputs.json b/wdl-engine/tests/tasks/array-access/outputs.json new file mode 100644 index 00000000..3039cf6a --- /dev/null +++ b/wdl-engine/tests/tasks/array-access/outputs.json @@ -0,0 +1,3 @@ +{ + "array_access.s": "hello" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/array-access/source.wdl b/wdl-engine/tests/tasks/array-access/source.wdl new file mode 100644 index 00000000..c16990dd --- /dev/null +++ b/wdl-engine/tests/tasks/array-access/source.wdl @@ -0,0 +1,14 @@ +version 1.2 + +task array_access { + input { + Array[String] strings + Int index + } + + command <<<>>> + + output { + String s = strings[index] + } +} diff --git a/wdl-engine/tests/tasks/array-access/stderr b/wdl-engine/tests/tasks/array-access/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/array-access/stdout b/wdl-engine/tests/tasks/array-access/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/array-map-equality/inputs.json b/wdl-engine/tests/tasks/array-map-equality/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/array-map-equality/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/array-map-equality/outputs.json b/wdl-engine/tests/tasks/array-map-equality/outputs.json new file mode 100644 index 00000000..533fba7b --- /dev/null +++ b/wdl-engine/tests/tasks/array-map-equality/outputs.json @@ -0,0 +1,6 @@ +{ + "array_map_equality.is_true1": true, + "array_map_equality.is_true2": true, + "array_map_equality.is_false1": false, + "array_map_equality.is_false2": false +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/array-map-equality/source.wdl b/wdl-engine/tests/tasks/array-map-equality/source.wdl new file mode 100644 index 00000000..05fd6743 --- /dev/null +++ b/wdl-engine/tests/tasks/array-map-equality/source.wdl @@ -0,0 +1,15 @@ +version 1.2 + +task array_map_equality { + command <<<>>> + + output { + # arrays and maps with the same elements in the same order are equal + Boolean is_true1 = [1, 2, 3] == [1, 2, 3] + Boolean is_true2 = {"a": 1, "b": 2} == {"a": 1, "b": 2} + + # arrays and maps with the same elements in different orders are not equal + Boolean is_false1 = [1, 2, 3] == [2, 1, 3] + Boolean is_false2 = {"a": 1, "b": 2} == {"b": 2, "a": 1} + } +} diff --git a/wdl-engine/tests/tasks/array-map-equality/stderr b/wdl-engine/tests/tasks/array-map-equality/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/array-map-equality/stdout b/wdl-engine/tests/tasks/array-map-equality/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/compare-coerced/inputs.json b/wdl-engine/tests/tasks/compare-coerced/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/compare-coerced/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/compare-coerced/outputs.json b/wdl-engine/tests/tasks/compare-coerced/outputs.json new file mode 100644 index 00000000..5c81c920 --- /dev/null +++ b/wdl-engine/tests/tasks/compare-coerced/outputs.json @@ -0,0 +1,3 @@ +{ + "compare_coerced.is_true": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/compare-coerced/source.wdl b/wdl-engine/tests/tasks/compare-coerced/source.wdl new file mode 100644 index 00000000..1d7ffa32 --- /dev/null +++ b/wdl-engine/tests/tasks/compare-coerced/source.wdl @@ -0,0 +1,14 @@ +version 1.2 + +task compare_coerced { + Array[Int] i = [1, 2, 3] + Array[Float] f1 = i + Array[Float] f2 = [1.0, 2.0, 3.0] + + command <<<>>> + + output { + # Ints are automatically coerced to Floats for comparison + Boolean is_true = f1 == f2 + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/compare-coerced/stderr b/wdl-engine/tests/tasks/compare-coerced/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/compare-coerced/stdout b/wdl-engine/tests/tasks/compare-coerced/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/compare-optionals/inputs.json b/wdl-engine/tests/tasks/compare-optionals/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/compare-optionals/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/compare-optionals/outputs.json b/wdl-engine/tests/tasks/compare-optionals/outputs.json new file mode 100644 index 00000000..6dfc9cea --- /dev/null +++ b/wdl-engine/tests/tasks/compare-optionals/outputs.json @@ -0,0 +1,6 @@ +{ + "compare_optionals.is_true1": true, + "compare_optionals.is_true2": true, + "compare_optionals.is_false1": false, + "compare_optionals.is_false2": false +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/compare-optionals/source.wdl b/wdl-engine/tests/tasks/compare-optionals/source.wdl new file mode 100644 index 00000000..dab1cb88 --- /dev/null +++ b/wdl-engine/tests/tasks/compare-optionals/source.wdl @@ -0,0 +1,19 @@ +version 1.2 + +task compare_optionals { + Int i = 1 + Int? j = 1 + Int? k = None + + command <<<>>> + + output { + # equal values of the same type are equal even if one is optional + Boolean is_true1 = i == j + # k is undefined (None), and so is only equal to None + Boolean is_true2 = k == None + # these comparisons are valid and evaluate to false + Boolean is_false1 = i == k + Boolean is_false2 = j == k + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/compare-optionals/stderr b/wdl-engine/tests/tasks/compare-optionals/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/compare-optionals/stdout b/wdl-engine/tests/tasks/compare-optionals/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/concat-optional/inputs.json b/wdl-engine/tests/tasks/concat-optional/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/concat-optional/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/concat-optional/outputs.json b/wdl-engine/tests/tasks/concat-optional/outputs.json new file mode 100644 index 00000000..14df2077 --- /dev/null +++ b/wdl-engine/tests/tasks/concat-optional/outputs.json @@ -0,0 +1,4 @@ +{ + "concat_optional.greeting1": "nice to meet you!", + "concat_optional.greeting2": "hello Fred, nice to meet you!" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/concat-optional/source.wdl b/wdl-engine/tests/tasks/concat-optional/source.wdl new file mode 100644 index 00000000..710e158e --- /dev/null +++ b/wdl-engine/tests/tasks/concat-optional/source.wdl @@ -0,0 +1,21 @@ +version 1.2 + +task concat_optional { + input { + String salutation = "hello" + String? name1 + String? name2 = "Fred" + } + + command <<<>>> + + output { + # since name1 is undefined, the evaluation of the expression in the placeholder fails, and the + # value of greeting1 = "nice to meet you!" + String greeting1 = "~{salutation + ' ' + name1 + ' '}nice to meet you!" + + # since name2 is defined, the evaluation of the expression in the placeholder succeeds, and the + # value of greeting2 = "hello Fred, nice to meet you!" + String greeting2 = "~{salutation + ' ' + name2 + ', '}nice to meet you!" + } +} diff --git a/wdl-engine/tests/tasks/concat-optional/stderr b/wdl-engine/tests/tasks/concat-optional/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/concat-optional/stdout b/wdl-engine/tests/tasks/concat-optional/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/declarations/inputs.json b/wdl-engine/tests/tasks/declarations/inputs.json new file mode 100644 index 00000000..5953069d --- /dev/null +++ b/wdl-engine/tests/tasks/declarations/inputs.json @@ -0,0 +1,5 @@ +{ + "declarations.m": { + "a": "b" + } +} diff --git a/wdl-engine/tests/tasks/declarations/outputs.json b/wdl-engine/tests/tasks/declarations/outputs.json new file mode 100644 index 00000000..eef0eecd --- /dev/null +++ b/wdl-engine/tests/tasks/declarations/outputs.json @@ -0,0 +1,3 @@ +{ + "declarations.pi": 3.14 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/declarations/source.wdl b/wdl-engine/tests/tasks/declarations/source.wdl new file mode 100644 index 00000000..1a7b7661 --- /dev/null +++ b/wdl-engine/tests/tasks/declarations/source.wdl @@ -0,0 +1,19 @@ +version 1.2 + +task declarations { + input { + # these "unbound" declarations are only allowed in the input section + File? x # optional - defaults to None + Map[String, String] m # required + # this is a "bound" declaration + String y = "abc" + } + + Int i = 1 + 2 # Private declarations must be bound + + command <<<>>> + + output { + Float pi = i + .14 # output declarations must also be bound + } +} diff --git a/wdl-engine/tests/tasks/declarations/stderr b/wdl-engine/tests/tasks/declarations/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/declarations/stdout b/wdl-engine/tests/tasks/declarations/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/default-option-task/files/result1 b/wdl-engine/tests/tasks/default-option-task/files/result1 new file mode 100644 index 00000000..f6ea0495 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/files/result1 @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/wdl-engine/tests/tasks/default-option-task/files/result2 b/wdl-engine/tests/tasks/default-option-task/files/result2 new file mode 100644 index 00000000..f6ea0495 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/files/result2 @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/wdl-engine/tests/tasks/default-option-task/files/result3 b/wdl-engine/tests/tasks/default-option-task/files/result3 new file mode 100644 index 00000000..f6ea0495 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/files/result3 @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/wdl-engine/tests/tasks/default-option-task/inputs.json b/wdl-engine/tests/tasks/default-option-task/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/default-option-task/outputs.json b/wdl-engine/tests/tasks/default-option-task/outputs.json new file mode 100644 index 00000000..727bf8ea --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/outputs.json @@ -0,0 +1,4 @@ +{ + "default_option.is_true1": true, + "default_option.is_true2": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/default-option-task/source.wdl b/wdl-engine/tests/tasks/default-option-task/source.wdl new file mode 100644 index 00000000..7c595847 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task default_option { + input { + String? s + } + + command <<< + printf ~{default="foobar" s} > result1 + printf ~{if defined(s) then "~{select_first([s])}" else "foobar"} > result2 + printf ~{select_first([s, "foobar"])} > result3 + >>> + + output { + Boolean is_true1 = read_string("result1") == read_string("result2") + Boolean is_true2 = read_string("result1") == read_string("result3") + } +} diff --git a/wdl-engine/tests/tasks/default-option-task/stderr b/wdl-engine/tests/tasks/default-option-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/default-option-task/stdout b/wdl-engine/tests/tasks/default-option-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/expressions-task/files/hello.txt b/wdl-engine/tests/tasks/expressions-task/files/hello.txt new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/wdl-engine/tests/tasks/expressions-task/files/hello.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/wdl-engine/tests/tasks/expressions-task/inputs.json b/wdl-engine/tests/tasks/expressions-task/inputs.json new file mode 100644 index 00000000..2c2b5005 --- /dev/null +++ b/wdl-engine/tests/tasks/expressions-task/inputs.json @@ -0,0 +1,3 @@ +{ + "expressions.x": 5 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/expressions-task/outputs.json b/wdl-engine/tests/tasks/expressions-task/outputs.json new file mode 100644 index 00000000..fd9617f6 --- /dev/null +++ b/wdl-engine/tests/tasks/expressions-task/outputs.json @@ -0,0 +1,11 @@ +{ + "expressions.f": 3.2, + "expressions.b": false, + "expressions.m": { + "a": 1, + "b": 2, + "c": 3 + }, + "expressions.i": 8, + "expressions.s": "hello" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/expressions-task/source.wdl b/wdl-engine/tests/tasks/expressions-task/source.wdl new file mode 100644 index 00000000..d8ac8e98 --- /dev/null +++ b/wdl-engine/tests/tasks/expressions-task/source.wdl @@ -0,0 +1,23 @@ +version 1.2 + +task expressions { + input { + Int x + } + + command <<< + printf "hello" > hello.txt + >>> + + output { + # simple expressions + Float f = 1 + 2.2 + Boolean b = if 1 > 2 then true else false + Map[String, Int] m = as_map(zip(["a", "b", "c"], [1, 2, 3])) + + # non-simple expressions + Int i = x + 3 # requires knowing the value of x + # requires reading a file that might only exist at runtime + String s = read_string("hello.txt") + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/expressions-task/stderr b/wdl-engine/tests/tasks/expressions-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/expressions-task/stdout b/wdl-engine/tests/tasks/expressions-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/file-output-task/files/foo.goodbye b/wdl-engine/tests/tasks/file-output-task/files/foo.goodbye new file mode 100644 index 00000000..a21e91b1 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/files/foo.goodbye @@ -0,0 +1 @@ +goodbye \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-output-task/files/foo.hello b/wdl-engine/tests/tasks/file-output-task/files/foo.hello new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/files/foo.hello @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-output-task/inputs.json b/wdl-engine/tests/tasks/file-output-task/inputs.json new file mode 100644 index 00000000..65a748b3 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/inputs.json @@ -0,0 +1,3 @@ +{ + "file_output.prefix": "foo" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-output-task/outputs.json b/wdl-engine/tests/tasks/file-output-task/outputs.json new file mode 100644 index 00000000..ea0fb010 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/outputs.json @@ -0,0 +1,6 @@ +{ + "file_output.basenames": [ + "foo.hello", + "foo.goodbye" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-output-task/source.wdl b/wdl-engine/tests/tasks/file-output-task/source.wdl new file mode 100644 index 00000000..22615e35 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task file_output { + input { + String prefix + } + + command <<< + printf "hello" > ~{prefix}.hello + printf "goodbye" > ~{prefix}.goodbye + >>> + + output { + Array[String] basenames = [basename("~{prefix}.hello"), basename("~{prefix}.goodbye")] + } +} diff --git a/wdl-engine/tests/tasks/file-output-task/stderr b/wdl-engine/tests/tasks/file-output-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/file-output-task/stdout b/wdl-engine/tests/tasks/file-output-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/flags-task/greetings.txt b/wdl-engine/tests/tasks/flags-task/greetings.txt new file mode 100644 index 00000000..2d4bd240 --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/greetings.txt @@ -0,0 +1,3 @@ +hello, friend! +and also... +hello world! diff --git a/wdl-engine/tests/tasks/flags-task/inputs.json b/wdl-engine/tests/tasks/flags-task/inputs.json new file mode 100644 index 00000000..f8553df2 --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/inputs.json @@ -0,0 +1,4 @@ +{ + "flags.infile": "greetings.txt", + "flags.pattern": "hello" +} diff --git a/wdl-engine/tests/tasks/flags-task/outputs.json b/wdl-engine/tests/tasks/flags-task/outputs.json new file mode 100644 index 00000000..004b0ebe --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/outputs.json @@ -0,0 +1,3 @@ +{ + "flags.num_matches": 2 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/flags-task/source.wdl b/wdl-engine/tests/tasks/flags-task/source.wdl new file mode 100644 index 00000000..94a7e579 --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/source.wdl @@ -0,0 +1,24 @@ +version 1.2 + +task flags { + input { + File infile + String pattern + Int? max_matches + } + + command <<< + # If `max_matches` is `None`, the command + # grep -m ~{max_matches} ~{pattern} ~{infile} + # would evaluate to + # 'grep -m ', which would be an error. + + # Instead, make both the flag and the value conditional on `max_matches` + # being defined. + grep ~{"-m " + max_matches} ~{pattern} ~{infile} | wc -l | sed 's/^ *//' + >>> + + output { + Int num_matches = read_int(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/flags-task/stderr b/wdl-engine/tests/tasks/flags-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/flags-task/stdout b/wdl-engine/tests/tasks/flags-task/stdout new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/stdout @@ -0,0 +1 @@ +2 diff --git a/wdl-engine/tests/tasks/glob-task/files/file_1.txt b/wdl-engine/tests/tasks/glob-task/files/file_1.txt new file mode 100644 index 00000000..56a6051c --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/files/file_1.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/files/file_2.txt b/wdl-engine/tests/tasks/glob-task/files/file_2.txt new file mode 100644 index 00000000..d8263ee9 --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/files/file_2.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/files/file_3.txt b/wdl-engine/tests/tasks/glob-task/files/file_3.txt new file mode 100644 index 00000000..e440e5c8 --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/files/file_3.txt @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/inputs.json b/wdl-engine/tests/tasks/glob-task/inputs.json new file mode 100644 index 00000000..68f23d0d --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/inputs.json @@ -0,0 +1,3 @@ +{ + "glob.num_files": 3 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/outputs.json b/wdl-engine/tests/tasks/glob-task/outputs.json new file mode 100644 index 00000000..0da7cfc3 --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/outputs.json @@ -0,0 +1,8 @@ +{ + "glob.outfiles": [ + "work/file_1.txt", + "work/file_2.txt", + "work/file_3.txt" + ], + "glob.last_file_contents": 3 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/source.wdl b/wdl-engine/tests/tasks/glob-task/source.wdl new file mode 100644 index 00000000..d6ec765f --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task glob { + input { + Int num_files + } + + command <<< + for i in {1..~{num_files}}; do + printf ${i} > file_${i}.txt + done + >>> + + output { + Array[File] outfiles = glob("*.txt") + Int last_file_contents = read_int(outfiles[num_files-1]) + } +} diff --git a/wdl-engine/tests/tasks/glob-task/stderr b/wdl-engine/tests/tasks/glob-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/glob-task/stdout b/wdl-engine/tests/tasks/glob-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/hello/greetings.txt b/wdl-engine/tests/tasks/hello/greetings.txt new file mode 100644 index 00000000..2d4bd240 --- /dev/null +++ b/wdl-engine/tests/tasks/hello/greetings.txt @@ -0,0 +1,3 @@ +hello, friend! +and also... +hello world! diff --git a/wdl-engine/tests/tasks/hello/inputs.json b/wdl-engine/tests/tasks/hello/inputs.json new file mode 100644 index 00000000..98ca8633 --- /dev/null +++ b/wdl-engine/tests/tasks/hello/inputs.json @@ -0,0 +1,4 @@ +{ + "hello_task.infile": "greetings.txt", + "hello_task.pattern": "hello.*" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/hello/outputs.json b/wdl-engine/tests/tasks/hello/outputs.json new file mode 100644 index 00000000..d5706a09 --- /dev/null +++ b/wdl-engine/tests/tasks/hello/outputs.json @@ -0,0 +1,6 @@ +{ + "hello_task.matches": [ + "hello, friend!", + "hello world!" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/hello/source.wdl b/wdl-engine/tests/tasks/hello/source.wdl new file mode 100644 index 00000000..8a12d2ec --- /dev/null +++ b/wdl-engine/tests/tasks/hello/source.wdl @@ -0,0 +1,20 @@ +version 1.2 + +task hello_task { + input { + File infile + String pattern + } + + command <<< + grep -E '~{pattern}' '~{infile}' + >>> + + requirements { + container: "ubuntu:latest" + } + + output { + Array[String] matches = read_lines(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/hello/stderr b/wdl-engine/tests/tasks/hello/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/hello/stdout b/wdl-engine/tests/tasks/hello/stdout new file mode 100644 index 00000000..0896e598 --- /dev/null +++ b/wdl-engine/tests/tasks/hello/stdout @@ -0,0 +1,2 @@ +hello, friend! +hello world! diff --git a/wdl-engine/tests/tasks/import-structs/inputs.json b/wdl-engine/tests/tasks/import-structs/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/import-structs/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/import-structs/outputs.json b/wdl-engine/tests/tasks/import-structs/outputs.json new file mode 100644 index 00000000..064251c6 --- /dev/null +++ b/wdl-engine/tests/tasks/import-structs/outputs.json @@ -0,0 +1,3 @@ +{ + "calculate_bill.bill": 175000.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/import-structs/source.wdl b/wdl-engine/tests/tasks/import-structs/source.wdl new file mode 100644 index 00000000..0958d385 --- /dev/null +++ b/wdl-engine/tests/tasks/import-structs/source.wdl @@ -0,0 +1,78 @@ +version 1.2 + +import "../person-struct-task/source.wdl" + alias Person as Patient + alias Income as PatientIncome + +# This struct has the same name as a struct in 'structs.wdl', +# but they have identical definitions so an alias is not required. +struct Name { + String first + String last +} + +# This struct also has the same name as a struct in 'structs.wdl', +# but their definitions are different, so it was necessary to +# import the struct under a different name. +struct Income { + Float dollars + Boolean annual +} + +struct Person { + Int age + Name name + Float? height + Income income +} + +task calculate_bill { + input { + Person doctor = Person { + age: 10, + name: Name { + first: "Joe", + last: "Josephs" + }, + income: Income { + dollars: 140000, + annual: true + } + } + + Patient patient = Patient { + name: Name { + first: "Bill", + last: "Williamson" + }, + age: 42, + income: PatientIncome { + amount: 350, + currency: "Yen", + period: "hourly" + }, + assay_data: { + "glucose": "hello.txt" + } + } + + PatientIncome average_income = PatientIncome { + amount: 50000, + currency: "USD", + period: "annually" + } + } + + PatientIncome income = select_first([patient.income, average_income]) + String currency = select_first([income.currency, "USD"]) + Float hourly_income = if income.period == "hourly" then income.amount else income.amount / 2000 + Float hourly_income_usd = if currency == "USD" then hourly_income else hourly_income * 100 + + command <<< + printf "The patient makes $~{hourly_income_usd} per hour\n" + >>> + + output { + Float bill = hourly_income_usd * 5 + } +} diff --git a/wdl-engine/tests/tasks/import-structs/stderr b/wdl-engine/tests/tasks/import-structs/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/import-structs/stdout b/wdl-engine/tests/tasks/import-structs/stdout new file mode 100644 index 00000000..ef92ce81 --- /dev/null +++ b/wdl-engine/tests/tasks/import-structs/stdout @@ -0,0 +1 @@ +The patient makes 5000.000000 per hour diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/files/result b/wdl-engine/tests/tasks/input-type-qualifiers/files/result new file mode 100644 index 00000000..b1e67221 --- /dev/null +++ b/wdl-engine/tests/tasks/input-type-qualifiers/files/result @@ -0,0 +1,3 @@ +A +B +C diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/inputs.json b/wdl-engine/tests/tasks/input-type-qualifiers/inputs.json new file mode 100644 index 00000000..ed1534d8 --- /dev/null +++ b/wdl-engine/tests/tasks/input-type-qualifiers/inputs.json @@ -0,0 +1,10 @@ +{ + "input_type_quantifiers.a": [], + "input_type_quantifiers.b": [ + "A", + "B" + ], + "input_type_quantifiers.e": [ + "C" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/outputs.json b/wdl-engine/tests/tasks/input-type-qualifiers/outputs.json new file mode 100644 index 00000000..31d0a272 --- /dev/null +++ b/wdl-engine/tests/tasks/input-type-qualifiers/outputs.json @@ -0,0 +1,7 @@ +{ + "input_type_quantifiers.lines": [ + "A", + "B", + "C" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl b/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl new file mode 100644 index 00000000..cabeb568 --- /dev/null +++ b/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl @@ -0,0 +1,33 @@ +version 1.2 + +task input_type_quantifiers { + input { + Array[String] a + Array[String]+ b + Array[String]? c + # If the next line were uncommented it would cause an error + # + only applies to Array, not File + #File+ d + # An optional array that, if defined, must contain at least one element + Array[String]+? e + } + + command <<< + cat ~{write_lines(a)} >> result + cat ~{write_lines(b)} >> result + ~{if defined(c) then + "cat ~{write_lines(select_first([c]))} >> result" + else ""} + ~{if defined(e) then + "cat ~{write_lines(select_first([e]))} >> result" + else ""} + >>> + + output { + Array[String] lines = read_lines("result") + } + + requirements { + container: "ubuntu:latest" + } +} diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/stderr b/wdl-engine/tests/tasks/input-type-qualifiers/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/stdout b/wdl-engine/tests/tasks/input-type-qualifiers/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/member-access/inputs.json b/wdl-engine/tests/tasks/member-access/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/member-access/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/member-access/outputs.json b/wdl-engine/tests/tasks/member-access/outputs.json new file mode 100644 index 00000000..6810fc8b --- /dev/null +++ b/wdl-engine/tests/tasks/member-access/outputs.json @@ -0,0 +1,4 @@ +{ + "foo.bar": "bar", + "foo.hello": "hello" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/member-access/source.wdl b/wdl-engine/tests/tasks/member-access/source.wdl new file mode 100644 index 00000000..07707b29 --- /dev/null +++ b/wdl-engine/tests/tasks/member-access/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +struct MyType { + String s +} + +task foo { + command <<< + printf "bar" + >>> + + MyType my = MyType { s: "hello" } + + output { + String bar = read_string(stdout()) + String hello = my.s + } +} diff --git a/wdl-engine/tests/tasks/member-access/stderr b/wdl-engine/tests/tasks/member-access/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/member-access/stdout b/wdl-engine/tests/tasks/member-access/stdout new file mode 100644 index 00000000..ba0e162e --- /dev/null +++ b/wdl-engine/tests/tasks/member-access/stdout @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/wdl-engine/tests/tasks/missing-output-file/error.txt b/wdl-engine/tests/tasks/missing-output-file/error.txt new file mode 100644 index 00000000..220976e6 --- /dev/null +++ b/wdl-engine/tests/tasks/missing-output-file/error.txt @@ -0,0 +1,9 @@ +error: failed to evaluate output `foo` for task `test` + +Caused by: + file `work/foo.txt` does not exist + ┌─ tests/tasks/missing-output-file/source.wdl:9:14 + │ +9 │ File foo = "foo.txt" + │ ^^^ + diff --git a/wdl-engine/tests/tasks/missing-output-file/inputs.json b/wdl-engine/tests/tasks/missing-output-file/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/missing-output-file/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/missing-output-file/source.wdl b/wdl-engine/tests/tasks/missing-output-file/source.wdl new file mode 100644 index 00000000..210619ce --- /dev/null +++ b/wdl-engine/tests/tasks/missing-output-file/source.wdl @@ -0,0 +1,11 @@ +version 1.2 + +task test { + command <<< + echo this task forgot to write to foo.txt! + >>> + + output { + File foo = "foo.txt" + } +} diff --git a/wdl-engine/tests/tasks/missing-output-file/stderr b/wdl-engine/tests/tasks/missing-output-file/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/missing-output-file/stdout b/wdl-engine/tests/tasks/missing-output-file/stdout new file mode 100644 index 00000000..30d90451 --- /dev/null +++ b/wdl-engine/tests/tasks/missing-output-file/stdout @@ -0,0 +1 @@ +this task forgot to write to foo.txt! diff --git a/wdl-engine/tests/tasks/multiline-placeholders/inputs.json b/wdl-engine/tests/tasks/multiline-placeholders/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-placeholders/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/multiline-placeholders/outputs.json b/wdl-engine/tests/tasks/multiline-placeholders/outputs.json new file mode 100644 index 00000000..728d70ee --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-placeholders/outputs.json @@ -0,0 +1,6 @@ +{ + "multiline_strings.spaces": " ", + "multiline_strings.name": "Henry", + "multiline_strings.company": "Acme", + "multiline_strings.multi_line": " Hello Henry,/n Welcome to Acme!" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-placeholders/source.wdl b/wdl-engine/tests/tasks/multiline-placeholders/source.wdl new file mode 100644 index 00000000..4d64d339 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-placeholders/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task multiline_strings { + command <<<>>> + + output { + String spaces = " " + String name = "Henry" + String company = "Acme" + # This string evaluates to: " Hello Henry,\n Welcome to Acme!" + # The string still has spaces because the placeholders are evaluated after removing the + # common leading whitespace. + String multi_line = <<< + ~{spaces}Hello ~{name}, + ~{spaces}Welcome to ~{company}! + >>> + } +} diff --git a/wdl-engine/tests/tasks/multiline-placeholders/stderr b/wdl-engine/tests/tasks/multiline-placeholders/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-placeholders/stdout b/wdl-engine/tests/tasks/multiline-placeholders/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings1/inputs.json b/wdl-engine/tests/tasks/multiline-strings1/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings1/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/multiline-strings1/outputs.json b/wdl-engine/tests/tasks/multiline-strings1/outputs.json new file mode 100644 index 00000000..f9d0dd61 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings1/outputs.json @@ -0,0 +1,3 @@ +{ + "multiline_strings1.s": "This is a/nmulti-line string!" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings1/source.wdl b/wdl-engine/tests/tasks/multiline-strings1/source.wdl new file mode 100644 index 00000000..805097ee --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings1/source.wdl @@ -0,0 +1,12 @@ +version 1.2 + +task multiline_strings1 { + command <<<>>> + + output { + String s = <<< + This is a + multi-line string! + >>> + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings1/stderr b/wdl-engine/tests/tasks/multiline-strings1/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings1/stdout b/wdl-engine/tests/tasks/multiline-strings1/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings2/inputs.json b/wdl-engine/tests/tasks/multiline-strings2/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings2/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/multiline-strings2/outputs.json b/wdl-engine/tests/tasks/multiline-strings2/outputs.json new file mode 100644 index 00000000..03dc2eb9 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings2/outputs.json @@ -0,0 +1,10 @@ +{ + "multiline_strings2.hw0": "hello world", + "multiline_strings2.hw1": "hello world", + "multiline_strings2.hw2": "hello world", + "multiline_strings2.hw3": "hello world", + "multiline_strings2.hw4": "hello world", + "multiline_strings2.hw5": "hello world", + "multiline_strings2.hw6": "hello world", + "multiline_strings2.not_equivalent": "hello ///n world" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings2/source.wdl b/wdl-engine/tests/tasks/multiline-strings2/source.wdl new file mode 100644 index 00000000..67bfe37d --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings2/source.wdl @@ -0,0 +1,34 @@ +version 1.2 + +task multiline_strings2 { + command <<<>>> + output { + # all of these strings evaluate to "hello world" + String hw0 = "hello world" + String hw1 = <<>> + String hw2 = <<< hello world >>> + String hw3 = <<< + hello world>>> + String hw4 = <<< + hello world + >>> + String hw5 = <<< + hello world + >>> + # The line continuation causes the newline and all whitespace preceding 'world' to be + # removed - to put two spaces between 'hello' and world' we need to put them before + # the line continuation. + String hw6 = <<< + hello \ + world + >>> + + # This string is not equivalent - the first line ends in two backslashes, which is an + # escaped backslash, not a line continuation. So this string evaluates to + # "hello \\\n world". + String not_equivalent = <<< + hello \\ + world + >>> + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings2/stderr b/wdl-engine/tests/tasks/multiline-strings2/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings2/stdout b/wdl-engine/tests/tasks/multiline-strings2/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings3/inputs.json b/wdl-engine/tests/tasks/multiline-strings3/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings3/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/multiline-strings3/outputs.json b/wdl-engine/tests/tasks/multiline-strings3/outputs.json new file mode 100644 index 00000000..502e89b9 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings3/outputs.json @@ -0,0 +1,6 @@ +{ + "multiline_strings3.multi_line_A": "/nthis is a/n/n multi-line string/n", + "multiline_strings3.multi_line_B": "/nthis is a/n/n multi-line string/n", + "multiline_strings3.multi_line_C": "/nthis is a/n/n multi-line string/n", + "multiline_strings3.multi_line_D": "/nthis is a/n/n multi-line string/n" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings3/source.wdl b/wdl-engine/tests/tasks/multiline-strings3/source.wdl new file mode 100644 index 00000000..0817000b --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings3/source.wdl @@ -0,0 +1,39 @@ +version 1.2 + +task multiline_strings3 { + command <<<>>> + output { + # These strings are all equivalent. In strings B, C, and D, the middle lines are blank and + # so do not count towards the common leading whitespace determination. + + String multi_line_A = "\nthis is a\n\n multi-line string\n" + + # This string's common leading whitespace is 0. + String multi_line_B = <<< + + this is a + + multi-line string + + >>> + + # This string's common leading whitespace is 2. The middle blank line contains two spaces + # that are also removed. + String multi_line_C = <<< + + this is a + + multi-line string + + >>> + + # This string's common leading whitespace is 8. + String multi_line_D = <<< + + this is a + + multi-line string + + >>> + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings3/stderr b/wdl-engine/tests/tasks/multiline-strings3/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings3/stdout b/wdl-engine/tests/tasks/multiline-strings3/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings4/inputs.json b/wdl-engine/tests/tasks/multiline-strings4/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings4/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/multiline-strings4/outputs.json b/wdl-engine/tests/tasks/multiline-strings4/outputs.json new file mode 100644 index 00000000..28a4a788 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings4/outputs.json @@ -0,0 +1,3 @@ +{ + "multiline_strings4.multi_line_with_quotes": "multi-line string with 'single' and /"double/" quotes" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings4/source.wdl b/wdl-engine/tests/tasks/multiline-strings4/source.wdl new file mode 100644 index 00000000..e72cc812 --- /dev/null +++ b/wdl-engine/tests/tasks/multiline-strings4/source.wdl @@ -0,0 +1,11 @@ +version 1.2 + +task multiline_strings4 { + command <<<>>> + output { + String multi_line_with_quotes = <<< + multi-line string \ + with 'single' and "double" quotes + >>> + } +} diff --git a/wdl-engine/tests/tasks/multiline-strings4/stderr b/wdl-engine/tests/tasks/multiline-strings4/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings4/stdout b/wdl-engine/tests/tasks/multiline-strings4/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-access/inputs.json b/wdl-engine/tests/tasks/nested-access/inputs.json new file mode 100644 index 00000000..581a6269 --- /dev/null +++ b/wdl-engine/tests/tasks/nested-access/inputs.json @@ -0,0 +1,26 @@ +{ + "nested_access.my_experiments": [ + { + "id": "mouse_size", + "variables": [ + "name", + "height" + ], + "data": { + "name": "Pinky", + "height": 7 + } + }, + { + "id": "pig_weight", + "variables": [ + "name", + "weight" + ], + "data": { + "name": "Porky", + "weight": 1000 + } + } + ] +} diff --git a/wdl-engine/tests/tasks/nested-access/outputs.json b/wdl-engine/tests/tasks/nested-access/outputs.json new file mode 100644 index 00000000..1f28018d --- /dev/null +++ b/wdl-engine/tests/tasks/nested-access/outputs.json @@ -0,0 +1,6 @@ +{ + "nested_access.first_var": "name", + "nested_access.first_var_from_first_experiment": "name", + "nested_access.subject_name": "Pinky", + "nested_access.subject_name_from_first_experiment": "Pinky" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/nested-access/source.wdl b/wdl-engine/tests/tasks/nested-access/source.wdl new file mode 100644 index 00000000..d1ae26c7 --- /dev/null +++ b/wdl-engine/tests/tasks/nested-access/source.wdl @@ -0,0 +1,27 @@ +version 1.2 + +struct Experiment { + String id + Array[String] variables + Object data +} + +task nested_access { + input { + Array[Experiment]+ my_experiments + } + + Experiment first_experiment = my_experiments[0] + + command <<<>>> + + output { + # these are equivalent + String first_var = first_experiment.variables[0] + String first_var_from_first_experiment = my_experiments[0].variables[0] + + # these are equivalent + String subject_name = first_experiment.data.name + String subject_name_from_first_experiment = my_experiments[0].data.name + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/nested-access/stderr b/wdl-engine/tests/tasks/nested-access/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-access/stdout b/wdl-engine/tests/tasks/nested-access/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-placeholders/inputs.json b/wdl-engine/tests/tasks/nested-placeholders/inputs.json new file mode 100644 index 00000000..ca1f455b --- /dev/null +++ b/wdl-engine/tests/tasks/nested-placeholders/inputs.json @@ -0,0 +1,4 @@ +{ + "nested_placeholders.i": 3, + "nested_placeholders.b": true +} diff --git a/wdl-engine/tests/tasks/nested-placeholders/outputs.json b/wdl-engine/tests/tasks/nested-placeholders/outputs.json new file mode 100644 index 00000000..884736fd --- /dev/null +++ b/wdl-engine/tests/tasks/nested-placeholders/outputs.json @@ -0,0 +1,3 @@ +{ + "nested_placeholders.s": "4" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/nested-placeholders/source.wdl b/wdl-engine/tests/tasks/nested-placeholders/source.wdl new file mode 100644 index 00000000..9e830baf --- /dev/null +++ b/wdl-engine/tests/tasks/nested-placeholders/source.wdl @@ -0,0 +1,14 @@ +version 1.2 + +task nested_placeholders { + input { + Int i + Boolean b + } + + command <<<>>> + + output { + String s = "~{if b then '~{1 + i}' else '0'}" + } +} diff --git a/wdl-engine/tests/tasks/nested-placeholders/stderr b/wdl-engine/tests/tasks/nested-placeholders/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-placeholders/stdout b/wdl-engine/tests/tasks/nested-placeholders/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/non-empty-optional/inputs.json b/wdl-engine/tests/tasks/non-empty-optional/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/non-empty-optional/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/non-empty-optional/outputs.json b/wdl-engine/tests/tasks/non-empty-optional/outputs.json new file mode 100644 index 00000000..ff701a6b --- /dev/null +++ b/wdl-engine/tests/tasks/non-empty-optional/outputs.json @@ -0,0 +1,13 @@ +{ + "non_empty_optional.nonempty1": [ + 0.0 + ], + "non_empty_optional.nonempty2": [ + null, + 1 + ], + "non_empty_optional.nonempty3": null, + "non_empty_optional.nonempty4": [ + 0 + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/non-empty-optional/source.wdl b/wdl-engine/tests/tasks/non-empty-optional/source.wdl new file mode 100644 index 00000000..957298d3 --- /dev/null +++ b/wdl-engine/tests/tasks/non-empty-optional/source.wdl @@ -0,0 +1,15 @@ +version 1.2 + +task non_empty_optional { + command <<<>>> + + output { + # array that must contain at least one Float + Array[Float]+ nonempty1 = [0.0] + # array that must contain at least one Int? (which may have an undefined value) + Array[Int?]+ nonempty2 = [None, 1] + # array that can be undefined or must contain at least one Int + Array[Int]+? nonempty3 = None + Array[Int]+? nonempty4 = [0] + } +} diff --git a/wdl-engine/tests/tasks/non-empty-optional/stderr b/wdl-engine/tests/tasks/non-empty-optional/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/non-empty-optional/stdout b/wdl-engine/tests/tasks/non-empty-optional/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/optional-output-task/files/example1.txt b/wdl-engine/tests/tasks/optional-output-task/files/example1.txt new file mode 100644 index 00000000..56a6051c --- /dev/null +++ b/wdl-engine/tests/tasks/optional-output-task/files/example1.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optional-output-task/inputs.json b/wdl-engine/tests/tasks/optional-output-task/inputs.json new file mode 100644 index 00000000..7d04d330 --- /dev/null +++ b/wdl-engine/tests/tasks/optional-output-task/inputs.json @@ -0,0 +1,3 @@ +{ + "optional_output.make_example2": false +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optional-output-task/outputs.json b/wdl-engine/tests/tasks/optional-output-task/outputs.json new file mode 100644 index 00000000..8b5bb8ff --- /dev/null +++ b/wdl-engine/tests/tasks/optional-output-task/outputs.json @@ -0,0 +1,9 @@ +{ + "optional_output.example1": "work/example1.txt", + "optional_output.example2": null, + "optional_output.file_array": [ + "work/example1.txt", + null + ], + "optional_output.file_array_len": 1 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optional-output-task/source.wdl b/wdl-engine/tests/tasks/optional-output-task/source.wdl new file mode 100644 index 00000000..11d162cf --- /dev/null +++ b/wdl-engine/tests/tasks/optional-output-task/source.wdl @@ -0,0 +1,21 @@ +version 1.2 + +task optional_output { + input { + Boolean make_example2 + } + + command <<< + printf "1" > example1.txt + if ~{make_example2}; then + printf "2" > example2.txt + fi + >>> + + output { + File example1 = "example1.txt" + File? example2 = "example2.txt" + Array[File?] file_array = ["example1.txt", "example2.txt"] + Int file_array_len = length(select_all(file_array)) + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optional-output-task/stderr b/wdl-engine/tests/tasks/optional-output-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/optional-output-task/stdout b/wdl-engine/tests/tasks/optional-output-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/optionals/inputs.json b/wdl-engine/tests/tasks/optionals/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/optionals/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/optionals/outputs.json b/wdl-engine/tests/tasks/optionals/outputs.json new file mode 100644 index 00000000..3c821935 --- /dev/null +++ b/wdl-engine/tests/tasks/optionals/outputs.json @@ -0,0 +1,7 @@ +{ + "optionals.test_defined": false, + "optionals.test_defined2": true, + "optionals.test_is_none": true, + "optionals.test_not_none": false, + "optionals.test_non_equal": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optionals/source.wdl b/wdl-engine/tests/tasks/optionals/source.wdl new file mode 100644 index 00000000..11f17b29 --- /dev/null +++ b/wdl-engine/tests/tasks/optionals/source.wdl @@ -0,0 +1,22 @@ +version 1.2 + +task optionals { + input { + Int certainly_five = 5 # an non-optional declaration + Int? maybe_five_and_is = 5 # a defined optional declaration + + # the following are equivalent undefined optional declarations + String? maybe_five_but_is_not + String? also_maybe_five_but_is_not = None + } + + command <<<>>> + + output { + Boolean test_defined = defined(maybe_five_but_is_not) # Evaluates to false + Boolean test_defined2 = defined(maybe_five_and_is) # Evaluates to true + Boolean test_is_none = maybe_five_but_is_not == None # Evaluates to true + Boolean test_not_none = maybe_five_but_is_not != None # Evaluates to false + Boolean test_non_equal = maybe_five_but_is_not == also_maybe_five_but_is_not + } +} diff --git a/wdl-engine/tests/tasks/optionals/stderr b/wdl-engine/tests/tasks/optionals/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/optionals/stdout b/wdl-engine/tests/tasks/optionals/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/outputs-task/files/a.csv b/wdl-engine/tests/tasks/outputs-task/files/a.csv new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/outputs-task/files/b.csv b/wdl-engine/tests/tasks/outputs-task/files/b.csv new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/outputs-task/files/threshold.txt b/wdl-engine/tests/tasks/outputs-task/files/threshold.txt new file mode 100644 index 00000000..7813681f --- /dev/null +++ b/wdl-engine/tests/tasks/outputs-task/files/threshold.txt @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/outputs-task/inputs.json b/wdl-engine/tests/tasks/outputs-task/inputs.json new file mode 100644 index 00000000..880328ad --- /dev/null +++ b/wdl-engine/tests/tasks/outputs-task/inputs.json @@ -0,0 +1,3 @@ +{ + "outputs.t": 5 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/outputs-task/outputs.json b/wdl-engine/tests/tasks/outputs-task/outputs.json new file mode 100644 index 00000000..4070df5a --- /dev/null +++ b/wdl-engine/tests/tasks/outputs-task/outputs.json @@ -0,0 +1,8 @@ +{ + "outputs.threshold": 5, + "outputs.csvs": [ + "work/a.csv", + "work/b.csv" + ], + "outputs.two_csvs": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/outputs-task/source.wdl b/wdl-engine/tests/tasks/outputs-task/source.wdl new file mode 100644 index 00000000..7e5cc7bb --- /dev/null +++ b/wdl-engine/tests/tasks/outputs-task/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task outputs { + input { + Int t + } + + command <<< + printf ~{t} > threshold.txt + touch a.csv b.csv + >>> + + output { + Int threshold = read_int("threshold.txt") + Array[File]+ csvs = glob("*.csv") + Boolean two_csvs = length(csvs) == 2 + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/outputs-task/stderr b/wdl-engine/tests/tasks/outputs-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/outputs-task/stdout b/wdl-engine/tests/tasks/outputs-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/person-struct-task/inputs.json b/wdl-engine/tests/tasks/person-struct-task/inputs.json new file mode 100644 index 00000000..08fae68c --- /dev/null +++ b/wdl-engine/tests/tasks/person-struct-task/inputs.json @@ -0,0 +1,16 @@ +{ + "greet_person.person": { + "name": { + "first": "Richard", + "last": "Rich" + }, + "age": 14, + "income": { + "amount": 1000000, + "period": "annually" + }, + "assay_data": { + "wealthitis": "hello.txt" + } + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/person-struct-task/outputs.json b/wdl-engine/tests/tasks/person-struct-task/outputs.json new file mode 100644 index 00000000..b9611a21 --- /dev/null +++ b/wdl-engine/tests/tasks/person-struct-task/outputs.json @@ -0,0 +1,3 @@ +{ + "greet_person.message": "Hello Richard! You have 1 test result(s) available./nPlease transfer USD 500 to continue" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/person-struct-task/source.wdl b/wdl-engine/tests/tasks/person-struct-task/source.wdl new file mode 100644 index 00000000..7b78610d --- /dev/null +++ b/wdl-engine/tests/tasks/person-struct-task/source.wdl @@ -0,0 +1,53 @@ +version 1.2 + +struct Name { + String first + String last +} + +struct Income { + Int amount + String period + String? currency +} + +struct Person { + Name name + Int age + Income? income + Map[String, File] assay_data + + meta { + description: "Encapsulates data about a person" + } + + parameter_meta { + name: "The person's name" + age: "The person's age" + income: "How much the person makes (optional)" + assay_data: "Mapping of assay name to the file that contains the assay data" + } +} + +task greet_person { + input { + Person person + } + + Array[Pair[String, File]] assay_array = as_pairs(person.assay_data) + + command <<< + printf "Hello ~{person.name.first}! You have ~{length(assay_array)} test result(s) available.\n" + + if ~{defined(person.income)}; then + if [ "~{select_first([person.income]).amount}" -gt 1000 ]; then + currency="~{select_first([select_first([person.income]).currency, "USD"])}" + printf "Please transfer $currency 500 to continue" + fi + fi + >>> + + output { + String message = read_string(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/person-struct-task/stderr b/wdl-engine/tests/tasks/person-struct-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/person-struct-task/stdout b/wdl-engine/tests/tasks/person-struct-task/stdout new file mode 100644 index 00000000..31a24489 --- /dev/null +++ b/wdl-engine/tests/tasks/person-struct-task/stdout @@ -0,0 +1,2 @@ +Hello Richard! You have 1 test result(s) available. +Please transfer USD 500 to continue \ No newline at end of file diff --git a/wdl-engine/tests/tasks/placeholder-coercion/inputs.json b/wdl-engine/tests/tasks/placeholder-coercion/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-coercion/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/placeholder-coercion/outputs.json b/wdl-engine/tests/tasks/placeholder-coercion/outputs.json new file mode 100644 index 00000000..cc62424c --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-coercion/outputs.json @@ -0,0 +1,9 @@ +{ + "placeholder_coercion.is_true1": true, + "placeholder_coercion.is_true2": true, + "placeholder_coercion.is_true3": true, + "placeholder_coercion.is_true4": true, + "placeholder_coercion.is_true5": true, + "placeholder_coercion.is_true6": true, + "placeholder_coercion.is_true7": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/placeholder-coercion/source.wdl b/wdl-engine/tests/tasks/placeholder-coercion/source.wdl new file mode 100644 index 00000000..3bbac6ad --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-coercion/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task placeholder_coercion { + File x = "/hij" + Int? i = None + + command <<<>>> + + output { + Boolean is_true1 = "~{"abc"}" == "abc" + Boolean is_true2 = "~{x}" == "/hij" + Boolean is_true3 = "~{5}" == "5" + Boolean is_true4 = "~{3.141}" == "3.141000" + Boolean is_true5 = "~{3.141 * 1E-10}" == "0.000000" + Boolean is_true6 = "~{3.141 * 1E10}" == "31410000000.000000" + Boolean is_true7 = "~{i}" == "" + } +} diff --git a/wdl-engine/tests/tasks/placeholder-coercion/stderr b/wdl-engine/tests/tasks/placeholder-coercion/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholder-coercion/stdout b/wdl-engine/tests/tasks/placeholder-coercion/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholder-none/inputs.json b/wdl-engine/tests/tasks/placeholder-none/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-none/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/placeholder-none/outputs.json b/wdl-engine/tests/tasks/placeholder-none/outputs.json new file mode 100644 index 00000000..d977bafd --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-none/outputs.json @@ -0,0 +1,4 @@ +{ + "placeholder_none.foo": null, + "placeholder_none.s": "Foo is " +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/placeholder-none/source.wdl b/wdl-engine/tests/tasks/placeholder-none/source.wdl new file mode 100644 index 00000000..c16276f5 --- /dev/null +++ b/wdl-engine/tests/tasks/placeholder-none/source.wdl @@ -0,0 +1,13 @@ +version 1.2 + +task placeholder_none { + command <<<>>> + + output { + String? foo = None + # The expression in this string results in an error (calling `select_first` on an array + # containing no non-`None` values) and so the placeholder evaluates to the empty string and + # `s` evalutes to: "Foo is " + String s = "Foo is ~{select_first([foo])}" + } +} diff --git a/wdl-engine/tests/tasks/placeholder-none/stderr b/wdl-engine/tests/tasks/placeholder-none/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholder-none/stdout b/wdl-engine/tests/tasks/placeholder-none/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholders/inputs.json b/wdl-engine/tests/tasks/placeholders/inputs.json new file mode 100644 index 00000000..5472e758 --- /dev/null +++ b/wdl-engine/tests/tasks/placeholders/inputs.json @@ -0,0 +1,5 @@ +{ + "placeholders.start": "h", + "placeholders.end": "o", + "placeholders.instr": "hello" +} diff --git a/wdl-engine/tests/tasks/placeholders/outputs.json b/wdl-engine/tests/tasks/placeholders/outputs.json new file mode 100644 index 00000000..9f369bd0 --- /dev/null +++ b/wdl-engine/tests/tasks/placeholders/outputs.json @@ -0,0 +1,4 @@ +{ + "placeholders.s": "4", + "placeholders.cmd": "grep 'h...o' hello" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/placeholders/source.wdl b/wdl-engine/tests/tasks/placeholders/source.wdl new file mode 100644 index 00000000..b38d0cbc --- /dev/null +++ b/wdl-engine/tests/tasks/placeholders/source.wdl @@ -0,0 +1,17 @@ +version 1.2 + +task placeholders { + input { + Int i = 3 + String start + String end + String instr + } + + command <<<>>> + + output { + String s = "~{1 + i}" + String cmd = "grep '~{start}...~{end}' ~{instr}" + } +} diff --git a/wdl-engine/tests/tasks/placeholders/stderr b/wdl-engine/tests/tasks/placeholders/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholders/stdout b/wdl-engine/tests/tasks/placeholders/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/primitive-literals/files/testdir/hello.txt b/wdl-engine/tests/tasks/primitive-literals/files/testdir/hello.txt new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-literals/files/testdir/hello.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/wdl-engine/tests/tasks/primitive-literals/inputs.json b/wdl-engine/tests/tasks/primitive-literals/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-literals/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/primitive-literals/outputs.json b/wdl-engine/tests/tasks/primitive-literals/outputs.json new file mode 100644 index 00000000..94cc5a20 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-literals/outputs.json @@ -0,0 +1,8 @@ +{ + "write_file_task.b": true, + "write_file_task.i": 0, + "write_file_task.f": 27.3, + "write_file_task.s": "hello, world", + "write_file_task.x": "work/testdir/hello.txt", + "write_file_task.d": "work/testdir" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/primitive-literals/source.wdl b/wdl-engine/tests/tasks/primitive-literals/source.wdl new file mode 100644 index 00000000..9410b166 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-literals/source.wdl @@ -0,0 +1,17 @@ +version 1.2 + +task write_file_task { + command <<< + mkdir -p testdir + printf "hello" > testdir/hello.txt + >>> + + output { + Boolean b = true + Int i = 0 + Float f = 27.3 + String s = "hello, world" + File x = "testdir/hello.txt" + Directory d = "testdir" + } +} diff --git a/wdl-engine/tests/tasks/primitive-literals/stderr b/wdl-engine/tests/tasks/primitive-literals/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/primitive-literals/stdout b/wdl-engine/tests/tasks/primitive-literals/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/primitive-to-string/inputs.json b/wdl-engine/tests/tasks/primitive-to-string/inputs.json new file mode 100644 index 00000000..702a495b --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-to-string/inputs.json @@ -0,0 +1,3 @@ +{ + "primitive_to_string.i": 3 +} diff --git a/wdl-engine/tests/tasks/primitive-to-string/outputs.json b/wdl-engine/tests/tasks/primitive-to-string/outputs.json new file mode 100644 index 00000000..9356f072 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-to-string/outputs.json @@ -0,0 +1,3 @@ +{ + "primitive_to_string.istring": "3" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/primitive-to-string/source.wdl b/wdl-engine/tests/tasks/primitive-to-string/source.wdl new file mode 100644 index 00000000..fb6b773e --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-to-string/source.wdl @@ -0,0 +1,13 @@ +version 1.2 + +task primitive_to_string { + input { + Int i = 5 + } + + command <<<>>> + + output { + String istring = "~{i}" + } +} diff --git a/wdl-engine/tests/tasks/primitive-to-string/stderr b/wdl-engine/tests/tasks/primitive-to-string/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/primitive-to-string/stdout b/wdl-engine/tests/tasks/primitive-to-string/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/private-declaration-task/inputs.json b/wdl-engine/tests/tasks/private-declaration-task/inputs.json new file mode 100644 index 00000000..2cdd3417 --- /dev/null +++ b/wdl-engine/tests/tasks/private-declaration-task/inputs.json @@ -0,0 +1,8 @@ +{ + "private_declaration.lines": [ + "A", + "B", + "C", + "D" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/private-declaration-task/outputs.json b/wdl-engine/tests/tasks/private-declaration-task/outputs.json new file mode 100644 index 00000000..8ec0763f --- /dev/null +++ b/wdl-engine/tests/tasks/private-declaration-task/outputs.json @@ -0,0 +1,7 @@ +{ + "private_declaration.out_lines": [ + "A", + "B", + "C" + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/private-declaration-task/source.wdl b/wdl-engine/tests/tasks/private-declaration-task/source.wdl new file mode 100644 index 00000000..1bcd2a70 --- /dev/null +++ b/wdl-engine/tests/tasks/private-declaration-task/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task private_declaration { + input { + Array[String] lines + } + + Int num_lines = length(lines) + Int num_lines_clamped = if num_lines > 3 then 3 else num_lines + + command <<< + head -~{num_lines_clamped} ~{write_lines(lines)} + >>> + + output { + Array[String] out_lines = read_lines(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/private-declaration-task/stderr b/wdl-engine/tests/tasks/private-declaration-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/private-declaration-task/stdout b/wdl-engine/tests/tasks/private-declaration-task/stdout new file mode 100644 index 00000000..b1e67221 --- /dev/null +++ b/wdl-engine/tests/tasks/private-declaration-task/stdout @@ -0,0 +1,3 @@ +A +B +C diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/files/my/path/to/something.txt b/wdl-engine/tests/tasks/relative-and-absolute-task/files/my/path/to/something.txt new file mode 100644 index 00000000..a459bc24 --- /dev/null +++ b/wdl-engine/tests/tasks/relative-and-absolute-task/files/my/path/to/something.txt @@ -0,0 +1 @@ +something \ No newline at end of file diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/inputs.json b/wdl-engine/tests/tasks/relative-and-absolute-task/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/relative-and-absolute-task/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/outputs.json b/wdl-engine/tests/tasks/relative-and-absolute-task/outputs.json new file mode 100644 index 00000000..cb82c961 --- /dev/null +++ b/wdl-engine/tests/tasks/relative-and-absolute-task/outputs.json @@ -0,0 +1,3 @@ +{ + "relative_and_absolute.something": "something" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/source.wdl b/wdl-engine/tests/tasks/relative-and-absolute-task/source.wdl new file mode 100644 index 00000000..97e81c43 --- /dev/null +++ b/wdl-engine/tests/tasks/relative-and-absolute-task/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task relative_and_absolute { + command <<< + mkdir -p my/path/to + printf "something" > my/path/to/something.txt + >>> + + output { + String something = read_string("my/path/to/something.txt") + } + + requirements { + container: "ubuntu:focal" + } +} diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/stderr b/wdl-engine/tests/tasks/relative-and-absolute-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/stdout b/wdl-engine/tests/tasks/relative-and-absolute-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/seo-option-to-function/inputs.json b/wdl-engine/tests/tasks/seo-option-to-function/inputs.json new file mode 100644 index 00000000..26ebe211 --- /dev/null +++ b/wdl-engine/tests/tasks/seo-option-to-function/inputs.json @@ -0,0 +1,12 @@ +{ + "sep_option_to_function.str_array": [ + "A", + "B", + "C" + ], + "sep_option_to_function.int_array": [ + 1, + 2, + 3 + ] +} diff --git a/wdl-engine/tests/tasks/seo-option-to-function/outputs.json b/wdl-engine/tests/tasks/seo-option-to-function/outputs.json new file mode 100644 index 00000000..27de4bf4 --- /dev/null +++ b/wdl-engine/tests/tasks/seo-option-to-function/outputs.json @@ -0,0 +1,4 @@ +{ + "sep_option_to_function.is_true1": true, + "sep_option_to_function.is_true2": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/seo-option-to-function/source.wdl b/wdl-engine/tests/tasks/seo-option-to-function/source.wdl new file mode 100644 index 00000000..fcb39542 --- /dev/null +++ b/wdl-engine/tests/tasks/seo-option-to-function/source.wdl @@ -0,0 +1,15 @@ +version 1.2 + +task sep_option_to_function { + input { + Array[String] str_array + Array[Int] int_array + } + + command <<<>>> + + output { + Boolean is_true1 = "~{sep(' ', str_array)}" == "~{sep=' ' str_array}" + Boolean is_true2 = "~{sep(',', quote(int_array))}" == "~{sep=',' quote(int_array)}" + } +} diff --git a/wdl-engine/tests/tasks/seo-option-to-function/stderr b/wdl-engine/tests/tasks/seo-option-to-function/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/seo-option-to-function/stdout b/wdl-engine/tests/tasks/seo-option-to-function/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/string-to-file/inputs.json b/wdl-engine/tests/tasks/string-to-file/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/string-to-file/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/string-to-file/outputs.json b/wdl-engine/tests/tasks/string-to-file/outputs.json new file mode 100644 index 00000000..712ab42d --- /dev/null +++ b/wdl-engine/tests/tasks/string-to-file/outputs.json @@ -0,0 +1,3 @@ +{ + "string_to_file.paths_equal": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/string-to-file/source.wdl b/wdl-engine/tests/tasks/string-to-file/source.wdl new file mode 100644 index 00000000..f7e8f6b4 --- /dev/null +++ b/wdl-engine/tests/tasks/string-to-file/source.wdl @@ -0,0 +1,15 @@ +version 1.2 + +task string_to_file { + String path1 = "/path/to/file" + File path2 = "/path/to/file" + + # valid - String coerces unambiguously to File + File path3 = path1 + + command <<<>>> + + output { + Boolean paths_equal = path2 == path3 + } +} diff --git a/wdl-engine/tests/tasks/string-to-file/stderr b/wdl-engine/tests/tasks/string-to-file/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/string-to-file/stdout b/wdl-engine/tests/tasks/string-to-file/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/struct-to-struct/inputs.json b/wdl-engine/tests/tasks/struct-to-struct/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/struct-to-struct/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/struct-to-struct/outputs.json b/wdl-engine/tests/tasks/struct-to-struct/outputs.json new file mode 100644 index 00000000..10299a3d --- /dev/null +++ b/wdl-engine/tests/tasks/struct-to-struct/outputs.json @@ -0,0 +1,8 @@ +{ + "struct_to_struct.my_d": { + "a_struct": { + "s": "hello" + }, + "i": 10 + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/struct-to-struct/source.wdl b/wdl-engine/tests/tasks/struct-to-struct/source.wdl new file mode 100644 index 00000000..33a9250b --- /dev/null +++ b/wdl-engine/tests/tasks/struct-to-struct/source.wdl @@ -0,0 +1,36 @@ +version 1.2 + +struct A { + String s +} + +struct B { + A a_struct + Int i +} + +struct C { + String s +} + +struct D { + C a_struct + Int i +} + +task struct_to_struct { + B my_b = B { + a_struct: A { s: 'hello' }, + i: 10 + } + # We can coerce `my_b` from type `B` to type `D` because `B` and `D` + # have members with the same names and compatible types. Type `A` can + # be coerced to type `C` because they also have members with the same + # names and compatible types. + + command <<<>>> + + output { + D my_d = my_b + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/struct-to-struct/stderr b/wdl-engine/tests/tasks/struct-to-struct/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/struct-to-struct/stdout b/wdl-engine/tests/tasks/struct-to-struct/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/sum-task/inputs.json b/wdl-engine/tests/tasks/sum-task/inputs.json new file mode 100644 index 00000000..25cc7300 --- /dev/null +++ b/wdl-engine/tests/tasks/sum-task/inputs.json @@ -0,0 +1,7 @@ +{ + "sum.ints": [ + "1", + "2", + "3" + ] +} diff --git a/wdl-engine/tests/tasks/sum-task/outputs.json b/wdl-engine/tests/tasks/sum-task/outputs.json new file mode 100644 index 00000000..fc637452 --- /dev/null +++ b/wdl-engine/tests/tasks/sum-task/outputs.json @@ -0,0 +1,3 @@ +{ + "sum.total": 6 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/sum-task/source.wdl b/wdl-engine/tests/tasks/sum-task/source.wdl new file mode 100644 index 00000000..971e796f --- /dev/null +++ b/wdl-engine/tests/tasks/sum-task/source.wdl @@ -0,0 +1,16 @@ +## Example from https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#arrayx +version 1.2 + +task sum { + input { + Array[String]+ ints + } + + command <<< + printf '~{sep(" ", ints)}' | awk '{tot=0; for(i=1;i<=NF;i++) tot+=$i; print tot}' + >>> + + output { + Int total = read_int(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/sum-task/stderr b/wdl-engine/tests/tasks/sum-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/sum-task/stdout b/wdl-engine/tests/tasks/sum-task/stdout new file mode 100644 index 00000000..1e8b3149 --- /dev/null +++ b/wdl-engine/tests/tasks/sum-task/stdout @@ -0,0 +1 @@ +6 diff --git a/wdl-engine/tests/tasks/task-fail/error.txt b/wdl-engine/tests/tasks/task-fail/error.txt new file mode 100644 index 00000000..031c4b52 --- /dev/null +++ b/wdl-engine/tests/tasks/task-fail/error.txt @@ -0,0 +1 @@ +task process has terminated with status code 1; see standard error output `stderr` \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-fail/inputs.json b/wdl-engine/tests/tasks/task-fail/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/task-fail/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/task-fail/source.wdl b/wdl-engine/tests/tasks/task-fail/source.wdl new file mode 100644 index 00000000..0979acec --- /dev/null +++ b/wdl-engine/tests/tasks/task-fail/source.wdl @@ -0,0 +1,8 @@ +version 1.2 + +task test { + command <<< + >&2 echo this task is going to fail! + exit 1 + >>> +} diff --git a/wdl-engine/tests/tasks/task-fail/stderr b/wdl-engine/tests/tasks/task-fail/stderr new file mode 100644 index 00000000..5991015e --- /dev/null +++ b/wdl-engine/tests/tasks/task-fail/stderr @@ -0,0 +1 @@ +this task is going to fail! diff --git a/wdl-engine/tests/tasks/task-fail/stdout b/wdl-engine/tests/tasks/task-fail/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/task-inputs-task/inputs.json b/wdl-engine/tests/tasks/task-inputs-task/inputs.json new file mode 100644 index 00000000..799aeda0 --- /dev/null +++ b/wdl-engine/tests/tasks/task-inputs-task/inputs.json @@ -0,0 +1,3 @@ +{ + "task_inputs.i": 1 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-inputs-task/outputs.json b/wdl-engine/tests/tasks/task-inputs-task/outputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/task-inputs-task/outputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-inputs-task/source.wdl b/wdl-engine/tests/tasks/task-inputs-task/source.wdl new file mode 100644 index 00000000..bb7e1c4f --- /dev/null +++ b/wdl-engine/tests/tasks/task-inputs-task/source.wdl @@ -0,0 +1,19 @@ +version 1.2 + +task task_inputs { + input { + Int i # a required input parameter + String s = "hello" # an input parameter with a default value + File? f # an optional input parameter + Directory? d = "/etc" # an optional input parameter with a default value + } + + command <<< + for i in 1..~{i}; do + printf "~{s}\n" + done + if ~{defined(f)}; then + cat ~{f} + fi + >>> +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-inputs-task/stderr b/wdl-engine/tests/tasks/task-inputs-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/task-inputs-task/stdout b/wdl-engine/tests/tasks/task-inputs-task/stdout new file mode 100644 index 00000000..ce013625 --- /dev/null +++ b/wdl-engine/tests/tasks/task-inputs-task/stdout @@ -0,0 +1 @@ +hello diff --git a/wdl-engine/tests/tasks/task-with-comments/inputs.json b/wdl-engine/tests/tasks/task-with-comments/inputs.json new file mode 100644 index 00000000..2601aded --- /dev/null +++ b/wdl-engine/tests/tasks/task-with-comments/inputs.json @@ -0,0 +1,3 @@ +{ + "task_with_comments.number": 1 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-with-comments/outputs.json b/wdl-engine/tests/tasks/task-with-comments/outputs.json new file mode 100644 index 00000000..f98cb27a --- /dev/null +++ b/wdl-engine/tests/tasks/task-with-comments/outputs.json @@ -0,0 +1,3 @@ +{ + "task_with_comments.result": 2 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-with-comments/source.wdl b/wdl-engine/tests/tasks/task-with-comments/source.wdl new file mode 100644 index 00000000..261bca26 --- /dev/null +++ b/wdl-engine/tests/tasks/task-with-comments/source.wdl @@ -0,0 +1,27 @@ +# Comments are allowed before version +version 1.2 + +# This is how you +# write a long +# multiline +# comment + +task task_with_comments { + input { + Int number # This comment comes after a variable declaration + } + + # This comment will not be included within the command + command <<< + # This comment WILL be included within the command after it has been parsed + echo ~{number * 2} + >>> + + output { + Int result = read_int(stdout()) + } + + requirements { + container: "ubuntu:latest" + } +} diff --git a/wdl-engine/tests/tasks/task-with-comments/stderr b/wdl-engine/tests/tasks/task-with-comments/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/task-with-comments/stdout b/wdl-engine/tests/tasks/task-with-comments/stdout new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/wdl-engine/tests/tasks/task-with-comments/stdout @@ -0,0 +1 @@ +2 diff --git a/wdl-engine/tests/tasks/ternary/inputs.json b/wdl-engine/tests/tasks/ternary/inputs.json new file mode 100644 index 00000000..5616984c --- /dev/null +++ b/wdl-engine/tests/tasks/ternary/inputs.json @@ -0,0 +1,3 @@ +{ + "ternary.morning": true +} diff --git a/wdl-engine/tests/tasks/ternary/outputs.json b/wdl-engine/tests/tasks/ternary/outputs.json new file mode 100644 index 00000000..f0e52a48 --- /dev/null +++ b/wdl-engine/tests/tasks/ternary/outputs.json @@ -0,0 +1,3 @@ +{ + "ternary.greeting": "good morning" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/ternary/source.wdl b/wdl-engine/tests/tasks/ternary/source.wdl new file mode 100644 index 00000000..27c9e371 --- /dev/null +++ b/wdl-engine/tests/tasks/ternary/source.wdl @@ -0,0 +1,24 @@ +version 1.2 + +task ternary { + input { + Boolean morning + Array[String] array = ["x", "y", "z"] + } + + Int array_length = length(array) + # choose how much memory to use for a task + String memory = if array_length > 100 then "2GB" else "1GB" + + command <<< + >>> + + requirements { + memory: memory + } + + output { + # Choose whether to say "good morning" or "good afternoon" + String greeting = "good ~{if morning then "morning" else "afternoon"}" + } +} diff --git a/wdl-engine/tests/tasks/ternary/stderr b/wdl-engine/tests/tasks/ternary/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/ternary/stdout b/wdl-engine/tests/tasks/ternary/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-map/inputs.json b/wdl-engine/tests/tasks/test-map/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/test-map/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/test-map/outputs.json b/wdl-engine/tests/tasks/test-map/outputs.json new file mode 100644 index 00000000..69bc7792 --- /dev/null +++ b/wdl-engine/tests/tasks/test-map/outputs.json @@ -0,0 +1,9 @@ +{ + "test_map.ten": 10, + "test_map.b": 2, + "test_map.ints": [ + 0, + 1, + 2 + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-map/source.wdl b/wdl-engine/tests/tasks/test-map/source.wdl new file mode 100644 index 00000000..811bf29e --- /dev/null +++ b/wdl-engine/tests/tasks/test-map/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task test_map { + Map[Int, Int] int_to_int = {1: 10, 2: 11} + Map[String, Int] string_to_int = { "a": 1, "b": 2 } + Map[File, Array[Int]] file_to_ints = { + "/path/to/file1": [0, 1, 2], + "/path/to/file2": [9, 8, 7] + } + + command <<<>>> + + output { + Int ten = int_to_int[1] # evaluates to 10 + Int b = string_to_int["b"] # evaluates to 2 + Array[Int] ints = file_to_ints["/path/to/file1"] # evaluates to [0, 1, 2] + } +} diff --git a/wdl-engine/tests/tasks/test-map/stderr b/wdl-engine/tests/tasks/test-map/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-map/stdout b/wdl-engine/tests/tasks/test-map/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-object/inputs.json b/wdl-engine/tests/tasks/test-object/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/test-object/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/test-object/outputs.json b/wdl-engine/tests/tasks/test-object/outputs.json new file mode 100644 index 00000000..8efdd6b8 --- /dev/null +++ b/wdl-engine/tests/tasks/test-object/outputs.json @@ -0,0 +1,7 @@ +{ + "test_object.obj": { + "a": 10, + "b": "hello" + }, + "test_object.i": 10 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-object/source.wdl b/wdl-engine/tests/tasks/test-object/source.wdl new file mode 100644 index 00000000..b0df9365 --- /dev/null +++ b/wdl-engine/tests/tasks/test-object/source.wdl @@ -0,0 +1,13 @@ +version 1.2 + +task test_object { + command <<<>>> + + output { + Object obj = object { + a: 10, + b: "hello" + } + Int i = obj.a + } +} diff --git a/wdl-engine/tests/tasks/test-object/stderr b/wdl-engine/tests/tasks/test-object/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-object/stdout b/wdl-engine/tests/tasks/test-object/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-pairs/inputs.json b/wdl-engine/tests/tasks/test-pairs/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/test-pairs/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/test-pairs/outputs.json b/wdl-engine/tests/tasks/test-pairs/outputs.json new file mode 100644 index 00000000..6fa83692 --- /dev/null +++ b/wdl-engine/tests/tasks/test-pairs/outputs.json @@ -0,0 +1,4 @@ +{ + "test_pairs.five": 5, + "test_pairs.hello": "hello" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-pairs/source.wdl b/wdl-engine/tests/tasks/test-pairs/source.wdl new file mode 100644 index 00000000..7e6709b1 --- /dev/null +++ b/wdl-engine/tests/tasks/test-pairs/source.wdl @@ -0,0 +1,12 @@ +version 1.2 + +task test_pairs { + Pair[Int, Array[String]] data = (5, ["hello", "goodbye"]) + + command <<<>>> + + output { + Int five = data.left # evaluates to 5 + String hello = data.right[0] # evaluates to "hello" + } +} diff --git a/wdl-engine/tests/tasks/test-pairs/stderr b/wdl-engine/tests/tasks/test-pairs/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-pairs/stdout b/wdl-engine/tests/tasks/test-pairs/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-placeholders-task/greetings.txt b/wdl-engine/tests/tasks/test-placeholders-task/greetings.txt new file mode 100644 index 00000000..db5baaf3 --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/greetings.txt @@ -0,0 +1,5 @@ +hello +world +hi_world +hello +friend diff --git a/wdl-engine/tests/tasks/test-placeholders-task/inputs.json b/wdl-engine/tests/tasks/test-placeholders-task/inputs.json new file mode 100644 index 00000000..13e248bc --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/inputs.json @@ -0,0 +1,3 @@ +{ + "test_placeholders.infile": "greetings.txt" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-placeholders-task/outputs.json b/wdl-engine/tests/tasks/test-placeholders-task/outputs.json new file mode 100644 index 00000000..9c20a0ba --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/outputs.json @@ -0,0 +1,3 @@ +{ + "test_placeholders.result": "hello world hi_world hello friend" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-placeholders-task/source.wdl b/wdl-engine/tests/tasks/test-placeholders-task/source.wdl new file mode 100644 index 00000000..1a2f2382 --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/source.wdl @@ -0,0 +1,20 @@ +version 1.2 + +task test_placeholders { + input { + File infile + } + + command <<< + # The `read_lines` function reads the lines from a file into an + # array. The `sep` function concatenates the lines with a space + # (" ") delimiter. The resulting string is then printed to stdout. + printf "~{sep(" ", read_lines(infile))}" + >>> + + output { + # The `stdout` function returns a file with the contents of stdout. + # The `read_string` function reads the entire file into a String. + String result = read_string(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/test-placeholders-task/stderr b/wdl-engine/tests/tasks/test-placeholders-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-placeholders-task/stdout b/wdl-engine/tests/tasks/test-placeholders-task/stdout new file mode 100644 index 00000000..3709c03e --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/stdout @@ -0,0 +1 @@ +hello world hi_world hello friend \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-struct/inputs.json b/wdl-engine/tests/tasks/test-struct/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/test-struct/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/test-struct/outputs.json b/wdl-engine/tests/tasks/test-struct/outputs.json new file mode 100644 index 00000000..8ff2a427 --- /dev/null +++ b/wdl-engine/tests/tasks/test-struct/outputs.json @@ -0,0 +1,18 @@ +{ + "test_struct.john": { + "name": "John", + "account": { + "account_number": "123456", + "routing_number": 300211325, + "balance": 3.5, + "pin_digits": [ + 1, + 2, + 3, + 4 + ], + "username": null + } + }, + "test_struct.has_account": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-struct/source.wdl b/wdl-engine/tests/tasks/test-struct/source.wdl new file mode 100644 index 00000000..97675573 --- /dev/null +++ b/wdl-engine/tests/tasks/test-struct/source.wdl @@ -0,0 +1,32 @@ +version 1.2 + +struct BankAccount { + String account_number + Int routing_number + Float balance + Array[Int]+ pin_digits + String? username +} + +struct Person { + String name + BankAccount? account +} + +task test_struct { + command <<<>>> + + output { + Person john = Person { + name: "John", + # it's okay to leave out username since it's optional + account: BankAccount { + account_number: "123456", + routing_number: 300211325, + balance: 3.50, + pin_digits: [1, 2, 3, 4] + } + } + Boolean has_account = defined(john.account) + } +} diff --git a/wdl-engine/tests/tasks/test-struct/stderr b/wdl-engine/tests/tasks/test-struct/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-struct/stdout b/wdl-engine/tests/tasks/test-struct/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/true-false-ternary/files/result1 b/wdl-engine/tests/tasks/true-false-ternary/files/result1 new file mode 100644 index 00000000..95d09f2b --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/files/result1 @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/wdl-engine/tests/tasks/true-false-ternary/files/result2 b/wdl-engine/tests/tasks/true-false-ternary/files/result2 new file mode 100644 index 00000000..95d09f2b --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/files/result2 @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/wdl-engine/tests/tasks/true-false-ternary/inputs.json b/wdl-engine/tests/tasks/true-false-ternary/inputs.json new file mode 100644 index 00000000..21cf8efe --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/inputs.json @@ -0,0 +1,4 @@ +{ + "true_false_ternary.message": "hello world", + "true_false_ternary.newline": false +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/true-false-ternary/outputs.json b/wdl-engine/tests/tasks/true-false-ternary/outputs.json new file mode 100644 index 00000000..66cfea74 --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/outputs.json @@ -0,0 +1,3 @@ +{ + "true_false_ternary.is_true": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/true-false-ternary/source.wdl b/wdl-engine/tests/tasks/true-false-ternary/source.wdl new file mode 100644 index 00000000..3f2b8101 --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/source.wdl @@ -0,0 +1,18 @@ +version 1.2 + +task true_false_ternary { + input { + String message + Boolean newline + } + + command <<< + # these two commands have the same result + printf "~{message}~{true="\n" false="" newline}" > result1 + printf "~{message}~{if newline then "\n" else ""}" > result2 + >>> + + output { + Boolean is_true = read_string("result1") == read_string("result2") + } +} diff --git a/wdl-engine/tests/tasks/true-false-ternary/stderr b/wdl-engine/tests/tasks/true-false-ternary/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/true-false-ternary/stdout b/wdl-engine/tests/tasks/true-false-ternary/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-format/tests/format.rs b/wdl-format/tests/format.rs index db573370..4c3c4b49 100644 --- a/wdl-format/tests/format.rs +++ b/wdl-format/tests/format.rs @@ -108,8 +108,9 @@ fn compare_result(path: &Path, result: &str) -> Result<(), String> { if expected != result { return Err(format!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), )); } diff --git a/wdl-grammar/tests/parsing.rs b/wdl-grammar/tests/parsing.rs index 8dda10c4..edf08c94 100644 --- a/wdl-grammar/tests/parsing.rs +++ b/wdl-grammar/tests/parsing.rs @@ -112,8 +112,9 @@ fn compare_result(path: &Path, result: &str, is_error: bool) -> Result<(), Strin if expected != result { return Err(format!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), )); } diff --git a/wdl-lint/tests/lints.rs b/wdl-lint/tests/lints.rs index d87048ca..12b4d761 100644 --- a/wdl-lint/tests/lints.rs +++ b/wdl-lint/tests/lints.rs @@ -108,8 +108,9 @@ fn compare_result(path: &Path, result: &str) -> Result<(), String> { if expected != result { return Err(format!( - "result is not as expected:\n{}", - StrComparison::new(&expected, &result), + "result from `{path}` is not as expected:\n{diff}", + path = path.display(), + diff = StrComparison::new(&expected, &result), )); } diff --git a/wdl/Cargo.toml b/wdl/Cargo.toml index e1996a06..04481f5b 100644 --- a/wdl/Cargo.toml +++ b/wdl/Cargo.toml @@ -19,13 +19,13 @@ wdl-analysis = { path = "../wdl-analysis", version = "0.5.0", optional = true } wdl-lsp = { path = "../wdl-lsp", version = "0.5.0", optional = true } wdl-format = { path = "../wdl-format", version = "0.3.0", optional = true } wdl-doc = { version = "0.0.0", path = "../wdl-doc", optional = true } -# TODO: uncomment this when `wdl-engine` is ready for release -#wdl-engine = { path = "../wdl-engine", version = "0.0.0", optional = true } +wdl-engine = { path = "../wdl-engine", version = "0.0.0", optional = true } tracing-subscriber = { workspace = true, optional = true } clap = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } colored = { workspace = true, optional = true } codespan-reporting = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } indicatif = { workspace = true, optional = true } tokio = { workspace = true, optional = true } clap-verbosity-flag = { workspace = true, optional = true } @@ -42,17 +42,17 @@ codespan-reporting = { workspace = true } default = ["ast", "grammar", "lint", "format"] analysis = ["dep:wdl-analysis"] ast = ["dep:wdl-ast"] +engine = ["dep:wdl-engine"] format = ["dep:wdl-format"] doc = ["dep:wdl-doc"] grammar = ["dep:wdl-grammar"] lint = ["dep:wdl-lint"] lsp = ["dep:wdl-lsp"] -# TOOD: uncomment this when `wdl-engine` is ready for release. -#engine = ["dep:wdl-engine"] codespan = ["ast", "wdl-ast/codespan", "dep:codespan-reporting"] cli = [ "analysis", "codespan", + "engine", "lint", "format", "doc", @@ -66,6 +66,7 @@ cli = [ "dep:tracing-log", "dep:tracing", "dep:url", + "dep:serde_json", ] [lints] diff --git a/wdl/src/bin/wdl.rs b/wdl/src/bin/wdl.rs index 53f952e6..c5bc5bbd 100644 --- a/wdl/src/bin/wdl.rs +++ b/wdl/src/bin/wdl.rs @@ -11,9 +11,11 @@ use std::io::Read; use std::io::stderr; use std::path::Path; use std::path::PathBuf; +use std::path::absolute; use anyhow::Context; use anyhow::Result; +use anyhow::anyhow; use anyhow::bail; use clap::Args; use clap::Parser; @@ -31,7 +33,6 @@ use tracing_log::AsTrace; use url::Url; use wdl::ast::Diagnostic; use wdl::ast::Document; -use wdl::ast::SyntaxNode; use wdl::ast::Validator; use wdl::lint::LintVisitor; use wdl_analysis::AnalysisResult; @@ -42,6 +43,11 @@ use wdl_analysis::rules; use wdl_ast::Node; use wdl_ast::Severity; use wdl_doc::document_workspace; +use wdl_engine::Engine; +use wdl_engine::EvaluationError; +use wdl_engine::Inputs; +use wdl_engine::local::LocalTaskExecutionBackend; +use wdl_engine::v1::TaskEvaluator; use wdl_format::Formatter; use wdl_format::element::node::AstNodeFormatExt as _; @@ -79,7 +85,7 @@ fn emit_diagnostics(path: &str, source: &str, diagnostics: &[Diagnostic]) -> Res /// Analyzes a path. async fn analyze>( rules: impl IntoIterator, - file: String, + file: &str, lint: bool, ) -> Result> { let bar = ProgressBar::new(0); @@ -106,14 +112,14 @@ async fn analyze>( }, ); - if let Ok(url) = Url::parse(&file) { + if let Ok(url) = Url::parse(file) { analyzer.add_document(url).await?; - } else if fs::metadata(&file) + } else if fs::metadata(file) .with_context(|| format!("failed to read metadata for file `{file}`"))? .is_dir() { analyzer.add_directory(file.into()).await?; - } else if let Some(url) = path_to_uri(&file) { + } else if let Some(url) = path_to_uri(file) { analyzer.add_document(url).await?; } else { bail!("failed to convert `{file}` to a URI", file = file) @@ -128,7 +134,7 @@ async fn analyze>( let mut errors = 0; let cwd = std::env::current_dir().ok(); - for result in &results { + for result in results.iter() { let path = result.document().uri().to_file_path().ok(); // Attempt to strip the CWD from the result path @@ -149,11 +155,7 @@ async fn analyze>( if !diagnostics.is_empty() { errors += emit_diagnostics( &path, - &result - .document() - .root() - .map(|n| SyntaxNode::new_root(n.clone()).text().to_string()) - .unwrap_or(String::new()), + &result.document().node().syntax().text().to_string(), &diagnostics, )?; } @@ -281,7 +283,7 @@ impl CheckCommand { /// Executes the `check` subcommand. async fn exec(self) -> Result<()> { self.options.check_for_conflicts()?; - analyze(self.options.into_rules(), self.file, false).await?; + analyze(self.options.into_rules(), &self.file, false).await?; Ok(()) } } @@ -347,7 +349,7 @@ impl AnalyzeCommand { /// Executes the `analyze` subcommand. async fn exec(self) -> Result<()> { self.options.check_for_conflicts()?; - let results = analyze(self.options.into_rules(), self.file, self.lint).await?; + let results = analyze(self.options.into_rules(), &self.file, self.lint).await?; println!("{:#?}", results); Ok(()) } @@ -408,6 +410,187 @@ impl DocCommand { } } +/// Runs a WDL workflow or task using local execution. +#[derive(Args)] +#[clap(disable_version_flag = true)] +pub struct RunCommand { + /// The path or URL to the source WDL file. + #[clap(value_name = "PATH or URL")] + pub file: String, + + /// The path to the inputs file; defaults to an empty set of inputs. + #[clap(short, long, value_name = "INPUTS", conflicts_with = "name")] + pub inputs: Option, + + /// The name of the workflow or task to run; defaults to the name specified + /// in the inputs file; required if the inputs file is not specified. + #[clap(short, long, value_name = "NAME")] + pub name: Option, + + /// The task execution output directory; defaults to the task name. + #[clap(short, long, value_name = "OUTPUT_DIR")] + pub output: Option, + + /// The analysis options. + #[clap(flatten)] + pub options: AnalysisOptions, +} + +impl RunCommand { + /// Executes the `check` subcommand. + async fn exec(self) -> Result<()> { + self.options.check_for_conflicts()?; + + if Path::new(&self.file).is_dir() { + bail!("specified path cannot be a directory"); + } + + let results = analyze(self.options.into_rules(), &self.file, false).await?; + + let uri = if let Ok(uri) = Url::parse(&self.file) { + uri + } else { + path_to_uri(&self.file).expect("file should be a local path") + }; + + let result = results + .iter() + .find(|r| **r.document().uri() == uri) + .context("failed to find document in analysis results")?; + let document = result.document(); + + // TODO: support other backends in the future + let mut engine = Engine::new(LocalTaskExecutionBackend::new()); + let (path, name, inputs) = if let Some(path) = self.inputs { + let abs_path = absolute(&path).with_context(|| { + format!( + "failed to determine the absolute path of `{path}`", + path = path.display() + ) + })?; + match Inputs::parse(engine.types_mut(), document, &abs_path)? { + Some((name, inputs)) => (Some(path), name, inputs), + None => bail!( + "inputs file `{path}` is empty; use the `--name` option to specify the name \ + of the task or workflow to run", + path = path.display() + ), + } + } else if let Some(name) = self.name { + if document.task_by_name(&name).is_some() { + (None, name, Inputs::Task(Default::default())) + } else if document.workflow().is_some() { + (None, name, Inputs::Workflow(Default::default())) + } else { + bail!("document does not contain a task or workflow named `{name}`"); + } + } else { + let mut iter = document.tasks(); + let (name, inputs) = iter + .next() + .map(|t| (t.name().to_string(), Inputs::Task(Default::default()))) + .or_else(|| { + document + .workflow() + .map(|w| (w.name().to_string(), Inputs::Workflow(Default::default()))) + }) + .context( + "inputs file is empty and the WDL document contains no tasks or workflow", + )?; + + if iter.next().is_some() { + bail!("inputs file is empty and the WDL document contains more than one task"); + } + + (None, name, inputs) + }; + + let output_dir = self + .output + .unwrap_or_else(|| Path::new(&name).to_path_buf()); + + match inputs { + Inputs::Task(mut inputs) => { + // Make any paths specified in the inputs absolute + let task = document + .task_by_name(&name) + .ok_or_else(|| anyhow!("document does not contain a task named `{name}`"))?; + + // Ensure all the paths specified in the inputs file are relative to the file's + // directory + if let Some(path) = path.as_ref().and_then(|p| p.parent()) { + inputs.join_paths(engine.types_mut(), document, task, path); + } + + let mut evaluator = TaskEvaluator::new(&mut engine); + match evaluator + .evaluate(document, task, &inputs, &output_dir) + .await + { + Ok(evaluated) => { + match evaluated.into_result() { + Ok(outputs) => { + // Buffer the entire output before writing it out in case there are + // errors during serialization. + let mut buffer = Vec::new(); + let mut serializer = serde_json::Serializer::pretty(&mut buffer); + outputs.serialize(engine.types(), &mut serializer)?; + println!( + "{buffer}\n", + buffer = std::str::from_utf8(&buffer) + .expect("output should be UTF-8") + ); + } + Err(e) => match e { + EvaluationError::Source(diagnostic) => { + emit_diagnostics( + &self.file, + &document.node().syntax().text().to_string(), + &[diagnostic], + )?; + + bail!("aborting due to task evaluation failure"); + } + EvaluationError::Other(e) => return Err(e), + }, + } + } + Err(e) => match e { + EvaluationError::Source(diagnostic) => { + emit_diagnostics( + &self.file, + &document.node().syntax().text().to_string(), + &[diagnostic], + )?; + + bail!("aborting due to task evaluation failure"); + } + EvaluationError::Other(e) => return Err(e), + }, + } + } + Inputs::Workflow(mut inputs) => { + let workflow = document + .workflow() + .ok_or_else(|| anyhow!("document does not contain a workflow"))?; + if workflow.name() != name { + bail!("document does not contain a workflow named `{name}`"); + } + + // Ensure all the paths specified in the inputs file are relative to the file's + // directory + if let Some(path) = path.as_ref().and_then(|p| p.parent()) { + inputs.join_paths(engine.types_mut(), document, workflow, path); + } + + bail!("running workflows is not yet supported") + } + } + + Ok(()) + } +} + /// A tool for parsing, validating, and linting WDL source code. /// /// This command line tool is intended as an entrypoint to work with and develop @@ -453,6 +636,9 @@ enum Command { /// Documents a workspace. Doc(DocCommand), + + /// Runs a workflow or task. + Run(RunCommand), } #[tokio::main] @@ -473,6 +659,7 @@ async fn main() -> Result<()> { Command::Analyze(cmd) => cmd.exec().await, Command::Format(cmd) => cmd.exec().await, Command::Doc(cmd) => cmd.exec().await, + Command::Run(cmd) => cmd.exec().await, } { eprintln!( "{error}: {e:?}", diff --git a/wdl/src/lib.rs b/wdl/src/lib.rs index 3498822a..752dd23f 100644 --- a/wdl/src/lib.rs +++ b/wdl/src/lib.rs @@ -85,6 +85,9 @@ pub use wdl_ast as ast; #[cfg(feature = "doc")] #[doc(inline)] pub use wdl_doc as doc; +#[cfg(feature = "engine")] +#[doc(inline)] +pub use wdl_engine as engine; #[cfg(feature = "format")] #[doc(inline)] pub use wdl_format as format; From 140e37e1c0449d18ef600397e2b228a86a17a19b Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Mon, 9 Dec 2024 20:18:18 -0500 Subject: [PATCH 03/11] fix: attempt to fix CI on Windows. --- wdl-engine/tests/tasks.rs | 23 +++++++++++-------- wdl-engine/tests/tasks/flags-task/source.wdl | 2 +- .../tasks/input-type-qualifiers/source.wdl | 8 +++---- .../tasks/multiline-strings2/outputs.json | 2 +- .../tasks/private-declaration-task/source.wdl | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs index ed80e531..84dc775d 100644 --- a/wdl-engine/tests/tasks.rs +++ b/wdl-engine/tests/tasks.rs @@ -22,7 +22,6 @@ use std::collections::HashSet; use std::env; use std::ffi::OsStr; use std::fs; -use std::path::MAIN_SEPARATOR; use std::path::Path; use std::path::PathBuf; use std::path::absolute; @@ -87,19 +86,23 @@ fn find_tests() -> Vec { /// Normalizes a result. fn normalize(root: &Path, s: &str) -> String { + // Normalize paths separation characters first + let s = s + .replace("\\", "/") + .replace("//", "/") + .replace("\r\n", "\n"); + // Strip any paths that start with the root directory - let s: Cow<'_, str> = if let Some(mut root) = root.to_str().map(str::to_string) { - if !root.ends_with(MAIN_SEPARATOR) { - root.push(MAIN_SEPARATOR); + if let Some(root) = root.to_str().map(str::to_string) { + let mut root = root.replace('\\', "/"); + if !root.ends_with('/') { + root.push('/'); } - s.replace(&root, "").into() + s.replace(&root, "") } else { - s.into() - }; - - // Normalize paths separation characters - s.replace('\\', "/").replace("\r\n", "\n") + s + } } /// Compares a single result. diff --git a/wdl-engine/tests/tasks/flags-task/source.wdl b/wdl-engine/tests/tasks/flags-task/source.wdl index 94a7e579..98e971d3 100644 --- a/wdl-engine/tests/tasks/flags-task/source.wdl +++ b/wdl-engine/tests/tasks/flags-task/source.wdl @@ -15,7 +15,7 @@ task flags { # Instead, make both the flag and the value conditional on `max_matches` # being defined. - grep ~{"-m " + max_matches} ~{pattern} ~{infile} | wc -l | sed 's/^ *//' + grep ~{"-m " + max_matches} ~{pattern} '~{infile}' | wc -l | sed 's/^ *//' >>> output { diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl b/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl index cabeb568..6f7147ca 100644 --- a/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl +++ b/wdl-engine/tests/tasks/input-type-qualifiers/source.wdl @@ -13,13 +13,13 @@ task input_type_quantifiers { } command <<< - cat ~{write_lines(a)} >> result - cat ~{write_lines(b)} >> result + cat '~{write_lines(a)}' >> result + cat '~{write_lines(b)}' >> result ~{if defined(c) then - "cat ~{write_lines(select_first([c]))} >> result" + "cat '~{write_lines(select_first([c]))}' >> result" else ""} ~{if defined(e) then - "cat ~{write_lines(select_first([e]))} >> result" + "cat '~{write_lines(select_first([e]))}' >> result" else ""} >>> diff --git a/wdl-engine/tests/tasks/multiline-strings2/outputs.json b/wdl-engine/tests/tasks/multiline-strings2/outputs.json index 03dc2eb9..905587c9 100644 --- a/wdl-engine/tests/tasks/multiline-strings2/outputs.json +++ b/wdl-engine/tests/tasks/multiline-strings2/outputs.json @@ -6,5 +6,5 @@ "multiline_strings2.hw4": "hello world", "multiline_strings2.hw5": "hello world", "multiline_strings2.hw6": "hello world", - "multiline_strings2.not_equivalent": "hello ///n world" + "multiline_strings2.not_equivalent": "hello //n world" } \ No newline at end of file diff --git a/wdl-engine/tests/tasks/private-declaration-task/source.wdl b/wdl-engine/tests/tasks/private-declaration-task/source.wdl index 1bcd2a70..2e7786aa 100644 --- a/wdl-engine/tests/tasks/private-declaration-task/source.wdl +++ b/wdl-engine/tests/tasks/private-declaration-task/source.wdl @@ -9,7 +9,7 @@ task private_declaration { Int num_lines_clamped = if num_lines > 3 then 3 else num_lines command <<< - head -~{num_lines_clamped} ~{write_lines(lines)} + head -~{num_lines_clamped} '~{write_lines(lines)}' >>> output { From 0b9909e218fcfef468daf7a881c54d8b1b1b9c2e Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Mon, 9 Dec 2024 22:11:27 -0500 Subject: [PATCH 04/11] chore: more file tests for task evaluation. These test cases come directly from the 1.2 WDL specification. --- .../change-extension-task/files/foo.data | 1 + .../change-extension-task/files/foo.index | 1 + .../tasks/change-extension-task/inputs.json | 3 ++ .../tasks/change-extension-task/outputs.json | 5 +++ .../tasks/change-extension-task/source.wdl | 22 +++++++++++ .../tests/tasks/change-extension-task/stderr | 0 .../tests/tasks/change-extension-task/stdout | 0 .../tests/tasks/file-sizes-task/error.txt | 10 +++++ .../tasks/file-sizes-task/files/created_file | 1 + .../tests/tasks/file-sizes-task/inputs.json | 1 + .../tests/tasks/file-sizes-task/outputs.json | 7 ++++ .../tests/tasks/file-sizes-task/source.wdl | 24 ++++++++++++ wdl-engine/tests/tasks/file-sizes-task/stderr | 0 wdl-engine/tests/tasks/file-sizes-task/stdout | 0 .../tests/tasks/test-basename/inputs.json | 1 + .../tests/tasks/test-basename/outputs.json | 5 +++ .../tests/tasks/test-basename/source.wdl | 11 ++++++ wdl-engine/tests/tasks/test-basename/stderr | 0 wdl-engine/tests/tasks/test-basename/stdout | 0 wdl-engine/tests/tasks/test-ceil/inputs.json | 3 ++ wdl-engine/tests/tasks/test-ceil/outputs.json | 6 +++ wdl-engine/tests/tasks/test-ceil/source.wdl | 17 +++++++++ wdl-engine/tests/tasks/test-ceil/stderr | 0 wdl-engine/tests/tasks/test-ceil/stdout | 0 wdl-engine/tests/tasks/test-find/inputs.json | 1 + wdl-engine/tests/tasks/test-find/outputs.json | 4 ++ wdl-engine/tests/tasks/test-find/source.wdl | 16 ++++++++ wdl-engine/tests/tasks/test-find/stderr | 0 wdl-engine/tests/tasks/test-find/stdout | 0 wdl-engine/tests/tasks/test-floor/inputs.json | 3 ++ .../tests/tasks/test-floor/outputs.json | 6 +++ wdl-engine/tests/tasks/test-floor/source.wdl | 17 +++++++++ wdl-engine/tests/tasks/test-floor/stderr | 0 wdl-engine/tests/tasks/test-floor/stdout | 0 .../test-join-paths/files/mydir/mydata.txt | 1 + .../tests/tasks/test-join-paths/inputs.json | 1 + .../tests/tasks/test-join-paths/outputs.json | 5 +++ .../tests/tasks/test-join-paths/source.wdl | 38 +++++++++++++++++++ wdl-engine/tests/tasks/test-join-paths/stderr | 0 wdl-engine/tests/tasks/test-join-paths/stdout | 0 .../tests/tasks/test-matches/inputs.json | 3 ++ .../tests/tasks/test-matches/outputs.json | 4 ++ .../tests/tasks/test-matches/source.wdl | 14 +++++++ wdl-engine/tests/tasks/test-matches/stderr | 0 wdl-engine/tests/tasks/test-matches/stdout | 0 wdl-engine/tests/tasks/test-max/inputs.json | 4 ++ wdl-engine/tests/tasks/test-max/outputs.json | 4 ++ wdl-engine/tests/tasks/test-max/source.wdl | 16 ++++++++ wdl-engine/tests/tasks/test-max/stderr | 0 wdl-engine/tests/tasks/test-max/stdout | 0 wdl-engine/tests/tasks/test-min/inputs.json | 4 ++ wdl-engine/tests/tasks/test-min/outputs.json | 4 ++ wdl-engine/tests/tasks/test-min/source.wdl | 16 ++++++++ wdl-engine/tests/tasks/test-min/stderr | 0 wdl-engine/tests/tasks/test-min/stdout | 0 wdl-engine/tests/tasks/test-round/inputs.json | 3 ++ .../tests/tasks/test-round/outputs.json | 6 +++ wdl-engine/tests/tasks/test-round/source.wdl | 17 +++++++++ wdl-engine/tests/tasks/test-round/stderr | 0 wdl-engine/tests/tasks/test-round/stdout | 0 .../tests/tasks/test-stderr/inputs.json | 1 + .../tests/tasks/test-stderr/outputs.json | 3 ++ wdl-engine/tests/tasks/test-stderr/source.wdl | 9 +++++ wdl-engine/tests/tasks/test-stderr/stderr | 1 + wdl-engine/tests/tasks/test-stderr/stdout | 0 .../tests/tasks/test-stdout/inputs.json | 1 + .../tests/tasks/test-stdout/outputs.json | 3 ++ wdl-engine/tests/tasks/test-stdout/source.wdl | 9 +++++ wdl-engine/tests/tasks/test-stdout/stderr | 0 wdl-engine/tests/tasks/test-stdout/stdout | 1 + wdl-engine/tests/tasks/test-sub/error.txt | 9 +++++ wdl-engine/tests/tasks/test-sub/inputs.json | 1 + wdl-engine/tests/tasks/test-sub/outputs.json | 8 ++++ wdl-engine/tests/tasks/test-sub/source.wdl | 16 ++++++++ wdl-engine/tests/tasks/test-sub/stderr | 0 wdl-engine/tests/tasks/test-sub/stdout | 0 76 files changed, 367 insertions(+) create mode 100644 wdl-engine/tests/tasks/change-extension-task/files/foo.data create mode 100644 wdl-engine/tests/tasks/change-extension-task/files/foo.index create mode 100644 wdl-engine/tests/tasks/change-extension-task/inputs.json create mode 100644 wdl-engine/tests/tasks/change-extension-task/outputs.json create mode 100644 wdl-engine/tests/tasks/change-extension-task/source.wdl create mode 100644 wdl-engine/tests/tasks/change-extension-task/stderr create mode 100644 wdl-engine/tests/tasks/change-extension-task/stdout create mode 100644 wdl-engine/tests/tasks/file-sizes-task/error.txt create mode 100644 wdl-engine/tests/tasks/file-sizes-task/files/created_file create mode 100644 wdl-engine/tests/tasks/file-sizes-task/inputs.json create mode 100644 wdl-engine/tests/tasks/file-sizes-task/outputs.json create mode 100644 wdl-engine/tests/tasks/file-sizes-task/source.wdl create mode 100644 wdl-engine/tests/tasks/file-sizes-task/stderr create mode 100644 wdl-engine/tests/tasks/file-sizes-task/stdout create mode 100644 wdl-engine/tests/tasks/test-basename/inputs.json create mode 100644 wdl-engine/tests/tasks/test-basename/outputs.json create mode 100644 wdl-engine/tests/tasks/test-basename/source.wdl create mode 100644 wdl-engine/tests/tasks/test-basename/stderr create mode 100644 wdl-engine/tests/tasks/test-basename/stdout create mode 100644 wdl-engine/tests/tasks/test-ceil/inputs.json create mode 100644 wdl-engine/tests/tasks/test-ceil/outputs.json create mode 100644 wdl-engine/tests/tasks/test-ceil/source.wdl create mode 100644 wdl-engine/tests/tasks/test-ceil/stderr create mode 100644 wdl-engine/tests/tasks/test-ceil/stdout create mode 100644 wdl-engine/tests/tasks/test-find/inputs.json create mode 100644 wdl-engine/tests/tasks/test-find/outputs.json create mode 100644 wdl-engine/tests/tasks/test-find/source.wdl create mode 100644 wdl-engine/tests/tasks/test-find/stderr create mode 100644 wdl-engine/tests/tasks/test-find/stdout create mode 100644 wdl-engine/tests/tasks/test-floor/inputs.json create mode 100644 wdl-engine/tests/tasks/test-floor/outputs.json create mode 100644 wdl-engine/tests/tasks/test-floor/source.wdl create mode 100644 wdl-engine/tests/tasks/test-floor/stderr create mode 100644 wdl-engine/tests/tasks/test-floor/stdout create mode 100644 wdl-engine/tests/tasks/test-join-paths/files/mydir/mydata.txt create mode 100644 wdl-engine/tests/tasks/test-join-paths/inputs.json create mode 100644 wdl-engine/tests/tasks/test-join-paths/outputs.json create mode 100644 wdl-engine/tests/tasks/test-join-paths/source.wdl create mode 100644 wdl-engine/tests/tasks/test-join-paths/stderr create mode 100644 wdl-engine/tests/tasks/test-join-paths/stdout create mode 100644 wdl-engine/tests/tasks/test-matches/inputs.json create mode 100644 wdl-engine/tests/tasks/test-matches/outputs.json create mode 100644 wdl-engine/tests/tasks/test-matches/source.wdl create mode 100644 wdl-engine/tests/tasks/test-matches/stderr create mode 100644 wdl-engine/tests/tasks/test-matches/stdout create mode 100644 wdl-engine/tests/tasks/test-max/inputs.json create mode 100644 wdl-engine/tests/tasks/test-max/outputs.json create mode 100644 wdl-engine/tests/tasks/test-max/source.wdl create mode 100644 wdl-engine/tests/tasks/test-max/stderr create mode 100644 wdl-engine/tests/tasks/test-max/stdout create mode 100644 wdl-engine/tests/tasks/test-min/inputs.json create mode 100644 wdl-engine/tests/tasks/test-min/outputs.json create mode 100644 wdl-engine/tests/tasks/test-min/source.wdl create mode 100644 wdl-engine/tests/tasks/test-min/stderr create mode 100644 wdl-engine/tests/tasks/test-min/stdout create mode 100644 wdl-engine/tests/tasks/test-round/inputs.json create mode 100644 wdl-engine/tests/tasks/test-round/outputs.json create mode 100644 wdl-engine/tests/tasks/test-round/source.wdl create mode 100644 wdl-engine/tests/tasks/test-round/stderr create mode 100644 wdl-engine/tests/tasks/test-round/stdout create mode 100644 wdl-engine/tests/tasks/test-stderr/inputs.json create mode 100644 wdl-engine/tests/tasks/test-stderr/outputs.json create mode 100644 wdl-engine/tests/tasks/test-stderr/source.wdl create mode 100644 wdl-engine/tests/tasks/test-stderr/stderr create mode 100644 wdl-engine/tests/tasks/test-stderr/stdout create mode 100644 wdl-engine/tests/tasks/test-stdout/inputs.json create mode 100644 wdl-engine/tests/tasks/test-stdout/outputs.json create mode 100644 wdl-engine/tests/tasks/test-stdout/source.wdl create mode 100644 wdl-engine/tests/tasks/test-stdout/stderr create mode 100644 wdl-engine/tests/tasks/test-stdout/stdout create mode 100644 wdl-engine/tests/tasks/test-sub/error.txt create mode 100644 wdl-engine/tests/tasks/test-sub/inputs.json create mode 100644 wdl-engine/tests/tasks/test-sub/outputs.json create mode 100644 wdl-engine/tests/tasks/test-sub/source.wdl create mode 100644 wdl-engine/tests/tasks/test-sub/stderr create mode 100644 wdl-engine/tests/tasks/test-sub/stdout diff --git a/wdl-engine/tests/tasks/change-extension-task/files/foo.data b/wdl-engine/tests/tasks/change-extension-task/files/foo.data new file mode 100644 index 00000000..6320cd24 --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/files/foo.data @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/wdl-engine/tests/tasks/change-extension-task/files/foo.index b/wdl-engine/tests/tasks/change-extension-task/files/foo.index new file mode 100644 index 00000000..b2d525b2 --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/files/foo.index @@ -0,0 +1 @@ +index \ No newline at end of file diff --git a/wdl-engine/tests/tasks/change-extension-task/inputs.json b/wdl-engine/tests/tasks/change-extension-task/inputs.json new file mode 100644 index 00000000..2642c3ef --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/inputs.json @@ -0,0 +1,3 @@ +{ + "change_extension.prefix": "foo" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/change-extension-task/outputs.json b/wdl-engine/tests/tasks/change-extension-task/outputs.json new file mode 100644 index 00000000..e393186a --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/outputs.json @@ -0,0 +1,5 @@ +{ + "change_extension.data_file": "work/foo.data", + "change_extension.data": "data", + "change_extension.index": "index" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/change-extension-task/source.wdl b/wdl-engine/tests/tasks/change-extension-task/source.wdl new file mode 100644 index 00000000..7e2dc897 --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/source.wdl @@ -0,0 +1,22 @@ +version 1.2 + +task change_extension { + input { + String prefix + } + + command <<< + printf "data" > '~{prefix}.data' + printf "index" > '~{prefix}.index' + >>> + + output { + File data_file = "~{prefix}.data" + String data = read_string(data_file) + String index = read_string(sub(data_file, "\\.data$", ".index")) + } + + requirements { + container: "ubuntu:latest" + } +} diff --git a/wdl-engine/tests/tasks/change-extension-task/stderr b/wdl-engine/tests/tasks/change-extension-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/change-extension-task/stdout b/wdl-engine/tests/tasks/change-extension-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/file-sizes-task/error.txt b/wdl-engine/tests/tasks/file-sizes-task/error.txt new file mode 100644 index 00000000..069956c7 --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/error.txt @@ -0,0 +1,10 @@ +error: type mismatch: argument to function `size` expects type `None`, `File?`, `String?`, `Directory?`, or `X` where `X`: any compound type that recursively contains a `File` or `Directory`, but found type `Map[String, Pair[Int, String?]]` + ┌─ tests/tasks/file-sizes-task/source.wdl:14:31 + │ +14 │ Float nested_bytes = size({ + │ ╭───────────────────────────────^ +15 │ │ "a": (10, "created_file"), +16 │ │ "b": (50, missing_file) +17 │ │ }) + │ ╰─────^ this is type `Map[String, Pair[Int, String?]]` + diff --git a/wdl-engine/tests/tasks/file-sizes-task/files/created_file b/wdl-engine/tests/tasks/file-sizes-task/files/created_file new file mode 100644 index 00000000..4e63abf3 --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/files/created_file @@ -0,0 +1 @@ +this file is 22 bytes diff --git a/wdl-engine/tests/tasks/file-sizes-task/inputs.json b/wdl-engine/tests/tasks/file-sizes-task/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-sizes-task/outputs.json b/wdl-engine/tests/tasks/file-sizes-task/outputs.json new file mode 100644 index 00000000..4f4fd74f --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/outputs.json @@ -0,0 +1,7 @@ +{ + "file_sizes.created_file": "work/created_file", + "file_sizes.missing_file_bytes": 0.0, + "file_sizes.created_file_bytes": 22.0, + "file_sizes.multi_file_kb": 0.022, + "file_sizes.nested_bytes": 22.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-sizes-task/source.wdl b/wdl-engine/tests/tasks/file-sizes-task/source.wdl new file mode 100644 index 00000000..6949c17c --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/source.wdl @@ -0,0 +1,24 @@ +version 1.2 + +task file_sizes { + command <<< + printf "this file is 22 bytes\n" > created_file + >>> + + File? missing_file = None + + output { + File created_file = "created_file" + Float missing_file_bytes = size(missing_file) + Float created_file_bytes = size(created_file, "B") + Float multi_file_kb = size([created_file, missing_file], "K") + Float nested_bytes = size({ + "a": (10, created_file), + "b": (50, missing_file) + }) + } + + requirements { + container: "ubuntu:latest" + } +} diff --git a/wdl-engine/tests/tasks/file-sizes-task/stderr b/wdl-engine/tests/tasks/file-sizes-task/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/file-sizes-task/stdout b/wdl-engine/tests/tasks/file-sizes-task/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-basename/inputs.json b/wdl-engine/tests/tasks/test-basename/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/test-basename/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-basename/outputs.json b/wdl-engine/tests/tasks/test-basename/outputs.json new file mode 100644 index 00000000..1c027fbe --- /dev/null +++ b/wdl-engine/tests/tasks/test-basename/outputs.json @@ -0,0 +1,5 @@ +{ + "test_basename.is_true1": true, + "test_basename.is_true2": true, + "test_basename.is_true3": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-basename/source.wdl b/wdl-engine/tests/tasks/test-basename/source.wdl new file mode 100644 index 00000000..e6ebbd24 --- /dev/null +++ b/wdl-engine/tests/tasks/test-basename/source.wdl @@ -0,0 +1,11 @@ +version 1.2 + +task test_basename { + command <<<>>> + + output { + Boolean is_true1 = basename("/path/to/file.txt") == "file.txt" + Boolean is_true2 = basename("/path/to/file.txt", ".txt") == "file" + Boolean is_true3 = basename("/path/to/dir") == "dir" + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-basename/stderr b/wdl-engine/tests/tasks/test-basename/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-basename/stdout b/wdl-engine/tests/tasks/test-basename/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-ceil/inputs.json b/wdl-engine/tests/tasks/test-ceil/inputs.json new file mode 100644 index 00000000..aeeea6d3 --- /dev/null +++ b/wdl-engine/tests/tasks/test-ceil/inputs.json @@ -0,0 +1,3 @@ +{ + "test_ceil.i1": 2 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-ceil/outputs.json b/wdl-engine/tests/tasks/test-ceil/outputs.json new file mode 100644 index 00000000..e4d39962 --- /dev/null +++ b/wdl-engine/tests/tasks/test-ceil/outputs.json @@ -0,0 +1,6 @@ +{ + "test_ceil.all_true": [ + true, + true + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-ceil/source.wdl b/wdl-engine/tests/tasks/test-ceil/source.wdl new file mode 100644 index 00000000..f9aee51f --- /dev/null +++ b/wdl-engine/tests/tasks/test-ceil/source.wdl @@ -0,0 +1,17 @@ +version 1.2 + +task test_ceil { + input { + Int i1 + } + + Int i2 = i1 + 1 + Float f1 = i1 + Float f2 = i1 + 0.1 + + command <<<>>> + + output { + Array[Boolean] all_true = [ceil(f1) == i1, ceil(f2) == i2] + } +} diff --git a/wdl-engine/tests/tasks/test-ceil/stderr b/wdl-engine/tests/tasks/test-ceil/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-ceil/stdout b/wdl-engine/tests/tasks/test-ceil/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-find/inputs.json b/wdl-engine/tests/tasks/test-find/inputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/wdl-engine/tests/tasks/test-find/inputs.json @@ -0,0 +1 @@ +{} diff --git a/wdl-engine/tests/tasks/test-find/outputs.json b/wdl-engine/tests/tasks/test-find/outputs.json new file mode 100644 index 00000000..17c52550 --- /dev/null +++ b/wdl-engine/tests/tasks/test-find/outputs.json @@ -0,0 +1,4 @@ +{ + "find_string.match1": "ello", + "find_string.match2": null +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-find/source.wdl b/wdl-engine/tests/tasks/test-find/source.wdl new file mode 100644 index 00000000..5e6cd3fd --- /dev/null +++ b/wdl-engine/tests/tasks/test-find/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task find_string { + input { + String in = "hello world" + String pattern1 = "e..o" + String pattern2 = "goodbye" + } + + command <<<>>> + + output { + String? match1 = find(in, pattern1) # "ello" + String? match2 = find(in, pattern2) # None + } +} diff --git a/wdl-engine/tests/tasks/test-find/stderr b/wdl-engine/tests/tasks/test-find/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-find/stdout b/wdl-engine/tests/tasks/test-find/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-floor/inputs.json b/wdl-engine/tests/tasks/test-floor/inputs.json new file mode 100644 index 00000000..53a46472 --- /dev/null +++ b/wdl-engine/tests/tasks/test-floor/inputs.json @@ -0,0 +1,3 @@ +{ + "test_floor.i1": 2 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-floor/outputs.json b/wdl-engine/tests/tasks/test-floor/outputs.json new file mode 100644 index 00000000..49a2f560 --- /dev/null +++ b/wdl-engine/tests/tasks/test-floor/outputs.json @@ -0,0 +1,6 @@ +{ + "test_floor.all_true": [ + true, + true + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-floor/source.wdl b/wdl-engine/tests/tasks/test-floor/source.wdl new file mode 100644 index 00000000..1df138cb --- /dev/null +++ b/wdl-engine/tests/tasks/test-floor/source.wdl @@ -0,0 +1,17 @@ +version 1.2 + +task test_floor { + input { + Int i1 + } + + Int i2 = i1 - 1 + Float f1 = i1 + Float f2 = i1 - 0.1 + + command <<<>>> + + output { + Array[Boolean] all_true = [floor(f1) == i1, floor(f2) == i2] + } +} diff --git a/wdl-engine/tests/tasks/test-floor/stderr b/wdl-engine/tests/tasks/test-floor/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-floor/stdout b/wdl-engine/tests/tasks/test-floor/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-join-paths/files/mydir/mydata.txt b/wdl-engine/tests/tasks/test-join-paths/files/mydir/mydata.txt new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/wdl-engine/tests/tasks/test-join-paths/files/mydir/mydata.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-join-paths/inputs.json b/wdl-engine/tests/tasks/test-join-paths/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/test-join-paths/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-join-paths/outputs.json b/wdl-engine/tests/tasks/test-join-paths/outputs.json new file mode 100644 index 00000000..21f11291 --- /dev/null +++ b/wdl-engine/tests/tasks/test-join-paths/outputs.json @@ -0,0 +1,5 @@ +{ + "resolve_paths_task.bins_equal": true, + "resolve_paths_task.result": "hello", + "resolve_paths_task.missing_file": null +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-join-paths/source.wdl b/wdl-engine/tests/tasks/test-join-paths/source.wdl new file mode 100644 index 00000000..c99eb981 --- /dev/null +++ b/wdl-engine/tests/tasks/test-join-paths/source.wdl @@ -0,0 +1,38 @@ +version 1.2 + +task resolve_paths_task { + input { + File abs_file = "/usr" + String abs_str = "/usr" + String rel_dir_str = "bin" + File rel_file = "echo" + File rel_dir_file = "mydir" + String rel_str = "mydata.txt" + } + + # these are all equivalent to '/usr/bin/echo' + File bin1 = join_paths(abs_file, [rel_dir_str, rel_file]) + File bin2 = join_paths(abs_str, [rel_dir_str, rel_file]) + File bin3 = join_paths([abs_str, rel_dir_str, rel_file]) + + # the default behavior is that this resolves to + # '/mydir/mydata.txt' + File data = join_paths(rel_dir_file, rel_str) + + # this resolves to '/bin/echo', which is non-existent + File doesnt_exist = join_paths([rel_dir_str, rel_file]) + command <<< + mkdir '~{rel_dir_file}' + echo -n "hello" > '~{data}' + >>> + + output { + Boolean bins_equal = (bin1 == bin2) && (bin1 == bin3) + String result = read_string(data) + File? missing_file = doesnt_exist + } + + runtime { + container: "ubuntu:latest" + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-join-paths/stderr b/wdl-engine/tests/tasks/test-join-paths/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-join-paths/stdout b/wdl-engine/tests/tasks/test-join-paths/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-matches/inputs.json b/wdl-engine/tests/tasks/test-matches/inputs.json new file mode 100644 index 00000000..d03ec750 --- /dev/null +++ b/wdl-engine/tests/tasks/test-matches/inputs.json @@ -0,0 +1,3 @@ +{ + "contains_string.fastq": "sample1234_R1.fastq" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-matches/outputs.json b/wdl-engine/tests/tasks/test-matches/outputs.json new file mode 100644 index 00000000..c77f6b3e --- /dev/null +++ b/wdl-engine/tests/tasks/test-matches/outputs.json @@ -0,0 +1,4 @@ +{ + "contains_string.is_compressed": false, + "contains_string.is_read1": true +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-matches/source.wdl b/wdl-engine/tests/tasks/test-matches/source.wdl new file mode 100644 index 00000000..a1a43540 --- /dev/null +++ b/wdl-engine/tests/tasks/test-matches/source.wdl @@ -0,0 +1,14 @@ +version 1.2 + +task contains_string { + input { + File fastq + } + + command <<<>>> + + output { + Boolean is_compressed = matches(basename(fastq), "\\.(gz|zip|zstd)") + Boolean is_read1 = matches(basename(fastq), "_R1") + } +} diff --git a/wdl-engine/tests/tasks/test-matches/stderr b/wdl-engine/tests/tasks/test-matches/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-matches/stdout b/wdl-engine/tests/tasks/test-matches/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-max/inputs.json b/wdl-engine/tests/tasks/test-max/inputs.json new file mode 100644 index 00000000..bcc3991d --- /dev/null +++ b/wdl-engine/tests/tasks/test-max/inputs.json @@ -0,0 +1,4 @@ +{ + "test_max.value1": 1, + "test_max.value2": 2.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-max/outputs.json b/wdl-engine/tests/tasks/test-max/outputs.json new file mode 100644 index 00000000..1cb15bd6 --- /dev/null +++ b/wdl-engine/tests/tasks/test-max/outputs.json @@ -0,0 +1,4 @@ +{ + "test_max.max1": 2.0, + "test_max.max2": 2.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-max/source.wdl b/wdl-engine/tests/tasks/test-max/source.wdl new file mode 100644 index 00000000..72f90762 --- /dev/null +++ b/wdl-engine/tests/tasks/test-max/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task test_max { + input { + Int value1 + Float value2 + } + + command <<<>>> + + output { + # these two expressions are equivalent + Float max1 = if value1 > value2 then value1 else value2 + Float max2 = max(value1, value2) + } +} diff --git a/wdl-engine/tests/tasks/test-max/stderr b/wdl-engine/tests/tasks/test-max/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-max/stdout b/wdl-engine/tests/tasks/test-max/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-min/inputs.json b/wdl-engine/tests/tasks/test-min/inputs.json new file mode 100644 index 00000000..af215b7c --- /dev/null +++ b/wdl-engine/tests/tasks/test-min/inputs.json @@ -0,0 +1,4 @@ +{ + "test_min.value1": 1, + "test_min.value2": 2.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-min/outputs.json b/wdl-engine/tests/tasks/test-min/outputs.json new file mode 100644 index 00000000..d902c083 --- /dev/null +++ b/wdl-engine/tests/tasks/test-min/outputs.json @@ -0,0 +1,4 @@ +{ + "test_min.min1": 1.0, + "test_min.min2": 1.0 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-min/source.wdl b/wdl-engine/tests/tasks/test-min/source.wdl new file mode 100644 index 00000000..b090c82c --- /dev/null +++ b/wdl-engine/tests/tasks/test-min/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task test_min { + input { + Int value1 + Float value2 + } + + command <<<>>> + + output { + # these two expressions are equivalent + Float min1 = if value1 < value2 then value1 else value2 + Float min2 = min(value1, value2) + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-min/stderr b/wdl-engine/tests/tasks/test-min/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-min/stdout b/wdl-engine/tests/tasks/test-min/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-round/inputs.json b/wdl-engine/tests/tasks/test-round/inputs.json new file mode 100644 index 00000000..8389aa87 --- /dev/null +++ b/wdl-engine/tests/tasks/test-round/inputs.json @@ -0,0 +1,3 @@ +{ + "test_round.i1": 2 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-round/outputs.json b/wdl-engine/tests/tasks/test-round/outputs.json new file mode 100644 index 00000000..320cd5d5 --- /dev/null +++ b/wdl-engine/tests/tasks/test-round/outputs.json @@ -0,0 +1,6 @@ +{ + "test_round.all_true": [ + true, + true + ] +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-round/source.wdl b/wdl-engine/tests/tasks/test-round/source.wdl new file mode 100644 index 00000000..b4cf0d79 --- /dev/null +++ b/wdl-engine/tests/tasks/test-round/source.wdl @@ -0,0 +1,17 @@ +version 1.2 + +task test_round { + input { + Int i1 + } + + Int i2 = i1 + 1 + Float f1 = i1 + 0.49 + Float f2 = i1 + 0.50 + + command <<<>>> + + output { + Array[Boolean] all_true = [round(f1) == i1, round(f2) == i2] + } +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-round/stderr b/wdl-engine/tests/tasks/test-round/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-round/stdout b/wdl-engine/tests/tasks/test-round/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-stderr/inputs.json b/wdl-engine/tests/tasks/test-stderr/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/test-stderr/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stderr/outputs.json b/wdl-engine/tests/tasks/test-stderr/outputs.json new file mode 100644 index 00000000..36926275 --- /dev/null +++ b/wdl-engine/tests/tasks/test-stderr/outputs.json @@ -0,0 +1,3 @@ +{ + "echo_stderr.message": "hello world" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stderr/source.wdl b/wdl-engine/tests/tasks/test-stderr/source.wdl new file mode 100644 index 00000000..2bc6c055 --- /dev/null +++ b/wdl-engine/tests/tasks/test-stderr/source.wdl @@ -0,0 +1,9 @@ +version 1.2 + +task echo_stderr { + command <<< >&2 printf "hello world" >>> + + output { + String message = read_string(stderr()) + } +} diff --git a/wdl-engine/tests/tasks/test-stderr/stderr b/wdl-engine/tests/tasks/test-stderr/stderr new file mode 100644 index 00000000..95d09f2b --- /dev/null +++ b/wdl-engine/tests/tasks/test-stderr/stderr @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stderr/stdout b/wdl-engine/tests/tasks/test-stderr/stdout new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-stdout/inputs.json b/wdl-engine/tests/tasks/test-stdout/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/test-stdout/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stdout/outputs.json b/wdl-engine/tests/tasks/test-stdout/outputs.json new file mode 100644 index 00000000..dcb6f41c --- /dev/null +++ b/wdl-engine/tests/tasks/test-stdout/outputs.json @@ -0,0 +1,3 @@ +{ + "echo_stdout.message": "hello world" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stdout/source.wdl b/wdl-engine/tests/tasks/test-stdout/source.wdl new file mode 100644 index 00000000..2054a4aa --- /dev/null +++ b/wdl-engine/tests/tasks/test-stdout/source.wdl @@ -0,0 +1,9 @@ +version 1.2 + +task echo_stdout { + command <<< printf "hello world" >>> + + output { + String message = read_string(stdout()) + } +} diff --git a/wdl-engine/tests/tasks/test-stdout/stderr b/wdl-engine/tests/tasks/test-stdout/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-stdout/stdout b/wdl-engine/tests/tasks/test-stdout/stdout new file mode 100644 index 00000000..95d09f2b --- /dev/null +++ b/wdl-engine/tests/tasks/test-stdout/stdout @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-sub/error.txt b/wdl-engine/tests/tasks/test-sub/error.txt new file mode 100644 index 00000000..44c4dab8 --- /dev/null +++ b/wdl-engine/tests/tasks/test-sub/error.txt @@ -0,0 +1,9 @@ +error: regex parse error: + * + ^ +error: repetition operator missing expression + ┌─ tests/tasks/test-sub/source.wdl:13:36 + │ +13 │ String choco4 = sub(chocolike, "*", " 4444 ") # I 4444 chocolate 4444/nit's late + │ ^^^ + diff --git a/wdl-engine/tests/tasks/test-sub/inputs.json b/wdl-engine/tests/tasks/test-sub/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/test-sub/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-sub/outputs.json b/wdl-engine/tests/tasks/test-sub/outputs.json new file mode 100644 index 00000000..6ea30ea0 --- /dev/null +++ b/wdl-engine/tests/tasks/test-sub/outputs.json @@ -0,0 +1,8 @@ +{ + "test_sub.chocolove": "I love chocolate when/nit's late", + "test_sub.chocoearly": "I like chocoearly when/nit's late", + "test_sub.chocolate": "I like chocolate when/nit's early", + "test_sub.chocoearlylate": "I like chocearly when/nit's late", + "test_sub.choco4": "I 4444 chocolate when/nit's late", + "test_sub.no_newline": "I like chocolate when it's late" +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-sub/source.wdl b/wdl-engine/tests/tasks/test-sub/source.wdl new file mode 100644 index 00000000..44713213 --- /dev/null +++ b/wdl-engine/tests/tasks/test-sub/source.wdl @@ -0,0 +1,16 @@ +version 1.2 + +task test_sub { + String chocolike = "I like chocolate when\nit's late" + + command <<<>>> + + output { + String chocolove = sub(chocolike, "like", "love") # I love chocolate when\nit's late + String chocoearly = sub(chocolike, "late", "early") # I like chocoearly when\nit's early + String chocolate = sub(chocolike, "late$", "early") # I like chocolate when\nit's early + String chocoearlylate = sub(chocolike, "[^ ]late", "early") # I like chocearly when\nit's late + String choco4 = sub(chocolike, " [[:alpha:]]{4} ", " 4444 ") # I 4444 chocolate when\nit's late + String no_newline = sub(chocolike, "\\n", " ") # "I like chocolate when it's late" + } +} diff --git a/wdl-engine/tests/tasks/test-sub/stderr b/wdl-engine/tests/tasks/test-sub/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-sub/stdout b/wdl-engine/tests/tasks/test-sub/stdout new file mode 100644 index 00000000..e69de29b From 3c30cd1764d3ed11257e797225401bcf92b3b91b Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 10:50:09 -0500 Subject: [PATCH 05/11] fix: add command file outputs to task evaluation file tests. --- wdl-engine/src/eval/v1/expr.rs | 8 +- wdl-engine/tests/tasks.rs | 121 ++++++++++++------ wdl-engine/tests/tasks/array-access/command | 0 .../tests/tasks/array-map-equality/command | 0 .../tests/tasks/change-extension-task/command | 2 + .../tests/tasks/compare-coerced/command | 0 .../tests/tasks/compare-optionals/command | 0 .../tests/tasks/concat-optional/command | 0 wdl-engine/tests/tasks/declarations/command | 0 .../tests/tasks/default-option-task/command | 3 + .../tests/tasks/expressions-task/command | 1 + .../tests/tasks/file-output-task/command | 2 + .../tests/tasks/file-sizes-task/command | 1 + wdl-engine/tests/tasks/flags-task/command | 8 ++ wdl-engine/tests/tasks/glob-task/command | 3 + wdl-engine/tests/tasks/hello/command | 1 + wdl-engine/tests/tasks/import-structs/command | 1 + .../tests/tasks/input-type-qualifiers/command | 4 + wdl-engine/tests/tasks/member-access/command | 1 + .../tests/tasks/missing-output-file/command | 1 + .../tasks/multiline-placeholders/command | 0 .../tests/tasks/multiline-strings1/command | 0 .../tests/tasks/multiline-strings2/command | 0 .../tasks/multiline-strings2/outputs.json | 2 +- .../tests/tasks/multiline-strings3/command | 0 .../tests/tasks/multiline-strings4/command | 0 wdl-engine/tests/tasks/nested-access/command | 0 .../tests/tasks/nested-placeholders/command | 0 .../tests/tasks/non-empty-optional/command | 0 .../tests/tasks/optional-output-task/command | 4 + wdl-engine/tests/tasks/optionals/command | 0 wdl-engine/tests/tasks/outputs-task/command | 2 + .../tests/tasks/person-struct-task/command | 8 ++ .../tests/tasks/placeholder-coercion/command | 0 .../tests/tasks/placeholder-none/command | 0 wdl-engine/tests/tasks/placeholders/command | 0 .../tests/tasks/primitive-literals/command | 2 + .../tests/tasks/primitive-to-string/command | 0 .../tasks/private-declaration-task/command | 1 + .../tasks/relative-and-absolute-task/command | 2 + .../tasks/seo-option-to-function/command | 0 wdl-engine/tests/tasks/string-to-file/command | 0 .../tests/tasks/struct-to-struct/command | 0 wdl-engine/tests/tasks/sum-task/command | 1 + wdl-engine/tests/tasks/task-fail/command | 2 + .../tests/tasks/task-inputs-task/command | 6 + .../tests/tasks/task-with-comments/command | 2 + wdl-engine/tests/tasks/ternary/command | 0 wdl-engine/tests/tasks/test-basename/command | 0 wdl-engine/tests/tasks/test-ceil/command | 0 wdl-engine/tests/tasks/test-find/command | 0 wdl-engine/tests/tasks/test-floor/command | 0 .../tests/tasks/test-join-paths/command | 2 + wdl-engine/tests/tasks/test-map/command | 0 wdl-engine/tests/tasks/test-matches/command | 0 wdl-engine/tests/tasks/test-max/command | 0 wdl-engine/tests/tasks/test-min/command | 0 wdl-engine/tests/tasks/test-object/command | 0 wdl-engine/tests/tasks/test-pairs/command | 0 .../tasks/test-placeholders-task/command | 4 + wdl-engine/tests/tasks/test-round/command | 0 wdl-engine/tests/tasks/test-stderr/command | 1 + wdl-engine/tests/tasks/test-stdout/command | 1 + wdl-engine/tests/tasks/test-struct/command | 0 wdl-engine/tests/tasks/test-sub/command | 0 .../tests/tasks/true-false-ternary/command | 3 + 66 files changed, 155 insertions(+), 45 deletions(-) create mode 100644 wdl-engine/tests/tasks/array-access/command create mode 100644 wdl-engine/tests/tasks/array-map-equality/command create mode 100644 wdl-engine/tests/tasks/change-extension-task/command create mode 100644 wdl-engine/tests/tasks/compare-coerced/command create mode 100644 wdl-engine/tests/tasks/compare-optionals/command create mode 100644 wdl-engine/tests/tasks/concat-optional/command create mode 100644 wdl-engine/tests/tasks/declarations/command create mode 100644 wdl-engine/tests/tasks/default-option-task/command create mode 100644 wdl-engine/tests/tasks/expressions-task/command create mode 100644 wdl-engine/tests/tasks/file-output-task/command create mode 100644 wdl-engine/tests/tasks/file-sizes-task/command create mode 100644 wdl-engine/tests/tasks/flags-task/command create mode 100644 wdl-engine/tests/tasks/glob-task/command create mode 100644 wdl-engine/tests/tasks/hello/command create mode 100644 wdl-engine/tests/tasks/import-structs/command create mode 100644 wdl-engine/tests/tasks/input-type-qualifiers/command create mode 100644 wdl-engine/tests/tasks/member-access/command create mode 100644 wdl-engine/tests/tasks/missing-output-file/command create mode 100644 wdl-engine/tests/tasks/multiline-placeholders/command create mode 100644 wdl-engine/tests/tasks/multiline-strings1/command create mode 100644 wdl-engine/tests/tasks/multiline-strings2/command create mode 100644 wdl-engine/tests/tasks/multiline-strings3/command create mode 100644 wdl-engine/tests/tasks/multiline-strings4/command create mode 100644 wdl-engine/tests/tasks/nested-access/command create mode 100644 wdl-engine/tests/tasks/nested-placeholders/command create mode 100644 wdl-engine/tests/tasks/non-empty-optional/command create mode 100644 wdl-engine/tests/tasks/optional-output-task/command create mode 100644 wdl-engine/tests/tasks/optionals/command create mode 100644 wdl-engine/tests/tasks/outputs-task/command create mode 100644 wdl-engine/tests/tasks/person-struct-task/command create mode 100644 wdl-engine/tests/tasks/placeholder-coercion/command create mode 100644 wdl-engine/tests/tasks/placeholder-none/command create mode 100644 wdl-engine/tests/tasks/placeholders/command create mode 100644 wdl-engine/tests/tasks/primitive-literals/command create mode 100644 wdl-engine/tests/tasks/primitive-to-string/command create mode 100644 wdl-engine/tests/tasks/private-declaration-task/command create mode 100644 wdl-engine/tests/tasks/relative-and-absolute-task/command create mode 100644 wdl-engine/tests/tasks/seo-option-to-function/command create mode 100644 wdl-engine/tests/tasks/string-to-file/command create mode 100644 wdl-engine/tests/tasks/struct-to-struct/command create mode 100644 wdl-engine/tests/tasks/sum-task/command create mode 100644 wdl-engine/tests/tasks/task-fail/command create mode 100644 wdl-engine/tests/tasks/task-inputs-task/command create mode 100644 wdl-engine/tests/tasks/task-with-comments/command create mode 100644 wdl-engine/tests/tasks/ternary/command create mode 100644 wdl-engine/tests/tasks/test-basename/command create mode 100644 wdl-engine/tests/tasks/test-ceil/command create mode 100644 wdl-engine/tests/tasks/test-find/command create mode 100644 wdl-engine/tests/tasks/test-floor/command create mode 100644 wdl-engine/tests/tasks/test-join-paths/command create mode 100644 wdl-engine/tests/tasks/test-map/command create mode 100644 wdl-engine/tests/tasks/test-matches/command create mode 100644 wdl-engine/tests/tasks/test-max/command create mode 100644 wdl-engine/tests/tasks/test-min/command create mode 100644 wdl-engine/tests/tasks/test-object/command create mode 100644 wdl-engine/tests/tasks/test-pairs/command create mode 100644 wdl-engine/tests/tasks/test-placeholders-task/command create mode 100644 wdl-engine/tests/tasks/test-round/command create mode 100644 wdl-engine/tests/tasks/test-stderr/command create mode 100644 wdl-engine/tests/tasks/test-stdout/command create mode 100644 wdl-engine/tests/tasks/test-struct/command create mode 100644 wdl-engine/tests/tasks/test-sub/command create mode 100644 wdl-engine/tests/tasks/true-false-ternary/command diff --git a/wdl-engine/src/eval/v1/expr.rs b/wdl-engine/src/eval/v1/expr.rs index 5648107b..0ea83cb5 100644 --- a/wdl-engine/src/eval/v1/expr.rs +++ b/wdl-engine/src/eval/v1/expr.rs @@ -1563,7 +1563,7 @@ pub(crate) mod test { self.env .scope() .lookup(name.as_str()) - .map(|v| v.clone()) + .cloned() .ok_or_else(|| unknown_name(name.as_str(), name.span())) } @@ -1638,7 +1638,7 @@ pub(crate) mod test { ); let output = parser.finish(); assert_eq!( - output.diagnostics.iter().next(), + output.diagnostics.first(), None, "the provided WDL source failed to parse" ); @@ -1717,10 +1717,10 @@ pub(crate) mod test { approx::assert_relative_eq!(value.unwrap_float(), -12345.6789); let value = eval_v1_expr(&mut env, V1::Two, "1.7976931348623157E+308").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), 1.7976931348623157E+308); + approx::assert_relative_eq!(value.unwrap_float(), 1.797_693_134_862_315_7E308); let value = eval_v1_expr(&mut env, V1::Two, "-1.7976931348623157E+308").unwrap(); - approx::assert_relative_eq!(value.unwrap_float(), -1.7976931348623157E+308); + approx::assert_relative_eq!(value.unwrap_float(), -1.797_693_134_862_315_7E308); let diagnostic = eval_v1_expr(&mut env, V1::Two, "2.7976931348623157E+308").expect_err("should fail"); diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs index 84dc775d..35bab321 100644 --- a/wdl-engine/tests/tasks.rs +++ b/wdl-engine/tests/tasks.rs @@ -26,6 +26,7 @@ use std::path::Path; use std::path::PathBuf; use std::path::absolute; use std::process::exit; +use std::sync::LazyLock; use std::thread::available_parallelism; use anyhow::Context; @@ -41,6 +42,7 @@ use futures::StreamExt; use futures::stream; use path_clean::clean; use pretty_assertions::StrComparison; +use regex::Regex; use tempfile::TempDir; use walkdir::WalkDir; use wdl_analysis::AnalysisResult; @@ -56,6 +58,11 @@ use wdl_engine::Inputs; use wdl_engine::local::LocalTaskExecutionBackend; use wdl_engine::v1::TaskEvaluator; +/// Regex used to replace temporary file names in task command files with +/// consistent names for test baselines. +static TEMP_FILENAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new("tmp[[:alnum:]]{6}").expect("invalid regex")); + /// Finds tests to run as part of the analysis test suite. fn find_tests() -> Vec { // Check for filter arguments consisting of test names @@ -84,30 +91,42 @@ fn find_tests() -> Vec { tests } -/// Normalizes a result. -fn normalize(root: &Path, s: &str) -> String { - // Normalize paths separation characters first - let s = s - .replace("\\", "/") - .replace("//", "/") - .replace("\r\n", "\n"); +/// Strips paths from the given string. +fn strip_paths(root: &Path, s: &str) -> String { + #[cfg(windows)] + { + // First try it with a single slash + let mut pattern = root.to_str().expect("path is not UTF-8").to_string(); + if !pattern.ends_with('\\') { + pattern.push('\\'); + } - // Strip any paths that start with the root directory - if let Some(root) = root.to_str().map(str::to_string) { - let mut root = root.replace('\\', "/"); - if !root.ends_with('/') { - root.push('/'); + // Next try with double slashes in case there were escaped backslashes + let s = s.replace(&pattern, ""); + let pattern = pattern.replace('\\', "\\\\"); + s.replace(&pattern, "") + } + + #[cfg(unix)] + { + let mut pattern = root.to_str().expect("path is not UTF-8").to_string(); + if !pattern.ends_with('/') { + pattern.push('/'); } - s.replace(&root, "") - } else { - s + s.replace(&pattern, "") } } +/// Normalizes a result. +fn normalize(s: &str) -> String { + // Normalize paths separation characters first + s.replace("\\", "/").replace("\r\n", "\n") +} + /// Compares a single result. -fn compare_result(root: &Path, path: &Path, result: &str) -> Result<()> { - let result = normalize(root, result); +fn compare_result(path: &Path, result: &str) -> Result<()> { + let result = normalize(result); if env::var_os("BLESS").is_some() { fs::write(path, &result).with_context(|| { format!( @@ -151,7 +170,7 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { }; if let Some(diagnostic) = diagnostics.iter().find(|d| d.severity() == Severity::Error) { - bail!(diagnostic_to_string(result.document(), &path, &diagnostic)); + bail!(diagnostic_to_string(result.document(), &path, diagnostic)); } let mut engine = Engine::new(LocalTaskExecutionBackend::new()); @@ -179,17 +198,14 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { } }; + let test_dir = absolute(test).expect("failed to get absolute directory"); + // Make any paths specified in the inputs file relative to the test directory let task = result .document() .task_by_name(&name) .ok_or_else(|| anyhow!("document does not contain a task named `{name}`"))?; - inputs.join_paths( - engine.types_mut(), - result.document(), - task, - &absolute(test).expect("failed to get absolute directory"), - ); + inputs.join_paths(engine.types_mut(), result.document(), task, &test_dir); let dir = TempDir::new().context("failed to create temporary directory")?; let mut evaluator = TaskEvaluator::new(&mut engine); @@ -198,7 +214,7 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { .await { Ok(evaluated) => { - compare_evaluation_results(test, dir.path(), &evaluated)?; + compare_evaluation_results(&test_dir, dir.path(), &evaluated)?; match evaluated.into_result() { Ok(outputs) => { @@ -207,8 +223,8 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { let mut serializer = serde_json::Serializer::pretty(&mut buffer); outputs.serialize(engine.types(), &mut serializer)?; let outputs = String::from_utf8(buffer).expect("output should be UTF-8"); - let outputs_path = test.join("outputs.json"); - compare_result(dir.path(), &outputs_path, &outputs)?; + let outputs = strip_paths(dir.path(), &outputs); + compare_result(&test.join("outputs.json"), &outputs)?; } Err(e) => { let error = match e { @@ -217,9 +233,8 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { } EvaluationError::Other(e) => format!("{e:?}"), }; - - let error_path = test.join("error.txt"); - compare_result(dir.path(), &error_path, &error)?; + let error = strip_paths(dir.path(), &error); + compare_result(&test.join("error.txt"), &error)?; } } } @@ -230,16 +245,26 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { } EvaluationError::Other(e) => format!("{e:?}"), }; - - let error_path = test.join("error.txt"); - compare_result(dir.path(), &error_path, &error)?; + let error = strip_paths(dir.path(), &error); + compare_result(&test.join("error.txt"), &error)?; } } Ok(()) } -fn compare_evaluation_results(test: &Path, dir: &Path, evaluated: &EvaluatedTask) -> Result<()> { +/// Compares the evaluation output files against the baselines. +fn compare_evaluation_results( + test_dir: &Path, + temp_dir: &Path, + evaluated: &EvaluatedTask, +) -> Result<()> { + let command = fs::read_to_string(evaluated.command()).with_context(|| { + format!( + "failed to read task command file `{path}`", + path = evaluated.command().display() + ) + })?; let stdout = fs::read_to_string(evaluated.stdout().as_file().unwrap().as_str()).with_context(|| { format!( @@ -255,15 +280,31 @@ fn compare_evaluation_results(test: &Path, dir: &Path, evaluated: &EvaluatedTask ) })?; - let stdout_path = test.join("stdout"); - compare_result(dir, &stdout_path, &stdout)?; + // Strip both temp paths and test dir (input file) paths from the outputs + let command = strip_paths(temp_dir, &command); + let mut command = strip_paths(test_dir, &command); + + // Replace any temporary file names in the command + for i in 0..usize::MAX { + match TEMP_FILENAME_REGEX.replace(&command, format!("tmp{i}")) { + Cow::Borrowed(_) => break, + Cow::Owned(s) => command = s, + } + } + + compare_result(&test_dir.join("command"), &command)?; + + let stdout = strip_paths(temp_dir, &stdout); + let stdout = strip_paths(test_dir, &stdout); + compare_result(&test_dir.join("stdout"), &stdout)?; - let stderr_path = test.join("stderr"); - compare_result(dir, &stderr_path, &stderr)?; + let stderr = strip_paths(temp_dir, &stderr); + let stderr = strip_paths(test_dir, &stderr); + compare_result(&test_dir.join("stderr"), &stderr)?; // Compare expected output files let mut had_files = false; - let files_dir = test.join("files"); + let files_dir = test_dir.join("files"); for entry in WalkDir::new(evaluated.work_dir()) { let entry = entry.with_context(|| { format!( @@ -301,7 +342,7 @@ fn compare_evaluation_results(test: &Path, dir: &Path, evaluated: &EvaluatedTask .expect("should have parent directory"), ) .context("failed to create output file directory")?; - compare_result(dir, &expected_path, &contents)?; + compare_result(&expected_path, &contents)?; } // Look for missing output files diff --git a/wdl-engine/tests/tasks/array-access/command b/wdl-engine/tests/tasks/array-access/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/array-map-equality/command b/wdl-engine/tests/tasks/array-map-equality/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/change-extension-task/command b/wdl-engine/tests/tasks/change-extension-task/command new file mode 100644 index 00000000..179723fc --- /dev/null +++ b/wdl-engine/tests/tasks/change-extension-task/command @@ -0,0 +1,2 @@ +printf "data" > 'foo.data' +printf "index" > 'foo.index' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/compare-coerced/command b/wdl-engine/tests/tasks/compare-coerced/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/compare-optionals/command b/wdl-engine/tests/tasks/compare-optionals/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/concat-optional/command b/wdl-engine/tests/tasks/concat-optional/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/declarations/command b/wdl-engine/tests/tasks/declarations/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/default-option-task/command b/wdl-engine/tests/tasks/default-option-task/command new file mode 100644 index 00000000..5f9f1cf4 --- /dev/null +++ b/wdl-engine/tests/tasks/default-option-task/command @@ -0,0 +1,3 @@ +printf foobar > result1 +printf foobar > result2 +printf foobar > result3 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/expressions-task/command b/wdl-engine/tests/tasks/expressions-task/command new file mode 100644 index 00000000..cce3679f --- /dev/null +++ b/wdl-engine/tests/tasks/expressions-task/command @@ -0,0 +1 @@ +printf "hello" > hello.txt \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-output-task/command b/wdl-engine/tests/tasks/file-output-task/command new file mode 100644 index 00000000..fb131c70 --- /dev/null +++ b/wdl-engine/tests/tasks/file-output-task/command @@ -0,0 +1,2 @@ +printf "hello" > foo.hello +printf "goodbye" > foo.goodbye \ No newline at end of file diff --git a/wdl-engine/tests/tasks/file-sizes-task/command b/wdl-engine/tests/tasks/file-sizes-task/command new file mode 100644 index 00000000..e7281c31 --- /dev/null +++ b/wdl-engine/tests/tasks/file-sizes-task/command @@ -0,0 +1 @@ +printf "this file is 22 bytes/n" > created_file \ No newline at end of file diff --git a/wdl-engine/tests/tasks/flags-task/command b/wdl-engine/tests/tasks/flags-task/command new file mode 100644 index 00000000..7dfbbe74 --- /dev/null +++ b/wdl-engine/tests/tasks/flags-task/command @@ -0,0 +1,8 @@ +# If `max_matches` is `None`, the command +# grep -m hello greetings.txt +# would evaluate to +# 'grep -m ', which would be an error. + +# Instead, make both the flag and the value conditional on `max_matches` +# being defined. +grep hello 'greetings.txt' | wc -l | sed 's/^ *//' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/glob-task/command b/wdl-engine/tests/tasks/glob-task/command new file mode 100644 index 00000000..61ae9b28 --- /dev/null +++ b/wdl-engine/tests/tasks/glob-task/command @@ -0,0 +1,3 @@ +for i in {1..3}; do + printf ${i} > file_${i}.txt +done \ No newline at end of file diff --git a/wdl-engine/tests/tasks/hello/command b/wdl-engine/tests/tasks/hello/command new file mode 100644 index 00000000..365e69b6 --- /dev/null +++ b/wdl-engine/tests/tasks/hello/command @@ -0,0 +1 @@ +grep -E 'hello.*' 'greetings.txt' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/import-structs/command b/wdl-engine/tests/tasks/import-structs/command new file mode 100644 index 00000000..153eb03e --- /dev/null +++ b/wdl-engine/tests/tasks/import-structs/command @@ -0,0 +1 @@ +printf "The patient makes $35000.000000 per hour/n" \ No newline at end of file diff --git a/wdl-engine/tests/tasks/input-type-qualifiers/command b/wdl-engine/tests/tasks/input-type-qualifiers/command new file mode 100644 index 00000000..cb9e156a --- /dev/null +++ b/wdl-engine/tests/tasks/input-type-qualifiers/command @@ -0,0 +1,4 @@ +cat 'tmp/tmp0' >> result +cat 'tmp/tmp1' >> result + +cat 'tmp/tmp2' >> result \ No newline at end of file diff --git a/wdl-engine/tests/tasks/member-access/command b/wdl-engine/tests/tasks/member-access/command new file mode 100644 index 00000000..3f18e168 --- /dev/null +++ b/wdl-engine/tests/tasks/member-access/command @@ -0,0 +1 @@ +printf "bar" \ No newline at end of file diff --git a/wdl-engine/tests/tasks/missing-output-file/command b/wdl-engine/tests/tasks/missing-output-file/command new file mode 100644 index 00000000..0f2d5906 --- /dev/null +++ b/wdl-engine/tests/tasks/missing-output-file/command @@ -0,0 +1 @@ +echo this task forgot to write to foo.txt! \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-placeholders/command b/wdl-engine/tests/tasks/multiline-placeholders/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings1/command b/wdl-engine/tests/tasks/multiline-strings1/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings2/command b/wdl-engine/tests/tasks/multiline-strings2/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings2/outputs.json b/wdl-engine/tests/tasks/multiline-strings2/outputs.json index 905587c9..03dc2eb9 100644 --- a/wdl-engine/tests/tasks/multiline-strings2/outputs.json +++ b/wdl-engine/tests/tasks/multiline-strings2/outputs.json @@ -6,5 +6,5 @@ "multiline_strings2.hw4": "hello world", "multiline_strings2.hw5": "hello world", "multiline_strings2.hw6": "hello world", - "multiline_strings2.not_equivalent": "hello //n world" + "multiline_strings2.not_equivalent": "hello ///n world" } \ No newline at end of file diff --git a/wdl-engine/tests/tasks/multiline-strings3/command b/wdl-engine/tests/tasks/multiline-strings3/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/multiline-strings4/command b/wdl-engine/tests/tasks/multiline-strings4/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-access/command b/wdl-engine/tests/tasks/nested-access/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/nested-placeholders/command b/wdl-engine/tests/tasks/nested-placeholders/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/non-empty-optional/command b/wdl-engine/tests/tasks/non-empty-optional/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/optional-output-task/command b/wdl-engine/tests/tasks/optional-output-task/command new file mode 100644 index 00000000..1fb02025 --- /dev/null +++ b/wdl-engine/tests/tasks/optional-output-task/command @@ -0,0 +1,4 @@ +printf "1" > example1.txt +if false; then + printf "2" > example2.txt +fi \ No newline at end of file diff --git a/wdl-engine/tests/tasks/optionals/command b/wdl-engine/tests/tasks/optionals/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/outputs-task/command b/wdl-engine/tests/tasks/outputs-task/command new file mode 100644 index 00000000..47223366 --- /dev/null +++ b/wdl-engine/tests/tasks/outputs-task/command @@ -0,0 +1,2 @@ +printf 5 > threshold.txt +touch a.csv b.csv \ No newline at end of file diff --git a/wdl-engine/tests/tasks/person-struct-task/command b/wdl-engine/tests/tasks/person-struct-task/command new file mode 100644 index 00000000..15a46afb --- /dev/null +++ b/wdl-engine/tests/tasks/person-struct-task/command @@ -0,0 +1,8 @@ +printf "Hello Richard! You have 1 test result(s) available./n" + +if true; then + if [ "1000000" -gt 1000 ]; then + currency="USD" + printf "Please transfer $currency 500 to continue" + fi +fi \ No newline at end of file diff --git a/wdl-engine/tests/tasks/placeholder-coercion/command b/wdl-engine/tests/tasks/placeholder-coercion/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholder-none/command b/wdl-engine/tests/tasks/placeholder-none/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/placeholders/command b/wdl-engine/tests/tasks/placeholders/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/primitive-literals/command b/wdl-engine/tests/tasks/primitive-literals/command new file mode 100644 index 00000000..65baa631 --- /dev/null +++ b/wdl-engine/tests/tasks/primitive-literals/command @@ -0,0 +1,2 @@ +mkdir -p testdir +printf "hello" > testdir/hello.txt \ No newline at end of file diff --git a/wdl-engine/tests/tasks/primitive-to-string/command b/wdl-engine/tests/tasks/primitive-to-string/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/private-declaration-task/command b/wdl-engine/tests/tasks/private-declaration-task/command new file mode 100644 index 00000000..fdb413f4 --- /dev/null +++ b/wdl-engine/tests/tasks/private-declaration-task/command @@ -0,0 +1 @@ +head -3 'tmp/tmp0' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/relative-and-absolute-task/command b/wdl-engine/tests/tasks/relative-and-absolute-task/command new file mode 100644 index 00000000..37f28e61 --- /dev/null +++ b/wdl-engine/tests/tasks/relative-and-absolute-task/command @@ -0,0 +1,2 @@ +mkdir -p my/path/to +printf "something" > my/path/to/something.txt \ No newline at end of file diff --git a/wdl-engine/tests/tasks/seo-option-to-function/command b/wdl-engine/tests/tasks/seo-option-to-function/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/string-to-file/command b/wdl-engine/tests/tasks/string-to-file/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/struct-to-struct/command b/wdl-engine/tests/tasks/struct-to-struct/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/sum-task/command b/wdl-engine/tests/tasks/sum-task/command new file mode 100644 index 00000000..31b34873 --- /dev/null +++ b/wdl-engine/tests/tasks/sum-task/command @@ -0,0 +1 @@ +printf '1 2 3' | awk '{tot=0; for(i=1;i<=NF;i++) tot+=$i; print tot}' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-fail/command b/wdl-engine/tests/tasks/task-fail/command new file mode 100644 index 00000000..e23bee76 --- /dev/null +++ b/wdl-engine/tests/tasks/task-fail/command @@ -0,0 +1,2 @@ +>&2 echo this task is going to fail! +exit 1 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-inputs-task/command b/wdl-engine/tests/tasks/task-inputs-task/command new file mode 100644 index 00000000..98936b72 --- /dev/null +++ b/wdl-engine/tests/tasks/task-inputs-task/command @@ -0,0 +1,6 @@ +for i in 1..1; do + printf "hello/n" +done +if false; then + cat +fi \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-with-comments/command b/wdl-engine/tests/tasks/task-with-comments/command new file mode 100644 index 00000000..d934476e --- /dev/null +++ b/wdl-engine/tests/tasks/task-with-comments/command @@ -0,0 +1,2 @@ +# This comment WILL be included within the command after it has been parsed +echo 2 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/ternary/command b/wdl-engine/tests/tasks/ternary/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-basename/command b/wdl-engine/tests/tasks/test-basename/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-ceil/command b/wdl-engine/tests/tasks/test-ceil/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-find/command b/wdl-engine/tests/tasks/test-find/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-floor/command b/wdl-engine/tests/tasks/test-floor/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-join-paths/command b/wdl-engine/tests/tasks/test-join-paths/command new file mode 100644 index 00000000..6d467918 --- /dev/null +++ b/wdl-engine/tests/tasks/test-join-paths/command @@ -0,0 +1,2 @@ +mkdir 'mydir' +echo -n "hello" > 'mydir/mydata.txt' \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-map/command b/wdl-engine/tests/tasks/test-map/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-matches/command b/wdl-engine/tests/tasks/test-matches/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-max/command b/wdl-engine/tests/tasks/test-max/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-min/command b/wdl-engine/tests/tasks/test-min/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-object/command b/wdl-engine/tests/tasks/test-object/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-pairs/command b/wdl-engine/tests/tasks/test-pairs/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-placeholders-task/command b/wdl-engine/tests/tasks/test-placeholders-task/command new file mode 100644 index 00000000..3779317d --- /dev/null +++ b/wdl-engine/tests/tasks/test-placeholders-task/command @@ -0,0 +1,4 @@ +# The `read_lines` function reads the lines from a file into an +# array. The `sep` function concatenates the lines with a space +# (" ") delimiter. The resulting string is then printed to stdout. +printf "hello world hi_world hello friend" \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-round/command b/wdl-engine/tests/tasks/test-round/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-stderr/command b/wdl-engine/tests/tasks/test-stderr/command new file mode 100644 index 00000000..cf6c5cfa --- /dev/null +++ b/wdl-engine/tests/tasks/test-stderr/command @@ -0,0 +1 @@ +>&2 printf "hello world" \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-stdout/command b/wdl-engine/tests/tasks/test-stdout/command new file mode 100644 index 00000000..53209c7c --- /dev/null +++ b/wdl-engine/tests/tasks/test-stdout/command @@ -0,0 +1 @@ +printf "hello world" \ No newline at end of file diff --git a/wdl-engine/tests/tasks/test-struct/command b/wdl-engine/tests/tasks/test-struct/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/test-sub/command b/wdl-engine/tests/tasks/test-sub/command new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/true-false-ternary/command b/wdl-engine/tests/tasks/true-false-ternary/command new file mode 100644 index 00000000..5dcc3dda --- /dev/null +++ b/wdl-engine/tests/tasks/true-false-ternary/command @@ -0,0 +1,3 @@ +# these two commands have the same result +printf "hello world" > result1 +printf "hello world" > result2 \ No newline at end of file From 97c7dc0480ed0609214e0672cbd111696c3a44b7 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 12:55:46 -0500 Subject: [PATCH 06/11] chore: code review feedback. Use constants for task fields, requirement names, and hint names. Improve error messages. Fix typos. --- wdl-analysis/src/types/v1.rs | 130 +++++++++++++----- wdl-ast/src/v1/task.rs | 84 ++++++++++- .../v1/task/requirements/item/container.rs | 6 +- wdl-ast/src/v1/task/runtime/item/container.rs | 7 +- wdl-ast/src/v1/workflow.rs | 15 +- wdl-ast/src/validation/keys.rs | 42 ++++-- wdl-ast/src/validation/requirements.rs | 33 +++-- wdl-engine/src/backend/local.rs | 24 ++-- wdl-engine/src/eval.rs | 6 +- wdl-engine/src/value.rs | 46 +++++-- wdl-engine/tests/inputs.rs | 2 +- wdl-engine/tests/tasks.rs | 4 +- wdl-lint/src/rules/runtime_section_keys.rs | 55 +++++--- 13 files changed, 345 insertions(+), 109 deletions(-) diff --git a/wdl-analysis/src/types/v1.rs b/wdl-analysis/src/types/v1.rs index db951ddc..c59acbdd 100644 --- a/wdl-analysis/src/types/v1.rs +++ b/wdl-analysis/src/types/v1.rs @@ -37,6 +37,44 @@ use wdl_ast::v1::NegationExpr; use wdl_ast::v1::Placeholder; use wdl_ast::v1::PlaceholderOption; use wdl_ast::v1::StringPart; +use wdl_ast::v1::TASK_FIELD_ATTEMPT; +use wdl_ast::v1::TASK_FIELD_CONTAINER; +use wdl_ast::v1::TASK_FIELD_CPU; +use wdl_ast::v1::TASK_FIELD_DISKS; +use wdl_ast::v1::TASK_FIELD_END_TIME; +use wdl_ast::v1::TASK_FIELD_EXT; +use wdl_ast::v1::TASK_FIELD_FPGA; +use wdl_ast::v1::TASK_FIELD_GPU; +use wdl_ast::v1::TASK_FIELD_ID; +use wdl_ast::v1::TASK_FIELD_MEMORY; +use wdl_ast::v1::TASK_FIELD_META; +use wdl_ast::v1::TASK_FIELD_NAME; +use wdl_ast::v1::TASK_FIELD_PARAMETER_META; +use wdl_ast::v1::TASK_FIELD_RETURN_CODE; +use wdl_ast::v1::TASK_HINT_DISKS; +use wdl_ast::v1::TASK_HINT_FPGA; +use wdl_ast::v1::TASK_HINT_GPU; +use wdl_ast::v1::TASK_HINT_INPUTS; +use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL; +use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS; +use wdl_ast::v1::TASK_HINT_MAX_CPU; +use wdl_ast::v1::TASK_HINT_MAX_CPU_ALIAS; +use wdl_ast::v1::TASK_HINT_MAX_MEMORY; +use wdl_ast::v1::TASK_HINT_MAX_MEMORY_ALIAS; +use wdl_ast::v1::TASK_HINT_OUTPUTS; +use wdl_ast::v1::TASK_HINT_SHORT_TASK; +use wdl_ast::v1::TASK_HINT_SHORT_TASK_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER; +use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_CPU; +use wdl_ast::v1::TASK_REQUIREMENT_DISKS; +use wdl_ast::v1::TASK_REQUIREMENT_FPGA; +use wdl_ast::v1::TASK_REQUIREMENT_GPU; +use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES; +use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_MEMORY; +use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES; +use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS; use wdl_ast::version::V1; use super::ArrayType; @@ -97,13 +135,21 @@ use crate::types::Coercible; /// Returns `None` if the given member name is unknown. pub fn task_member_type(name: &str) -> Option { match name { - "name" | "id" | "container" => Some(PrimitiveTypeKind::String.into()), - "cpu" => Some(PrimitiveTypeKind::Float.into()), - "memory" | "attempt" => Some(PrimitiveTypeKind::Integer.into()), - "gpu" | "fpga" => Some(STDLIB.array_string_type()), - "disks" => Some(STDLIB.map_string_int_type()), - "end_time" | "return_code" => Some(Type::from(PrimitiveTypeKind::Integer).optional()), - "meta" | "parameter_meta" | "ext" => Some(Type::Object), + n if n == TASK_FIELD_NAME || n == TASK_FIELD_ID || n == TASK_FIELD_CONTAINER => { + Some(PrimitiveTypeKind::String.into()) + } + n if n == TASK_FIELD_CPU => Some(PrimitiveTypeKind::Float.into()), + n if n == TASK_FIELD_MEMORY || n == TASK_FIELD_ATTEMPT => { + Some(PrimitiveTypeKind::Integer.into()) + } + n if n == TASK_FIELD_GPU || n == TASK_FIELD_FPGA => Some(STDLIB.array_string_type()), + n if n == TASK_FIELD_DISKS => Some(STDLIB.map_string_int_type()), + n if n == TASK_FIELD_END_TIME || n == TASK_FIELD_RETURN_CODE => { + Some(Type::from(PrimitiveTypeKind::Integer).optional()) + } + n if n == TASK_FIELD_META || n == TASK_FIELD_PARAMETER_META || n == TASK_FIELD_EXT => { + Some(Type::Object) + } _ => None, } } @@ -155,16 +201,24 @@ pub fn task_requirement_types(version: SupportedVersion, name: &str) -> Option<& }); match name { - "container" | "docker" => Some(&CONTAINER_TYPES), - "cpu" => Some(CPU_TYPES), - "disks" => Some(&DISKS_TYPES), - "gpu" => Some(GPU_TYPES), - "fpga" if version >= SupportedVersion::V1(V1::Two) => Some(FPGA_TYPES), - "max_retries" if version >= SupportedVersion::V1(V1::Two) => Some(MAX_RETRIES_TYPES), - "maxRetries" => Some(MAX_RETRIES_TYPES), - "memory" => Some(MEMORY_TYPES), - "return_codes" if version >= SupportedVersion::V1(V1::Two) => Some(&RETURN_CODES_TYPES), - "returnCodes" => Some(&RETURN_CODES_TYPES), + n if n == TASK_REQUIREMENT_CONTAINER || n == TASK_REQUIREMENT_CONTAINER_ALIAS => { + Some(&CONTAINER_TYPES) + } + n if n == TASK_REQUIREMENT_CPU => Some(CPU_TYPES), + n if n == TASK_REQUIREMENT_DISKS => Some(&DISKS_TYPES), + n if n == TASK_REQUIREMENT_GPU => Some(GPU_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_REQUIREMENT_FPGA => { + Some(FPGA_TYPES) + } + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_REQUIREMENT_MAX_RETRIES => { + Some(MAX_RETRIES_TYPES) + } + n if n == TASK_REQUIREMENT_MAX_RETRIES_ALIAS => Some(MAX_RETRIES_TYPES), + n if n == TASK_REQUIREMENT_MEMORY => Some(MEMORY_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_REQUIREMENT_RETURN_CODES => { + Some(&RETURN_CODES_TYPES) + } + n if n == TASK_REQUIREMENT_RETURN_CODES_ALIAS => Some(&RETURN_CODES_TYPES), _ => None, } } @@ -222,27 +276,39 @@ pub fn task_hint_types( ))]; match name { - "disks" => Some(&DISKS_TYPES), - "fpga" if version >= SupportedVersion::V1(V1::Two) => Some(FPGA_TYPES), - "gpu" => Some(GPU_TYPES), - "inputs" if use_hidden_types && version >= SupportedVersion::V1(V1::Two) => { + n if n == TASK_HINT_DISKS => Some(&DISKS_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_HINT_FPGA => Some(FPGA_TYPES), + n if n == TASK_HINT_GPU => Some(GPU_TYPES), + n if use_hidden_types + && version >= SupportedVersion::V1(V1::Two) + && n == TASK_HINT_INPUTS => + { Some(INPUTS_HIDDEN_TYPES) } - "inputs" => Some(INPUTS_TYPES), - "localization_optional" if version >= SupportedVersion::V1(V1::Two) => { + n if n == TASK_HINT_INPUTS => Some(INPUTS_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_HINT_LOCALIZATION_OPTIONAL => { Some(LOCALIZATION_OPTIONAL_TYPES) } - "localizationOptional" => Some(LOCALIZATION_OPTIONAL_TYPES), - "max_cpu" if version >= SupportedVersion::V1(V1::Two) => Some(MAX_CPU_TYPES), - "maxCpu" => Some(MAX_CPU_TYPES), - "max_memory" if version >= SupportedVersion::V1(V1::Two) => Some(MAX_MEMORY_TYPES), - "maxMemory" => Some(MAX_MEMORY_TYPES), - "outputs" if use_hidden_types && version >= SupportedVersion::V1(V1::Two) => { + n if n == TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS => Some(LOCALIZATION_OPTIONAL_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_HINT_MAX_CPU => { + Some(MAX_CPU_TYPES) + } + n if n == TASK_HINT_MAX_CPU_ALIAS => Some(MAX_CPU_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_HINT_MAX_MEMORY => { + Some(MAX_MEMORY_TYPES) + } + n if n == TASK_HINT_MAX_MEMORY_ALIAS => Some(MAX_MEMORY_TYPES), + n if use_hidden_types + && version >= SupportedVersion::V1(V1::Two) + && n == TASK_HINT_OUTPUTS => + { Some(OUTPUTS_HIDDEN_TYPES) } - "outputs" => Some(OUTPUTS_TYPES), - "short_task" if version >= SupportedVersion::V1(V1::Two) => Some(SHORT_TASK_TYPES), - "shortTask" => Some(SHORT_TASK_TYPES), + n if n == TASK_HINT_OUTPUTS => Some(OUTPUTS_TYPES), + n if version >= SupportedVersion::V1(V1::Two) && n == TASK_HINT_SHORT_TASK => { + Some(SHORT_TASK_TYPES) + } + n if n == TASK_HINT_SHORT_TASK_ALIAS => Some(SHORT_TASK_TYPES), _ => None, } } diff --git a/wdl-ast/src/v1/task.rs b/wdl-ast/src/v1/task.rs index 1c95accd..fdbaf6ea 100644 --- a/wdl-ast/src/v1/task.rs +++ b/wdl-ast/src/v1/task.rs @@ -28,6 +28,86 @@ pub mod common; pub mod requirements; pub mod runtime; +/// The name of the `name` task variable field. +pub const TASK_FIELD_NAME: &str = "name"; +/// The name of the `id` task variable field. +pub const TASK_FIELD_ID: &str = "id"; +/// The name of the `container` task variable field. +pub const TASK_FIELD_CONTAINER: &str = "container"; +/// The name of the `cpu` task variable field. +pub const TASK_FIELD_CPU: &str = "cpu"; +/// The name of the `memory` task variable field. +pub const TASK_FIELD_MEMORY: &str = "memory"; +/// The name of the `attempt` task variable field. +pub const TASK_FIELD_ATTEMPT: &str = "attempt"; +/// The name of the `gpu` task variable field. +pub const TASK_FIELD_GPU: &str = "gpu"; +/// The name of the `fpga` task variable field. +pub const TASK_FIELD_FPGA: &str = "fpga"; +/// The name of the `disks` task variable field. +pub const TASK_FIELD_DISKS: &str = "disks"; +/// The name of the `end_time` task variable field. +pub const TASK_FIELD_END_TIME: &str = "end_time"; +/// The name of the `return_code` task variable field. +pub const TASK_FIELD_RETURN_CODE: &str = "return_code"; +/// The name of the `meta` task variable field. +pub const TASK_FIELD_META: &str = "meta"; +/// The name of the `parameter_meta` task variable field. +pub const TASK_FIELD_PARAMETER_META: &str = "parameter_meta"; +/// The name of the `ext` task variable field. +pub const TASK_FIELD_EXT: &str = "ext"; + +/// The name of the `container` task requirement. +pub const TASK_REQUIREMENT_CONTAINER: &str = "container"; +/// The alias of the `container` task requirement (i.e. `docker`). +pub const TASK_REQUIREMENT_CONTAINER_ALIAS: &str = "docker"; +/// The name of the `cpu` task requirement. +pub const TASK_REQUIREMENT_CPU: &str = "cpu"; +/// The name of the `disks` task requirement. +pub const TASK_REQUIREMENT_DISKS: &str = "disks"; +/// The name of the `gpu` task requirement. +pub const TASK_REQUIREMENT_GPU: &str = "gpu"; +/// The name of the `fpga` task requirement. +pub const TASK_REQUIREMENT_FPGA: &str = "fpga"; +/// The name of the `max_retries` task requirement. +pub const TASK_REQUIREMENT_MAX_RETRIES: &str = "max_retries"; +/// The alias of the `max_retries` task requirement (i.e. `maxRetries``). +pub const TASK_REQUIREMENT_MAX_RETRIES_ALIAS: &str = "maxRetries"; +/// The name of the `memory` task requirement. +pub const TASK_REQUIREMENT_MEMORY: &str = "memory"; +/// The name of the `return_codes` task requirement. +pub const TASK_REQUIREMENT_RETURN_CODES: &str = "return_codes"; +/// The alias of the `return_codes` task requirement (i.e. `returnCodes`). +pub const TASK_REQUIREMENT_RETURN_CODES_ALIAS: &str = "returnCodes"; + +/// The name of the `disks` task hint. +pub const TASK_HINT_DISKS: &str = "disks"; +/// The name of the `gpu` task hint. +pub const TASK_HINT_GPU: &str = "gpu"; +/// The name of the `fpga` task hint. +pub const TASK_HINT_FPGA: &str = "fpga"; +/// The name of the `inputs` task hint. +pub const TASK_HINT_INPUTS: &str = "inputs"; +/// The name of the `localization_optional` task hint. +pub const TASK_HINT_LOCALIZATION_OPTIONAL: &str = "localization_optional"; +/// The alias of the `localization_optional` task hint (i.e. +/// `localizationOptional`). +pub const TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS: &str = "localizationOptional"; +/// The name of the `max_cpu` task hint. +pub const TASK_HINT_MAX_CPU: &str = "max_cpu"; +/// The alias of the `max_cpu` task hint (i.e. `maxCpu`). +pub const TASK_HINT_MAX_CPU_ALIAS: &str = "maxCpu"; +/// The name of the `max_memory` task hint. +pub const TASK_HINT_MAX_MEMORY: &str = "max_memory"; +/// The alias of the `max_memory` task hin (e.g. `maxMemory`). +pub const TASK_HINT_MAX_MEMORY_ALIAS: &str = "maxMemory"; +/// The name of the `outputs` task hint. +pub const TASK_HINT_OUTPUTS: &str = "outputs"; +/// The name of the `short_task` task hint. +pub const TASK_HINT_SHORT_TASK: &str = "short_task"; +/// The alias of the `short_task` task hint (e.g. `shortTask`). +pub const TASK_HINT_SHORT_TASK_ALIAS: &str = "shortTask"; + /// Unescapes command text. fn unescape_command_text(s: &str, heredoc: bool, buffer: &mut String) { let mut chars = s.chars().peekable(); @@ -1833,7 +1913,7 @@ task test { assert_eq!(requirements.parent().name().as_str(), "test"); let items: Vec<_> = requirements.items().collect(); assert_eq!(items.len(), 1); - assert_eq!(items[0].name().as_str(), "container"); + assert_eq!(items[0].name().as_str(), TASK_REQUIREMENT_CONTAINER); assert_eq!( items[0] .expr() @@ -1867,7 +1947,7 @@ task test { assert_eq!(runtime.parent().name().as_str(), "test"); let items: Vec<_> = runtime.items().collect(); assert_eq!(items.len(), 1); - assert_eq!(items[0].name().as_str(), "container"); + assert_eq!(items[0].name().as_str(), TASK_REQUIREMENT_CONTAINER); assert_eq!( items[0] .expr() diff --git a/wdl-ast/src/v1/task/requirements/item/container.rs b/wdl-ast/src/v1/task/requirements/item/container.rs index da794814..5ff85c92 100644 --- a/wdl-ast/src/v1/task/requirements/item/container.rs +++ b/wdl-ast/src/v1/task/requirements/item/container.rs @@ -5,12 +5,10 @@ use wdl_grammar::WorkflowDescriptionLanguage; use crate::AstToken; use crate::v1::RequirementsItem; +use crate::v1::TASK_REQUIREMENT_CONTAINER; use crate::v1::common::container::value; use crate::v1::common::container::value::Value; -/// The key name for a container requirements item. -const CONTAINER_KEY: &str = "container"; - /// The `container` item within a `requirements` block. #[derive(Debug)] pub struct Container(RequirementsItem); @@ -48,7 +46,7 @@ impl TryFrom for Container { type Error = (); fn try_from(value: RequirementsItem) -> Result { - if value.name().as_str() == CONTAINER_KEY { + if value.name().as_str() == TASK_REQUIREMENT_CONTAINER { return Ok(Self(value)); } diff --git a/wdl-ast/src/v1/task/runtime/item/container.rs b/wdl-ast/src/v1/task/runtime/item/container.rs index 8f66d515..34445814 100644 --- a/wdl-ast/src/v1/task/runtime/item/container.rs +++ b/wdl-ast/src/v1/task/runtime/item/container.rs @@ -5,12 +5,11 @@ use wdl_grammar::WorkflowDescriptionLanguage; use crate::AstToken; use crate::v1::RuntimeItem; +use crate::v1::TASK_REQUIREMENT_CONTAINER; +use crate::v1::TASK_REQUIREMENT_CONTAINER_ALIAS; use crate::v1::common::container::value; use crate::v1::common::container::value::Value; -/// The key name for a container runtime item. -const CONTAINER_KEYS: &[&str] = &["container", "docker"]; - /// The `container` item within a `runtime` block. #[derive(Debug)] pub struct Container(RuntimeItem); @@ -48,7 +47,7 @@ impl TryFrom for Container { type Error = (); fn try_from(value: RuntimeItem) -> Result { - if CONTAINER_KEYS + if [TASK_REQUIREMENT_CONTAINER, TASK_REQUIREMENT_CONTAINER_ALIAS] .iter() .any(|key| value.name().as_str() == *key) { diff --git a/wdl-ast/src/v1/workflow.rs b/wdl-ast/src/v1/workflow.rs index eb7b3f8b..78a3d9e6 100644 --- a/wdl-ast/src/v1/workflow.rs +++ b/wdl-ast/src/v1/workflow.rs @@ -26,6 +26,13 @@ use crate::support::child; use crate::support::children; use crate::token; +/// The name of the `allow_nested_inputs` workflow hint. +pub const WORKFLOW_HINT_ALLOW_NESTED_INPUTS: &str = "allow_nested_inputs"; + +/// The alias of the `allow_nested_inputs` workflow hint (e.g. +/// `allowNestedInputs`). +pub const WORKFLOW_HINT_ALLOW_NESTED_INPUTS_ALIAS: &str = "allowNestedInputs"; + /// Represents a workflow definition. #[derive(Clone, Debug, PartialEq, Eq)] pub struct WorkflowDefinition(pub(crate) SyntaxNode); @@ -87,10 +94,10 @@ impl WorkflowDefinition { // Check the hints section let allow = self.hints().and_then(|s| { s.items().find_map(|i| { - if matches!( - i.name().as_str(), - "allow_nested_inputs" | "allowNestedInputs" - ) { + let name = i.name(); + if name.as_str() == WORKFLOW_HINT_ALLOW_NESTED_INPUTS + || name.as_str() == WORKFLOW_HINT_ALLOW_NESTED_INPUTS_ALIAS + { match i.value() { WorkflowHintsItemValue::Boolean(v) => Some(v.value()), _ => Some(false), diff --git a/wdl-ast/src/validation/keys.rs b/wdl-ast/src/validation/keys.rs index 2eeb2348..7fb9b403 100644 --- a/wdl-ast/src/validation/keys.rs +++ b/wdl-ast/src/validation/keys.rs @@ -21,7 +21,21 @@ use crate::v1::MetadataSection; use crate::v1::ParameterMetadataSection; use crate::v1::RequirementsSection; use crate::v1::RuntimeSection; +use crate::v1::TASK_HINT_LOCALIZATION_OPTIONAL; +use crate::v1::TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS; +use crate::v1::TASK_HINT_MAX_CPU; +use crate::v1::TASK_HINT_MAX_CPU_ALIAS; +use crate::v1::TASK_HINT_MAX_MEMORY; +use crate::v1::TASK_HINT_MAX_MEMORY_ALIAS; +use crate::v1::TASK_REQUIREMENT_CONTAINER; +use crate::v1::TASK_REQUIREMENT_CONTAINER_ALIAS; +use crate::v1::TASK_REQUIREMENT_MAX_RETRIES; +use crate::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS; +use crate::v1::TASK_REQUIREMENT_RETURN_CODES; +use crate::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS; use crate::v1::TaskHintsSection; +use crate::v1::WORKFLOW_HINT_ALLOW_NESTED_INPUTS; +use crate::v1::WORKFLOW_HINT_ALLOW_NESTED_INPUTS_ALIAS; use crate::v1::WorkflowHintsSection; /// Represents context about a unique key validation error. @@ -167,9 +181,15 @@ impl Visitor for UniqueKeysVisitor { check_duplicate_keys( &mut self.0, &[ - ("container", "docker"), - ("max_retries", "maxRetries"), - ("return_codes", "returnCodes"), + (TASK_REQUIREMENT_CONTAINER, TASK_REQUIREMENT_CONTAINER_ALIAS), + ( + TASK_REQUIREMENT_MAX_RETRIES, + TASK_REQUIREMENT_MAX_RETRIES_ALIAS, + ), + ( + TASK_REQUIREMENT_RETURN_CODES, + TASK_REQUIREMENT_RETURN_CODES_ALIAS, + ), ], section.items().map(|i| i.name()), Context::RequirementsSection, @@ -190,9 +210,12 @@ impl Visitor for UniqueKeysVisitor { check_duplicate_keys( &mut self.0, &[ - ("max_cpu", "maxCpu"), - ("max_memory", "maxMemory"), - ("localization_optional", "localizationOptional"), + (TASK_HINT_MAX_CPU, TASK_HINT_MAX_CPU_ALIAS), + (TASK_HINT_MAX_MEMORY, TASK_HINT_MAX_MEMORY_ALIAS), + ( + TASK_HINT_LOCALIZATION_OPTIONAL, + TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS, + ), ], section.items().map(|i| i.name()), Context::HintsSection, @@ -212,7 +235,10 @@ impl Visitor for UniqueKeysVisitor { check_duplicate_keys( &mut self.0, - &[("allow_nested_inputs", "allowNestedInputs")], + &[( + WORKFLOW_HINT_ALLOW_NESTED_INPUTS, + WORKFLOW_HINT_ALLOW_NESTED_INPUTS_ALIAS, + )], section.items().map(|i| i.name()), Context::HintsSection, state, @@ -231,7 +257,7 @@ impl Visitor for UniqueKeysVisitor { check_duplicate_keys( &mut self.0, - &[("container", "docker")], + &[(TASK_REQUIREMENT_CONTAINER, TASK_REQUIREMENT_CONTAINER_ALIAS)], section.items().map(|i| i.name()), Context::RuntimeSection, state, diff --git a/wdl-ast/src/validation/requirements.rs b/wdl-ast/src/validation/requirements.rs index 11ee064a..f4bbb760 100644 --- a/wdl-ast/src/validation/requirements.rs +++ b/wdl-ast/src/validation/requirements.rs @@ -9,6 +9,17 @@ use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; use crate::v1; +use crate::v1::TASK_REQUIREMENT_CONTAINER; +use crate::v1::TASK_REQUIREMENT_CONTAINER_ALIAS; +use crate::v1::TASK_REQUIREMENT_CPU; +use crate::v1::TASK_REQUIREMENT_DISKS; +use crate::v1::TASK_REQUIREMENT_FPGA; +use crate::v1::TASK_REQUIREMENT_GPU; +use crate::v1::TASK_REQUIREMENT_MAX_RETRIES; +use crate::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS; +use crate::v1::TASK_REQUIREMENT_MEMORY; +use crate::v1::TASK_REQUIREMENT_RETURN_CODES; +use crate::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS; /// Creates an "unsupported requirements key" diagnostic. fn unsupported_requirements_key(name: &Ident) -> Diagnostic { @@ -50,17 +61,17 @@ impl Visitor for RequirementsVisitor { ) { /// The supported set of requirement keys as of 1.2 const SUPPORTED_KEYS: &[&str] = &[ - "container", - "docker", // alias of `container` to be removed in 2.0 - "cpu", - "memory", - "gpu", - "fpga", - "disks", - "max_retries", - "maxRetries", // alias of `max_retries` - "return_codes", - "returnCodes", // alias of `return_codes` + TASK_REQUIREMENT_CONTAINER, + TASK_REQUIREMENT_CONTAINER_ALIAS, + TASK_REQUIREMENT_CPU, + TASK_REQUIREMENT_MEMORY, + TASK_REQUIREMENT_GPU, + TASK_REQUIREMENT_FPGA, + TASK_REQUIREMENT_DISKS, + TASK_REQUIREMENT_MAX_RETRIES, + TASK_REQUIREMENT_MAX_RETRIES_ALIAS, + TASK_REQUIREMENT_RETURN_CODES, + TASK_REQUIREMENT_RETURN_CODES_ALIAS, ]; if reason == VisitReason::Exit { diff --git a/wdl-engine/src/backend/local.rs b/wdl-engine/src/backend/local.rs index 2be52b32..91f1487a 100644 --- a/wdl-engine/src/backend/local.rs +++ b/wdl-engine/src/backend/local.rs @@ -16,6 +16,8 @@ use futures::future::BoxFuture; use tokio::process::Command; use tracing::info; use wdl_analysis::types::PrimitiveTypeKind; +use wdl_ast::v1::TASK_REQUIREMENT_CPU; +use wdl_ast::v1::TASK_REQUIREMENT_MEMORY; use super::TaskExecution; use super::TaskExecutionBackend; @@ -114,7 +116,7 @@ impl TaskExecution for LocalTaskExecution { ) -> Result { let num_cpus: f64 = engine.system().cpus().len() as f64; let min_cpu = requirements - .get("cpu") + .get(TASK_REQUIREMENT_CPU) .map(|v| { v.coerce(engine.types(), PrimitiveTypeKind::Float.into()) .expect("type should coerce") @@ -123,10 +125,9 @@ impl TaskExecution for LocalTaskExecution { .unwrap_or(1.0); if num_cpus < min_cpu { bail!( - "task requires at least {min_cpu} CPU{s}, but only {num_cpus} CPU{s2} are \ + "task requires at least {min_cpu} CPU{s}, but the host only has {num_cpus} \ available", s = if min_cpu == 1.0 { "" } else { "s" }, - s2 = if num_cpus == 1.0 { "" } else { "s" } ); } @@ -135,8 +136,10 @@ impl TaskExecution for LocalTaskExecution { .total_memory() .try_into() .context("system has too much memory to describe as a WDL value")?; + + // The default value for `memory` is 2 GiB let min_memory = requirements - .get("memory") + .get(TASK_REQUIREMENT_MEMORY) .map(|v| { if let Some(v) = v.as_integer() { return Ok(v); @@ -153,13 +156,16 @@ impl TaskExecution for LocalTaskExecution { unreachable!("value should be an integer or string"); }) .transpose()? - .unwrap_or(1); + .unwrap_or(2 * 1024 * 1024 * 1024); // 2GiB is the default for `memory` + if memory < min_memory { + // Display the error in GiB, as it is the most common unit for memory + let memory = memory as f64 / (1024.0 * 1024.0 * 1024.0); + let min_memory = min_memory as f64 / (1024.0 * 1024.0 * 1024.0); + bail!( - "task requires at least {min_memory} byte{s} of memory, but only {memory} \ - byte{s2} are available", - s = if min_memory == 1 { "" } else { "s" }, - s2 = if memory == 1 { "" } else { "s" } + "task requires at least {min_memory} GiB of memory, but the host only has \ + {memory} GiB available", ); } diff --git a/wdl-engine/src/eval.rs b/wdl-engine/src/eval.rs index c7c4b9bd..8f3444ef 100644 --- a/wdl-engine/src/eval.rs +++ b/wdl-engine/src/eval.rs @@ -13,6 +13,8 @@ use wdl_analysis::types::Types; use wdl_ast::Diagnostic; use wdl_ast::Ident; use wdl_ast::SupportedVersion; +use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES; +use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS; use crate::CompoundValue; use crate::Outputs; @@ -313,8 +315,8 @@ impl EvaluatedTask { fn handle_exit(&self, requirements: &HashMap) -> anyhow::Result<()> { let mut error = true; if let Some(return_codes) = requirements - .get("return_codes") - .or_else(|| requirements.get("returnCodes")) + .get(TASK_REQUIREMENT_RETURN_CODES) + .or_else(|| requirements.get(TASK_REQUIREMENT_RETURN_CODES_ALIAS)) { match return_codes { Value::Primitive(PrimitiveValue::String(s)) if s.as_ref() == "*" => { diff --git a/wdl-engine/src/value.rs b/wdl-engine/src/value.rs index ffcdfafc..240202c9 100644 --- a/wdl-engine/src/value.rs +++ b/wdl-engine/src/value.rs @@ -27,6 +27,20 @@ use wdl_analysis::types::TypeEq; use wdl_analysis::types::Types; use wdl_ast::AstToken; use wdl_ast::v1; +use wdl_ast::v1::TASK_FIELD_ATTEMPT; +use wdl_ast::v1::TASK_FIELD_CONTAINER; +use wdl_ast::v1::TASK_FIELD_CPU; +use wdl_ast::v1::TASK_FIELD_DISKS; +use wdl_ast::v1::TASK_FIELD_END_TIME; +use wdl_ast::v1::TASK_FIELD_EXT; +use wdl_ast::v1::TASK_FIELD_FPGA; +use wdl_ast::v1::TASK_FIELD_GPU; +use wdl_ast::v1::TASK_FIELD_ID; +use wdl_ast::v1::TASK_FIELD_MEMORY; +use wdl_ast::v1::TASK_FIELD_META; +use wdl_ast::v1::TASK_FIELD_NAME; +use wdl_ast::v1::TASK_FIELD_PARAMETER_META; +use wdl_ast::v1::TASK_FIELD_RETURN_CODE; use wdl_grammar::lexer::v1::is_ident; use crate::TaskExecutionConstraints; @@ -2794,25 +2808,29 @@ impl TaskValue { /// Returns `None` if the name is not a known field name. pub fn field(&self, name: &str) -> Option { match name { - "name" => Some(PrimitiveValue::String(self.name.clone()).into()), - "id" => Some(PrimitiveValue::String(self.id.clone()).into()), - "container" => Some( + n if n == TASK_FIELD_NAME => Some(PrimitiveValue::String(self.name.clone()).into()), + n if n == TASK_FIELD_ID => Some(PrimitiveValue::String(self.id.clone()).into()), + n if n == TASK_FIELD_CONTAINER => Some( self.container .clone() .map(|c| PrimitiveValue::String(c).into()) .unwrap_or(Value::None), ), - "cpu" => Some(self.cpu.into()), - "memory" => Some(self.memory.into()), - "gpu" => Some(self.gpu.clone().into()), - "fpga" => Some(self.fpga.clone().into()), - "disks" => Some(self.disks.clone().into()), - "attempt" => Some(self.attempt.into()), - "end_time" => Some(self.end_time.map(Into::into).unwrap_or(Value::None)), - "return_code" => Some(self.return_code.map(Into::into).unwrap_or(Value::None)), - "meta" => Some(self.meta.clone().into()), - "parameter_meta" => Some(self.parameter_meta.clone().into()), - "ext" => Some(self.ext.clone().into()), + n if n == TASK_FIELD_CPU => Some(self.cpu.into()), + n if n == TASK_FIELD_MEMORY => Some(self.memory.into()), + n if n == TASK_FIELD_GPU => Some(self.gpu.clone().into()), + n if n == TASK_FIELD_FPGA => Some(self.fpga.clone().into()), + n if n == TASK_FIELD_DISKS => Some(self.disks.clone().into()), + n if n == TASK_FIELD_ATTEMPT => Some(self.attempt.into()), + n if n == TASK_FIELD_END_TIME => { + Some(self.end_time.map(Into::into).unwrap_or(Value::None)) + } + n if n == TASK_FIELD_RETURN_CODE => { + Some(self.return_code.map(Into::into).unwrap_or(Value::None)) + } + n if n == TASK_FIELD_META => Some(self.meta.clone().into()), + n if n == TASK_FIELD_PARAMETER_META => Some(self.parameter_meta.clone().into()), + n if n == TASK_FIELD_EXT => Some(self.ext.clone().into()), _ => None, } } diff --git a/wdl-engine/tests/inputs.rs b/wdl-engine/tests/inputs.rs index 34144de2..1fb3c2ed 100644 --- a/wdl-engine/tests/inputs.rs +++ b/wdl-engine/tests/inputs.rs @@ -111,7 +111,7 @@ fn compare_result(path: &Path, result: &str) -> Result<()> { Ok(()) } -/// Runts the test given the provided analysis result. +/// Runs the test given the provided analysis result. fn run_test(test: &Path, result: AnalysisResult, ntests: &AtomicUsize) -> Result<()> { let cwd = std::env::current_dir().expect("must have a CWD"); let mut buffer = Buffer::no_color(); diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs index 35bab321..728b1c44 100644 --- a/wdl-engine/tests/tasks.rs +++ b/wdl-engine/tests/tasks.rs @@ -8,7 +8,7 @@ //! contain no static analysis errors, but may fail at evaluation time. //! * `error.txt` - the expected evaluation error, if any. //! * `inputs.json` - the inputs to the task. -//! * `outputs.json` - the expected outputs from the task, if the task run +//! * `outputs.json` - the expected outputs from the task, if the task runs //! successfully. //! * `stdout` - the expected stdout from the task. //! * `stderr` - the expected stderr from the task. @@ -152,7 +152,7 @@ fn compare_result(path: &Path, result: &str) -> Result<()> { Ok(()) } -/// Runts the test given the provided analysis result. +/// Runs the test given the provided analysis result. async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { let cwd = std::env::current_dir().expect("must have a CWD"); // Attempt to strip the CWD from the result path diff --git a/wdl-lint/src/rules/runtime_section_keys.rs b/wdl-lint/src/rules/runtime_section_keys.rs index a18ad37f..81805a67 100644 --- a/wdl-lint/src/rules/runtime_section_keys.rs +++ b/wdl-lint/src/rules/runtime_section_keys.rs @@ -23,6 +23,20 @@ use wdl_ast::VisitReason; use wdl_ast::Visitor; use wdl_ast::v1::RuntimeItem; use wdl_ast::v1::RuntimeSection; +use wdl_ast::v1::TASK_HINT_INPUTS; +use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS; +use wdl_ast::v1::TASK_HINT_MAX_CPU_ALIAS; +use wdl_ast::v1::TASK_HINT_MAX_MEMORY_ALIAS; +use wdl_ast::v1::TASK_HINT_OUTPUTS; +use wdl_ast::v1::TASK_HINT_SHORT_TASK_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER; +use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_CPU; +use wdl_ast::v1::TASK_REQUIREMENT_DISKS; +use wdl_ast::v1::TASK_REQUIREMENT_GPU; +use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS; +use wdl_ast::v1::TASK_REQUIREMENT_MEMORY; +use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS; use wdl_ast::v1::TaskDefinition; use wdl_ast::version::V1; @@ -71,8 +85,8 @@ fn keys_v1_0() -> &'static HashMap<&'static str, KeyKind> { KEYS_V1_0.get_or_init(|| { let mut keys = HashMap::new(); - keys.insert("docker", KeyKind::Recommended); - keys.insert("memory", KeyKind::Recommended); + keys.insert(TASK_REQUIREMENT_CONTAINER_ALIAS, KeyKind::Recommended); + keys.insert(TASK_REQUIREMENT_MEMORY, KeyKind::Recommended); keys }) } @@ -86,20 +100,29 @@ fn keys_v1_1() -> &'static HashMap<&'static str, KeyKind> { KEYS_V1_1.get_or_init(|| { let mut keys = HashMap::new(); - keys.insert("container", KeyKind::Recommended); - keys.insert("docker", KeyKind::Deprecated("container")); - keys.insert("cpu", KeyKind::ReservedMandatory); - keys.insert("memory", KeyKind::ReservedMandatory); - keys.insert("gpu", KeyKind::ReservedMandatory); - keys.insert("disks", KeyKind::ReservedMandatory); - keys.insert("maxRetries", KeyKind::ReservedMandatory); - keys.insert("returnCodes", KeyKind::ReservedMandatory); - keys.insert("maxCpu", KeyKind::ReservedHint); - keys.insert("maxMemory", KeyKind::ReservedHint); - keys.insert("shortTask", KeyKind::ReservedHint); - keys.insert("localizationOptional", KeyKind::ReservedHint); - keys.insert("inputs", KeyKind::ReservedHint); - keys.insert("outputs", KeyKind::ReservedHint); + keys.insert(TASK_REQUIREMENT_CONTAINER, KeyKind::Recommended); + keys.insert( + TASK_REQUIREMENT_CONTAINER_ALIAS, + KeyKind::Deprecated(TASK_REQUIREMENT_CONTAINER), + ); + keys.insert(TASK_REQUIREMENT_CPU, KeyKind::ReservedMandatory); + keys.insert(TASK_REQUIREMENT_MEMORY, KeyKind::ReservedMandatory); + keys.insert(TASK_REQUIREMENT_GPU, KeyKind::ReservedMandatory); + keys.insert(TASK_REQUIREMENT_DISKS, KeyKind::ReservedMandatory); + keys.insert( + TASK_REQUIREMENT_MAX_RETRIES_ALIAS, + KeyKind::ReservedMandatory, + ); + keys.insert( + TASK_REQUIREMENT_RETURN_CODES_ALIAS, + KeyKind::ReservedMandatory, + ); + keys.insert(TASK_HINT_MAX_CPU_ALIAS, KeyKind::ReservedHint); + keys.insert(TASK_HINT_MAX_MEMORY_ALIAS, KeyKind::ReservedHint); + keys.insert(TASK_HINT_SHORT_TASK_ALIAS, KeyKind::ReservedHint); + keys.insert(TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS, KeyKind::ReservedHint); + keys.insert(TASK_HINT_INPUTS, KeyKind::ReservedHint); + keys.insert(TASK_HINT_OUTPUTS, KeyKind::ReservedHint); keys }) } From 26521f5745a8e1015041037e1ed5a3d2c1765875 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 13:06:43 -0500 Subject: [PATCH 07/11] chore: fix CI on Windows. --- wdl-engine/tests/tasks.rs | 4 +++- wdl-engine/tests/tasks/multiline-strings2/outputs.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs index 728b1c44..d32cbe9f 100644 --- a/wdl-engine/tests/tasks.rs +++ b/wdl-engine/tests/tasks.rs @@ -121,7 +121,9 @@ fn strip_paths(root: &Path, s: &str) -> String { /// Normalizes a result. fn normalize(s: &str) -> String { // Normalize paths separation characters first - s.replace("\\", "/").replace("\r\n", "\n") + s.replace("\\\\", "/") + .replace("\\", "/") + .replace("\r\n", "\n") } /// Compares a single result. diff --git a/wdl-engine/tests/tasks/multiline-strings2/outputs.json b/wdl-engine/tests/tasks/multiline-strings2/outputs.json index 03dc2eb9..905587c9 100644 --- a/wdl-engine/tests/tasks/multiline-strings2/outputs.json +++ b/wdl-engine/tests/tasks/multiline-strings2/outputs.json @@ -6,5 +6,5 @@ "multiline_strings2.hw4": "hello world", "multiline_strings2.hw5": "hello world", "multiline_strings2.hw6": "hello world", - "multiline_strings2.not_equivalent": "hello ///n world" + "multiline_strings2.not_equivalent": "hello //n world" } \ No newline at end of file From fe999342d421f062875d8a945b534824b5590460 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 13:42:48 -0500 Subject: [PATCH 08/11] chore: code review feedback. Improve error messages. Don't delete the temp directory on retry (implement retry in the future). Expand on comments. Add an `--overwrite` option to `wdl run`. --- wdl-engine/src/backend/local.rs | 10 +--------- wdl-engine/src/eval.rs | 16 +++++++++++---- wdl-engine/src/eval/v1/task.rs | 10 ++++++++-- wdl-engine/tests/tasks/task-fail/error.txt | 2 +- wdl/Cargo.toml | 2 +- wdl/src/bin/wdl.rs | 23 ++++++++++++++++++++++ wdl/src/lib.rs | 4 ---- 7 files changed, 46 insertions(+), 21 deletions(-) diff --git a/wdl-engine/src/backend/local.rs b/wdl-engine/src/backend/local.rs index 91f1487a..9371c98e 100644 --- a/wdl-engine/src/backend/local.rs +++ b/wdl-engine/src/backend/local.rs @@ -55,16 +55,8 @@ impl LocalTaskExecution { ) })?; - // Recreate the temp directory as it may be needed for task evaluation + // Create the temp directory now as it may be needed for task evaluation let temp_dir = root.join("tmp"); - if temp_dir.exists() { - fs::remove_dir_all(&temp_dir).with_context(|| { - format!( - "failed to remove directory `{path}`", - path = temp_dir.display() - ) - })?; - } fs::create_dir_all(&temp_dir).with_context(|| { format!( "failed to create directory `{path}`", diff --git a/wdl-engine/src/eval.rs b/wdl-engine/src/eval.rs index 8f3444ef..2904a548 100644 --- a/wdl-engine/src/eval.rs +++ b/wdl-engine/src/eval.rs @@ -1,6 +1,7 @@ //! Module for evaluation. use std::collections::HashMap; +use std::path::MAIN_SEPARATOR; use std::path::Path; use std::path::PathBuf; @@ -323,7 +324,10 @@ impl EvaluatedTask { error = false; } Value::Primitive(PrimitiveValue::String(s)) => { - bail!("invalid return code value `{s}`; only `*` is accepted"); + bail!( + "invalid return code value `{s}`: only `*` is accepted when the return \ + code is specified as a string" + ); } Value::Primitive(PrimitiveValue::Integer(ok)) => { if self.status_code == i32::try_from(*ok).unwrap_or_default() { @@ -345,10 +349,14 @@ impl EvaluatedTask { if error { bail!( - "task process has terminated with status code {code}; see standard error output \ - `{stderr}`", + "task process has terminated with status code {code}; see the `stdout` and \ + `stderr` files in execution directory `{dir}{MAIN_SEPARATOR}` for task command \ + output", code = self.status_code, - stderr = self.stderr.as_file().unwrap().as_str(), + dir = Path::new(self.stderr.as_file().unwrap().as_str()) + .parent() + .expect("parent should exist") + .display(), ); } diff --git a/wdl-engine/src/eval/v1/task.rs b/wdl-engine/src/eval/v1/task.rs index 2ee9944a..de4f169e 100644 --- a/wdl-engine/src/eval/v1/task.rs +++ b/wdl-engine/src/eval/v1/task.rs @@ -253,8 +253,14 @@ impl<'a> TaskEvaluator<'a> { uri = document.uri() ); - // Tasks only have a root scope (0), an output scope (1), and a `task` variable - // scope (2) + // Tasks have a root scope (index 0), an output scope (index 1), and a `task` + // variable scope (index 2). The output scope inherits from the root scope and + // the task scope inherits from the output scope. Inputs and private + // declarations are evaluated into the root scope. Outputs are evaluated into + // the output scope. The task scope is used for evaluating expressions in both + // the command and output sections. Only the `task` variable in WDL 1.2 is + // introduced into the task scope; in previous WDL versions, the task scope will + // not have any local names. let mut scopes = [ Scope::new(None), Scope::new(Some(ROOT_SCOPE_INDEX.into())), diff --git a/wdl-engine/tests/tasks/task-fail/error.txt b/wdl-engine/tests/tasks/task-fail/error.txt index 031c4b52..2126ad81 100644 --- a/wdl-engine/tests/tasks/task-fail/error.txt +++ b/wdl-engine/tests/tasks/task-fail/error.txt @@ -1 +1 @@ -task process has terminated with status code 1; see standard error output `stderr` \ No newline at end of file +task process has terminated with status code 1; see the `stdout` and `stderr` files in execution directory `` for task command output \ No newline at end of file diff --git a/wdl/Cargo.toml b/wdl/Cargo.toml index 04481f5b..6b15a3ae 100644 --- a/wdl/Cargo.toml +++ b/wdl/Cargo.toml @@ -39,7 +39,7 @@ anyhow = { workspace = true } codespan-reporting = { workspace = true } [features] -default = ["ast", "grammar", "lint", "format"] +default = ["ast", "grammar", "lint", "format", "engine"] analysis = ["dep:wdl-analysis"] ast = ["dep:wdl-ast"] engine = ["dep:wdl-engine"] diff --git a/wdl/src/bin/wdl.rs b/wdl/src/bin/wdl.rs index c5bc5bbd..1863a2a9 100644 --- a/wdl/src/bin/wdl.rs +++ b/wdl/src/bin/wdl.rs @@ -431,6 +431,10 @@ pub struct RunCommand { #[clap(short, long, value_name = "OUTPUT_DIR")] pub output: Option, + /// Overwrites the task execution output directory if it exists. + #[clap(long)] + pub overwrite: bool, + /// The analysis options. #[clap(flatten)] pub options: AnalysisOptions, @@ -509,6 +513,25 @@ impl RunCommand { .output .unwrap_or_else(|| Path::new(&name).to_path_buf()); + // Check to see if the output directory already exists and if it should be + // removed + if output_dir.exists() { + if !self.overwrite { + bail!( + "output directory `{dir}` exists; use the `--overwrite` option to overwrite \ + its contents", + dir = output_dir.display() + ); + } + + fs::remove_dir_all(&output_dir).with_context(|| { + format!( + "failed to remove output directory `{dir}`", + dir = output_dir.display() + ) + })?; + } + match inputs { Inputs::Task(mut inputs) => { // Make any paths specified in the inputs absolute diff --git a/wdl/src/lib.rs b/wdl/src/lib.rs index 752dd23f..f57cc898 100644 --- a/wdl/src/lib.rs +++ b/wdl/src/lib.rs @@ -78,10 +78,6 @@ pub use wdl_analysis as analysis; #[cfg(feature = "ast")] #[doc(inline)] pub use wdl_ast as ast; -// TODO: uncomment this when wdl-engine is ready for release. -// #[cfg(feature = "engine")] -// #[doc(inline)] -// pub use wdl_engine as engine; #[cfg(feature = "doc")] #[doc(inline)] pub use wdl_doc as doc; From 224aa1bbacc0f7d6c48ee92b35e379a96e571a6f Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 13:55:46 -0500 Subject: [PATCH 09/11] chore: code review feedback. Pass the task identifier through when evaluating a task. Add a test for the task variable in 1.2. --- wdl-engine/src/eval/v1/task.rs | 5 ++-- wdl-engine/tests/tasks.rs | 2 +- wdl-engine/tests/tasks/task-variable/command | 5 ++++ .../tests/tasks/task-variable/inputs.json | 1 + .../tests/tasks/task-variable/outputs.json | 7 +++++ .../tests/tasks/task-variable/source.wdl | 27 +++++++++++++++++++ wdl-engine/tests/tasks/task-variable/stderr | 0 wdl-engine/tests/tasks/task-variable/stdout | 4 +++ wdl/src/bin/wdl.rs | 2 +- 9 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 wdl-engine/tests/tasks/task-variable/command create mode 100644 wdl-engine/tests/tasks/task-variable/inputs.json create mode 100644 wdl-engine/tests/tasks/task-variable/outputs.json create mode 100644 wdl-engine/tests/tasks/task-variable/source.wdl create mode 100644 wdl-engine/tests/tasks/task-variable/stderr create mode 100644 wdl-engine/tests/tasks/task-variable/stdout diff --git a/wdl-engine/src/eval/v1/task.rs b/wdl-engine/src/eval/v1/task.rs index de4f169e..d87a07f3 100644 --- a/wdl-engine/src/eval/v1/task.rs +++ b/wdl-engine/src/eval/v1/task.rs @@ -207,6 +207,7 @@ impl<'a> TaskEvaluator<'a> { task: &Task, inputs: &TaskInputs, root: &Path, + id: &str, ) -> EvaluationResult { // Return the first error analysis diagnostic if there was one // With this check, we can assume certain correctness properties of the document @@ -316,7 +317,7 @@ impl<'a> TaskEvaluator<'a> { // section and the outputs section if version >= SupportedVersion::V1(V1::Two) { let task = - TaskValue::new_v1(task.name(), "bar", &definition, constraints); + TaskValue::new_v1(task.name(), id, &definition, constraints); scopes[TASK_SCOPE_INDEX].insert(TASK_VAR_NAME, Value::Task(task)); } @@ -763,7 +764,7 @@ impl<'a> TaskEvaluator<'a> { document, execution.work_dir(), execution.temp_dir(), - ScopeRef::new(scopes, OUTPUT_SCOPE_INDEX), + ScopeRef::new(scopes, TASK_SCOPE_INDEX), )); let mut command = String::new(); diff --git a/wdl-engine/tests/tasks.rs b/wdl-engine/tests/tasks.rs index d32cbe9f..f5807b33 100644 --- a/wdl-engine/tests/tasks.rs +++ b/wdl-engine/tests/tasks.rs @@ -212,7 +212,7 @@ async fn run_test(test: &Path, result: AnalysisResult) -> Result<()> { let dir = TempDir::new().context("failed to create temporary directory")?; let mut evaluator = TaskEvaluator::new(&mut engine); match evaluator - .evaluate(result.document(), task, &inputs, dir.path()) + .evaluate(result.document(), task, &inputs, dir.path(), &name) .await { Ok(evaluated) => { diff --git a/wdl-engine/tests/tasks/task-variable/command b/wdl-engine/tests/tasks/task-variable/command new file mode 100644 index 00000000..13e3097b --- /dev/null +++ b/wdl-engine/tests/tasks/task-variable/command @@ -0,0 +1,5 @@ +echo "Task name: test" +echo "Task id: test" +echo "Task description: Task that shows how to use the implicit 'task' variable" +echo "Task container: " +exit 1 \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-variable/inputs.json b/wdl-engine/tests/tasks/task-variable/inputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/wdl-engine/tests/tasks/task-variable/inputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-variable/outputs.json b/wdl-engine/tests/tasks/task-variable/outputs.json new file mode 100644 index 00000000..84f6b745 --- /dev/null +++ b/wdl-engine/tests/tasks/task-variable/outputs.json @@ -0,0 +1,7 @@ +{ + "test.name": "test", + "test.id": "test", + "test.description": "Task that shows how to use the implicit 'task' variable", + "test.container": null, + "test.return_code": 1 +} \ No newline at end of file diff --git a/wdl-engine/tests/tasks/task-variable/source.wdl b/wdl-engine/tests/tasks/task-variable/source.wdl new file mode 100644 index 00000000..3271403f --- /dev/null +++ b/wdl-engine/tests/tasks/task-variable/source.wdl @@ -0,0 +1,27 @@ +version 1.2 + +task test { + meta { + description: "Task that shows how to use the implicit 'task' variable" + } + + command <<< + echo "Task name: ~{task.name}" + echo "Task id: ~{task.id}" + echo "Task description: ~{task.meta.description}" + echo "Task container: ~{task.container}" + exit 1 + >>> + + output { + String name = task.name + String id = task.id + String description = task.meta.description + String? container = task.container + Int? return_code = task.return_code + } + + requirements { + return_codes: [0, 1] + } +} diff --git a/wdl-engine/tests/tasks/task-variable/stderr b/wdl-engine/tests/tasks/task-variable/stderr new file mode 100644 index 00000000..e69de29b diff --git a/wdl-engine/tests/tasks/task-variable/stdout b/wdl-engine/tests/tasks/task-variable/stdout new file mode 100644 index 00000000..5c0c59eb --- /dev/null +++ b/wdl-engine/tests/tasks/task-variable/stdout @@ -0,0 +1,4 @@ +Task name: test +Task id: test +Task description: Task that shows how to use the implicit 'task' variable +Task container: diff --git a/wdl/src/bin/wdl.rs b/wdl/src/bin/wdl.rs index 1863a2a9..5e4202f5 100644 --- a/wdl/src/bin/wdl.rs +++ b/wdl/src/bin/wdl.rs @@ -547,7 +547,7 @@ impl RunCommand { let mut evaluator = TaskEvaluator::new(&mut engine); match evaluator - .evaluate(document, task, &inputs, &output_dir) + .evaluate(document, task, &inputs, &output_dir, &name) .await { Ok(evaluated) => { From 797556c3fee1efb9aa552e7b11937db48123bbb9 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 17:20:18 -0500 Subject: [PATCH 10/11] chore: updated README and CHANGLOGs. --- README.md | 2 ++ wdl-analysis/CHANGELOG.md | 5 +++++ wdl-ast/CHANGELOG.md | 7 +++++++ wdl-engine/CHANGELOG.md | 2 ++ wdl/CHANGELOG.md | 5 +++++ 5 files changed, 21 insertions(+) diff --git a/README.md b/README.md index cc95fe52..bf28bb85 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ The `wdl` CLI tool currently supports the following subcommands: - `format` - Parses, validates, and then formats a single WDL document, printing the result to STDOUT. - `doc` - Builds documentation for a WDL workspace. +- `run` - Parses, validates, and then runs a workflow or task from a single WDL + document using a local (i.e. not in a container) executor. Each of the subcommands supports passing `-` as the file path to denote reading from STDIN instead of a file on disk. diff --git a/wdl-analysis/CHANGELOG.md b/wdl-analysis/CHANGELOG.md index 935f1974..2c84473a 100644 --- a/wdl-analysis/CHANGELOG.md +++ b/wdl-analysis/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* Refactored the `AnalysisResult` and `Document` types to move properties of + the former into the latter; this will assist in evaluation of documents in + that the `Document` along can be passed into evaluation ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). * Removed the "optional type" constraint for the `select_first`, `select_all`, and `defined` functions; instead, these functions now accepted non-optional types and analysis emits a warning when the functions are called with @@ -35,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Fixed an issue where imported structs weren't always checked correctly for + type equivalence with local structs ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). * Common type calculation now supports discovering common types between the compound types containing Union and None as inner types, e.g. `Array[String] | Array[None] -> Array[String?]` ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). diff --git a/wdl-ast/CHANGELOG.md b/wdl-ast/CHANGELOG.md index 75e77c93..84824276 100644 --- a/wdl-ast/CHANGELOG.md +++ b/wdl-ast/CHANGELOG.md @@ -9,9 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added constants for the task variable fields, task requirement names, and + task hint names ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). * Added `allows_nested_inputs` function to `Workflow` (#[241](https://github.com/stjude-rust-labs/wdl/pull/241)). * `strip_whitespace()` method to `LiteralString` and `CommandSection` AST nodes ([#238](https://github.com/stjude-rust-labs/wdl/pull/238)). +### Changed + +* Reduced allocations in stripping whitespace from commands and multiline + strings and provided unescaping of escape sequences ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). + ## 0.9.0 - 10-22-2024 ### Changed diff --git a/wdl-engine/CHANGELOG.md b/wdl-engine/CHANGELOG.md index 7dab3e61..6d15a935 100644 --- a/wdl-engine/CHANGELOG.md +++ b/wdl-engine/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Implement task evaluation with local execution and remaining WDL 1.2 + functionality ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). * Implement the `defined` and `length` functions from the WDL standard library ([#258](https://github.com/stjude-rust-labs/wdl/pull/258)). * Fixed `Map` values not accepting `None` for keys ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). * Implement the generic map functions from the WDL standard library ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). diff --git a/wdl/CHANGELOG.md b/wdl/CHANGELOG.md index 1213fa65..43a1d1f0 100644 --- a/wdl/CHANGELOG.md +++ b/wdl/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* Added the `engine` module containing the implementation of `wdl-engine` ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). +* Implemented the `wdl run` subcommand for running tasks ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). + ### Fixed * Fixed accepting directories for the `check` and `analyze` commands for the From b1db3859212d0ba98db8c14ce34e6918850e0367 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 10 Dec 2024 21:45:14 -0500 Subject: [PATCH 11/11] Update wdl-analysis/CHANGELOG.md Co-authored-by: Andrew Frantz --- wdl-analysis/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wdl-analysis/CHANGELOG.md b/wdl-analysis/CHANGELOG.md index 2c84473a..5bbae155 100644 --- a/wdl-analysis/CHANGELOG.md +++ b/wdl-analysis/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Refactored the `AnalysisResult` and `Document` types to move properties of the former into the latter; this will assist in evaluation of documents in - that the `Document` along can be passed into evaluation ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). + that the `Document` alone can be passed into evaluation ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). * Removed the "optional type" constraint for the `select_first`, `select_all`, and `defined` functions; instead, these functions now accepted non-optional types and analysis emits a warning when the functions are called with