Repository: casey/just
Pull request #1825 (2024-01-08 20:50:19)

Qualified names


Patch:
diff --git a/src/analyzer.rs b/src/analyzer.rs
index dcaf93e0..eb306421 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -47,7 +47,7 @@ impl<'src> Analyzer<'src> {
             (*original, name)
           };
 
-          return Err(redefinition.token().error(Redefinition {
+          return Err(redefinition.token.error(Redefinition {
             first_type,
             second_type,
             name: name.lexeme(),
@@ -83,7 +83,7 @@ impl<'src> Analyzer<'src> {
             if let Some(absolute) = absolute {
               define(*name, "module", false)?;
               modules.insert(
-                name.to_string(),
+                name.lexeme().into(),
                 (*name, Self::analyze(loaded, paths, asts, absolute)?),
               );
             }
@@ -160,7 +160,7 @@ impl<'src> Analyzer<'src> {
 
     for parameter in &recipe.parameters {
       if parameters.contains(parameter.name.lexeme()) {
-        return Err(parameter.name.token().error(DuplicateParameter {
+        return Err(parameter.name.token.error(DuplicateParameter {
           recipe: recipe.name.lexeme(),
           parameter: parameter.name.lexeme(),
         }));
@@ -173,7 +173,7 @@ impl<'src> Analyzer<'src> {
         return Err(
           parameter
             .name
-            .token()
+            .token
             .error(RequiredParameterFollowsDefaultParameter {
               parameter: parameter.name.lexeme(),
             }),
@@ -201,7 +201,7 @@ impl<'src> Analyzer<'src> {
 
   fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src> {
     if self.assignments.contains_key(assignment.name.lexeme()) {
-      return Err(assignment.name.token().error(DuplicateVariable {
+      return Err(assignment.name.token.error(DuplicateVariable {
         variable: assignment.name.lexeme(),
       }));
     }
@@ -213,7 +213,7 @@ impl<'src> Analyzer<'src> {
 
     for attr in &alias.attributes {
       if *attr != Attribute::Private {
-        return Err(alias.name.token().error(AliasInvalidAttribute {
+        return Err(alias.name.token.error(AliasInvalidAttribute {
           alias: name,
           attr: *attr,
         }));
@@ -238,10 +238,9 @@ impl<'src> Analyzer<'src> {
     recipes: &Table<'src, Rc<Recipe<'src>>>,
     alias: Alias<'src, Name<'src>>,
   ) -> CompileResult<'src, Alias<'src>> {
-    let token = alias.name.token();
     // Make sure the alias doesn't conflict with any recipe
     if let Some(recipe) = recipes.get(alias.name.lexeme()) {
-      return Err(token.error(AliasShadowsRecipe {
+      return Err(alias.name.token.error(AliasShadowsRecipe {
         alias: alias.name.lexeme(),
         recipe_line: recipe.line_number(),
       }));
@@ -250,7 +249,7 @@ impl<'src> Analyzer<'src> {
     // Make sure the target recipe exists
     match recipes.get(alias.target.lexeme()) {
       Some(target) => Ok(alias.resolve(Rc::clone(target))),
-      None => Err(token.error(UnknownAliasTarget {
+      None => Err(alias.name.token.error(UnknownAliasTarget {
         alias: alias.name.lexeme(),
         target: alias.target.lexeme(),
       })),
diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs
index ee428104..2223900b 100644
--- a/src/assignment_resolver.rs
+++ b/src/assignment_resolver.rs
@@ -59,16 +59,19 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
         if self.evaluated.contains(variable) {
           Ok(())
         } else if self.stack.contains(&variable) {
-          let token = self.assignments[variable].name.token();
           self.stack.push(variable);
-          Err(token.error(CircularVariableDependency {
-            variable,
-            circle: self.stack.clone(),
-          }))
+          Err(
+            self.assignments[variable]
+              .name
+              .error(CircularVariableDependency {
+                variable,
+                circle: self.stack.clone(),
+              }),
+          )
         } else if self.assignments.contains_key(variable) {
           self.resolve_assignment(variable)
         } else {
-          Err(name.token().error(UndefinedVariable { variable }))
+          Err(name.token.error(UndefinedVariable { variable }))
         }
       }
       Expression::Call { thunk } => match thunk {
diff --git a/src/compiler.rs b/src/compiler.rs
index 40573a22..c58408c6 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -13,17 +13,17 @@ impl Compiler {
     let mut srcs: HashMap<PathBuf, &str> = HashMap::new();
     let mut loaded = Vec::new();
 
-    let mut stack: Vec<(PathBuf, u32)> = Vec::new();
-    stack.push((root.into(), 0));
+    let mut stack = Vec::new();
+    stack.push(Source::root(root));
 
-    while let Some((current, depth)) = stack.pop() {
-      let (relative, src) = loader.load(root, &current)?;
+    while let Some(current) = stack.pop() {
+      let (relative, src) = loader.load(root, &current.path)?;
       loaded.push(relative.into());
       let tokens = Lexer::lex(relative, src)?;
-      let mut ast = Parser::parse(depth, &current, &tokens)?;
+      let mut ast = Parser::parse(&current.path, &current.namepath, current.depth, &tokens)?;
 
-      paths.insert(current.clone(), relative.into());
-      srcs.insert(current.clone(), src);
+      paths.insert(current.path.clone(), relative.into());
+      srcs.insert(current.path.clone(), src);
 
       for item in &mut ast.items {
         match item {
@@ -39,7 +39,7 @@ impl Compiler {
               });
             }
 
-            let parent = current.parent().unwrap();
+            let parent = current.path.parent().unwrap();
 
             let import = if let Some(relative) = relative {
               let path = parent.join(Self::expand_tilde(&relative.cooked)?);
@@ -55,10 +55,13 @@ impl Compiler {
 
             if let Some(import) = import {
               if srcs.contains_key(&import) {
-                return Err(Error::CircularImport { current, import });
+                return Err(Error::CircularImport {
+                  current: current.path,
+                  import,
+                });
               }
               *absolute = Some(import.clone());
-              stack.push((import, depth + 1));
+              stack.push(current.module(*name, import));
             } else if !*optional {
               return Err(Error::MissingModuleFile { module: *name });
             }
@@ -70,6 +73,7 @@ impl Compiler {
             path,
           } => {
             let import = current
+              .path
               .parent()
               .unwrap()
               .join(Self::expand_tilde(&relative.cooked)?)
@@ -77,10 +81,13 @@ impl Compiler {
 
             if import.is_file() {
               if srcs.contains_key(&import) {
-                return Err(Error::CircularImport { current, import });
+                return Err(Error::CircularImport {
+                  current: current.path,
+                  import,
+                });
               }
               *absolute = Some(import.clone());
-              stack.push((import, depth + 1));
+              stack.push(current.import(import));
             } else if !*optional {
               return Err(Error::MissingImportFile { path: *path });
             }
@@ -89,7 +96,7 @@ impl Compiler {
         }
       }
 
-      asts.insert(current.clone(), ast.clone());
+      asts.insert(current.path, ast.clone());
     }
 
     let justfile = Analyzer::analyze(&loaded, &paths, &asts, root)?;
@@ -155,7 +162,7 @@ impl Compiler {
   #[cfg(test)]
   pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
     let tokens = Lexer::test_lex(src)?;
-    let ast = Parser::parse(0, &PathBuf::new(), &tokens)?;
+    let ast = Parser::parse(&PathBuf::new(), &Namepath::default(), 0, &tokens)?;
     let root = PathBuf::from("justfile");
     let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
     asts.insert(root.clone(), ast);
diff --git a/src/error.rs b/src/error.rs
index 1cc072d3..c71cbb92 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -179,11 +179,11 @@ impl<'src> Error<'src> {
   fn context(&self) -> Option<Token<'src>> {
     match self {
       Self::AmbiguousModuleFile { module, .. } | Self::MissingModuleFile { module, .. } => {
-        Some(module.token())
+        Some(module.token)
       }
       Self::Backtick { token, .. } => Some(*token),
       Self::Compile { compile_error } => Some(compile_error.context()),
-      Self::FunctionCall { function, .. } => Some(function.token()),
+      Self::FunctionCall { function, .. } => Some(function.token),
       Self::MissingImportFile { path } => Some(*path),
       _ => None,
     }
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 067d0b49..c768625d 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -255,7 +255,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
     config: &'run Config,
     dotenv: &'run BTreeMap<String, String>,
     parameters: &[Parameter<'src>],
-    arguments: &[&str],
+    arguments: &[String],
     scope: &'run Scope<'src, 'run>,
     settings: &'run Settings,
     search: &'run Search,
@@ -289,13 +289,13 @@ impl<'src, 'run> Evaluator<'src, 'run> {
         }
       } else if parameter.kind.is_variadic() {
         for value in rest {
-          positional.push((*value).to_owned());
+          positional.push(value.clone());
         }
         let value = rest.to_vec().join(" ");
         rest = &[];
         value
       } else {
-        let value = rest[0].to_owned();
+        let value = rest[0].clone();
         positional.push(value.clone());
         rest = &rest[1..];
         value
diff --git a/src/justfile.rs b/src/justfile.rs
index 031b6679..55cc70df 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -271,7 +271,7 @@ impl<'src> Justfile<'src> {
       });
     }
 
-    let mut ran = BTreeSet::new();
+    let mut ran = Ran::default();
     for invocation in invocations {
       let context = RecipeContext {
         settings: invocation.settings,
@@ -283,7 +283,12 @@ impl<'src> Justfile<'src> {
       Self::run_recipe(
         &context,
         invocation.recipe,
-        &invocation.arguments,
+        &invocation
+          .arguments
+          .iter()
+          .copied()
+          .map(str::to_string)
+          .collect::<Vec<String>>(),
         &dotenv,
         search,
         &mut ran,
@@ -399,17 +404,12 @@ impl<'src> Justfile<'src> {
   fn run_recipe(
     context: &RecipeContext<'src, '_>,
     recipe: &Recipe<'src>,
-    arguments: &[&str],
+    arguments: &[String],
     dotenv: &BTreeMap<String, String>,
     search: &Search,
-    ran: &mut BTreeSet<Vec<String>>,
+    ran: &mut Ran<'src>,
   ) -> RunResult<'src> {
-    let mut invocation = vec![recipe.name().to_owned()];
-    for argument in arguments {
-      invocation.push((*argument).to_string());
-    }
-
-    if ran.contains(&invocation) {
+    if ran.has_run(&recipe.namepath, arguments) {
       return Ok(());
     }
 
@@ -440,20 +440,13 @@ impl<'src> Justfile<'src> {
         .map(|argument| evaluator.evaluate_expression(argument))
         .collect::<RunResult<Vec<String>>>()?;
 
-      Self::run_recipe(
-        context,
-        recipe,
-        &arguments.iter().map(String::as_ref).collect::<Vec<&str>>(),
-        dotenv,
-        search,
-        ran,
-      )?;
+      Self::run_recipe(context, recipe, &arguments, dotenv, search, ran)?;
     }
 
     recipe.run(context, dotenv, scope.child(), search, &positional)?;
 
     {
-      let mut ran = BTreeSet::new();
+      let mut ran = Ran::default();
 
       for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) {
         let mut evaluated = Vec::new();
@@ -462,18 +455,11 @@ impl<'src> Justfile<'src> {
           evaluated.push(evaluator.evaluate_expression(argument)?);
         }
 
-        Self::run_recipe(
-          context,
-          recipe,
-          &evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
-          dotenv,
-          search,
-          &mut ran,
-        )?;
+        Self::run_recipe(context, recipe, &evaluated, dotenv, search, &mut ran)?;
       }
     }
 
-    ran.insert(invocation);
+    ran.ran(&recipe.namepath, arguments.to_vec());
     Ok(())
   }
 
diff --git a/src/lib.rs b/src/lib.rs
index a30dd45b..445e8190 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,17 +25,17 @@ pub(crate) use {
     fragment::Fragment, function::Function, function_context::FunctionContext,
     interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
     justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
-    load_dotenv::load_dotenv, loader::Loader, name::Name, ordinal::Ordinal, output::output,
-    output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
-    platform::Platform, platform_interface::PlatformInterface, position::Position,
-    positional::Positional, range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
-    recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig,
-    search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
-    shell::Shell, show_whitespace::ShowWhitespace, string_kind::StringKind,
-    string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
-    thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
-    unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
-    verbosity::Verbosity, warning::Warning,
+    load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
+    output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
+    parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
+    positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
+    recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
+    search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
+    settings::Settings, shebang::Shebang, shell::Shell, show_whitespace::ShowWhitespace,
+    source::Source, string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
+    suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
+    unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
+    use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
   },
   std::{
     cmp,
@@ -47,6 +47,7 @@ pub(crate) use {
     io::{self, Cursor, Write},
     iter::{self, FromIterator},
     mem,
+    ops::Deref,
     ops::{Index, Range, RangeInclusive},
     path::{self, Path, PathBuf},
     process::{self, Command, ExitStatus, Stdio},
@@ -149,6 +150,7 @@ mod list;
 mod load_dotenv;
 mod loader;
 mod name;
+mod namepath;
 mod ordinal;
 mod output;
 mod output_error;
@@ -159,6 +161,7 @@ mod platform;
 mod platform_interface;
 mod position;
 mod positional;
+mod ran;
 mod range_ext;
 mod recipe;
 mod recipe_context;
@@ -174,6 +177,7 @@ mod settings;
 mod shebang;
 mod shell;
 mod show_whitespace;
+mod source;
 mod string_kind;
 mod string_literal;
 mod subcommand;
diff --git a/src/name.rs b/src/name.rs
index 9823430e..f86063b3 100644
--- a/src/name.rs
+++ b/src/name.rs
@@ -1,50 +1,24 @@
 use super::*;
 
-/// A name. This is effectively just a `Token` of kind `Identifier`, but we give
-/// it its own type for clarity.
+/// A name. This is just a `Token` of kind `Identifier`, but we give it its own
+/// type for clarity.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
 pub(crate) struct Name<'src> {
-  pub(crate) column: usize,
-  pub(crate) length: usize,
-  pub(crate) line: usize,
-  pub(crate) offset: usize,
-  pub(crate) path: &'src Path,
-  pub(crate) src: &'src str,
+  pub(crate) token: Token<'src>,
 }
 
 impl<'src> Name<'src> {
-  /// The name's text contents
-  pub(crate) fn lexeme(&self) -> &'src str {
-    &self.src[self.offset..self.offset + self.length]
-  }
-
-  /// Turn this name back into a token
-  pub(crate) fn token(&self) -> Token<'src> {
-    Token {
-      column: self.column,
-      kind: TokenKind::Identifier,
-      length: self.length,
-      line: self.line,
-      offset: self.offset,
-      path: self.path,
-      src: self.src,
-    }
-  }
-
-  pub(crate) fn from_identifier(token: Token<'src>) -> Name {
+  pub(crate) fn from_identifier(token: Token<'src>) -> Self {
     assert_eq!(token.kind, TokenKind::Identifier);
-    Name {
-      column: token.column,
-      length: token.length,
-      line: token.line,
-      offset: token.offset,
-      path: token.path,
-      src: token.src,
-    }
+    Self { token }
   }
+}
+
+impl<'src> Deref for Name<'src> {
+  type Target = Token<'src>;
 
-  pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {
-    self.token().error(kind)
+  fn deref(&self) -> &Self::Target {
+    &self.token
   }
 }
 
diff --git a/src/namepath.rs b/src/namepath.rs
new file mode 100644
index 00000000..899b32fb
--- /dev/null
+++ b/src/namepath.rs
@@ -0,0 +1,28 @@
+use super::*;
+
+#[derive(Default, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub(crate) struct Namepath<'src>(Vec<Name<'src>>);
+
+impl<'src> Namepath<'src> {
+  pub(crate) fn join(&self, name: Name<'src>) -> Self {
+    Self(self.0.iter().copied().chain(iter::once(name)).collect())
+  }
+}
+
+impl<'str> Serialize for Namepath<'str> {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: Serializer,
+  {
+    let mut path = String::new();
+
+    for (i, name) in self.0.iter().enumerate() {
+      if i > 0 {
+        path.push_str("::");
+      }
+      path.push_str(name.lexeme());
+    }
+
+    serializer.serialize_str(&path)
+  }
+}
diff --git a/src/parser.rs b/src/parser.rs
index 92941a83..d4662eac 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -23,34 +23,31 @@ use {super::*, TokenKind::*};
 /// find it, it adds that token to the set. When the parser accepts a token, the
 /// set is cleared. If the parser finds a token which is unexpected, the
 /// contents of the set is printed in the resultant error message.
-pub(crate) struct Parser<'tokens, 'src> {
-  /// Source tokens
-  tokens: &'tokens [Token<'src>],
-  /// Index of the next un-parsed token
-  next: usize,
-  /// Current expected tokens
-  expected: BTreeSet<TokenKind>,
-  /// Current recursion depth
-  depth: usize,
-  /// Path to the file being parsed
-  path: PathBuf,
-  /// Depth of submodule being parsed
-  submodule: u32,
+pub(crate) struct Parser<'run, 'src> {
+  expected_tokens: BTreeSet<TokenKind>,
+  file_path: &'run Path,
+  module_namepath: &'run Namepath<'src>,
+  next_token: usize,
+  recursion_depth: usize,
+  submodule_depth: u32,
+  tokens: &'run [Token<'src>],
 }
 
-impl<'tokens, 'src> Parser<'tokens, 'src> {
+impl<'run, 'src> Parser<'run, 'src> {
   /// Parse `tokens` into an `Ast`
   pub(crate) fn parse(
-    submodule: u32,
-    path: &Path,
-    tokens: &'tokens [Token<'src>],
+    file_path: &'run Path,
+    module_namepath: &'run Namepath<'src>,
+    submodule_depth: u32,
+    tokens: &'run [Token<'src>],
   ) -> CompileResult<'src, Ast<'src>> {
-    Parser {
-      depth: 0,
-      expected: BTreeSet::new(),
-      next: 0,
-      path: path.into(),
-      submodule,
+    Self {
+      expected_tokens: BTreeSet::new(),
+      file_path,
+      module_namepath,
+      next_token: 0,
+      recursion_depth: 0,
+      submodule_depth,
       tokens,
     }
     .parse_ast()
@@ -65,7 +62,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
   fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> {
     self.error(CompileErrorKind::UnexpectedToken {
       expected: self
-        .expected
+        .expected_tokens
         .iter()
         .copied()
         .filter(|kind| *kind != ByteOrderMark)
@@ -81,8 +78,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
   }
 
   /// An iterator over the remaining significant tokens
-  fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'tokens {
-    self.tokens[self.next..]
+  fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'run {
+    self.tokens[self.next_token..]
       .iter()
       .copied()
       .filter(|token| token.kind != Whitespace)
@@ -107,7 +104,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
   /// The first token in `kinds` will be added to the expected token set.
   fn next_are(&mut self, kinds: &[TokenKind]) -> bool {
     if let Some(&kind) = kinds.first() {
-      self.expected.insert(kind);
+      self.expected_tokens.insert(kind);
     }
 
     let mut rest = self.rest();
@@ -126,10 +123,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
 
   /// Advance past one significant token, clearing the expected token set.
   fn advance(&mut self) -> CompileResult<'src, Token<'src>> {
-    self.expected.clear();
+    self.expected_tokens.clear();
 
-    for skipped in &self.tokens[self.next..] {
-      self.next += 1;
+    for skipped in &self.tokens[self.next_token..] {
+      self.next_token += 1;
 
       if skipped.kind != Whitespace {
         return Ok(*skipped);
@@ -419,7 +416,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       }
     }
 
-    if self.next == self.tokens.len() {
+    if self.next_token == self.tokens.len() {
       Ok(Ast {
         warnings: Vec::new(),
         items,
@@ -427,7 +424,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
     } else {
       Err(self.internal_error(format!(
         "Parse completed with {} unparsed tokens",
-        self.tokens.len() - self.next,
+        self.tokens.len() - self.next_token,
       ))?)
     }
   }
@@ -464,7 +461,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
 
   /// Parse an expression, e.g. `1 + 2`
   fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {
-    if self.depth == if cfg!(windows) { 48 } else { 256 } {
+    if self.recursion_depth == if cfg!(windows) { 48 } else { 256 } {
       let token = self.next()?;
       return Err(CompileError::new(
         token,
@@ -472,7 +469,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       ));
     }
 
-    self.depth += 1;
+    self.recursion_depth += 1;
 
     let expression = if self.accepted_keyword(Keyword::If)? {
       self.parse_conditional()?
@@ -496,7 +493,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       }
     };
 
-    self.depth -= 1;
+    self.recursion_depth -= 1;
 
     Ok(expression)
   }
@@ -740,11 +737,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       doc,
       name,
       parameters: positional.into_iter().chain(variadic).collect(),
-      path: self.path.clone(),
+      file_path: self.file_path.into(),
       priors,
       private: name.lexeme().starts_with('_'),
       quiet,
-      depth: self.submodule,
+      depth: self.submodule_depth,
+      namepath: self.module_namepath.join(name),
     })
   }
 
@@ -962,7 +960,8 @@ mod tests {
   fn test(text: &str, want: Tree) {
     let unindented = unindent(text);
     let tokens = Lexer::test_lex(&unindented).expect("lexing failed");
-    let justfile = Parser::parse(0, &PathBuf::new(), &tokens).expect("parsing failed");
+    let justfile =
+      Parser::parse(&PathBuf::new(), &Namepath::default(), 0, &tokens).expect("parsing failed");
     let have = justfile.tree();
     if have != want {
       println!("parsed text: {unindented}");
@@ -1000,7 +999,7 @@ mod tests {
   ) {
     let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-    match Parser::parse(0, &PathBuf::new(), &tokens) {
+    match Parser::parse(&PathBuf::new(), &Namepath::default(), 0, &tokens) {
       Ok(_) => panic!("Parsing unexpectedly succeeded"),
       Err(have) => {
         let want = CompileError {
diff --git a/src/ran.rs b/src/ran.rs
new file mode 100644
index 00000000..a19c12db
--- /dev/null
+++ b/src/ran.rs
@@ -0,0 +1,18 @@
+use super::*;
+
+#[derive(Default)]
+pub(crate) struct Ran<'src>(BTreeMap<Namepath<'src>, BTreeSet<Vec<String>>>);
+
+impl<'src> Ran<'src> {
+  pub(crate) fn has_run(&self, recipe: &Namepath<'src>, arguments: &[String]) -> bool {
+    self
+      .0
+      .get(recipe)
+      .map(|ran| ran.contains(arguments))
+      .unwrap_or_default()
+  }
+
+  pub(crate) fn ran(&mut self, recipe: &Namepath<'src>, arguments: Vec<String>) {
+    self.0.entry(recipe.clone()).or_default().insert(arguments);
+  }
+}
diff --git a/src/recipe.rs b/src/recipe.rs
index 7acd1cea..01aa273f 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -25,17 +25,18 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
   pub(crate) attributes: BTreeSet<Attribute>,
   pub(crate) body: Vec<Line<'src>>,
   pub(crate) dependencies: Vec<D>,
+  #[serde(skip)]
+  pub(crate) depth: u32,
   pub(crate) doc: Option<&'src str>,
+  #[serde(skip)]
+  pub(crate) file_path: PathBuf,
   pub(crate) name: Name<'src>,
+  pub(crate) namepath: Namepath<'src>,
   pub(crate) parameters: Vec<Parameter<'src>>,
-  #[serde(skip)]
-  pub(crate) path: PathBuf,
   pub(crate) priors: usize,
   pub(crate) private: bool,
   pub(crate) quiet: bool,
   pub(crate) shebang: bool,
-  #[serde(skip)]
-  pub(crate) depth: u32,
 }
 
 impl<'src, D> Recipe<'src, D> {
@@ -223,7 +224,7 @@ impl<'src, D> Recipe<'src, D> {
 
       if self.change_directory() {
         cmd.current_dir(if self.depth > 0 {
-          self.path.parent().unwrap()
+          self.file_path.parent().unwrap()
         } else {
           &context.search.working_directory
         });
@@ -363,7 +364,7 @@ impl<'src, D> Recipe<'src, D> {
       &path,
       if self.change_directory() {
         if self.depth > 0 {
-          Some(self.path.parent().unwrap())
+          Some(self.file_path.parent().unwrap())
         } else {
           Some(&context.search.working_directory)
         }
diff --git a/src/source.rs b/src/source.rs
new file mode 100644
index 00000000..8efd3c86
--- /dev/null
+++ b/src/source.rs
@@ -0,0 +1,33 @@
+use super::*;
+
+pub(crate) struct Source<'src> {
+  pub(crate) path: PathBuf,
+  pub(crate) depth: u32,
+  pub(crate) namepath: Namepath<'src>,
+}
+
+impl<'src> Source<'src> {
+  pub(crate) fn root(path: &Path) -> Self {
+    Self {
+      path: path.into(),
+      depth: 0,
+      namepath: Namepath::default(),
+    }
+  }
+
+  pub(crate) fn import(&self, path: PathBuf) -> Self {
+    Self {
+      depth: self.depth + 1,
+      path,
+      namepath: self.namepath.clone(),
+    }
+  }
+
+  pub(crate) fn module(&self, name: Name<'src>, path: PathBuf) -> Self {
+    Self {
+      path,
+      depth: self.depth + 1,
+      namepath: self.namepath.join(name),
+    }
+  }
+}
diff --git a/src/token.rs b/src/token.rs
index d8a7bf21..5ff1a53a 100644
--- a/src/token.rs
+++ b/src/token.rs
@@ -1,6 +1,6 @@
 use super::*;
 
-#[derive(Debug, PartialEq, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
 pub(crate) struct Token<'src> {
   pub(crate) column: usize,
   pub(crate) kind: TokenKind,
diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs
index c545394a..170d0cd9 100644
--- a/src/unresolved_recipe.rs
+++ b/src/unresolved_recipe.rs
@@ -50,9 +50,10 @@ impl<'src> UnresolvedRecipe<'src> {
       dependencies,
       depth: self.depth,
       doc: self.doc,
+      file_path: self.file_path,
       name: self.name,
+      namepath: self.namepath,
       parameters: self.parameters,
-      path: self.path,
       priors: self.priors,
       private: self.private,
       quiet: self.quiet,
diff --git a/src/variables.rs b/src/variables.rs
index 821f6503..8a17254a 100644
--- a/src/variables.rs
+++ b/src/variables.rs
@@ -60,7 +60,7 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
           self.stack.push(rhs);
           self.stack.push(lhs);
         }
-        Expression::Variable { name, .. } => return Some(name.token()),
+        Expression::Variable { name, .. } => return Some(name.token),
         Expression::Concatenation { lhs, rhs } => {
           self.stack.push(rhs);
           self.stack.push(lhs);


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index e97883f8..8a0e626b 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -59,7 +59,8 @@ pub(crate) fn analysis_error(
 ) {
   let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-  let ast = Parser::parse(0, &PathBuf::new(), &tokens).expect("Parsing failed in analysis test...");
+  let ast = Parser::parse(&PathBuf::new(), &Namepath::default(), 0, &tokens)
+    .expect("Parsing failed in analysis test...");
 
   let root = PathBuf::from("justfile");
   let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
diff --git a/tests/json.rs b/tests/json.rs
index 48c2e45b..c70063f2 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -34,6 +34,7 @@ fn alias() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -118,6 +119,7 @@ fn body() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -161,6 +163,7 @@ fn dependencies() {
           "attributes": [],
           "doc": null,
           "name": "bar",
+          "namepath": "bar",
           "body": [],
           "dependencies": [{
             "arguments": [],
@@ -177,6 +180,7 @@ fn dependencies() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -239,6 +243,7 @@ fn dependency_argument() {
         "bar": {
           "doc": null,
           "name": "bar",
+          "namepath": "bar",
           "body": [],
           "dependencies": [{
             "arguments": [
@@ -267,6 +272,7 @@ fn dependency_argument() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [
             {
               "name": "args",
@@ -328,6 +334,7 @@ fn duplicate_recipes() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [
             {
               "name": "bar",
@@ -377,6 +384,7 @@ fn doc_comment() {
           "dependencies": [],
           "doc": "hello",
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -456,6 +464,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "a",
+          "namepath": "a",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -467,6 +476,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "b",
+          "namepath": "b",
           "parameters": [
             {
               "name": "x",
@@ -486,6 +496,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "c",
+          "namepath": "c",
           "parameters": [
             {
               "name": "x",
@@ -505,6 +516,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "d",
+          "namepath": "d",
           "parameters": [
             {
               "name": "x",
@@ -524,6 +536,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "e",
+          "namepath": "e",
           "parameters": [
             {
               "name": "x",
@@ -543,6 +556,7 @@ fn parameters() {
           "dependencies": [],
           "doc": null,
           "name": "f",
+          "namepath": "f",
           "parameters": [
             {
               "name": "x",
@@ -596,6 +610,7 @@ fn priors() {
           "dependencies": [],
           "doc": null,
           "name": "a",
+          "namepath": "a",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -617,6 +632,7 @@ fn priors() {
           ],
           "doc": null,
           "name": "b",
+          "namepath": "b",
           "private": false,
           "quiet": false,
           "shebang": false,
@@ -629,6 +645,7 @@ fn priors() {
           "dependencies": [],
           "doc": null,
           "name": "c",
+          "namepath": "c",
           "parameters": [],
           "private": false,
           "quiet": false,
@@ -672,6 +689,7 @@ fn private() {
           "dependencies": [],
           "doc": null,
           "name": "_foo",
+          "namepath": "_foo",
           "parameters": [],
           "priors": 0,
           "private": true,
@@ -714,6 +732,7 @@ fn quiet() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -767,6 +786,7 @@ fn settings() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -815,6 +835,7 @@ fn shebang() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -857,6 +878,7 @@ fn simple() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -903,6 +925,7 @@ fn attribute() {
           "dependencies": [],
           "doc": null,
           "name": "foo",
+          "namepath": "foo",
           "parameters": [],
           "priors": 0,
           "private": false,
@@ -961,6 +984,7 @@ fn module() {
                 "dependencies": [],
                 "doc": null,
                 "name": "bar",
+                "namepath": "foo::bar",
                 "parameters": [],
                 "priors": 0,
                 "private": false,
diff --git a/tests/modules.rs b/tests/modules.rs
index f2ef01a4..63e067fe 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -684,3 +684,23 @@ fn module_paths_beginning_with_tilde_are_expanded_to_homdir() {
     .env("HOME", "foobar")
     .run();
 }
+
+#[test]
+fn recipes_with_same_name_are_both_run() {
+  Test::new()
+    .write("foo.just", "bar:\n @echo MODULE")
+    .justfile(
+      "
+        mod foo
+
+        bar:
+          @echo ROOT
+      ",
+    )
+    .test_round_trip(false)
+    .arg("--unstable")
+    .arg("foo::bar")
+    .arg("bar")
+    .stdout("MODULE\nROOT\n")
+    .run();
+}