Repository: casey/just
Pull request #2088 (2024-05-25 09:03:21)

Add source_file() and source_directory() functions


Patch:
diff --git a/README.md b/README.md
index cbfaa26b..1b5adc47 100644
--- a/README.md
+++ b/README.md
@@ -1420,6 +1420,18 @@ script:
   ./{{justfile_directory()}}/scripts/some_script
 ```
 
+#### Source and Source Directory
+
+- `source()`<sup>master</sup> - Retrieves the path of the current source file.
+
+- `source_directory()`<sup>master</sup> - Retrieves the path of the parent directory of the
+  current source file.
+
+`source()` and `source_directory()` behave the same as `justfile()` and
+`justfile_directory()` in the root `justfile`, but will return the path and
+directory, respectively, of the current `import` or `mod` source file when
+called from within an import or submodule.
+
 #### Just Executable
 
 - `just_executable()` - Absolute path to the `just` executable.
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 0ccafb98..2794b467 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -68,25 +68,13 @@ impl<'src, 'run> Evaluator<'src, 'run> {
       Expression::Call { thunk } => {
         use Thunk::*;
 
-        match thunk {
-          Nullary { name, function, .. } => function(self).map_err(|message| Error::FunctionCall {
-            function: *name,
-            message,
-          }),
-          Unary {
-            name,
-            function,
-            arg,
-            ..
-          } => {
+        let result = match thunk {
+          Nullary { function, .. } => function(function::Context::new(self, thunk.name())),
+          Unary { function, arg, .. } => {
             let arg = self.evaluate_expression(arg)?;
-            function(self, &arg).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(function::Context::new(self, thunk.name()), &arg)
           }
           UnaryOpt {
-            name,
             function,
             args: (a, b),
             ..
@@ -97,13 +85,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
               None => None,
             };
 
-            function(self, &a, b.as_deref()).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(function::Context::new(self, thunk.name()), &a, b.as_deref())
           }
           UnaryPlus {
-            name,
             function,
             args: (a, rest),
             ..
@@ -113,26 +97,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
             for arg in rest {
               rest_evaluated.push(self.evaluate_expression(arg)?);
             }
-            function(self, &a, &rest_evaluated).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(
+              function::Context::new(self, thunk.name()),
+              &a,
+              &rest_evaluated,
+            )
           }
           Binary {
-            name,
             function,
             args: [a, b],
             ..
           } => {
             let a = self.evaluate_expression(a)?;
             let b = self.evaluate_expression(b)?;
-            function(self, &a, &b).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(function::Context::new(self, thunk.name()), &a, &b)
           }
           BinaryPlus {
-            name,
             function,
             args: ([a, b], rest),
             ..
@@ -143,13 +123,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
             for arg in rest {
               rest_evaluated.push(self.evaluate_expression(arg)?);
             }
-            function(self, &a, &b, &rest_evaluated).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(
+              function::Context::new(self, thunk.name()),
+              &a,
+              &b,
+              &rest_evaluated,
+            )
           }
           Ternary {
-            name,
             function,
             args: [a, b, c],
             ..
@@ -157,12 +138,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
             let a = self.evaluate_expression(a)?;
             let b = self.evaluate_expression(b)?;
             let c = self.evaluate_expression(c)?;
-            function(self, &a, &b, &c).map_err(|message| Error::FunctionCall {
-              function: *name,
-              message,
-            })
+            function(function::Context::new(self, thunk.name()), &a, &b, &c)
           }
-        }
+        };
+
+        result.map_err(|message| Error::FunctionCall {
+          function: thunk.name(),
+          message,
+        })
       }
       Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
       Expression::Backtick { contents, token } => {
diff --git a/src/function.rs b/src/function.rs
index 0321508a..52fcf00e 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -11,13 +11,24 @@ use {
 };
 
 pub(crate) enum Function {
-  Nullary(fn(&Evaluator) -> Result<String, String>),
-  Unary(fn(&Evaluator, &str) -> Result<String, String>),
-  UnaryOpt(fn(&Evaluator, &str, Option<&str>) -> Result<String, String>),
-  UnaryPlus(fn(&Evaluator, &str, &[String]) -> Result<String, String>),
-  Binary(fn(&Evaluator, &str, &str) -> Result<String, String>),
-  BinaryPlus(fn(&Evaluator, &str, &str, &[String]) -> Result<String, String>),
-  Ternary(fn(&Evaluator, &str, &str, &str) -> Result<String, String>),
+  Nullary(fn(Context) -> Result<String, String>),
+  Unary(fn(Context, &str) -> Result<String, String>),
+  UnaryOpt(fn(Context, &str, Option<&str>) -> Result<String, String>),
+  UnaryPlus(fn(Context, &str, &[String]) -> Result<String, String>),
+  Binary(fn(Context, &str, &str) -> Result<String, String>),
+  BinaryPlus(fn(Context, &str, &str, &[String]) -> Result<String, String>),
+  Ternary(fn(Context, &str, &str, &str) -> Result<String, String>),
+}
+
+pub(crate) struct Context<'src: 'run, 'run> {
+  pub(crate) evaluator: &'run Evaluator<'src, 'run>,
+  pub(crate) name: Name<'src>,
+}
+
+impl<'src: 'run, 'run> Context<'src, 'run> {
+  pub(crate) fn new(evaluator: &'run Evaluator<'src, 'run>, name: Name<'src>) -> Self {
+    Self { evaluator, name }
+  }
 }
 
 pub(crate) fn get(name: &str) -> Option<Function> {
@@ -72,6 +83,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
     "shoutykebabcase" => Unary(shoutykebabcase),
     "shoutysnakecase" => Unary(shoutysnakecase),
     "snakecase" => Unary(snakecase),
+    "source_directory" => Nullary(source_directory),
+    "source_file" => Nullary(source_file),
     "titlecase" => Unary(titlecase),
     "trim" => Unary(trim),
     "trim_end" => Unary(trim_end),
@@ -103,18 +116,23 @@ impl Function {
   }
 }
 
-fn absolute_path(evaluator: &Evaluator, path: &str) -> Result<String, String> {
-  let abs_path_unchecked = evaluator.search.working_directory.join(path).lexiclean();
+fn absolute_path(context: Context, path: &str) -> Result<String, String> {
+  let abs_path_unchecked = context
+    .evaluator
+    .search
+    .working_directory
+    .join(path)
+    .lexiclean();
   match abs_path_unchecked.to_str() {
     Some(absolute_path) => Ok(absolute_path.to_owned()),
     None => Err(format!(
       "Working directory is not valid unicode: {}",
-      evaluator.search.working_directory.display()
+      context.evaluator.search.working_directory.display()
     )),
   }
 }
 
-fn append(_evaluator: &Evaluator, suffix: &str, s: &str) -> Result<String, String> {
+fn append(_context: Context, suffix: &str, s: &str) -> Result<String, String> {
   Ok(
     s.split_whitespace()
       .map(|s| format!("{s}{suffix}"))
@@ -123,16 +141,16 @@ fn append(_evaluator: &Evaluator, suffix: &str, s: &str) -> Result<String, Strin
   )
 }
 
-fn arch(_evaluator: &Evaluator) -> Result<String, String> {
+fn arch(_context: Context) -> Result<String, String> {
   Ok(target::arch().to_owned())
 }
 
-fn blake3(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn blake3(_context: Context, s: &str) -> Result<String, String> {
   Ok(blake3::hash(s.as_bytes()).to_string())
 }
 
-fn blake3_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
-  let path = evaluator.search.working_directory.join(path);
+fn blake3_file(context: Context, path: &str) -> Result<String, String> {
+  let path = context.evaluator.search.working_directory.join(path);
   let mut hasher = blake3::Hasher::new();
   hasher
     .update_mmap_rayon(&path)
@@ -140,7 +158,7 @@ fn blake3_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
   Ok(hasher.finalize().to_string())
 }
 
-fn canonicalize(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn canonicalize(_context: Context, path: &str) -> Result<String, String> {
   let canonical =
     std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
 
@@ -152,7 +170,7 @@ fn canonicalize(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
   })
 }
 
-fn capitalize(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn capitalize(_context: Context, s: &str) -> Result<String, String> {
   let mut capitalized = String::new();
   for (i, c) in s.chars().enumerate() {
     if i == 0 {
@@ -164,7 +182,7 @@ fn capitalize(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
   Ok(capitalized)
 }
 
-fn choose(_evaluator: &Evaluator, n: &str, alphabet: &str) -> Result<String, String> {
+fn choose(_context: Context, n: &str, alphabet: &str) -> Result<String, String> {
   if alphabet.is_empty() {
     return Err("empty alphabet".into());
   }
@@ -188,7 +206,7 @@ fn choose(_evaluator: &Evaluator, n: &str, alphabet: &str) -> Result<String, Str
   Ok((0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect())
 }
 
-fn clean(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn clean(_context: Context, path: &str) -> Result<String, String> {
   Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())
 }
 
@@ -208,7 +226,7 @@ fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> Result<String, String>
   }
 }
 
-fn encode_uri_component(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn encode_uri_component(_context: Context, s: &str) -> Result<String, String> {
   static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
     .remove(b'-')
     .remove(b'_')
@@ -222,10 +240,10 @@ fn encode_uri_component(_evaluator: &Evaluator, s: &str) -> Result<String, Strin
   Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string())
 }
 
-fn env_var(evaluator: &Evaluator, key: &str) -> Result<String, String> {
+fn env_var(context: Context, key: &str) -> Result<String, String> {
   use std::env::VarError::*;
 
-  if let Some(value) = evaluator.dotenv.get(key) {
+  if let Some(value) = context.evaluator.dotenv.get(key) {
     return Ok(value.clone());
   }
 
@@ -238,10 +256,10 @@ fn env_var(evaluator: &Evaluator, key: &str) -> Result<String, String> {
   }
 }
 
-fn env_var_or_default(evaluator: &Evaluator, key: &str, default: &str) -> Result<String, String> {
+fn env_var_or_default(context: Context, key: &str, default: &str) -> Result<String, String> {
   use std::env::VarError::*;
 
-  if let Some(value) = evaluator.dotenv.get(key) {
+  if let Some(value) = context.evaluator.dotenv.get(key) {
     return Ok(value.clone());
   }
 
@@ -254,48 +272,49 @@ fn env_var_or_default(evaluator: &Evaluator, key: &str, default: &str) -> Result
   }
 }
 
-fn env(evaluator: &Evaluator, key: &str, default: Option<&str>) -> Result<String, String> {
+fn env(context: Context, key: &str, default: Option<&str>) -> Result<String, String> {
   match default {
-    Some(val) => env_var_or_default(evaluator, key, val),
-    None => env_var(evaluator, key),
+    Some(val) => env_var_or_default(context, key, val),
+    None => env_var(context, key),
   }
 }
 
-fn error(_evaluator: &Evaluator, message: &str) -> Result<String, String> {
+fn error(_context: Context, message: &str) -> Result<String, String> {
   Err(message.to_owned())
 }
 
-fn extension(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn extension(_context: Context, path: &str) -> Result<String, String> {
   Utf8Path::new(path)
     .extension()
     .map(str::to_owned)
     .ok_or_else(|| format!("Could not extract extension from `{path}`"))
 }
 
-fn file_name(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn file_name(_context: Context, path: &str) -> Result<String, String> {
   Utf8Path::new(path)
     .file_name()
     .map(str::to_owned)
     .ok_or_else(|| format!("Could not extract file name from `{path}`"))
 }
 
-fn file_stem(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn file_stem(_context: Context, path: &str) -> Result<String, String> {
   Utf8Path::new(path)
     .file_stem()
     .map(str::to_owned)
     .ok_or_else(|| format!("Could not extract file stem from `{path}`"))
 }
 
-fn invocation_directory(evaluator: &Evaluator) -> Result<String, String> {
+fn invocation_directory(context: Context) -> Result<String, String> {
   Platform::convert_native_path(
-    &evaluator.search.working_directory,
-    &evaluator.config.invocation_directory,
+    &context.evaluator.search.working_directory,
+    &context.evaluator.config.invocation_directory,
   )
   .map_err(|e| format!("Error getting shell path: {e}"))
 }
 
-fn invocation_directory_native(evaluator: &Evaluator) -> Result<String, String> {
-  evaluator
+fn invocation_directory_native(context: Context) -> Result<String, String> {
+  context
+    .evaluator
     .config
     .invocation_directory
     .to_str()
@@ -303,12 +322,12 @@ fn invocation_directory_native(evaluator: &Evaluator) -> Result<String, String>
     .ok_or_else(|| {
       format!(
         "Invocation directory is not valid unicode: {}",
-        evaluator.config.invocation_directory.display()
+        context.evaluator.config.invocation_directory.display()
       )
     })
 }
 
-fn prepend(_evaluator: &Evaluator, prefix: &str, s: &str) -> Result<String, String> {
+fn prepend(_context: Context, prefix: &str, s: &str) -> Result<String, String> {
   Ok(
     s.split_whitespace()
       .map(|s| format!("{prefix}{s}"))
@@ -317,7 +336,7 @@ fn prepend(_evaluator: &Evaluator, prefix: &str, s: &str) -> Result<String, Stri
   )
 }
 
-fn join(_evaluator: &Evaluator, base: &str, with: &str, and: &[String]) -> Result<String, String> {
+fn join(_context: Context, base: &str, with: &str, and: &[String]) -> Result<String, String> {
   let mut result = Utf8Path::new(base).join(with);
   for arg in and {
     result.push(arg);
@@ -325,7 +344,7 @@ fn join(_evaluator: &Evaluator, base: &str, with: &str, and: &[String]) -> Resul
   Ok(result.to_string())
 }
 
-fn just_executable(_evaluator: &Evaluator) -> Result<String, String> {
+fn just_executable(_context: Context) -> Result<String, String> {
   let exe_path =
     env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?;
 
@@ -337,12 +356,13 @@ fn just_executable(_evaluator: &Evaluator) -> Result<String, String> {
   })
 }
 
-fn just_pid(_evaluator: &Evaluator) -> Result<String, String> {
+fn just_pid(_context: Context) -> Result<String, String> {
   Ok(std::process::id().to_string())
 }
 
-fn justfile(evaluator: &Evaluator) -> Result<String, String> {
-  evaluator
+fn justfile(context: Context) -> Result<String, String> {
+  context
+    .evaluator
     .search
     .justfile
     .to_str()
@@ -350,16 +370,16 @@ fn justfile(evaluator: &Evaluator) -> Result<String, String> {
     .ok_or_else(|| {
       format!(
         "Justfile path is not valid unicode: {}",
-        evaluator.search.justfile.display()
+        context.evaluator.search.justfile.display()
       )
     })
 }
 
-fn justfile_directory(evaluator: &Evaluator) -> Result<String, String> {
-  let justfile_directory = evaluator.search.justfile.parent().ok_or_else(|| {
+fn justfile_directory(context: Context) -> Result<String, String> {
+  let justfile_directory = context.evaluator.search.justfile.parent().ok_or_else(|| {
     format!(
       "Could not resolve justfile directory. Justfile `{}` had no parent.",
-      evaluator.search.justfile.display()
+      context.evaluator.search.justfile.display()
     )
   })?;
 
@@ -374,41 +394,42 @@ fn justfile_directory(evaluator: &Evaluator) -> Result<String, String> {
     })
 }
 
-fn kebabcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn kebabcase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_kebab_case())
 }
 
-fn lowercamelcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn lowercamelcase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_lower_camel_case())
 }
 
-fn lowercase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn lowercase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_lowercase())
 }
 
-fn num_cpus(_evaluator: &Evaluator) -> Result<String, String> {
+fn num_cpus(_context: Context) -> Result<String, String> {
   let num = num_cpus::get();
   Ok(num.to_string())
 }
 
-fn os(_evaluator: &Evaluator) -> Result<String, String> {
+fn os(_context: Context) -> Result<String, String> {
   Ok(target::os().to_owned())
 }
 
-fn os_family(_evaluator: &Evaluator) -> Result<String, String> {
+fn os_family(_context: Context) -> Result<String, String> {
   Ok(target::family().to_owned())
 }
 
-fn parent_directory(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn parent_directory(_context: Context, path: &str) -> Result<String, String> {
   Utf8Path::new(path)
     .parent()
     .map(Utf8Path::to_string)
     .ok_or_else(|| format!("Could not extract parent directory from `{path}`"))
 }
 
-fn path_exists(evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn path_exists(context: Context, path: &str) -> Result<String, String> {
   Ok(
-    evaluator
+    context
+      .evaluator
       .search
       .working_directory
       .join(path)
@@ -417,16 +438,16 @@ fn path_exists(evaluator: &Evaluator, path: &str) -> Result<String, String> {
   )
 }
 
-fn quote(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn quote(_context: Context, s: &str) -> Result<String, String> {
   Ok(format!("'{}'", s.replace('\'', "'\\''")))
 }
 
-fn replace(_evaluator: &Evaluator, s: &str, from: &str, to: &str) -> Result<String, String> {
+fn replace(_context: Context, s: &str, from: &str, to: &str) -> Result<String, String> {
   Ok(s.replace(from, to))
 }
 
 fn replace_regex(
-  _evaluator: &Evaluator,
+  _context: Context,
   s: &str,
   regex: &str,
   replacement: &str,
@@ -439,7 +460,7 @@ fn replace_regex(
   )
 }
 
-fn sha256(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn sha256(_context: Context, s: &str) -> Result<String, String> {
   use sha2::{Digest, Sha256};
   let mut hasher = Sha256::new();
   hasher.update(s);
@@ -447,9 +468,9 @@ fn sha256(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
   Ok(format!("{hash:x}"))
 }
 
-fn sha256_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn sha256_file(context: Context, path: &str) -> Result<String, String> {
   use sha2::{Digest, Sha256};
-  let path = evaluator.search.working_directory.join(path);
+  let path = context.evaluator.search.working_directory.join(path);
   let mut hasher = Sha256::new();
   let mut file =
     fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;
@@ -459,73 +480,112 @@ fn sha256_file(evaluator: &Evaluator, path: &str) -> Result<String, String> {
   Ok(format!("{hash:x}"))
 }
 
-fn shell(evaluator: &Evaluator, command: &str, args: &[String]) -> Result<String, String> {
+fn shell(context: Context, command: &str, args: &[String]) -> Result<String, String> {
   let args = iter::once(command)
     .chain(args.iter().map(String::as_str))
     .collect::<Vec<&str>>();
 
-  evaluator
+  context
+    .evaluator
     .run_command(command, &args)
     .map_err(|output_error| output_error.to_string())
 }
 
-fn shoutykebabcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn shoutykebabcase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_shouty_kebab_case())
 }
 
-fn shoutysnakecase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn shoutysnakecase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_shouty_snake_case())
 }
 
-fn snakecase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn snakecase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_snake_case())
 }
 
-fn titlecase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn source_directory(context: Context) -> Result<String, String> {
+  context
+    .evaluator
+    .search
+    .justfile
+    .parent()
+    .unwrap()
+    .join(context.name.token.path)
+    .parent()
+    .unwrap()
+    .to_str()
+    .map(str::to_owned)
+    .ok_or_else(|| {
+      format!(
+        "Source file path not valid unicode: {}",
+        context.name.token.path.display(),
+      )
+    })
+}
+
+fn source_file(context: Context) -> Result<String, String> {
+  context
+    .evaluator
+    .search
+    .justfile
+    .parent()
+    .unwrap()
+    .join(context.name.token.path)
+    .to_str()
+    .map(str::to_owned)
+    .ok_or_else(|| {
+      format!(
+        "Source file path not valid unicode: {}",
+        context.name.token.path.display(),
+      )
+    })
+}
+
+fn titlecase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_title_case())
 }
 
-fn trim(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn trim(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.trim().to_owned())
 }
 
-fn trim_end(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn trim_end(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.trim_end().to_owned())
 }
 
-fn trim_end_match(_evaluator: &Evaluator, s: &str, pat: &str) -> Result<String, String> {
+fn trim_end_match(_context: Context, s: &str, pat: &str) -> Result<String, String> {
   Ok(s.strip_suffix(pat).unwrap_or(s).to_owned())
 }
 
-fn trim_end_matches(_evaluator: &Evaluator, s: &str, pat: &str) -> Result<String, String> {
+fn trim_end_matches(_context: Context, s: &str, pat: &str) -> Result<String, String> {
   Ok(s.trim_end_matches(pat).to_owned())
 }
 
-fn trim_start(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn trim_start(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.trim_start().to_owned())
 }
 
-fn trim_start_match(_evaluator: &Evaluator, s: &str, pat: &str) -> Result<String, String> {
+fn trim_start_match(_context: Context, s: &str, pat: &str) -> Result<String, String> {
   Ok(s.strip_prefix(pat).unwrap_or(s).to_owned())
 }
 
-fn trim_start_matches(_evaluator: &Evaluator, s: &str, pat: &str) -> Result<String, String> {
+fn trim_start_matches(_context: Context, s: &str, pat: &str) -> Result<String, String> {
   Ok(s.trim_start_matches(pat).to_owned())
 }
 
-fn uppercamelcase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn uppercamelcase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_upper_camel_case())
 }
 
-fn uppercase(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+fn uppercase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_uppercase())
 }
 
-fn uuid(_evaluator: &Evaluator) -> Result<String, String> {
+fn uuid(_context: Context) -> Result<String, String> {
   Ok(uuid::Uuid::new_v4().to_string())
 }
 
-fn without_extension(_evaluator: &Evaluator, path: &str) -> Result<String, String> {
+fn without_extension(_context: Context, path: &str) -> Result<String, String> {
   let parent = Utf8Path::new(path)
     .parent()
     .ok_or_else(|| format!("Could not extract parent from `{path}`"))?;
@@ -539,11 +599,7 @@ fn without_extension(_evaluator: &Evaluator, path: &str) -> Result<String, Strin
 
 /// Check whether a string processes properly as semver (e.x. "0.1.0")
 /// and matches a given semver requirement (e.x. ">=0.1.0")
-fn semver_matches(
-  _evaluator: &Evaluator,
-  version: &str,
-  requirement: &str,
-) -> Result<String, String> {
+fn semver_matches(_context: Context, version: &str, requirement: &str) -> Result<String, String> {
   Ok(
     requirement
       .parse::<VersionReq>()
diff --git a/src/thunk.rs b/src/thunk.rs
index 87d66a3d..2ab203ab 100644
--- a/src/thunk.rs
+++ b/src/thunk.rs
@@ -6,48 +6,48 @@ pub(crate) enum Thunk<'src> {
   Nullary {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator) -> Result<String, String>,
+    function: fn(function::Context) -> Result<String, String>,
   },
   Unary {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str) -> Result<String, String>,
+    function: fn(function::Context, &str) -> Result<String, String>,
     arg: Box<Expression<'src>>,
   },
   UnaryOpt {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str, Option<&str>) -> Result<String, String>,
+    function: fn(function::Context, &str, Option<&str>) -> Result<String, String>,
     args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
   },
   UnaryPlus {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str, &[String]) -> Result<String, String>,
+    function: fn(function::Context, &str, &[String]) -> Result<String, String>,
     args: (Box<Expression<'src>>, Vec<Expression<'src>>),
   },
   Binary {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str, &str) -> Result<String, String>,
+    function: fn(function::Context, &str, &str) -> Result<String, String>,
     args: [Box<Expression<'src>>; 2],
   },
   BinaryPlus {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str, &str, &[String]) -> Result<String, String>,
+    function: fn(function::Context, &str, &str, &[String]) -> Result<String, String>,
     args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),
   },
   Ternary {
     name: Name<'src>,
     #[derivative(Debug = "ignore", PartialEq = "ignore")]
-    function: fn(&Evaluator, &str, &str, &str) -> Result<String, String>,
+    function: fn(function::Context, &str, &str, &str) -> Result<String, String>,
     args: [Box<Expression<'src>>; 3],
   },
 }
 
 impl<'src> Thunk<'src> {
-  fn name(&self) -> &Name<'src> {
+  pub(crate) fn name(&self) -> Name<'src> {
     match self {
       Self::Nullary { name, .. }
       | Self::Unary { name, .. }
@@ -55,7 +55,7 @@ impl<'src> Thunk<'src> {
       | Self::UnaryPlus { name, .. }
       | Self::Binary { name, .. }
       | Self::BinaryPlus { name, .. }
-      | Self::Ternary { name, .. } => name,
+      | Self::Ternary { name, .. } => *name,
     }
   }
 
@@ -190,7 +190,7 @@ impl<'src> Serialize for Thunk<'src> {
   {
     let mut seq = serializer.serialize_seq(None)?;
     seq.serialize_element("call")?;
-    seq.serialize_element(self.name())?;
+    seq.serialize_element(&self.name())?;
     match self {
       Self::Nullary { .. } => {}
       Self::Unary { arg, .. } => seq.serialize_element(&arg)?,


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index 4631f55b..514f5985 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -853,3 +853,54 @@ fn encode_uri_component() {
     .stdout("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!%22%23%24%25%26'()*%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~%20%09%0D%0A%F0%9F%8C%90")
     .run();
 }
+
+#[test]
+fn source_file() {
+  Test::new()
+    .args(["--evaluate", "x"])
+    .justfile("x := source_file()")
+    .stdout_regex(r".*[/\\]justfile")
+    .run();
+
+  Test::new()
+    .args(["--evaluate", "x"])
+    .test_round_trip(false)
+    .justfile(
+      "
+        import 'foo.just'
+      ",
+    )
+    .write("foo.just", "x := source_file()")
+    .stdout_regex(r".*[/\\]foo.just")
+    .run();
+
+  Test::new()
+    .args(["--unstable", "foo", "bar"])
+    .test_round_trip(false)
+    .justfile(
+      "
+        mod foo
+      ",
+    )
+    .write("foo.just", "x := source_file()\nbar:\n @echo '{{x}}'")
+    .stdout_regex(r".*[/\\]foo.just\n")
+    .run();
+}
+
+#[test]
+fn source_directory() {
+  Test::new()
+    .args(["--unstable", "foo", "bar"])
+    .test_round_trip(false)
+    .justfile(
+      "
+        mod foo
+      ",
+    )
+    .write(
+      "foo/mod.just",
+      "x := source_directory()\nbar:\n @echo '{{x}}'",
+    )
+    .stdout_regex(r".*[/\\]foo\n")
+    .run();
+}