Reform scope and binding (#556)
Clean up scope handling by introducing `Binding` and `Scope` objects.
This commit is contained in:
parent
d0e813cd8b
commit
2d3134a91c
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
18
src/binding.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
266
src/evaluator.rs
Normal 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`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
@ -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")?;
|
||||||
|
@ -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;
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)?;
|
||||||
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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
57
src/scope.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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("")
|
||||||
|
10
src/token.rs
10
src/token.rs
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user