Reform scope and binding (#556)

Clean up scope handling by introducing `Binding` and `Scope` objects.
This commit is contained in:
Casey Rodarmor 2019-12-07 03:09:21 -08:00 committed by GitHub
parent d0e813cd8b
commit 2d3134a91c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 542 additions and 437 deletions

View File

@ -1,18 +1,4 @@
use crate::common::*;
/// An assignment, e.g `foo := bar`
#[derive(Debug, PartialEq)]
pub(crate) struct Assignment<'src> {
/// Assignment was prefixed by the `export` keyword
pub(crate) export: bool,
/// Left-hand side of the assignment
pub(crate) name: Name<'src>,
/// Right-hand side of the assignment
pub(crate) expression: Expression<'src>,
}
impl<'src> Keyed<'src> for Assignment<'src> {
fn key(&self) -> &'src str {
self.name.lexeme()
}
}
pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;

View File

@ -1,231 +0,0 @@
use crate::common::*;
pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
pub(crate) assignments: &'b Table<'a, Assignment<'a>>,
pub(crate) config: &'a Config,
pub(crate) dotenv: &'b BTreeMap<String, String>,
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
pub(crate) working_directory: &'b Path,
pub(crate) overrides: &'b BTreeMap<String, String>,
pub(crate) settings: &'b Settings<'b>,
}
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
pub(crate) fn evaluate_assignments(
config: &'a Config,
working_directory: &'b Path,
dotenv: &'b BTreeMap<String, String>,
assignments: &Table<'a, Assignment<'a>>,
overrides: &BTreeMap<String, String>,
settings: &'b Settings<'b>,
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
let mut evaluator = AssignmentEvaluator {
evaluated: empty(),
scope: &empty(),
settings,
overrides,
config,
assignments,
working_directory,
dotenv,
};
for name in assignments.keys() {
evaluator.evaluate_assignment(name)?;
}
Ok(evaluator.evaluated)
}
pub(crate) fn evaluate_line(
&mut self,
line: &[Fragment<'a>],
arguments: &BTreeMap<&'a str, Cow<str>>,
) -> RunResult<'a, String> {
let mut evaluated = String::new();
for fragment in line {
match fragment {
Fragment::Text { token } => evaluated += token.lexeme(),
Fragment::Interpolation { expression } => {
evaluated += &self.evaluate_expression(expression, arguments)?;
}
}
}
Ok(evaluated)
}
fn evaluate_assignment(&mut self, name: &'a str) -> RunResult<'a, ()> {
if self.evaluated.contains_key(name) {
return Ok(());
}
if let Some(assignment) = self.assignments.get(name) {
if let Some(value) = self.overrides.get(name) {
self
.evaluated
.insert(name, (assignment.export, value.to_string()));
} else {
let value = self.evaluate_expression(&assignment.expression, &empty())?;
self.evaluated.insert(name, (assignment.export, value));
}
} else {
return Err(RuntimeError::Internal {
message: format!("attempted to evaluated unknown assignment {}", name),
});
}
Ok(())
}
pub(crate) fn evaluate_expression(
&mut self,
expression: &Expression<'a>,
arguments: &BTreeMap<&'a str, Cow<str>>,
) -> RunResult<'a, String> {
match expression {
Expression::Variable { name, .. } => {
let variable = name.lexeme();
if self.evaluated.contains_key(variable) {
Ok(self.evaluated[variable].1.clone())
} else if self.scope.contains_key(variable) {
Ok(self.scope[variable].1.clone())
} else if self.assignments.contains_key(variable) {
self.evaluate_assignment(variable)?;
Ok(self.evaluated[variable].1.clone())
} else if arguments.contains_key(variable) {
Ok(arguments[variable].to_string())
} else {
Err(RuntimeError::Internal {
message: format!("attempted to evaluate undefined variable `{}`", variable),
})
}
}
Expression::Call { thunk } => {
let context = FunctionContext {
invocation_directory: &self.config.invocation_directory,
working_directory: &self.working_directory,
dotenv: self.dotenv,
};
use Thunk::*;
match thunk {
Nullary { name, function, .. } => {
function(&context).map_err(|message| RuntimeError::FunctionCall {
function: *name,
message,
})
}
Unary {
name,
function,
arg,
..
} => function(&context, &self.evaluate_expression(arg, arguments)?).map_err(|message| {
RuntimeError::FunctionCall {
function: *name,
message,
}
}),
Binary {
name,
function,
args: [a, b],
..
} => function(
&context,
&self.evaluate_expression(a, arguments)?,
&self.evaluate_expression(b, arguments)?,
)
.map_err(|message| RuntimeError::FunctionCall {
function: *name,
message,
}),
}
}
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
Expression::Backtick { contents, token } => {
if self.config.dry_run {
Ok(format!("`{}`", contents))
} else {
Ok(self.run_backtick(self.dotenv, contents, token)?)
}
}
Expression::Concatination { lhs, rhs } => {
Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
}
Expression::Group { contents } => self.evaluate_expression(contents, arguments),
}
}
fn run_backtick(
&self,
dotenv: &BTreeMap<String, String>,
raw: &str,
token: &Token<'a>,
) -> RunResult<'a, String> {
let mut cmd = self.settings.shell_command(self.config);
cmd.arg(raw);
cmd.current_dir(self.working_directory);
cmd.export_environment_variables(self.scope, dotenv)?;
cmd.stdin(process::Stdio::inherit());
cmd.stderr(if self.config.quiet {
process::Stdio::null()
} else {
process::Stdio::inherit()
});
InterruptHandler::guard(|| {
output(cmd).map_err(|output_error| RuntimeError::Backtick {
token: *token,
output_error,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
run_error! {
name: backtick_code,
src: "
a:
echo {{`f() { return 100; }; f`}}
",
args: ["a"],
error: RuntimeError::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: r#"
export exported_variable = "A"
b = `echo $exported_variable`
recipe:
echo {{b}}
"#,
args: ["--quiet", "recipe"],
error: RuntimeError::Backtick {
token,
output_error: OutputError::Code(_),
},
check: {
assert_eq!(token.lexeme(), "`echo $exported_variable`");
}
}
}

View File

@ -2,17 +2,17 @@ use crate::common::*;
use CompilationErrorKind::*;
pub(crate) struct AssignmentResolver<'a: 'b, 'b> {
assignments: &'b Table<'a, Assignment<'a>>,
stack: Vec<&'a str>,
seen: BTreeSet<&'a str>,
evaluated: BTreeSet<&'a str>,
pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
assignments: &'run Table<'src, Assignment<'src>>,
stack: Vec<&'src str>,
seen: BTreeSet<&'src str>,
evaluated: BTreeSet<&'src str>,
}
impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
pub(crate) fn resolve_assignments(
assignments: &Table<'a, Assignment<'a>>,
) -> CompilationResult<'a, ()> {
assignments: &Table<'src, Assignment<'src>>,
) -> CompilationResult<'src, ()> {
let mut resolver = AssignmentResolver {
stack: empty(),
seen: empty(),
@ -27,7 +27,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
Ok(())
}
fn resolve_assignment(&mut self, name: &'a str) -> CompilationResult<'a, ()> {
fn resolve_assignment(&mut self, name: &'src str) -> CompilationResult<'src, ()> {
if self.evaluated.contains(name) {
return Ok(());
}
@ -36,7 +36,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
self.stack.push(name);
if let Some(assignment) = self.assignments.get(name) {
self.resolve_expression(&assignment.expression)?;
self.resolve_expression(&assignment.value)?;
self.evaluated.insert(name);
} else {
let message = format!("attempted to resolve unknown assignment `{}`", name);
@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
Ok(())
}
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompilationResult<'src, ()> {
match expression {
Expression::Variable { name } => {
let variable = name.lexeme();

18
src/binding.rs Normal file
View File

@ -0,0 +1,18 @@
use crate::common::*;
/// A binding of `name` to `value`
#[derive(Debug, PartialEq)]
pub(crate) struct Binding<'src, V = String> {
/// Export binding as an environment variable to child processes
pub(crate) export: bool,
/// Binding name
pub(crate) name: Name<'src>,
/// Binding value
pub(crate) value: V,
}
impl<'src, V> Keyed<'src> for Binding<'src, V> {
fn key(&self) -> &'src str {
self.name.lexeme()
}
}

View File

@ -1,29 +1,31 @@
use crate::common::*;
pub(crate) trait CommandExt {
fn export_environment_variables<'a>(
&mut self,
scope: &BTreeMap<&'a str, (bool, String)>,
dotenv: &BTreeMap<String, String>,
) -> RunResult<'a, ()>;
fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope);
fn export_scope(&mut self, scope: &Scope);
}
impl CommandExt for Command {
fn export_environment_variables<'a>(
&mut self,
scope: &BTreeMap<&'a str, (bool, String)>,
dotenv: &BTreeMap<String, String>,
) -> RunResult<'a, ()> {
fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope) {
for (name, value) in dotenv {
self.env(name, value);
}
for (name, (export, value)) in scope {
if *export {
self.env(name, value);
if let Some(parent) = scope.parent() {
self.export_scope(parent);
}
}
Ok(())
fn export_scope(&mut self, scope: &Scope) {
if let Some(parent) = scope.parent() {
self.export_scope(parent);
}
for binding in scope.bindings() {
if binding.export {
self.env(binding.name.lexeme(), &binding.value);
}
}
}
}

View File

@ -49,16 +49,16 @@ pub(crate) use crate::{
// structs and enums
pub(crate) use crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment,
assignment_evaluator::AssignmentEvaluator, assignment_resolver::AssignmentResolver, color::Color,
assignment_resolver::AssignmentResolver, binding::Binding, color::Color,
compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind,
compiler::Compiler, config::Config, config_error::ConfigError, count::Count,
dependency::Dependency, enclosure::Enclosure, expression::Expression, fragment::Fragment,
function::Function, function_context::FunctionContext, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
list::List, load_error::LoadError, module::Module, name::Name, output_error::OutputError,
parameter::Parameter, parser::Parser, platform::Platform, position::Position,
positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search,
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
name::Name, output_error::OutputError, parameter::Parameter, parser::Parser, platform::Platform,
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token,

View File

@ -1,55 +1,55 @@
use crate::common::*;
#[derive(Debug, PartialEq)]
pub(crate) enum CompilationErrorKind<'a> {
pub(crate) enum CompilationErrorKind<'src> {
AliasShadowsRecipe {
alias: &'a str,
alias: &'src str,
recipe_line: usize,
},
CircularRecipeDependency {
recipe: &'a str,
circle: Vec<&'a str>,
recipe: &'src str,
circle: Vec<&'src str>,
},
CircularVariableDependency {
variable: &'a str,
circle: Vec<&'a str>,
variable: &'src str,
circle: Vec<&'src str>,
},
DependencyHasParameters {
recipe: &'a str,
dependency: &'a str,
recipe: &'src str,
dependency: &'src str,
},
DuplicateAlias {
alias: &'a str,
alias: &'src str,
first: usize,
},
DuplicateDependency {
recipe: &'a str,
dependency: &'a str,
recipe: &'src str,
dependency: &'src str,
},
DuplicateParameter {
recipe: &'a str,
parameter: &'a str,
recipe: &'src str,
parameter: &'src str,
},
DuplicateRecipe {
recipe: &'a str,
recipe: &'src str,
first: usize,
},
DuplicateVariable {
variable: &'a str,
variable: &'src str,
},
DuplicateSet {
setting: &'a str,
setting: &'src str,
first: usize,
},
ExtraLeadingWhitespace,
FunctionArgumentCountMismatch {
function: &'a str,
function: &'src str,
found: usize,
expected: usize,
},
InconsistentLeadingWhitespace {
expected: &'a str,
found: &'a str,
expected: &'src str,
found: &'src str,
},
Internal {
message: String,
@ -58,38 +58,38 @@ pub(crate) enum CompilationErrorKind<'a> {
character: char,
},
MixedLeadingWhitespace {
whitespace: &'a str,
whitespace: &'src str,
},
ParameterFollowsVariadicParameter {
parameter: &'a str,
parameter: &'src str,
},
ParameterShadowsVariable {
parameter: &'a str,
parameter: &'src str,
},
RequiredParameterFollowsDefaultParameter {
parameter: &'a str,
parameter: &'src str,
},
UndefinedVariable {
variable: &'a str,
variable: &'src str,
},
UnexpectedToken {
expected: Vec<TokenKind>,
found: TokenKind,
},
UnknownAliasTarget {
alias: &'a str,
target: &'a str,
alias: &'src str,
target: &'src str,
},
UnknownDependency {
recipe: &'a str,
unknown: &'a str,
recipe: &'src str,
unknown: &'src str,
},
UnknownFunction {
function: &'a str,
function: &'src str,
},
UnknownStartOfToken,
UnknownSetting {
setting: &'a str,
setting: &'src str,
},
UnpairedCarriageReturn,
UnterminatedInterpolation,

View File

@ -1,4 +1,4 @@
use crate::common::*;
#[derive(PartialEq, Debug)]
pub(crate) struct Dependency<'a>(pub(crate) Rc<Recipe<'a>>);
pub(crate) struct Dependency<'src>(pub(crate) Rc<Recipe<'src>>);

266
src/evaluator.rs Normal file
View File

@ -0,0 +1,266 @@
use crate::common::*;
pub(crate) struct Evaluator<'src: 'run, 'run> {
assignments: Option<&'run Table<'src, Assignment<'src>>>,
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
scope: Scope<'src, 'run>,
settings: &'run Settings<'run>,
working_directory: &'run Path,
}
impl<'src, 'run> Evaluator<'src, 'run> {
pub(crate) fn evaluate_assignments(
assignments: &'run Table<'src, Assignment<'src>>,
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
overrides: Scope<'src, 'run>,
settings: &'run Settings<'run>,
working_directory: &'run Path,
) -> RunResult<'src, Scope<'src, 'run>> {
let mut evaluator = Evaluator {
scope: overrides,
assignments: Some(assignments),
config,
dotenv,
settings,
working_directory,
};
for assignment in assignments.values() {
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 = self.evaluate_expression(&assignment.value)?;
self.scope.bind(assignment.export, assignment.name, value);
}
Ok(self.scope.value(name).unwrap())
}
pub(crate) fn evaluate_expression(
&mut self,
expression: &Expression<'src>,
) -> RunResult<'src, String> {
match expression {
Expression::Variable { name, .. } => {
let variable = name.lexeme();
if let Some(value) = self.scope.value(variable) {
Ok(value.to_owned())
} else if let Some(assignment) = self
.assignments
.and_then(|assignments| assignments.get(variable))
{
Ok(self.evaluate_assignment(assignment)?.to_owned())
} else {
Err(RuntimeError::Internal {
message: format!("attempted to evaluate undefined variable `{}`", variable),
})
}
}
Expression::Call { thunk } => {
let context = FunctionContext {
invocation_directory: &self.config.invocation_directory,
working_directory: &self.working_directory,
dotenv: self.dotenv,
};
use Thunk::*;
match thunk {
Nullary { name, function, .. } => {
function(&context).map_err(|message| RuntimeError::FunctionCall {
function: *name,
message,
})
}
Unary {
name,
function,
arg,
..
} => function(&context, &self.evaluate_expression(arg)?).map_err(|message| {
RuntimeError::FunctionCall {
function: *name,
message,
}
}),
Binary {
name,
function,
args: [a, b],
..
} => function(
&context,
&self.evaluate_expression(a)?,
&self.evaluate_expression(b)?,
)
.map_err(|message| RuntimeError::FunctionCall {
function: *name,
message,
}),
}
}
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
Expression::Backtick { contents, token } => {
if self.config.dry_run {
Ok(format!("`{}`", contents))
} else {
Ok(self.run_backtick(contents, token)?)
}
}
Expression::Concatination { lhs, rhs } => {
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
}
Expression::Group { contents } => self.evaluate_expression(contents),
}
}
fn run_backtick(&self, raw: &str, token: &Token<'src>) -> RunResult<'src, String> {
let mut cmd = self.settings.shell_command(self.config);
cmd.arg(raw);
cmd.current_dir(self.working_directory);
cmd.export(self.dotenv, &self.scope);
cmd.stdin(process::Stdio::inherit());
cmd.stderr(if self.config.quiet {
process::Stdio::null()
} else {
process::Stdio::inherit()
});
InterruptHandler::guard(|| {
output(cmd).map_err(|output_error| RuntimeError::Backtick {
token: *token,
output_error,
})
})
}
pub(crate) fn evaluate_line(&mut self, line: &Line<'src>) -> RunResult<'src, String> {
let mut evaluated = String::new();
for fragment in &line.fragments {
match fragment {
Fragment::Text { token } => evaluated += token.lexeme(),
Fragment::Interpolation { expression } => {
evaluated += &self.evaluate_expression(expression)?;
}
}
}
Ok(evaluated)
}
pub(crate) fn evaluate_parameters(
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
parameters: &[Parameter<'src>],
arguments: &[&str],
scope: &'run Scope<'src, 'run>,
settings: &'run Settings,
working_directory: &'run Path,
) -> RunResult<'src, Scope<'src, 'run>> {
let mut evaluator = Evaluator {
assignments: None,
scope: Scope::child(scope),
settings,
dotenv,
config,
working_directory,
};
let mut scope = Scope::child(scope);
let mut rest = arguments;
for parameter in parameters {
let value = if rest.is_empty() {
match parameter.default {
Some(ref default) => evaluator.evaluate_expression(default)?,
None => {
return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(),
});
}
}
} else if parameter.variadic {
let value = rest.to_vec().join(" ");
rest = &[];
value
} else {
let value = rest[0].to_owned();
rest = &rest[1..];
value
};
scope.bind(false, parameter.name, value);
}
Ok(scope)
}
pub(crate) fn line_evaluator(
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
scope: &'run Scope<'src, 'run>,
settings: &'run Settings,
working_directory: &'run Path,
) -> Evaluator<'src, 'run> {
Evaluator {
assignments: None,
scope: Scope::child(scope),
settings,
dotenv,
config,
working_directory,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
run_error! {
name: backtick_code,
src: "
a:
echo {{`f() { return 100; }; f`}}
",
args: ["a"],
error: RuntimeError::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: r#"
export exported_variable = "A"
b = `echo $exported_variable`
recipe:
echo {{b}}
"#,
args: ["--quiet", "recipe"],
error: RuntimeError::Backtick {
token,
output_error: OutputError::Code(_),
},
check: {
assert_eq!(token.lexeme(), "`echo $exported_variable`");
}
}
}

View File

@ -1,7 +1,7 @@
use crate::common::*;
pub(crate) struct FunctionContext<'a> {
pub(crate) invocation_directory: &'a Path,
pub(crate) working_directory: &'a Path,
pub(crate) dotenv: &'a BTreeMap<String, String>,
pub(crate) struct FunctionContext<'run> {
pub(crate) invocation_directory: &'run Path,
pub(crate) working_directory: &'run Path,
pub(crate) dotenv: &'run BTreeMap<String, String>,
}

View File

@ -81,23 +81,48 @@ impl<'src> Justfile<'src> {
let dotenv = load_dotenv()?;
let scope = AssignmentEvaluator::evaluate_assignments(
config,
working_directory,
&dotenv,
let scope = {
let mut scope = Scope::new();
let mut unknown_overrides = Vec::new();
for (name, value) in overrides {
if let Some(assignment) = self.assignments.get(name) {
scope.bind(assignment.export, assignment.name, value.clone());
} else {
unknown_overrides.push(name.as_ref());
}
}
if !unknown_overrides.is_empty() {
return Err(RuntimeError::UnknownOverrides {
overrides: unknown_overrides,
});
}
Evaluator::evaluate_assignments(
&self.assignments,
overrides,
config,
&dotenv,
scope,
&self.settings,
)?;
working_directory,
)?
};
if let Subcommand::Evaluate { .. } = config.subcommand {
let mut width = 0;
for name in scope.keys() {
for name in scope.names() {
width = cmp::max(name.len(), width);
}
for (name, (_export, value)) in scope {
println!("{0:1$} := \"{2}\"", name, width, value);
for binding in scope.bindings() {
println!(
"{0:1$} := \"{2}\"",
binding.name.lexeme(),
width,
binding.value
);
}
return Ok(());
}
@ -152,7 +177,7 @@ impl<'src> Justfile<'src> {
let mut ran = empty();
for (recipe, arguments) in grouped {
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran, overrides)?
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
}
Ok(())
@ -172,21 +197,20 @@ impl<'src> Justfile<'src> {
}
}
fn run_recipe<'b>(
fn run_recipe<'run>(
&self,
context: &'b RecipeContext<'src>,
context: &'run RecipeContext<'src, 'run>,
recipe: &Recipe<'src>,
arguments: &[&'src str],
dotenv: &BTreeMap<String, String>,
ran: &mut BTreeSet<&'src str>,
overrides: &BTreeMap<String, String>,
) -> RunResult<()> {
) -> RunResult<'src, ()> {
for Dependency(dependency) in &recipe.dependencies {
if !ran.contains(dependency.name()) {
self.run_recipe(context, dependency, &[], dotenv, ran, overrides)?;
self.run_recipe(context, dependency, &[], dotenv, ran)?;
}
}
recipe.run(context, arguments, dotenv, overrides)?;
recipe.run(context, arguments, dotenv)?;
ran.insert(recipe.name());
Ok(())
}
@ -199,7 +223,7 @@ impl<'src> Display for Justfile<'src> {
if assignment.export {
write!(f, "export ")?;
}
write!(f, "{} := {}", name, assignment.expression)?;
write!(f, "{} := {}", name, assignment.value)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;

View File

@ -18,8 +18,8 @@ pub(crate) mod fuzzing;
mod alias;
mod analyzer;
mod assignment;
mod assignment_evaluator;
mod assignment_resolver;
mod binding;
mod color;
mod command_ext;
mod common;
@ -36,6 +36,7 @@ mod empty;
mod enclosure;
mod error;
mod error_result_ext;
mod evaluator;
mod expression;
mod fragment;
mod function;
@ -68,6 +69,7 @@ mod recipe_context;
mod recipe_resolver;
mod run;
mod runtime_error;
mod scope;
mod search;
mod search_config;
mod search_error;

View File

@ -42,7 +42,7 @@ impl<'src> Node<'src> for Assignment<'src> {
Tree::atom("assignment")
}
.push(self.name.lexeme())
.push(self.expression.tree())
.push(self.value.tree())
}
}

View File

@ -338,12 +338,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
fn parse_assignment(&mut self, export: bool) -> CompilationResult<'src, Assignment<'src>> {
let name = self.parse_name()?;
self.presume_any(&[Equals, ColonEquals])?;
let expression = self.parse_expression()?;
let value = self.parse_expression()?;
self.expect_eol()?;
Ok(Assignment {
name,
export,
expression,
value,
})
}

View File

@ -24,18 +24,18 @@ fn error_from_signal(
/// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug)]
pub(crate) struct Recipe<'a, D = Dependency<'a>> {
pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'a str>,
pub(crate) body: Vec<Line<'a>>,
pub(crate) name: Name<'a>,
pub(crate) parameters: Vec<Parameter<'a>>,
pub(crate) doc: Option<&'src str>,
pub(crate) body: Vec<Line<'src>>,
pub(crate) name: Name<'src>,
pub(crate) parameters: Vec<Parameter<'src>>,
pub(crate) private: bool,
pub(crate) quiet: bool,
pub(crate) shebang: bool,
}
impl<'a, D> Recipe<'a, D> {
impl<'src, D> Recipe<'src, D> {
pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {
self.min_arguments()..=self.max_arguments()
}
@ -56,7 +56,7 @@ impl<'a, D> Recipe<'a, D> {
}
}
pub(crate) fn name(&self) -> &'a str {
pub(crate) fn name(&self) -> &'src str {
self.name.lexeme()
}
@ -64,13 +64,12 @@ impl<'a, D> Recipe<'a, D> {
self.name.line
}
pub(crate) fn run(
pub(crate) fn run<'run>(
&self,
context: &RecipeContext<'a>,
arguments: &[&'a str],
context: &RecipeContext<'src, 'run>,
arguments: &[&'src str],
dotenv: &BTreeMap<String, String>,
overrides: &BTreeMap<String, String>,
) -> RunResult<'a, ()> {
) -> RunResult<'src, ()> {
let config = &context.config;
if config.verbosity.loquacious() {
@ -83,46 +82,28 @@ impl<'a, D> Recipe<'a, D> {
);
}
let mut argument_map = BTreeMap::new();
let mut evaluator = AssignmentEvaluator {
assignments: &empty(),
evaluated: empty(),
working_directory: context.working_directory,
scope: &context.scope,
settings: &context.settings,
overrides,
config,
let scope = Evaluator::evaluate_parameters(
context.config,
dotenv,
};
&self.parameters,
arguments,
&context.scope,
context.settings,
context.working_directory,
)?;
let mut rest = arguments;
for parameter in &self.parameters {
let value = if rest.is_empty() {
match parameter.default {
Some(ref default) => Cow::Owned(evaluator.evaluate_expression(default, &empty())?),
None => {
return Err(RuntimeError::Internal {
message: "missing parameter without default".to_string(),
});
}
}
} else if parameter.variadic {
let value = Cow::Owned(rest.to_vec().join(" "));
rest = &[];
value
} else {
let value = Cow::Borrowed(rest[0]);
rest = &rest[1..];
value
};
argument_map.insert(parameter.name.lexeme(), value);
}
let mut evaluator = Evaluator::line_evaluator(
context.config,
dotenv,
&scope,
context.settings,
context.working_directory,
);
if self.shebang {
let mut evaluated_lines = vec![];
for line in &self.body {
evaluated_lines.push(evaluator.evaluate_line(&line.fragments, &argument_map)?);
evaluated_lines.push(evaluator.evaluate_line(line)?);
}
if config.dry_run || self.quiet {
@ -202,7 +183,7 @@ impl<'a, D> Recipe<'a, D> {
output_error,
})?;
command.export_environment_variables(&context.scope, dotenv)?;
command.export(dotenv, &scope);
// run it!
match InterruptHandler::guard(|| command.status()) {
@ -242,7 +223,7 @@ impl<'a, D> Recipe<'a, D> {
}
let line = lines.next().unwrap();
line_number += 1;
evaluated += &evaluator.evaluate_line(&line.fragments, &argument_map)?;
evaluated += &evaluator.evaluate_line(line)?;
if line.is_continuation() {
evaluated.pop();
} else {
@ -286,7 +267,7 @@ impl<'a, D> Recipe<'a, D> {
cmd.stdout(Stdio::null());
}
cmd.export_environment_variables(&context.scope, dotenv)?;
cmd.export(dotenv, &scope);
match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) => {
@ -344,7 +325,7 @@ impl<'src, D> Keyed<'src> for Recipe<'src, D> {
}
}
impl<'a> Display for Recipe<'a> {
impl<'src> Display for Recipe<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
if let Some(doc) = self.doc {
writeln!(f, "# {}", doc)?;

View File

@ -1,8 +1,8 @@
use crate::common::*;
pub(crate) struct RecipeContext<'a> {
pub(crate) config: &'a Config,
pub(crate) scope: BTreeMap<&'a str, (bool, String)>,
pub(crate) working_directory: &'a Path,
pub(crate) settings: &'a Settings<'a>,
pub(crate) struct RecipeContext<'src: 'run, 'run> {
pub(crate) config: &'run Config,
pub(crate) scope: Scope<'src, 'run>,
pub(crate) working_directory: &'run Path,
pub(crate) settings: &'run Settings<'src>,
}

View File

@ -2,17 +2,17 @@ use crate::common::*;
use CompilationErrorKind::*;
pub(crate) struct RecipeResolver<'a: 'b, 'b> {
unresolved_recipes: Table<'a, Recipe<'a, Name<'a>>>,
resolved_recipes: Table<'a, Rc<Recipe<'a>>>,
assignments: &'b Table<'a, Assignment<'a>>,
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>,
resolved_recipes: Table<'src, Rc<Recipe<'src>>>,
assignments: &'run Table<'src, Assignment<'src>>,
}
impl<'a, 'b> RecipeResolver<'a, 'b> {
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
pub(crate) fn resolve_recipes(
unresolved_recipes: Table<'a, Recipe<'a, Name<'a>>>,
assignments: &Table<'a, Assignment<'a>>,
) -> CompilationResult<'a, Table<'a, Rc<Recipe<'a>>>> {
unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>,
assignments: &'run Table<'src, Assignment<'src>>,
) -> CompilationResult<'src, Table<'src, Rc<Recipe<'src>>>> {
let mut resolver = RecipeResolver {
resolved_recipes: empty(),
unresolved_recipes,
@ -48,9 +48,9 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
fn resolve_variable(
&self,
variable: &Token<'a>,
variable: &Token<'src>,
parameters: &[Parameter],
) -> CompilationResult<'a, ()> {
) -> CompilationResult<'src, ()> {
let name = variable.lexeme();
let undefined =
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
@ -64,9 +64,9 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
fn resolve_recipe(
&mut self,
stack: &mut Vec<&'a str>,
recipe: Recipe<'a, Name<'a>>,
) -> CompilationResult<'a, Rc<Recipe<'a>>> {
stack: &mut Vec<&'src str>,
recipe: Recipe<'src, Name<'src>>,
) -> CompilationResult<'src, Rc<Recipe<'src>>> {
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
return Ok(resolved.clone());
}

View File

@ -1,75 +1,75 @@
use crate::common::*;
#[derive(Debug)]
pub(crate) enum RuntimeError<'a> {
pub(crate) enum RuntimeError<'src> {
ArgumentCountMismatch {
recipe: &'a str,
parameters: Vec<&'a Parameter<'a>>,
recipe: &'src str,
parameters: Vec<&'src Parameter<'src>>,
found: usize,
min: usize,
max: usize,
},
Backtick {
token: Token<'a>,
token: Token<'src>,
output_error: OutputError,
},
Code {
recipe: &'a str,
recipe: &'src str,
line_number: Option<usize>,
code: i32,
},
Cygpath {
recipe: &'a str,
recipe: &'src str,
output_error: OutputError,
},
Dotenv {
dotenv_error: dotenv::Error,
},
FunctionCall {
function: Name<'a>,
function: Name<'src>,
message: String,
},
Internal {
message: String,
},
IoError {
recipe: &'a str,
recipe: &'src str,
io_error: io::Error,
},
Shebang {
recipe: &'a str,
recipe: &'src str,
command: String,
argument: Option<String>,
io_error: io::Error,
},
Signal {
recipe: &'a str,
recipe: &'src str,
line_number: Option<usize>,
signal: i32,
},
TmpdirIoError {
recipe: &'a str,
recipe: &'src str,
io_error: io::Error,
},
UnknownOverrides {
overrides: Vec<&'a str>,
overrides: Vec<&'src str>,
},
UnknownRecipes {
recipes: Vec<&'a str>,
suggestion: Option<&'a str>,
recipes: Vec<&'src str>,
suggestion: Option<&'src str>,
},
Unknown {
recipe: &'a str,
recipe: &'src str,
line_number: Option<usize>,
},
NoRecipes,
DefaultRecipeRequiresArguments {
recipe: &'a str,
recipe: &'src str,
min_arguments: usize,
},
}
impl Error for RuntimeError<'_> {
impl<'src> Error for RuntimeError<'src> {
fn code(&self) -> i32 {
match *self {
Self::Code { code, .. } => code,
@ -82,7 +82,7 @@ impl Error for RuntimeError<'_> {
}
}
impl<'a> RuntimeError<'a> {
impl<'src> RuntimeError<'src> {
fn context(&self) -> Option<Token> {
use RuntimeError::*;
match self {
@ -93,7 +93,7 @@ impl<'a> RuntimeError<'a> {
}
}
impl<'a> Display for RuntimeError<'a> {
impl<'src> Display for RuntimeError<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use RuntimeError::*;

57
src/scope.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::common::*;
#[derive(Debug)]
pub(crate) struct Scope<'src: 'run, 'run> {
parent: Option<&'run Scope<'src, 'run>>,
bindings: Table<'src, Binding<'src, String>>,
}
impl<'src, 'run> Scope<'src, 'run> {
pub(crate) fn child(parent: &'run Scope<'src, 'run>) -> Scope<'src, 'run> {
Scope {
parent: Some(parent),
bindings: Table::new(),
}
}
pub(crate) fn new() -> Scope<'src, 'run> {
Scope {
parent: None,
bindings: Table::new(),
}
}
pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, value: String) {
self.bindings.insert(Binding {
name,
export,
value,
});
}
pub(crate) fn bound(&self, name: &str) -> bool {
self.bindings.contains_key(name)
}
pub(crate) fn value(&self, name: &str) -> Option<&str> {
if let Some(binding) = self.bindings.get(name) {
Some(binding.value.as_ref())
} else if let Some(parent) = self.parent {
parent.value(name)
} else {
None
}
}
pub(crate) fn bindings(&self) -> impl Iterator<Item = &Binding<String>> {
self.bindings.values()
}
pub(crate) fn names(&self) -> impl Iterator<Item = &str> {
self.bindings.keys().cloned()
}
pub(crate) fn parent(&self) -> Option<&'run Scope<'src, 'run>> {
self.parent
}
}

View File

@ -1,15 +1,15 @@
pub(crate) struct Shebang<'a> {
pub(crate) interpreter: &'a str,
pub(crate) argument: Option<&'a str>,
pub(crate) struct Shebang<'line> {
pub(crate) interpreter: &'line str,
pub(crate) argument: Option<&'line str>,
}
impl<'a> Shebang<'a> {
pub(crate) fn new(text: &'a str) -> Option<Shebang<'a>> {
if !text.starts_with("#!") {
impl<'line> Shebang<'line> {
pub(crate) fn new(line: &'line str) -> Option<Shebang<'line>> {
if !line.starts_with("#!") {
return None;
}
let mut pieces = text[2..]
let mut pieces = line[2..]
.lines()
.nth(0)
.unwrap_or("")

View File

@ -1,21 +1,21 @@
use crate::common::*;
#[derive(Debug, PartialEq, Clone, Copy)]
pub(crate) struct Token<'a> {
pub(crate) struct Token<'src> {
pub(crate) offset: usize,
pub(crate) length: usize,
pub(crate) line: usize,
pub(crate) column: usize,
pub(crate) src: &'a str,
pub(crate) src: &'src str,
pub(crate) kind: TokenKind,
}
impl<'a> Token<'a> {
pub(crate) fn lexeme(&self) -> &'a str {
impl<'src> Token<'src> {
pub(crate) fn lexeme(&self) -> &'src str {
&self.src[self.offset..self.offset + self.length]
}
pub(crate) fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> {
pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
CompilationError { token: *self, kind }
}