use super::super::ErrorCause;
use super::super::attr::AttrChar;
use super::super::attr::Origin;
use super::super::phrase::Phrase;
use super::Env;
use super::Error;
use crate::Runtime;
use crate::expansion::AssignReadOnlyError;
use crate::expansion::expand_text;
use std::ops::Range;
use std::rc::Rc;
use yash_arith::eval;
use yash_env::option::Option::Unset;
use yash_env::option::State::{Off, On};
use yash_env::variable::Scope::Global;
use yash_syntax::source::Code;
use yash_syntax::source::Location;
use yash_syntax::source::Source;
use yash_syntax::syntax::Param;
use yash_syntax::syntax::Text;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum ArithError {
#[error("invalid numeric constant")]
InvalidNumericConstant,
#[error("invalid character")]
InvalidCharacter,
#[error("incomplete expression")]
IncompleteExpression,
#[error("expected an operator")]
MissingOperator,
#[error("unmatched parenthesis")]
UnclosedParenthesis { opening_location: Location },
#[error("`?` without matching `:`")]
QuestionWithoutColon { question_location: Location },
#[error("`:` without matching `?`")]
ColonWithoutQuestion,
#[error("invalid use of operator")]
InvalidOperator,
#[error("invalid variable value: {0:?}")]
InvalidVariableValue(String),
#[error("overflow")]
Overflow,
#[error("division by zero")]
DivisionByZero,
#[error("left-shifting a negative integer")]
LeftShiftingNegative,
#[error("negative shift width")]
ReverseShifting,
#[error("assignment to a non-variable")]
AssignmentToValue,
}
impl ArithError {
#[must_use]
pub fn related_location(&self) -> Option<(&Location, &'static str)> {
use ArithError::*;
match self {
InvalidNumericConstant
| InvalidCharacter
| IncompleteExpression
| MissingOperator
| ColonWithoutQuestion
| InvalidOperator
| InvalidVariableValue(_)
| Overflow
| DivisionByZero
| LeftShiftingNegative
| ReverseShifting
| AssignmentToValue => None,
UnclosedParenthesis { opening_location } => {
Some((opening_location, "the opening parenthesis was here"))
}
QuestionWithoutColon { question_location } => Some((question_location, "`?` was here")),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct UnsetVariable {
param: Param,
}
#[must_use]
fn convert_error_cause(
cause: yash_arith::ErrorCause<UnsetVariable, AssignReadOnlyError>,
source: &Rc<Code>,
) -> ErrorCause {
use ArithError::*;
match cause {
yash_arith::ErrorCause::SyntaxError(e) => match e {
yash_arith::SyntaxError::TokenError(e) => match e {
yash_arith::TokenError::InvalidNumericConstant => {
ErrorCause::ArithError(InvalidNumericConstant)
}
yash_arith::TokenError::InvalidCharacter => {
ErrorCause::ArithError(InvalidCharacter)
}
},
yash_arith::SyntaxError::IncompleteExpression => {
ErrorCause::ArithError(IncompleteExpression)
}
yash_arith::SyntaxError::MissingOperator => ErrorCause::ArithError(MissingOperator),
yash_arith::SyntaxError::UnclosedParenthesis { opening_location } => {
let opening_location = Location {
code: Rc::clone(source),
range: opening_location,
};
ErrorCause::ArithError(UnclosedParenthesis { opening_location })
}
yash_arith::SyntaxError::QuestionWithoutColon { question_location } => {
let question_location = Location {
code: Rc::clone(source),
range: question_location,
};
ErrorCause::ArithError(QuestionWithoutColon { question_location })
}
yash_arith::SyntaxError::ColonWithoutQuestion => {
ErrorCause::ArithError(ColonWithoutQuestion)
}
yash_arith::SyntaxError::InvalidOperator => ErrorCause::ArithError(InvalidOperator),
},
yash_arith::ErrorCause::EvalError(e) => match e {
yash_arith::EvalError::InvalidVariableValue(value) => {
ErrorCause::ArithError(InvalidVariableValue(value))
}
yash_arith::EvalError::Overflow => ErrorCause::ArithError(Overflow),
yash_arith::EvalError::DivisionByZero => ErrorCause::ArithError(DivisionByZero),
yash_arith::EvalError::LeftShiftingNegative => {
ErrorCause::ArithError(LeftShiftingNegative)
}
yash_arith::EvalError::ReverseShifting => ErrorCause::ArithError(ReverseShifting),
yash_arith::EvalError::AssignmentToValue => ErrorCause::ArithError(AssignmentToValue),
yash_arith::EvalError::GetVariableError(UnsetVariable { param }) => {
ErrorCause::UnsetParameter { param }
}
yash_arith::EvalError::AssignVariableError(e) => ErrorCause::AssignReadOnly(e),
},
}
}
struct VarEnv<'a, S> {
env: &'a mut yash_env::Env<S>,
expression: &'a str,
expansion_location: &'a Location,
}
impl<S> yash_arith::Env for VarEnv<'_, S> {
type GetVariableError = UnsetVariable;
type AssignVariableError = AssignReadOnlyError;
fn get_variable(&self, name: &str) -> Result<Option<&str>, UnsetVariable> {
match self.env.variables.get_scalar(name) {
Some(value) => Ok(Some(value)),
None => match self.env.options.get(Unset) {
Off => Err(UnsetVariable {
param: Param::variable(name),
}),
On => Ok(None),
},
}
}
fn assign_variable(
&mut self,
name: &str,
value: String,
range: Range<usize>,
) -> Result<(), AssignReadOnlyError> {
let code = Rc::new(Code {
value: self.expression.to_string().into(),
start_line_number: 1.try_into().unwrap(),
source: Source::Arith {
original: self.expansion_location.clone(),
}
.into(),
});
self.env
.get_or_create_variable(name, Global)
.assign(value, Location { code, range })
.map(drop)
.map_err(|e| AssignReadOnlyError {
name: name.to_owned(),
new_value: e.new_value,
read_only_location: e.read_only_location,
vacancy: None,
})
}
}
pub async fn expand<S: Runtime + 'static>(
text: &Text,
location: &Location,
env: &mut Env<'_, S>,
) -> Result<Phrase, Error> {
let (expression, exit_status) = expand_text(env.inner, text).await?;
if exit_status.is_some() {
env.last_command_subst_exit_status = exit_status;
}
let result = eval(
&expression,
&mut VarEnv {
env: env.inner,
expression: &expression,
expansion_location: location,
},
);
match result {
Ok(value) => {
let value = value.to_string();
let chars = value
.chars()
.map(|c| AttrChar {
value: c,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
})
.collect();
Ok(Phrase::Field(chars))
}
Err(error) => {
let code = Rc::new(Code {
value: expression.into(),
start_line_number: 1.try_into().unwrap(),
source: Source::Arith {
original: location.clone(),
}
.into(),
});
let cause = convert_error_cause(error.cause, &code);
Err(Error {
cause,
location: Location {
code,
range: error.location,
},
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use crate::tests::return_builtin;
use futures_util::FutureExt as _;
use yash_env::semantics::ExitStatus;
use yash_env::system::Errno;
use yash_env::test_helper::in_virtual_system;
use yash_env::variable::Scope::Global;
use yash_env::variable::Value::Scalar;
#[test]
fn var_env_get_variable_success() {
use yash_arith::Env as _;
let mut env = yash_env::Env::new_virtual();
env.variables
.get_or_new("v", Global)
.assign("value", None)
.unwrap();
let location = Location::dummy("my location");
let env = VarEnv {
env: &mut env,
expression: "v",
expansion_location: &location,
};
let result = env.get_variable("v");
assert_eq!(result, Ok(Some("value")));
}
#[test]
fn var_env_get_variable_unset() {
use yash_arith::Env as _;
let mut env = yash_env::Env::new_virtual();
let location = Location::dummy("my location");
let env = VarEnv {
env: &mut env,
expression: "v",
expansion_location: &location,
};
let result = env.get_variable("v");
assert_eq!(result, Ok(None));
}
#[test]
fn var_env_get_variable_nounset() {
use yash_arith::Env as _;
let mut env = yash_env::Env::new_virtual();
env.options.set(Unset, Off);
let location = Location::dummy("my location");
let env = VarEnv {
env: &mut env,
expression: "0+v",
expansion_location: &location,
};
let result = env.get_variable("v");
assert_eq!(
result,
Err(UnsetVariable {
param: Param::variable("v")
})
);
}
#[test]
fn successful_inner_text_expansion() {
let text = "17%9".parse().unwrap();
let location = Location::dummy("my location");
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let result = expand(&text, &location, &mut env).now_or_never().unwrap();
let c = AttrChar {
value: '8',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
};
assert_eq!(result, Ok(Phrase::Char(c)));
assert_eq!(env.last_command_subst_exit_status, None);
}
#[test]
fn non_zero_exit_status_from_inner_text_expansion() {
in_virtual_system(|mut env, _state| async move {
let text = "$(echo 0; return -n 63)".parse().unwrap();
let location = Location::dummy("my location");
env.builtins.insert("echo", echo_builtin());
env.builtins.insert("return", return_builtin());
let mut env = Env::new(&mut env);
let result = expand(&text, &location, &mut env).await;
let c = AttrChar {
value: '0',
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
};
assert_eq!(result, Ok(Phrase::Char(c)));
assert_eq!(env.last_command_subst_exit_status, Some(ExitStatus(63)));
})
}
#[test]
fn exit_status_is_kept_if_inner_text_expansion_contains_no_command_substitution() {
let text = "0".parse().unwrap();
let location = Location::dummy("my location");
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
env.last_command_subst_exit_status = Some(ExitStatus(123));
let _ = expand(&text, &location, &mut env).now_or_never().unwrap();
assert_eq!(env.last_command_subst_exit_status, Some(ExitStatus(123)));
}
#[test]
fn error_in_inner_text_expansion() {
let text = "$(x)".parse().unwrap();
let location = Location::dummy("my location");
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let result = expand(&text, &location, &mut env).now_or_never().unwrap();
let e = result.unwrap_err();
assert_eq!(e.cause, ErrorCause::CommandSubstError(Errno::ENOSYS));
assert_eq!(*e.location.code.value.borrow(), "$(x)");
assert_eq!(e.location.range, 0..4);
}
#[test]
fn variable_assigned_during_arithmetic_evaluation() {
let text = "3 + (x = 4 * 6)".parse().unwrap();
let location = Location::dummy("my location");
let mut env = yash_env::Env::new_virtual();
let mut env2 = Env::new(&mut env);
let _ = expand(&text, &location, &mut env2).now_or_never().unwrap();
let v = env.variables.get("x").unwrap();
assert_eq!(v.value, Some(Scalar("24".to_string())));
let location2 = v.last_assigned_location.as_ref().unwrap();
assert_eq!(*location2.code.value.borrow(), "3 + (x = 4 * 6)");
assert_eq!(location2.code.start_line_number.get(), 1);
assert_eq!(*location2.code.source, Source::Arith { original: location });
assert_eq!(location2.range, 5..6);
assert!(!v.is_exported);
assert_eq!(v.read_only_location, None);
}
#[test]
fn error_in_arithmetic_evaluation() {
let text = "09".parse().unwrap();
let location = Location::dummy("my location");
let mut env = yash_env::Env::new_virtual();
let mut env = Env::new(&mut env);
let result = expand(&text, &location, &mut env).now_or_never().unwrap();
let e = result.unwrap_err();
assert_eq!(
e.cause,
ErrorCause::ArithError(ArithError::InvalidNumericConstant)
);
assert_eq!(*e.location.code.value.borrow(), "09");
assert_eq!(
*e.location.code.source,
Source::Arith { original: location }
);
assert_eq!(e.location.range, 0..2);
}
}