use crate::Handle as _;
use crate::Runtime;
use crate::command::Command;
use crate::expansion::Field;
use crate::expansion::expand_word;
use std::ops::ControlFlow::Continue;
use std::rc::Rc;
use yash_env::Env;
use yash_env::function::DefineError;
use yash_env::function::Function;
use yash_env::function::FunctionBody;
use yash_env::function::FunctionBodyObject;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Result;
use yash_env::system::Isatty;
use yash_env::system::concurrency::WriteAll;
use yash_syntax::source::pretty::{Report, ReportType, Snippet, Span, SpanRole, add_span};
use yash_syntax::syntax;
#[derive(Debug)]
#[repr(transparent)]
pub(crate) struct BodyImpl(pub syntax::FullCompoundCommand);
impl std::fmt::Display for BodyImpl {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<S: Runtime + 'static> FunctionBody<S> for BodyImpl {
async fn execute(&self, env: &mut Env<S>) -> Result {
self.0.execute(env).await
}
}
impl<S: Runtime + 'static> Command<S> for syntax::FunctionDefinition {
async fn execute(&self, env: &mut Env<S>) -> Result {
define_function(env, self).await?;
env.apply_errexit()
}
}
async fn define_function<S: Runtime + 'static>(
env: &mut Env<S>,
def: &syntax::FunctionDefinition,
) -> Result {
let Field {
value: name,
origin,
} = match expand_word(env, &def.name).await {
Ok((field, _exit_status)) => field,
Err(error) => return error.handle(env).await,
};
let body: Rc<syntax::FullCompoundCommand> = Rc::clone(&def.body);
let body = Rc::into_raw(body).cast::<BodyImpl>();
let body = unsafe { Rc::from_raw(body) };
let function = Function::new(name, body as Rc<dyn FunctionBodyObject<S>>, origin);
match env.functions.define(function) {
Ok(_) => {
env.exit_status = ExitStatus::SUCCESS;
}
Err(error) => {
report_define_error(env, &error).await;
env.exit_status = ExitStatus::ERROR;
}
}
Continue(())
}
async fn report_define_error<S>(env: &mut Env<S>, error: &DefineError<S>)
where
S: Isatty + WriteAll,
{
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = error.to_string().into();
report.snippets =
Snippet::with_primary_span(&error.new.origin, "failed function redefinition".into());
add_span(
&error.existing.origin.code,
Span {
range: error.existing.origin.byte_range(),
role: SpanRole::Supplementary {
label: "existing function was defined here".into(),
},
},
&mut report.snippets,
);
let read_only_location = error.existing.read_only_location.as_ref().unwrap();
add_span(
&read_only_location.code,
Span {
range: read_only_location.byte_range(),
role: SpanRole::Supplementary {
label: "existing function was made read-only here".into(),
},
},
&mut report.snippets,
);
yash_env::io::print_report(env, &report).await;
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::FutureExt as _;
use std::ops::ControlFlow::Break;
use yash_env::VirtualSystem;
use yash_env::option::On;
use yash_env::option::Option::ErrExit;
use yash_env::semantics::Divert;
use yash_env::system::Concurrent;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::function::FunctionBodyStub;
use yash_syntax::source::Location;
#[test]
fn function_definition_new() {
let mut env = Env::new_virtual();
env.exit_status = ExitStatus::ERROR;
let definition = syntax::FunctionDefinition {
has_keyword: false,
name: "foo".parse().unwrap(),
body: Rc::new("{ :; }".parse().unwrap()),
};
let result = definition.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_eq!(env.functions.len(), 1);
let function = env.functions.get("foo").unwrap();
assert_eq!(function.name, "foo");
assert_eq!(function.origin, definition.name.location);
assert_eq!(function.read_only_location, None);
}
#[test]
fn function_definition_overwrite() {
let mut env = Env::new_virtual();
env.exit_status = ExitStatus::ERROR;
let function = Function {
name: "foo".to_string(),
body: FunctionBodyStub::rc_dyn(),
origin: Location::dummy("dummy"),
read_only_location: None,
};
env.functions.define(function).unwrap();
let definition = syntax::FunctionDefinition {
has_keyword: false,
name: "foo".parse().unwrap(),
body: Rc::new("( :; )".parse().unwrap()),
};
let result = definition.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_eq!(env.functions.len(), 1);
let function = env.functions.get("foo").unwrap();
assert_eq!(function.name, "foo");
assert_eq!(function.origin, definition.name.location);
assert_eq!(function.read_only_location, None);
}
#[test]
fn function_definition_read_only() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let function = Rc::new(Function {
name: "foo".to_string(),
body: FunctionBodyStub::rc_dyn(),
origin: Location::dummy("dummy"),
read_only_location: Some(Location::dummy("readonly")),
});
env.functions.define(Rc::clone(&function)).unwrap();
let definition = syntax::FunctionDefinition {
has_keyword: false,
name: "foo".parse().unwrap(),
body: Rc::new("( :; )".parse().unwrap()),
};
let result = definition.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::ERROR);
assert_eq!(env.functions.len(), 1);
assert_eq!(env.functions.get("foo").unwrap(), &function);
assert_stderr(&state, |stderr| {
assert!(
stderr.contains("foo"),
"error message should contain function name: {stderr:?}"
)
});
}
#[test]
fn function_definition_name_expansion() {
let mut env = Env::new_virtual();
let definition = syntax::FunctionDefinition {
has_keyword: false,
name: r"\a".parse().unwrap(),
body: Rc::new("{ :; }".parse().unwrap()),
};
let result = definition.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
let names: Vec<&str> = env.functions.iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, ["a"]);
}
#[test]
fn errexit_in_function_definition() {
let mut env = Env::new_virtual();
let function = Function {
name: "foo".to_string(),
body: FunctionBodyStub::rc_dyn(),
origin: Location::dummy("dummy"),
read_only_location: Some(Location::dummy("readonly")),
};
env.functions.define(function).unwrap();
let definition = syntax::FunctionDefinition {
has_keyword: false,
name: "foo".parse().unwrap(),
body: Rc::new("( :; )".parse().unwrap()),
};
env.options.set(ErrExit, On);
let result = definition.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Exit(None)));
assert_eq!(env.exit_status, ExitStatus::ERROR);
}
}