From 2d3134a91c8c6f260f6da309f86f454d9fe49f56 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 7 Dec 2019 03:09:21 -0800 Subject: [PATCH] Reform scope and binding (#556) Clean up scope handling by introducing `Binding` and `Scope` objects. --- src/assignment.rs | 16 +- src/assignment_evaluator.rs | 231 ----------------------------- src/assignment_resolver.rs | 22 +-- src/binding.rs | 18 +++ src/command_ext.rs | 32 ++-- src/common.rs | 16 +- src/compilation_error_kind.rs | 60 ++++---- src/dependency.rs | 2 +- src/evaluator.rs | 266 ++++++++++++++++++++++++++++++++++ src/function_context.rs | 8 +- src/justfile.rs | 62 +++++--- src/lib.rs | 4 +- src/node.rs | 2 +- src/parser.rs | 4 +- src/recipe.rs | 81 ++++------- src/recipe_context.rs | 10 +- src/recipe_resolver.rs | 26 ++-- src/runtime_error.rs | 38 ++--- src/scope.rs | 57 ++++++++ src/shebang.rs | 14 +- src/token.rs | 10 +- 21 files changed, 542 insertions(+), 437 deletions(-) delete mode 100644 src/assignment_evaluator.rs create mode 100644 src/binding.rs create mode 100644 src/evaluator.rs create mode 100644 src/scope.rs diff --git a/src/assignment.rs b/src/assignment.rs index 20007d3b..52c2e9cd 100644 --- a/src/assignment.rs +++ b/src/assignment.rs @@ -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>>; diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs deleted file mode 100644 index 6c99a1f2..00000000 --- a/src/assignment_evaluator.rs +++ /dev/null @@ -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, - 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, - 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, - assignments: &Table<'a, Assignment<'a>>, - overrides: &BTreeMap, - 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>, - ) -> 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>, - ) -> 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, - 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`"); - } - } -} diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index b172e35e..92142bef 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -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(); diff --git a/src/binding.rs b/src/binding.rs new file mode 100644 index 00000000..5b993c50 --- /dev/null +++ b/src/binding.rs @@ -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() + } +} diff --git a/src/command_ext.rs b/src/command_ext.rs index e17d7977..1568ae5b 100644 --- a/src/command_ext.rs +++ b/src/command_ext.rs @@ -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, - ) -> RunResult<'a, ()>; + fn export(&mut self, dotenv: &BTreeMap, 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, - ) -> RunResult<'a, ()> { + fn export(&mut self, dotenv: &BTreeMap, 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); + } + } + + fn export_scope(&mut self, scope: &Scope) { + if let Some(parent) = scope.parent() { + self.export_scope(parent); } - Ok(()) + for binding in scope.bindings() { + if binding.export { + self.env(binding.name.lexeme(), &binding.value); + } + } } } diff --git a/src/common.rs b/src/common.rs index 0074a204..98dd3f9c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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, diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs index a89dd378..4a3a9fbe 100644 --- a/src/compilation_error_kind.rs +++ b/src/compilation_error_kind.rs @@ -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, 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, diff --git a/src/dependency.rs b/src/dependency.rs index 113db968..38c9712f 100644 --- a/src/dependency.rs +++ b/src/dependency.rs @@ -1,4 +1,4 @@ use crate::common::*; #[derive(PartialEq, Debug)] -pub(crate) struct Dependency<'a>(pub(crate) Rc>); +pub(crate) struct Dependency<'src>(pub(crate) Rc>); diff --git a/src/evaluator.rs b/src/evaluator.rs new file mode 100644 index 00000000..5586b5d7 --- /dev/null +++ b/src/evaluator.rs @@ -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, + 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, + 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, + 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, + 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`"); + } + } +} diff --git a/src/function_context.rs b/src/function_context.rs index cc48df76..074b1486 100644 --- a/src/function_context.rs +++ b/src/function_context.rs @@ -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, +pub(crate) struct FunctionContext<'run> { + pub(crate) invocation_directory: &'run Path, + pub(crate) working_directory: &'run Path, + pub(crate) dotenv: &'run BTreeMap, } diff --git a/src/justfile.rs b/src/justfile.rs index 594bf51c..ae5a6a20 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -81,23 +81,48 @@ impl<'src> Justfile<'src> { let dotenv = load_dotenv()?; - let scope = AssignmentEvaluator::evaluate_assignments( - config, - working_directory, - &dotenv, - &self.assignments, - overrides, - &self.settings, - )?; + 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, + 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, ran: &mut BTreeSet<&'src str>, - overrides: &BTreeMap, - ) -> 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")?; diff --git a/src/lib.rs b/src/lib.rs index 85139710..55b6090d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/node.rs b/src/node.rs index e7a63cad..cc4bed5d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -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()) } } diff --git a/src/parser.rs b/src/parser.rs index 5f35005d..5744048b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, }) } diff --git a/src/recipe.rs b/src/recipe.rs index 250dca87..7a872e35 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -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, - pub(crate) doc: Option<&'a str>, - pub(crate) body: Vec>, - pub(crate) name: Name<'a>, - pub(crate) parameters: Vec>, + pub(crate) doc: Option<&'src str>, + pub(crate) body: Vec>, + pub(crate) name: Name<'src>, + pub(crate) parameters: Vec>, 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 { 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, - overrides: &BTreeMap, - ) -> 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)?; diff --git a/src/recipe_context.rs b/src/recipe_context.rs index 7c45b2c4..8e3abbb4 100644 --- a/src/recipe_context.rs +++ b/src/recipe_context.rs @@ -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>, } diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs index 3fcde9a2..91fb9fc4 100644 --- a/src/recipe_resolver.rs +++ b/src/recipe_resolver.rs @@ -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>>, - 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>>, + 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>>> { + unresolved_recipes: Table<'src, Recipe<'src, Name<'src>>>, + assignments: &'run Table<'src, Assignment<'src>>, + ) -> CompilationResult<'src, Table<'src, Rc>>> { 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>> { + stack: &mut Vec<&'src str>, + recipe: Recipe<'src, Name<'src>>, + ) -> CompilationResult<'src, Rc>> { if let Some(resolved) = self.resolved_recipes.get(recipe.name()) { return Ok(resolved.clone()); } diff --git a/src/runtime_error.rs b/src/runtime_error.rs index ecc59085..f683ed89 100644 --- a/src/runtime_error.rs +++ b/src/runtime_error.rs @@ -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, 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, io_error: io::Error, }, Signal { - recipe: &'a str, + recipe: &'src str, line_number: Option, 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, }, 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 { 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::*; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 00000000..155060d3 --- /dev/null +++ b/src/scope.rs @@ -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> { + self.bindings.values() + } + + pub(crate) fn names(&self) -> impl Iterator { + self.bindings.keys().cloned() + } + + pub(crate) fn parent(&self) -> Option<&'run Scope<'src, 'run>> { + self.parent + } +} diff --git a/src/shebang.rs b/src/shebang.rs index a5e08e99..907af730 100644 --- a/src/shebang.rs +++ b/src/shebang.rs @@ -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> { - if !text.starts_with("#!") { +impl<'line> Shebang<'line> { + pub(crate) fn new(line: &'line str) -> Option> { + if !line.starts_with("#!") { return None; } - let mut pieces = text[2..] + let mut pieces = line[2..] .lines() .nth(0) .unwrap_or("") diff --git a/src/token.rs b/src/token.rs index 8ca5696c..efdf6f49 100644 --- a/src/token.rs +++ b/src/token.rs @@ -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 } }