@@ -4830,6 +4830,16 @@ impl<M: cranelift_module::Module> Codegen<M> {
48304830 match &expr.node {
48314831 ast::ExprKind::Lit(lit) => self.compile_lit(builder, lit, expr.span),
48324832
4833+ // `nameOf x` — the qualified path of the value `x` resolves to
4834+ // (file.knot:line:name), as a compile-time string. Resolves through
4835+ // let-bindings (the same resolution as accessing the value).
4836+ ast::ExprKind::NameOf(operand) => {
4837+ let path = self.nameof_path(operand);
4838+ let (ptr, len) = self.string_ptr(builder, &path);
4839+ let slot = self.text_literal_slot(builder, &path);
4840+ self.call_rt(builder, "knot_value_text_intern", &[ptr, len, slot])
4841+ }
4842+
48334843 // `_` in value position: a TypeHole behaves exactly like
48344844 // `base.todo` — report the expected type + scope and exit.
48354845 ast::ExprKind::TypeHole => self.emit_todo(builder, expr.span, env),
@@ -6343,6 +6353,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
63436353 || matches!(name, "yield" | "__bind" | "__yield" | "__empty"))
63446354 };
63456355 match &expr.node {
6356+ ast::ExprKind::NameOf(_) => false,
63466357 Set { target, value } | FullSet { target, value } => {
63476358 let target_ok = match &target.node {
63486359 SourceRef { name, .. } => {
@@ -13294,6 +13305,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
1329413305 fn references_source(expr: &ast::Expr, source_name: &str) -> bool {
1329513306 match &expr.node {
1329613307 ast::ExprKind::SourceRef { name, .. } => name == source_name,
13308+ ast::ExprKind::NameOf(inner) => Self::references_source(inner, source_name),
1329713309 ast::ExprKind::ImplicitRef(_) => false,
1329813310 ast::ExprKind::CollectFold(_) => false,
1329913311 ast::ExprKind::TypeHole => false,
@@ -16381,6 +16393,41 @@ impl<M: cranelift_module::Module> Codegen<M> {
1638116393 rel
1638216394 }
1638316395
16396+ /// The qualified path of a value's original binding: `file.knot:line:name`.
16397+ /// Resolves through let-bindings (the same resolution as accessing the
16398+ /// value), so `nameOf y` where `y = x` gives `x`'s path.
16399+ fn nameof_path(&self, expr: &ast::Expr) -> String {
16400+ // The innermost named binding in the expression. For a source, the
16401+ // source name. For a variable, the binding's name (resolved through
16402+ // let-bindings). For anything else, the expression's span as a fallback.
16403+ let name = self.resolve_binding_name(expr);
16404+ // The line is the binding's declaration, not the reference. For a
16405+ // source, the source's declaration span; for a variable, the reference.
16406+ let span = if let Some(source_name) = self.resolve_source(expr) {
16407+ self.source_decl_spans.get(&source_name).copied().unwrap_or(expr.span)
16408+ } else {
16409+ expr.span
16410+ };
16411+ let (line, _) = knot::diagnostic::line_col(&self.source_text, span.start);
16412+ format!("{}:{}:{}", self.source_name, line, name)
16413+ }
16414+
16415+ /// Resolve a value's binding name (the innermost named binding). For a
16416+ /// source, the source name. For a variable, resolve through the binding to
16417+ /// the source (the same resolution as accessing the value), or the binding's
16418+ /// name if it's not a source.
16419+ fn resolve_binding_name(&self, expr: &ast::Expr) -> String {
16420+ // Resolve the value the way accessing it would: a source ref, or a var
16421+ // bound to a source (via let/source_var_binds).
16422+ if let Some(source_name) = self.resolve_source(expr) {
16423+ return source_name;
16424+ }
16425+ match &expr.node {
16426+ ast::ExprKind::Var(binding) => binding.as_user().unwrap_or("?").to_string(),
16427+ _ => format!("expr@{}", expr.span.start),
16428+ }
16429+ }
16430+
1638416431 /// Emit a runtime `knot_stm_track_read_pred` call refining the most-recent
1638516432 /// `All` filter for this table into a richer `Cols` filter built from
1638616433 /// `preds`. No-op when `preds` is `None` (the broad `knot_stm_track_read`
@@ -18456,6 +18503,7 @@ fn beta_reduce_inner(
1845618503 *fuel -= 1;
1845718504 let span = expr.span;
1845818505 let new_node = match &expr.node {
18506+ NameOf(inner) => NameOf(Box::new(beta_reduce_inner(inner, let_bindings, fun_bodies, visited, fuel))),
1845918507 Var(name) => {
1846018508 if !visited.contains(name.as_str()) {
1846118509 // Local let bindings shadow top-level functions: a let
@@ -18670,6 +18718,7 @@ fn substitute_inner(
1867018718 use ast::ExprKind::*;
1867118719 let span = expr.span;
1867218720 let new_node = match &expr.node {
18721+ NameOf(inner) => NameOf(Box::new(substitute(inner, var, value)?)),
1867318722 Var(name) if name == var => return Some(value.clone()),
1867418723 Var(_)
1867518724 | Lit(_)
@@ -18797,6 +18846,7 @@ fn expr_mentions_var(expr: &ast::Expr, var: &str) -> bool {
1879718846 };
1879818847 match &expr.node {
1879918848 Var(name) => name == var,
18849+ NameOf(inner) => expr_mentions_var(inner, var),
1880018850 Lit(_)
1880118851 | Constructor(_)
1880218852 | SourceRef { .. }
@@ -18846,6 +18896,7 @@ fn collect_free_vars_set(expr: &ast::Expr, bound: &HashSet<String>, free: &mut H
1884618896 free.insert(name.as_str().to_string());
1884718897 }
1884818898 }
18899+ NameOf(inner) => collect_free_vars_set(inner, bound, free),
1884918900 Lit(_)
1885018901 | Constructor(_)
1885118902 | SourceRef { .. }
@@ -20073,6 +20124,7 @@ fn collect_free_vars(expr: &ast::Expr, bound: &HashSet<&str>, free: &mut Vec<Str
2007320124 free.push(name.as_str().to_string());
2007420125 }
2007520126 }
20127+ ast::ExprKind::NameOf(inner) => collect_free_vars(inner, bound, free),
2007620128 ast::ExprKind::Lit(_) | ast::ExprKind::Constructor(_) => {}
2007720129 ast::ExprKind::SourceRef { .. } => {}
2007820130 ast::ExprKind::SourceDecl { .. } => {}
@@ -20234,6 +20286,7 @@ pub(crate) fn expr_refs_var(expr: &ast::Expr, var: &str) -> bool {
2023420286 let pat_binds_var = |pat: &ast::Pat| pat_bound_names(pat).iter().any(|n| n == var);
2023520287 match &expr.node {
2023620288 ast::ExprKind::Var(name) => name == var,
20289+ ast::ExprKind::NameOf(inner) => expr_refs_var(inner, var),
2023720290 ast::ExprKind::Lit(_)
2023820291 | ast::ExprKind::Constructor(_)
2023920292 | ast::ExprKind::SourceRef { .. }
@@ -20326,6 +20379,7 @@ fn expr_uses_var_as_value(expr: &ast::Expr, var: &str) -> bool {
2032620379 let pat_binds_var = |pat: &ast::Pat| pat_bound_names(pat).iter().any(|n| n == var);
2032720380 match &expr.node {
2032820381 ast::ExprKind::Var(name) => name == var,
20382+ ast::ExprKind::NameOf(inner) => expr_uses_var_as_value(inner, var),
2032920383 ast::ExprKind::Lit(_)
2033020384 | ast::ExprKind::Constructor(_)
2033120385 | ast::ExprKind::SourceRef { .. }
@@ -20637,6 +20691,7 @@ pub(crate) fn expr_has_tag_column(
2063720691fn pretty_expr(expr: &ast::Expr) -> String {
2063820692 match &expr.node {
2063920693 ast::ExprKind::Lit(lit) => pretty_lit(lit),
20694+ ast::ExprKind::NameOf(inner) => format!("nameOf {}", pretty_expr(inner)),
2064020695 ast::ExprKind::Var(name) => name.as_str().to_string(),
2064120696 ast::ExprKind::ImplicitRef(name) => format!("^{name}"),
2064220697 ast::ExprKind::CollectFold(name) => format!("<>{name}"),