use super::*;
pub(crate) struct Evaluator<'src: 'run, 'run> {
assignments: Option<&'run Table<'src, Assignment<'src>>>,
context: Option<ExecutionContext<'src, 'run>>,
env: BTreeMap<String, String>,
is_dependency: bool,
non_const_assignments: Table<'src, Name<'src>>,
overrides: &'run HashMap<Number, String>,
scope: Scope<'src, 'run>,
}
impl<'src, 'run> Evaluator<'src, 'run> {
fn context(
&self,
const_error: ConstError<'src>,
) -> Result<&ExecutionContext<'src, 'run>, ConstError<'src>> {
self.context.as_ref().ok_or(const_error)
}
pub(crate) fn evaluate_settings(
assignments: &'run Table<'src, Assignment<'src>>,
overrides: &'run HashMap<Number, String>,
scope: &'run Scope<'src, 'run>,
sets: Table<'src, Set<'src>>,
) -> RunResult<'src, Settings> {
let mut evaluator = Self {
assignments: Some(assignments),
context: None,
env: BTreeMap::new(),
is_dependency: false,
non_const_assignments: Table::new(),
overrides,
scope: scope.child(),
};
for assignment in assignments.values() {
match evaluator.evaluate_assignment(assignment) {
Err(Error::Const { .. }) => evaluator.non_const_assignments.insert(assignment.name),
Err(err) => return Err(err),
Ok(_) => {}
}
}
evaluator.evaluate_sets(sets)
}
fn evaluate_sets(&mut self, sets: Table<'src, Set<'src>>) -> RunResult<'src, Settings> {
let mut settings = Settings::default();
for (_name, set) in sets {
match set.value {
Setting::AllowDuplicateRecipes(value) => {
settings.allow_duplicate_recipes = value;
}
Setting::AllowDuplicateVariables(value) => {
settings.allow_duplicate_variables = value;
}
Setting::DotenvFilename(value) => {
settings.dotenv_filename = Some(self.evaluate_expression(&value)?);
}
Setting::DotenvLoad(value) => {
settings.dotenv_load = value;
}
Setting::DotenvPath(value) => {
settings.dotenv_path = Some(self.evaluate_expression(&value)?.into());
}
Setting::DotenvOverride(value) => {
settings.dotenv_override = value;
}
Setting::DotenvRequired(value) => {
settings.dotenv_required = value;
}
Setting::Export(value) => {
settings.export = value;
}
Setting::Fallback(value) => {
settings.fallback = value;
}
Setting::Guards(guards) => {
settings.guards = guards;
}
Setting::IgnoreComments(value) => {
settings.ignore_comments = value;
}
Setting::Lazy(value) => {
settings.lazy = value;
}
Setting::NoExitMessage(value) => {
settings.no_exit_message = value;
}
Setting::PositionalArguments(value) => {
settings.positional_arguments = value;
}
Setting::Quiet(value) => {
settings.quiet = value;
}
Setting::ScriptInterpreter(value) => {
settings.script_interpreter = Some(self.evaluate_interpreter(&value)?);
}
Setting::Shell(value) => {
settings.shell = Some(self.evaluate_interpreter(&value)?);
}
Setting::Unstable(value) => {
settings.unstable = value;
}
Setting::WindowsPowerShell(value) => {
settings.windows_powershell = value;
}
Setting::WindowsShell(value) => {
settings.windows_shell = Some(self.evaluate_interpreter(&value)?);
}
Setting::Tempdir(value) => {
settings.tempdir = Some(self.evaluate_expression(&value)?);
}
Setting::WorkingDirectory(value) => {
settings.working_directory = Some(self.evaluate_expression(&value)?.into());
}
}
}
Ok(settings)
}
pub(crate) fn evaluate_interpreter(
&mut self,
interpreter: &Interpreter<Expression<'src>>,
) -> RunResult<'src, Interpreter<String>> {
Ok(Interpreter {
command: self.evaluate_expression(&interpreter.command)?,
arguments: interpreter
.arguments
.iter()
.map(|argument| self.evaluate_expression(argument))
.collect::<RunResult<Vec<String>>>()?,
})
}
pub(crate) fn evaluate_assignments(
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
module: &'run Justfile<'src>,
overrides: &'run HashMap<Number, String>,
parent: &'run Scope<'src, 'run>,
search: &'run Search,
variable_references: Option<&HashSet<Number>>,
) -> RunResult<'src, Scope<'src, 'run>>
where
'src: 'run,
{
let context = ExecutionContext {
config,
dotenv,
module,
search,
};
let mut evaluator = Self {
assignments: Some(&module.assignments),
context: Some(context),
env: BTreeMap::new(),
is_dependency: false,
non_const_assignments: Table::new(),
overrides,
scope: parent.child(),
};
for assignment in module.assignments.values() {
if assignment.eager
|| assignment.export
|| module.settings.export
|| variable_references
.is_none_or(|variable_references| variable_references.contains(&assignment.number))
{
evaluator.evaluate_assignment(assignment)?;
}
}
Ok(evaluator.scope)
}
fn evaluate_assignment(&mut self, assignment: &Assignment<'src>) -> RunResult<'src, &str> {
let name = assignment.name.lexeme();
if !self.scope.bound(name) {
let value = if let Some(value) = self.overrides.get(&assignment.number) {
value.clone()
} else {
self.evaluate_expression(&assignment.value)?
};
self.scope.bind(Binding {
eager: assignment.eager,
export: assignment.export
|| self
.context
.is_some_and(|context| context.module.settings.export),
file_depth: 0,
name: assignment.name,
number: assignment.number,
prelude: false,
private: assignment.private,
value,
});
}
Ok(self.scope.value(name).unwrap())
}
fn function_context(&self, name: Name<'src>) -> RunResult<'src, function::Context> {
Ok(function::Context {
execution_context: self.context(ConstError::FunctionCall(name))?,
is_dependency: self.is_dependency,
name,
scope: &self.scope,
})
}
fn evaluate_defined_function(
&mut self,
function: &FunctionDefinition<'src>,
arguments: &[Expression<'src>],
) -> RunResult<'src, String> {
let context = *self.context.as_ref().unwrap();
let mut scope = Scope::root();
for ((name, number), argument) in function.parameters.iter().copied().zip(arguments) {
let value = self.evaluate_expression(argument)?;
scope.bind(Binding {
eager: false,
export: false,
file_depth: 0,
name,
number,
prelude: false,
private: false,
value: value.clone(),
});
}
let overrides = HashMap::new();
let mut evaluator = Evaluator {
assignments: Some(&context.module.assignments),
context: Some(context),
env: BTreeMap::new(),
is_dependency: false,
non_const_assignments: Table::new(),
overrides: &overrides,
scope,
};
evaluator.evaluate_expression(&function.body)
}
fn evaluate_builtin_function(
&mut self,
name: Name<'src>,
function: Function,
arguments: &[Expression<'src>],
) -> RunResult<'src, String> {
match function {
Function::Nullary(f) => f(self.function_context(name).unwrap()),
Function::Unary(f) => {
let a = self.evaluate_expression(&arguments[0])?;
f(self.function_context(name).unwrap(), &a)
}
Function::UnaryOpt(f) => {
let a = self.evaluate_expression(&arguments[0])?;
let b = if arguments.len() > 1 {
Some(self.evaluate_expression(&arguments[1])?)
} else {
None
};
f(self.function_context(name).unwrap(), &a, b.as_deref())
}
Function::UnaryPlus(f) => {
let a = self.evaluate_expression(&arguments[0])?;
let mut rest = Vec::new();
for arg in &arguments[1..] {
rest.push(self.evaluate_expression(arg)?);
}
f(self.function_context(name).unwrap(), &a, &rest)
}
Function::Binary(f) => {
let a = self.evaluate_expression(&arguments[0])?;
let b = self.evaluate_expression(&arguments[1])?;
f(self.function_context(name).unwrap(), &a, &b)
}
Function::BinaryPlus(f) => {
let a = self.evaluate_expression(&arguments[0])?;
let b = self.evaluate_expression(&arguments[1])?;
let mut rest = Vec::new();
for arg in &arguments[2..] {
rest.push(self.evaluate_expression(arg)?);
}
f(self.function_context(name).unwrap(), &a, &b, &rest)
}
Function::Ternary(f) => {
let a = self.evaluate_expression(&arguments[0])?;
let b = self.evaluate_expression(&arguments[1])?;
let c = self.evaluate_expression(&arguments[2])?;
f(self.function_context(name).unwrap(), &a, &b, &c)
}
}
.map_err(|message| Error::FunctionCall {
function: name,
message,
})
}
pub(crate) fn evaluate_expression(
&mut self,
expression: &Expression<'src>,
) -> RunResult<'src, String> {
match expression {
Expression::And { lhs, rhs } => {
let lhs = self.evaluate_expression(lhs)?;
if lhs.is_empty() {
return Ok(String::new());
}
self.evaluate_expression(rhs)
}
Expression::Assert {
condition,
error,
name,
} => {
if self.evaluate_condition(condition)? {
Ok(String::new())
} else {
Err(Error::Assert {
message: self.evaluate_expression(error)?,
name: *name,
})
}
}
Expression::Backtick { contents, token } => {
let context = self.context(ConstError::Backtick(*token))?;
if context.config.dry_run {
return Ok(format!("`{contents}`"));
}
Self::run_command(context, &self.env, &self.scope, contents, None).map_err(|output_error| {
Error::Backtick {
token: *token,
output_error,
}
})
}
Expression::Call { name, arguments } => {
let module = self.context(ConstError::FunctionCall(*name))?.module;
if let Some(function) = module.functions.get(name.lexeme()) {
self.evaluate_defined_function(function, arguments)
} else if let Some(builtin) = function::get(name.lexeme()) {
self.evaluate_builtin_function(*name, builtin, arguments)
} else {
unreachable!();
}
}
Expression::Concatenation { lhs, rhs } => {
let lhs = self.evaluate_expression(lhs)?;
let rhs = self.evaluate_expression(rhs)?;
Ok(lhs + &rhs)
}
Expression::Conditional {
condition,
then,
otherwise,
} => {
if self.evaluate_condition(condition)? {
self.evaluate_expression(then)
} else {
self.evaluate_expression(otherwise)
}
}
Expression::FormatString { start, expressions } => {
let mut value = start.cooked.clone();
for (expression, string) in expressions {
value.push_str(&self.evaluate_expression(expression)?);
value.push_str(&string.cooked);
}
if start.kind.indented {
Ok(unindent(&value))
} else {
Ok(value)
}
}
Expression::Group { contents } => self.evaluate_expression(contents),
Expression::Join { lhs: None, rhs } => Ok("/".to_string() + &self.evaluate_expression(rhs)?),
Expression::Join {
lhs: Some(lhs),
rhs,
} => {
let lhs = self.evaluate_expression(lhs)?;
let rhs = self.evaluate_expression(rhs)?;
Ok(lhs + "/" + &rhs)
}
Expression::Or { lhs, rhs } => {
let lhs = self.evaluate_expression(lhs)?;
if !lhs.is_empty() {
return Ok(lhs);
}
self.evaluate_expression(rhs)
}
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
Expression::Variable { name, .. } => {
let variable = name.lexeme();
if let Some(value) = self.scope.value(variable) {
Ok(value.to_owned())
} else if self.non_const_assignments.contains_key(name.lexeme()) {
Err(ConstError::Variable(*name).into())
} else if let Some(assignment) = self
.assignments
.and_then(|assignments| assignments.get(variable))
{
Ok(self.evaluate_assignment(assignment)?.to_owned())
} else {
Err(Error::internal(format!(
"attempted to evaluate undefined variable `{variable}`"
)))
}
}
}
}
fn evaluate_condition(&mut self, condition: &Condition<'src>) -> RunResult<'src, bool> {
let lhs_value = self.evaluate_expression(&condition.lhs)?;
let rhs_value = self.evaluate_expression(&condition.rhs)?;
let condition = match condition.operator {
ConditionalOperator::Equality => lhs_value == rhs_value,
ConditionalOperator::Inequality => lhs_value != rhs_value,
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
.map_err(|source| Error::RegexCompile { source })?
.is_match(&lhs_value),
ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value)
.map_err(|source| Error::RegexCompile { source })?
.is_match(&lhs_value),
};
Ok(condition)
}
pub(crate) fn run_command(
context: &ExecutionContext,
env: &BTreeMap<String, String>,
scope: &Scope,
command: &str,
args: Option<&[String]>,
) -> Result<String, OutputError> {
let mut cmd = context.module.settings.shell_command(context.config);
cmd.arg(command);
if let Some(args) = args {
if ShellKind::from(&cmd).takes_shell_name() {
cmd.arg(command);
}
cmd.args(args);
}
cmd
.current_dir(context.working_directory())
.export(
&context.module.settings,
context.dotenv,
scope,
&context.module.unexports,
)
.stdin(Stdio::inherit())
.stderr(if context.config.verbosity.quiet() {
Stdio::null()
} else {
Stdio::inherit()
})
.stdout(Stdio::piped());
for (key, value) in env {
cmd.env(key, value);
}
cmd.output_guard_stdout()
}
pub(crate) fn evaluate_line(
&mut self,
line: &Line<'src>,
continued: bool,
) -> RunResult<'src, String> {
let mut evaluated = String::new();
for (i, fragment) in line.fragments.iter().enumerate() {
match fragment {
Fragment::Text { token } => {
let lexeme = token
.lexeme()
.replace(Lexer::INTERPOLATION_ESCAPE, Lexer::INTERPOLATION_START);
if i == 0 && continued {
evaluated += lexeme.trim_start();
} else {
evaluated += &lexeme;
}
}
Fragment::Interpolation { expression } => {
evaluated += &self.evaluate_expression(expression)?;
}
}
}
Ok(evaluated)
}
pub(crate) fn evaluate_parameters(
arguments: &[Vec<String>],
context: &ExecutionContext<'src, 'run>,
is_dependency: bool,
parameters: &[Parameter<'src>],
recipe: &Recipe<'src>,
scope: &'run Scope<'src, 'run>,
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
let env = recipe
.attributes
.iter()
.filter_map(|attribute| {
if let Attribute::Env(key, value) = attribute {
Some((key.cooked.clone(), value.cooked.clone()))
} else {
None
}
})
.collect();
let mut evaluator = Self::new(context, env, is_dependency, scope);
let mut positional = Vec::new();
if arguments.len() != parameters.len() {
return Err(Error::internal("arguments do not match parameter count"));
}
for (parameter, group) in parameters.iter().zip(arguments) {
let values = if group.is_empty() {
if let Some(ref default) = parameter.default {
let value = evaluator.evaluate_expression(default)?;
positional.push(value.clone());
vec![value]
} else if parameter.kind == ParameterKind::Star {
Vec::new()
} else {
return Err(Error::internal("missing parameter without default"));
}
} else if parameter.kind.is_variadic() {
positional.extend_from_slice(group);
group.clone()
} else {
if group.len() != 1 {
return Err(Error::internal(
"multiple values for non-variadic parameter",
));
}
let value = group[0].clone();
positional.push(value.clone());
vec![value]
};
for value in &values {
parameter.check_pattern_match(recipe, value)?;
}
evaluator.scope.bind(Binding {
eager: false,
export: parameter.export,
file_depth: 0,
name: parameter.name,
number: parameter.number,
prelude: false,
private: false,
value: values.join(" "),
});
}
Ok((evaluator.scope, positional))
}
pub(crate) fn new(
context: &ExecutionContext<'src, 'run>,
env: BTreeMap<String, String>,
is_dependency: bool,
scope: &'run Scope<'src, 'run>,
) -> Self {
static OVERRIDES: LazyLock<HashMap<Number, String>> = LazyLock::new(HashMap::new);
Self {
assignments: None,
context: Some(*context),
env,
is_dependency,
non_const_assignments: Table::new(),
overrides: &OVERRIDES,
scope: scope.child(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
run_error! {
name: backtick_code,
src: "
a:
echo {{`f() { return 100; }; f`}}
",
args: ["a"],
error: Error::Backtick {
token,
output_error: OutputError::Code(code),
},
check: {
assert_eq!(code, 100);
assert_eq!(token.lexeme(), "`f() { return 100; }; f`");
}
}
run_error! {
name: export_assignment_backtick,
src: "
export exported_variable := 'A'
b := `echo $exported_variable`
recipe:
echo {{b}}
",
args: ["--quiet", "recipe"],
error: Error::Backtick {
token,
output_error: OutputError::Code(_),
},
check: {
assert_eq!(token.lexeme(), "`echo $exported_variable`");
}
}
}