Resolve recipe dependencies (#547)

Make analysis resolve recipe dependencies from names (`Name`) to recipes
(`Rc<Recipe>`), to give type-level certainty that resolution was performed
correctly and remove the need to look up dependencies on run.
This commit is contained in:
Casey Rodarmor 2019-11-21 08:23:32 -06:00 committed by GitHub
parent 72bc85e4ea
commit 30c77f8d03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 138 additions and 97 deletions

View File

@ -6,13 +6,13 @@ where
'a: 'b, 'a: 'b,
{ {
aliases: &'b Table<'a, Alias<'a>>, aliases: &'b Table<'a, Alias<'a>>,
recipes: &'b Table<'a, Recipe<'a>>, recipes: &'b Table<'a, Rc<Recipe<'a>>>,
} }
impl<'a: 'b, 'b> AliasResolver<'a, 'b> { impl<'a: 'b, 'b> AliasResolver<'a, 'b> {
pub(crate) fn resolve_aliases( pub(crate) fn resolve_aliases(
aliases: &Table<'a, Alias<'a>>, aliases: &Table<'a, Alias<'a>>,
recipes: &Table<'a, Recipe<'a>>, recipes: &Table<'a, Rc<Recipe<'a>>>,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'a, ()> {
let resolver = AliasResolver { aliases, recipes }; let resolver = AliasResolver { aliases, recipes };

View File

@ -3,7 +3,7 @@ use crate::common::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
pub(crate) struct Analyzer<'src> { pub(crate) struct Analyzer<'src> {
recipes: Table<'src, Recipe<'src>>, recipes: Table<'src, Recipe<'src, Name<'src>>>,
assignments: Table<'src, Assignment<'src>>, assignments: Table<'src, Assignment<'src>>,
aliases: Table<'src, Alias<'src>>, aliases: Table<'src, Alias<'src>>,
sets: Table<'src, Set<'src>>, sets: Table<'src, Set<'src>>,
@ -50,13 +50,12 @@ impl<'src> Analyzer<'src> {
} }
} }
let recipes = self.recipes;
let assignments = self.assignments; let assignments = self.assignments;
let aliases = self.aliases; let aliases = self.aliases;
AssignmentResolver::resolve_assignments(&assignments)?; AssignmentResolver::resolve_assignments(&assignments)?;
RecipeResolver::resolve_recipes(&recipes, &assignments)?; let recipes = RecipeResolver::resolve_recipes(self.recipes, &assignments)?;
for recipe in recipes.values() { for recipe in recipes.values() {
for parameter in &recipe.parameters { for parameter in &recipe.parameters {
@ -66,15 +65,6 @@ impl<'src> Analyzer<'src> {
})); }));
} }
} }
for dependency in &recipe.dependencies {
if !recipes[dependency.lexeme()].parameters.is_empty() {
return Err(dependency.error(DependencyHasParameters {
recipe: recipe.name(),
dependency: dependency.lexeme(),
}));
}
}
} }
AliasResolver::resolve_aliases(&aliases, &recipes)?; AliasResolver::resolve_aliases(&aliases, &recipes)?;
@ -99,7 +89,7 @@ impl<'src> Analyzer<'src> {
}) })
} }
fn analyze_recipe(&self, recipe: &Recipe<'src>) -> CompilationResult<'src, ()> { fn analyze_recipe(&self, recipe: &Recipe<'src, Name<'src>>) -> CompilationResult<'src, ()> {
if let Some(original) = self.recipes.get(recipe.name.lexeme()) { if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
return Err(recipe.name.token().error(DuplicateRecipe { return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(), recipe: original.name(),

View File

@ -11,6 +11,7 @@ pub(crate) use std::{
ops::{Index, Range, RangeInclusive}, ops::{Index, Range, RangeInclusive},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{self, Command}, process::{self, Command},
rc::Rc,
str::{self, Chars}, str::{self, Chars},
sync::{Mutex, MutexGuard}, sync::{Mutex, MutexGuard},
usize, vec, usize, vec,
@ -50,12 +51,12 @@ pub(crate) use crate::{
assignment_evaluator::AssignmentEvaluator, assignment_resolver::AssignmentResolver, color::Color, assignment_evaluator::AssignmentEvaluator, assignment_resolver::AssignmentResolver, 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,
enclosure::Enclosure, expression::Expression, fragment::Fragment, function::Function, dependency::Dependency, enclosure::Enclosure, expression::Expression, fragment::Fragment,
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard, function::Function, function_context::FunctionContext, functions::Functions,
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, 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,

4
src/dependency.rs Normal file
View File

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

View File

@ -26,9 +26,7 @@ pub(crate) enum Expression<'src> {
/// `(contents)` /// `(contents)`
Group { contents: Box<Expression<'src>> }, Group { contents: Box<Expression<'src>> },
/// `"string_literal"` or `'string_literal'` /// `"string_literal"` or `'string_literal'`
StringLiteral { StringLiteral { string_literal: StringLiteral<'src> },
string_literal: StringLiteral<'src>,
},
/// `variable` /// `variable`
Variable { name: Name<'src> }, Variable { name: Name<'src> },
} }

View File

@ -5,6 +5,6 @@ use crate::common::*;
pub(crate) enum Item<'src> { pub(crate) enum Item<'src> {
Alias(Alias<'src>), Alias(Alias<'src>),
Assignment(Assignment<'src>), Assignment(Assignment<'src>),
Recipe(Recipe<'src>), Recipe(Recipe<'src, Name<'src>>),
Set(Set<'src>), Set(Set<'src>),
} }

View File

@ -2,7 +2,7 @@ use crate::common::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) struct Justfile<'src> { pub(crate) struct Justfile<'src> {
pub(crate) recipes: Table<'src, Recipe<'src>>, pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
pub(crate) assignments: Table<'src, Assignment<'src>>, pub(crate) assignments: Table<'src, Assignment<'src>>,
pub(crate) aliases: Table<'src, Alias<'src>>, pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) settings: Settings<'src>, pub(crate) settings: Settings<'src>,
@ -11,7 +11,7 @@ pub(crate) struct Justfile<'src> {
impl<'src> Justfile<'src> { impl<'src> Justfile<'src> {
pub(crate) fn first(&self) -> Option<&Recipe> { pub(crate) fn first(&self) -> Option<&Recipe> {
let mut first: Option<&Recipe> = None; let mut first: Option<&Recipe<Dependency>> = None;
for recipe in self.recipes.values() { for recipe in self.recipes.values() {
if let Some(first_recipe) = first { if let Some(first_recipe) = first {
if recipe.line_number() < first_recipe.line_number() { if recipe.line_number() < first_recipe.line_number() {
@ -166,7 +166,7 @@ impl<'src> Justfile<'src> {
if let Some(recipe) = self.recipes.get(name) { if let Some(recipe) = self.recipes.get(name) {
Some(recipe) Some(recipe)
} else if let Some(alias) = self.aliases.get(name) { } else if let Some(alias) = self.aliases.get(name) {
self.recipes.get(alias.target.lexeme()) self.recipes.get(alias.target.lexeme()).map(Rc::as_ref)
} else { } else {
None None
} }
@ -181,10 +181,9 @@ impl<'src> Justfile<'src> {
ran: &mut BTreeSet<&'src str>, ran: &mut BTreeSet<&'src str>,
overrides: &BTreeMap<String, String>, overrides: &BTreeMap<String, String>,
) -> RunResult<()> { ) -> RunResult<()> {
for dependency_name in &recipe.dependencies { for Dependency(dependency) in &recipe.dependencies {
let lexeme = dependency_name.lexeme(); if !ran.contains(dependency.name()) {
if !ran.contains(lexeme) { self.run_recipe(context, dependency, &[], dotenv, ran, overrides)?;
self.run_recipe(context, &self.recipes[lexeme], &[], dotenv, ran, overrides)?;
} }
} }
recipe.run(context, arguments, dotenv, overrides)?; recipe.run(context, arguments, dotenv, overrides)?;

View File

@ -1,3 +1,11 @@
use crate::common::*;
pub(crate) trait Keyed<'key> { pub(crate) trait Keyed<'key> {
fn key(&self) -> &'key str; fn key(&self) -> &'key str;
} }
impl<'key, T: Keyed<'key>> Keyed<'key> for Rc<T> {
fn key(&self) -> &'key str {
self.as_ref().key()
}
}

View File

@ -32,6 +32,7 @@ mod config;
mod config_error; mod config_error;
mod count; mod count;
mod default; mod default;
mod dependency;
mod empty; mod empty;
mod enclosure; mod enclosure;
mod error; mod error;

View File

@ -66,7 +66,7 @@ impl<'src> Node<'src> for Expression<'src> {
} }
} }
impl<'src> Node<'src> for Recipe<'src> { impl<'src> Node<'src> for Recipe<'src, Name<'src>> {
fn tree(&self) -> Tree<'src> { fn tree(&self) -> Tree<'src> {
let mut t = Tree::atom("recipe"); let mut t = Tree::atom("recipe");

View File

@ -471,7 +471,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
&mut self, &mut self,
doc: Option<&'src str>, doc: Option<&'src str>,
quiet: bool, quiet: bool,
) -> CompilationResult<'src, Recipe<'src>> { ) -> CompilationResult<'src, Recipe<'src, Name<'src>>> {
let name = self.parse_name()?; let name = self.parse_name()?;
let mut positional = Vec::new(); let mut positional = Vec::new();

View File

@ -24,8 +24,8 @@ 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> { pub(crate) struct Recipe<'a, D = Dependency<'a>> {
pub(crate) dependencies: Vec<Name<'a>>, pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'a str>, pub(crate) doc: Option<&'a str>,
pub(crate) body: Vec<Line<'a>>, pub(crate) body: Vec<Line<'a>>,
pub(crate) name: Name<'a>, pub(crate) name: Name<'a>,
@ -35,7 +35,7 @@ pub(crate) struct Recipe<'a> {
pub(crate) shebang: bool, pub(crate) shebang: bool,
} }
impl<'a> Recipe<'a> { impl<'a, D> Recipe<'a, 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()
} }
@ -319,7 +319,26 @@ impl<'a> Recipe<'a> {
} }
} }
impl<'src> Keyed<'src> for Recipe<'src> { impl<'src> Recipe<'src, Name<'src>> {
pub(crate) fn resolve(self, resolved: Vec<Dependency<'src>>) -> Recipe<'src> {
assert_eq!(self.dependencies.len(), resolved.len());
for (name, resolved) in self.dependencies.iter().zip(&resolved) {
assert_eq!(name.lexeme(), resolved.0.name.lexeme());
}
Recipe {
dependencies: resolved,
doc: self.doc,
body: self.body,
name: self.name,
parameters: self.parameters,
private: self.private,
quiet: self.quiet,
shebang: self.shebang,
}
}
}
impl<'src, D> Keyed<'src> for Recipe<'src, D> {
fn key(&self) -> &'src str { fn key(&self) -> &'src str {
self.name.lexeme() self.name.lexeme()
} }
@ -342,7 +361,7 @@ impl<'a> Display for Recipe<'a> {
} }
write!(f, ":")?; write!(f, ":")?;
for dependency in &self.dependencies { for dependency in &self.dependencies {
write!(f, " {}", dependency)?; write!(f, " {}", dependency.0.name())?;
} }
for (i, line) in self.body.iter().enumerate() { for (i, line) in self.body.iter().enumerate() {

View File

@ -3,36 +3,31 @@ use crate::common::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
pub(crate) struct RecipeResolver<'a: 'b, 'b> { pub(crate) struct RecipeResolver<'a: 'b, 'b> {
stack: Vec<&'a str>, unresolved_recipes: Table<'a, Recipe<'a, Name<'a>>>,
seen: BTreeSet<&'a str>, resolved_recipes: Table<'a, Rc<Recipe<'a>>>,
resolved: BTreeSet<&'a str>,
recipes: &'b Table<'a, Recipe<'a>>,
assignments: &'b Table<'a, Assignment<'a>>, assignments: &'b Table<'a, Assignment<'a>>,
} }
impl<'a, 'b> RecipeResolver<'a, 'b> { impl<'a, 'b> RecipeResolver<'a, 'b> {
pub(crate) fn resolve_recipes( pub(crate) fn resolve_recipes(
recipes: &Table<'a, Recipe<'a>>, unresolved_recipes: Table<'a, Recipe<'a, Name<'a>>>,
assignments: &Table<'a, Assignment<'a>>, assignments: &Table<'a, Assignment<'a>>,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'a, Table<'a, Rc<Recipe<'a>>>> {
let mut resolver = RecipeResolver { let mut resolver = RecipeResolver {
seen: empty(), resolved_recipes: empty(),
stack: empty(), unresolved_recipes,
resolved: empty(),
assignments, assignments,
recipes,
}; };
for recipe in recipes.values() { while let Some(unresolved) = resolver.unresolved_recipes.pop() {
resolver.resolve_recipe(recipe)?; resolver.resolve_recipe(&mut Vec::new(), unresolved)?;
resolver.seen = empty();
} }
for recipe in recipes.values() { for recipe in resolver.resolved_recipes.values() {
for parameter in &recipe.parameters { for parameter in &recipe.parameters {
if let Some(expression) = &parameter.default { if let Some(expression) = &parameter.default {
for (function, argc) in expression.functions() { for (function, argc) in expression.functions() {
resolver.resolve_function(function, argc)?; Function::resolve(&function, argc)?;
} }
for variable in expression.variables() { for variable in expression.variables() {
resolver.resolve_variable(&variable, &[])?; resolver.resolve_variable(&variable, &[])?;
@ -44,7 +39,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
for fragment in &line.fragments { for fragment in &line.fragments {
if let Fragment::Interpolation { expression, .. } = fragment { if let Fragment::Interpolation { expression, .. } = fragment {
for (function, argc) in expression.functions() { for (function, argc) in expression.functions() {
resolver.resolve_function(function, argc)?; Function::resolve(&function, argc)?;
} }
for variable in expression.variables() { for variable in expression.variables() {
resolver.resolve_variable(&variable, &recipe.parameters)?; resolver.resolve_variable(&variable, &recipe.parameters)?;
@ -54,11 +49,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
} }
} }
Ok(()) Ok(resolver.resolved_recipes)
}
fn resolve_function(&self, function: Token<'a>, argc: usize) -> CompilationResult<'a, ()> {
Function::resolve(&function, argc)
} }
fn resolve_variable( fn resolve_variable(
@ -77,49 +68,67 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
Ok(()) Ok(())
} }
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> { fn resolve_recipe(
if self.resolved.contains(recipe.name()) { &mut self,
return Ok(()); stack: &mut Vec<&'a str>,
recipe: Recipe<'a, Name<'a>>,
) -> CompilationResult<'a, Rc<Recipe<'a>>> {
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
return Ok(resolved.clone());
} }
self.stack.push(recipe.name());
self.seen.insert(recipe.name()); stack.push(recipe.name());
for dependency_token in recipe
.dependencies let mut dependencies: Vec<Dependency> = Vec::new();
.iter() for dependency in &recipe.dependencies {
.map(|dependency| dependency.token()) let name = dependency.lexeme();
{
match self.recipes.get(dependency_token.lexeme()) { if let Some(resolved) = self.resolved_recipes.get(name) {
Some(dependency) => { // dependency already resolved
if !self.resolved.contains(dependency.name()) { if !resolved.parameters.is_empty() {
if self.seen.contains(dependency.name()) { return Err(dependency.error(DependencyHasParameters {
let first = self.stack[0];
self.stack.push(first);
return Err(
dependency_token.error(CircularRecipeDependency {
recipe: recipe.name(),
circle: self
.stack
.iter()
.skip_while(|name| **name != dependency.name())
.cloned()
.collect(),
}),
);
}
self.resolve_recipe(dependency)?;
}
}
None => {
return Err(dependency_token.error(UnknownDependency {
recipe: recipe.name(), recipe: recipe.name(),
unknown: dependency_token.lexeme(), dependency: name,
})); }));
} }
dependencies.push(Dependency(resolved.clone()));
} else if stack.contains(&name) {
let first = stack[0];
stack.push(first);
return Err(
dependency.error(CircularRecipeDependency {
recipe: recipe.name(),
circle: stack
.iter()
.skip_while(|name| **name != dependency.lexeme())
.cloned()
.collect(),
}),
);
} else if let Some(unresolved) = self.unresolved_recipes.remove(name) {
// resolve unresolved dependency
if !unresolved.parameters.is_empty() {
return Err(dependency.error(DependencyHasParameters {
recipe: recipe.name(),
dependency: name,
}));
}
dependencies.push(Dependency(self.resolve_recipe(stack, unresolved)?));
} else {
// dependency is unknown
return Err(dependency.error(UnknownDependency {
recipe: recipe.name(),
unknown: name,
}));
} }
} }
self.resolved.insert(recipe.name());
self.stack.pop(); let resolved = Rc::new(recipe.resolve(dependencies));
Ok(()) self.resolved_recipes.insert(resolved.clone());
stack.pop();
Ok(resolved)
} }
} }

View File

@ -35,6 +35,18 @@ impl<'key, V: Keyed<'key>> Table<'key, V> {
pub(crate) fn iter(&self) -> btree_map::Iter<&'key str, V> { pub(crate) fn iter(&self) -> btree_map::Iter<&'key str, V> {
self.map.iter() self.map.iter()
} }
pub(crate) fn pop(&mut self) -> Option<V> {
if let Some(key) = self.map.keys().next().map(|key| *key) {
self.map.remove(key)
} else {
None
}
}
pub(crate) fn remove(&mut self, key: &str) -> Option<V> {
self.map.remove(key)
}
} }
impl<'key, V: Keyed<'key>> FromIterator<V> for Table<'key, V> { impl<'key, V: Keyed<'key>> FromIterator<V> for Table<'key, V> {