use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use futures::future::BoxFuture;
use tempfile::TempPath;
use wdl_analysis::stdlib::Binding;
use wdl_analysis::types::Type;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use crate::Coercible;
use crate::EvaluationContext;
use crate::EvaluationPath;
use crate::HostPath;
use crate::NoneValue;
use crate::PrimitiveValue;
use crate::Value;
use crate::diagnostics::function_call_failed;
use crate::http::Location;
use crate::http::Transferer;
mod as_map;
mod as_pairs;
mod basename;
mod ceil;
mod chunk;
mod collect_by_key;
mod contains;
mod contains_key;
mod cross;
mod defined;
mod find;
mod flatten;
mod floor;
mod glob;
mod join_paths;
mod keys;
mod length;
mod matches;
mod max;
mod min;
mod prefix;
mod quote;
mod range;
mod read_boolean;
mod read_float;
mod read_int;
mod read_json;
mod read_lines;
mod read_map;
mod read_object;
mod read_objects;
mod read_string;
mod read_tsv;
mod round;
mod select_all;
mod select_first;
mod sep;
mod size;
mod split;
mod squote;
mod stderr;
mod stdout;
mod sub;
mod suffix;
mod transpose;
mod unzip;
mod value;
mod values;
mod write_json;
mod write_lines;
mod write_map;
mod write_object;
mod write_objects;
mod write_tsv;
mod zip;
fn ensure_local_path(base_dir: &EvaluationPath, path: &str) -> Result<PathBuf> {
let joined = base_dir.join(path)?;
if joined.is_local() {
Ok(joined.unwrap_local())
} else {
let url = joined.unwrap_remote();
if url.scheme() != "file" {
bail!("operation not supported for URL `{path}`");
}
url.to_file_path()
.map_err(|_| anyhow!("URL `{path}` cannot be represented as a local file path"))
}
}
pub(crate) async fn download_file(
transferer: &dyn Transferer,
base_dir: &EvaluationPath,
path: &HostPath,
) -> Result<Location> {
let joined = base_dir.join(path.as_str())?;
if joined.is_local() {
Ok(Location::Path(joined.unwrap_local()))
} else {
let url = joined.unwrap_remote();
transferer
.download(&url)
.await
.map_err(|e| anyhow!("failed to download file `{path}`: {e:?}"))
}
}
pub(crate) fn temp_path_to_value(
context: CallContext<'_>,
path: TempPath,
function_name: &str,
) -> Result<Value, Diagnostic> {
let path = path.keep().map_err(|e| {
function_call_failed(
function_name,
format!("failed to keep temporary file: {e}"),
context.call_site,
)
})?;
let path = HostPath::new(path.into_os_string().into_string().map_err(|path| {
function_call_failed(
function_name,
format!(
"path `{path}` cannot be represented as UTF-8",
path = Path::new(&path).display()
),
context.call_site,
)
})?);
context.context.notify_file_created(&path).map_err(|e| {
function_call_failed(
function_name,
format!("failed to keep temporary file: {e}"),
context.call_site,
)
})?;
Ok(PrimitiveValue::File(path).into())
}
#[derive(Clone)]
pub struct CallArgument {
value: Value,
span: Span,
}
impl CallArgument {
pub const fn new(value: Value, span: Span) -> Self {
Self { value, span }
}
pub fn none() -> Self {
Self {
value: Value::None(NoneValue::untyped()),
span: Span::new(0, 0),
}
}
}
pub struct CallContext<'a> {
context: &'a mut dyn EvaluationContext,
call_site: Span,
arguments: &'a [CallArgument],
return_type: Type,
}
impl<'a> CallContext<'a> {
pub fn new(
context: &'a mut dyn EvaluationContext,
call_site: Span,
arguments: &'a [CallArgument],
return_type: Type,
) -> Self {
Self {
context,
call_site,
arguments,
return_type,
}
}
pub fn base_dir(&self) -> &EvaluationPath {
self.context.base_dir()
}
pub fn temp_dir(&self) -> &Path {
self.context.temp_dir()
}
pub fn stdout(&self) -> Option<&Value> {
self.context.stdout()
}
pub fn stderr(&self) -> Option<&Value> {
self.context.stderr()
}
pub fn transferer(&self) -> &dyn Transferer {
self.context.transferer()
}
pub fn inner(&self) -> &dyn EvaluationContext {
self.context
}
#[inline]
fn coerce_argument(&self, index: usize, ty: impl Into<Type>) -> Value {
self.arguments[index]
.value
.coerce(Some(self.context), &ty.into())
.expect("value should coerce")
}
#[allow(unused)]
fn return_type_eq(&self, ty: impl Into<Type>) -> bool {
self.return_type.eq(&ty.into())
}
}
#[derive(Debug, Clone, Copy)]
enum Callback {
Sync(fn(context: CallContext<'_>) -> Result<Value, Diagnostic>),
Async(for<'a> fn(context: CallContext<'a>) -> BoxFuture<'a, Result<Value, Diagnostic>>),
}
#[derive(Debug, Clone, Copy)]
pub struct Signature {
#[allow(unused)]
display: &'static str,
callback: Callback,
}
impl Signature {
const fn new(display: &'static str, callback: Callback) -> Self {
Self { display, callback }
}
}
#[derive(Debug, Clone, Copy)]
pub struct Function {
signatures: &'static [Signature],
}
impl Function {
const fn new(signatures: &'static [Signature]) -> Self {
Self { signatures }
}
#[inline]
pub async fn call(
&self,
binding: Binding<'_>,
context: CallContext<'_>,
) -> Result<Value, Diagnostic> {
match self.signatures[binding.index()].callback {
Callback::Sync(cb) => cb(context),
Callback::Async(cb) => cb(context).await,
}
}
}
#[derive(Debug)]
pub struct StandardLibrary {
functions: HashMap<&'static str, Function>,
}
impl StandardLibrary {
#[inline]
pub fn get(&self, name: &str) -> Option<Function> {
self.functions.get(name).copied()
}
}
pub static STDLIB: LazyLock<StandardLibrary> = LazyLock::new(|| {
macro_rules! func {
($name:ident) => {
(stringify!($name), $name::descriptor())
};
}
StandardLibrary {
functions: HashMap::from_iter([
func!(floor),
func!(ceil),
func!(round),
func!(min),
func!(max),
func!(find),
func!(matches),
func!(sub),
func!(split),
func!(basename),
func!(join_paths),
func!(glob),
func!(size),
func!(stdout),
func!(stderr),
func!(read_string),
func!(read_int),
func!(read_float),
func!(read_boolean),
func!(read_lines),
func!(write_lines),
func!(read_tsv),
func!(write_tsv),
func!(read_map),
func!(write_map),
func!(read_json),
func!(write_json),
func!(read_object),
func!(read_objects),
func!(write_object),
func!(write_objects),
func!(prefix),
func!(suffix),
func!(quote),
func!(squote),
func!(sep),
func!(range),
func!(transpose),
func!(cross),
func!(zip),
func!(unzip),
func!(contains),
func!(chunk),
func!(flatten),
func!(select_first),
func!(select_all),
func!(as_pairs),
func!(as_map),
func!(keys),
func!(contains_key),
func!(values),
func!(collect_by_key),
func!(value),
func!(defined),
func!(length),
]),
}
});
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB;
use wdl_analysis::stdlib::TypeParameters;
use super::*;
#[test]
fn verify_stdlib() {
for (name, func) in ANALYSIS_STDLIB.functions() {
match STDLIB.functions.get(name) {
Some(imp) => match func {
wdl_analysis::stdlib::Function::Monomorphic(f) => {
assert_eq!(
imp.signatures.len(),
1,
"signature count mismatch for function `{name}`"
);
let params = TypeParameters::new(f.signature().type_parameters());
assert_eq!(
f.signature().display(¶ms).to_string(),
imp.signatures[0].display,
"signature mismatch for function `{name}`"
);
}
wdl_analysis::stdlib::Function::Polymorphic(f) => {
assert_eq!(
imp.signatures.len(),
f.signatures().len(),
"signature count mismatch for function `{name}`"
);
for (i, sig) in f.signatures().iter().enumerate() {
let params = TypeParameters::new(sig.type_parameters());
assert_eq!(
sig.display(¶ms).to_string(),
imp.signatures[i].display,
"signature mismatch for function `{name}` (index {i})"
);
}
}
},
None => panic!(
"missing function `{name}` in the engine's standard library implementation"
),
}
}
}
}