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::*; use crate::common::*;
/// An assignment, e.g `foo := bar` /// An assignment, e.g `foo := bar`
#[derive(Debug, PartialEq)] pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;
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()
}
}

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::*; use CompilationErrorKind::*;
pub(crate) struct AssignmentResolver<'a: 'b, 'b> { pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
assignments: &'b Table<'a, Assignment<'a>>, assignments: &'run Table<'src, Assignment<'src>>,
stack: Vec<&'a str>, stack: Vec<&'src str>,
seen: BTreeSet<&'a str>, seen: BTreeSet<&'src str>,
evaluated: BTreeSet<&'a str>, evaluated: BTreeSet<&'src str>,
} }
impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> { impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
pub(crate) fn resolve_assignments( pub(crate) fn resolve_assignments(
assignments: &Table<'a, Assignment<'a>>, assignments: &Table<'src, Assignment<'src>>,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'src, ()> {
let mut resolver = AssignmentResolver { let mut resolver = AssignmentResolver {
stack: empty(), stack: empty(),
seen: empty(), seen: empty(),
@ -27,7 +27,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
Ok(()) 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) { if self.evaluated.contains(name) {
return Ok(()); return Ok(());
} }
@ -36,7 +36,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
self.stack.push(name); self.stack.push(name);
if let Some(assignment) = self.assignments.get(name) { if let Some(assignment) = self.assignments.get(name) {
self.resolve_expression(&assignment.expression)?; self.resolve_expression(&assignment.value)?;
self.evaluated.insert(name); self.evaluated.insert(name);
} else { } else {
let message = format!("attempted to resolve unknown assignment `{}`", name); let message = format!("attempted to resolve unknown assignment `{}`", name);
@ -56,7 +56,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
Ok(()) Ok(())
} }
fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> { fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompilationResult<'src, ()> {
match expression { match expression {
Expression::Variable { name } => { Expression::Variable { name } => {
let variable = name.lexeme(); 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::*; use crate::common::*;
pub(crate) trait CommandExt { pub(crate) trait CommandExt {
fn export_environment_variables<'a>( fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope);
&mut self,
scope: &BTreeMap<&'a str, (bool, String)>, fn export_scope(&mut self, scope: &Scope);
dotenv: &BTreeMap<String, String>,
) -> RunResult<'a, ()>;
} }
impl CommandExt for Command { impl CommandExt for Command {
fn export_environment_variables<'a>( fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope) {
&mut self,
scope: &BTreeMap<&'a str, (bool, String)>,
dotenv: &BTreeMap<String, String>,
) -> RunResult<'a, ()> {
for (name, value) in dotenv { for (name, value) in dotenv {
self.env(name, value); self.env(name, value);
} }
for (name, (export, value)) in scope { if let Some(parent) = scope.parent() {
if *export { self.export_scope(parent);
self.env(name, value);
} }
} }
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 // structs and enums
pub(crate) use crate::{ pub(crate) use crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment, 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, compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind,
compiler::Compiler, config::Config, config_error::ConfigError, count::Count, compiler::Compiler, config::Config, config_error::ConfigError, count::Count,
dependency::Dependency, enclosure::Enclosure, expression::Expression, fragment::Fragment, dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
function::Function, function_context::FunctionContext, interrupt_guard::InterruptGuard, fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line, interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
list::List, load_error::LoadError, module::Module, name::Name, output_error::OutputError, justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
parameter::Parameter, parser::Parser, platform::Platform, position::Position, name::Name, output_error::OutputError, parameter::Parameter, parser::Parser, platform::Platform,
positional::Positional, recipe::Recipe, recipe_context::RecipeContext, position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token, string_literal::StringLiteral, subcommand::Subcommand, table::Table, thunk::Thunk, token::Token,

View File

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

View File

@ -1,4 +1,4 @@
use crate::common::*; use crate::common::*;
#[derive(PartialEq, Debug)] #[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::*; use crate::common::*;
pub(crate) struct FunctionContext<'a> { pub(crate) struct FunctionContext<'run> {
pub(crate) invocation_directory: &'a Path, pub(crate) invocation_directory: &'run Path,
pub(crate) working_directory: &'a Path, pub(crate) working_directory: &'run Path,
pub(crate) dotenv: &'a BTreeMap<String, String>, pub(crate) dotenv: &'run BTreeMap<String, String>,
} }

View File

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

View File

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

View File

@ -42,7 +42,7 @@ impl<'src> Node<'src> for Assignment<'src> {
Tree::atom("assignment") Tree::atom("assignment")
} }
.push(self.name.lexeme()) .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>> { fn parse_assignment(&mut self, export: bool) -> CompilationResult<'src, Assignment<'src>> {
let name = self.parse_name()?; let name = self.parse_name()?;
self.presume_any(&[Equals, ColonEquals])?; self.presume_any(&[Equals, ColonEquals])?;
let expression = self.parse_expression()?; let value = self.parse_expression()?;
self.expect_eol()?; self.expect_eol()?;
Ok(Assignment { Ok(Assignment {
name, name,
export, export,
expression, value,
}) })
} }

View File

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

View File

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

View File

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

View File

@ -1,75 +1,75 @@
use crate::common::*; use crate::common::*;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum RuntimeError<'a> { pub(crate) enum RuntimeError<'src> {
ArgumentCountMismatch { ArgumentCountMismatch {
recipe: &'a str, recipe: &'src str,
parameters: Vec<&'a Parameter<'a>>, parameters: Vec<&'src Parameter<'src>>,
found: usize, found: usize,
min: usize, min: usize,
max: usize, max: usize,
}, },
Backtick { Backtick {
token: Token<'a>, token: Token<'src>,
output_error: OutputError, output_error: OutputError,
}, },
Code { Code {
recipe: &'a str, recipe: &'src str,
line_number: Option<usize>, line_number: Option<usize>,
code: i32, code: i32,
}, },
Cygpath { Cygpath {
recipe: &'a str, recipe: &'src str,
output_error: OutputError, output_error: OutputError,
}, },
Dotenv { Dotenv {
dotenv_error: dotenv::Error, dotenv_error: dotenv::Error,
}, },
FunctionCall { FunctionCall {
function: Name<'a>, function: Name<'src>,
message: String, message: String,
}, },
Internal { Internal {
message: String, message: String,
}, },
IoError { IoError {
recipe: &'a str, recipe: &'src str,
io_error: io::Error, io_error: io::Error,
}, },
Shebang { Shebang {
recipe: &'a str, recipe: &'src str,
command: String, command: String,
argument: Option<String>, argument: Option<String>,
io_error: io::Error, io_error: io::Error,
}, },
Signal { Signal {
recipe: &'a str, recipe: &'src str,
line_number: Option<usize>, line_number: Option<usize>,
signal: i32, signal: i32,
}, },
TmpdirIoError { TmpdirIoError {
recipe: &'a str, recipe: &'src str,
io_error: io::Error, io_error: io::Error,
}, },
UnknownOverrides { UnknownOverrides {
overrides: Vec<&'a str>, overrides: Vec<&'src str>,
}, },
UnknownRecipes { UnknownRecipes {
recipes: Vec<&'a str>, recipes: Vec<&'src str>,
suggestion: Option<&'a str>, suggestion: Option<&'src str>,
}, },
Unknown { Unknown {
recipe: &'a str, recipe: &'src str,
line_number: Option<usize>, line_number: Option<usize>,
}, },
NoRecipes, NoRecipes,
DefaultRecipeRequiresArguments { DefaultRecipeRequiresArguments {
recipe: &'a str, recipe: &'src str,
min_arguments: usize, min_arguments: usize,
}, },
} }
impl Error for RuntimeError<'_> { impl<'src> Error for RuntimeError<'src> {
fn code(&self) -> i32 { fn code(&self) -> i32 {
match *self { match *self {
Self::Code { code, .. } => code, 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> { fn context(&self) -> Option<Token> {
use RuntimeError::*; use RuntimeError::*;
match self { 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> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
use RuntimeError::*; 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) struct Shebang<'line> {
pub(crate) interpreter: &'a str, pub(crate) interpreter: &'line str,
pub(crate) argument: Option<&'a str>, pub(crate) argument: Option<&'line str>,
} }
impl<'a> Shebang<'a> { impl<'line> Shebang<'line> {
pub(crate) fn new(text: &'a str) -> Option<Shebang<'a>> { pub(crate) fn new(line: &'line str) -> Option<Shebang<'line>> {
if !text.starts_with("#!") { if !line.starts_with("#!") {
return None; return None;
} }
let mut pieces = text[2..] let mut pieces = line[2..]
.lines() .lines()
.nth(0) .nth(0)
.unwrap_or("") .unwrap_or("")

View File

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