on master help sign in
Add the nameOf operator: a value identity string (file:line:name)

nameOf x is a prefix keyword operator that returns the qualified path of the value x resolves to, as a compile-time string: file.knot:line:name. The line is the binding declaration (for a source, the source decl), not the reference. Resolves through let-bindings (the same resolution as accessing the value), so nameOf y where y = x gives x path (transparency). For a non-source, the binding name; for a computed value, a fallback id. Suite 291/0.

commit06a08932ac94214b54d039e06f3ac2d96c18ee76 authoriko <mail@iko.soy> dateMon 31 Aug 2026, 06:51 (1 month ago) parent0ddc115
14 files changed+95
crates/knot-compiler/src/base.rs+1 crates/knot-compiler/src/codegen.rs+55 crates/knot-compiler/src/desugar.rs+1 crates/knot-compiler/src/infer.rs+9 crates/knot-compiler/src/sql_lint.rs+2 crates/knot-compiler/src/types.rs+1 crates/knot-compiler/src/unused.rs+1 crates/knot-lsp/src/defs.rs+1 crates/knot-lsp/src/semantic_tokens.rs+4 crates/knot-lsp/src/utils.rs+1 crates/knot/src/ast.rs+4 crates/knot/src/format.rs+1 crates/knot/src/lexer.rs+4 crates/knot/src/parser.rs+10
crates/knot-compiler/src/base.rs+1
@@ -364,6 +364,7 @@ fn shift_expr_spans(e: &mut ast::Expr, offset: usize) {
364364 e.span.end += offset;
365365 match &mut e.node {
366366 TypeLiteral(_) => {}
367+ NameOf(inner) => shift_expr_spans(inner, offset),
367368 App { func, arg } => {
368369 shift_expr_spans(func, offset);
369370 shift_expr_spans(arg, offset);
crates/knot-compiler/src/codegen.rs+55
@@ -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}"),
crates/knot-compiler/src/desugar.rs+1
@@ -703,6 +703,7 @@ fn desugar_expr(expr: &mut Expr, io_fns: &IoFns, source_vars: &HashSet<String>)
703703/// by the caller).
704704fn recurse_into_children(expr: &mut Expr, io_fns: &IoFns, source_vars: &HashSet<String>) {
705705 match &mut expr.node {
706+ ExprKind::NameOf(inner) => recurse_into_children(inner, io_fns, source_vars),
706707 ExprKind::Lit(_)
707708 | ExprKind::Var(_)
708709 | ExprKind::TypeLiteral(_)
crates/knot-compiler/src/infer.rs+9
@@ -6214,6 +6214,13 @@ impl Infer {
62146214 fn infer_expr_inner(&mut self, expr: &ast::Expr) -> Ty {
62156215 match &expr.node {
62166216 ast::ExprKind::Lit(lit) => self.literal_type(lit, expr.span),
6217+ // `nameOf x` — the qualified path of the value `x` resolves to, as a
6218+ // compile-time string. The type is Text; the operand is type-checked
6219+ // (its provenance is resolved at codegen).
6220+ ast::ExprKind::NameOf(operand) => {
6221+ self.infer_expr(operand);
6222+ Ty::Text
6223+ }
62176224 // A type literal is only meaningful as a type-witness argument,
62186225 // consumed by the application diversion before inference reaches
62196226 // here. Standalone it's an error.
@@ -13738,6 +13745,7 @@ fn value_references_source_inner(
1373813745) -> bool {
1373913746 match &expr.node {
1374013747 ast::ExprKind::SourceRef { name, .. } => name == source_name,
13748+ ast::ExprKind::NameOf(inner) => value_references_source_inner(inner, source_name, aliases, let_bindings, visited),
1374113749 // `^x` reads a record field, never a source relation directly.
1374213750 ast::ExprKind::ImplicitRef(_) => false,
1374313751 // A type literal has no expression content.
@@ -15340,6 +15348,7 @@ fn rewrite_result_markers(expr: &mut ast::Expr, pure_spans: &HashSet<Span>) {
1534015348fn walk_expr_children_mut(expr: &mut ast::Expr, f: &mut impl FnMut(&mut ast::Expr)) {
1534115349 use ast::ExprKind::*;
1534215350 match &mut expr.node {
15351+ NameOf(inner) => f(inner),
1534315352 Lit(_) | Var(_) | Constructor(_) | SourceRef { .. } | ImplicitRef(_) | CollectFold(_) => {}
1534415353 TypeHole => {}
1534515354 TypeLiteral(_) => {}
crates/knot-compiler/src/sql_lint.rs+2
@@ -64,6 +64,7 @@ fn lint_expr(
6464 diags: &mut Vec<Diagnostic>,
6565) {
6666 match &expr.node {
67+ ExprKind::NameOf(inner) => lint_expr(inner, source_schemas, views, fun_bodies, diags),
6768 ExprKind::Set { target, value } => {
6869 if let ExprKind::SourceRef { name, .. } = &target.node {
6970 if let Some(schema) = source_schemas.get(name) {
@@ -919,6 +920,7 @@ fn match_filter_only<'a>(source_name: &str, value: &'a Expr) -> Option<(String,
919920fn references_source(expr: &Expr, source_name: &str) -> bool {
920921 match &expr.node {
921922 ExprKind::SourceRef { name, .. } => name == source_name,
923+ ExprKind::NameOf(inner) => references_source(inner, source_name),
922924 ExprKind::Lit(_)
923925 | ExprKind::Var(_)
924926 | ExprKind::Constructor(_)
crates/knot-compiler/src/types.rs+1
@@ -398,6 +398,7 @@ impl TypeEnv {
398398fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
399399 f(e);
400400 match &e.node {
401+ ExprKind::NameOf(inner) => walk_exprs(inner, f),
401402 ExprKind::TypeLiteral(_) => {}
402403 ExprKind::App { func, arg } => {
403404 walk_exprs(func, f);
crates/knot-compiler/src/unused.rs+1
@@ -213,6 +213,7 @@ fn walk_route_entry(e: &RouteEntry, r: &mut Refs) {
213213
214214fn walk_expr(e: &Expr, r: &mut Refs) {
215215 match &e.node {
216+ ExprKind::NameOf(inner) => walk_expr(inner, r),
216217 ExprKind::TypeLiteral(_) => {}
217218 ExprKind::Lit(_) => {}
218219 // `^x` reads a field of an in-scope record, but which record is only
crates/knot-lsp/src/defs.rs+1
@@ -427,6 +427,7 @@ impl<'a> DefResolver<'a> {
427427 ast::ExprKind::Var(name) => self.add_ref(expr.span, name.as_str()),
428428 ast::ExprKind::Constructor(name) => self.add_ref(expr.span, name),
429429 ast::ExprKind::TypeLiteral(_) => {}
430+ ast::ExprKind::NameOf(inner) => self.resolve_expr(inner),
430431 ast::ExprKind::SourceRef { name, .. } => self.add_ref(expr.span, name),
431432 // `^name` resolves to a record field at inference time; no single
432433 // binding site to reference here.
crates/knot-lsp/src/semantic_tokens.rs+4
@@ -418,6 +418,10 @@ impl<'a> TokenCollector<'a> {
418418
419419 fn visit_expr(&mut self, expr: &ast::Expr) {
420420 match &expr.node {
421+ ast::ExprKind::NameOf(inner) => {
422+ self.add(self.strip_parens(expr.span), TOK_FUNCTION, 0);
423+ self.visit_expr(inner);
424+ }
421425 ast::ExprKind::Var(name) => {
422426 let modifier = if EFFECTFUL_BUILTINS.contains(&name.as_str()) {
423427 MOD_EFFECTFUL
crates/knot-lsp/src/utils.rs+1
@@ -477,6 +477,7 @@ pub fn top_fields(program: &Expr) -> Vec<&RecordField> {
477477
478478pub fn recurse_expr<F: FnMut(&ast::Expr)>(expr: &ast::Expr, mut f: F) {
479479 match &expr.node {
480+ ast::ExprKind::NameOf(inner) => f(inner),
480481 ast::ExprKind::TypeLiteral(_) => {}
481482 ast::ExprKind::App { func, arg } => {
482483 f(func);
crates/knot/src/ast.rs+4
@@ -165,6 +165,10 @@ pub enum ExprKind {
165165 /// `42`, `3.14`, `"hello"`
166166 Lit(Literal),
167167
168+ /// `nameOf x` — the qualified path of the value `x` resolves to
169+ /// (file.knot:line:name), as a compile-time string.
170+ NameOf(Box<Expr>),
171+
168172 /// `x`, `formatTitle` — lowercase identifier.
169173 Var(Binding),
170174
crates/knot/src/format.rs+1
@@ -627,6 +627,7 @@ fn render_expr_inline(e: &Expr, parent: Prec) -> String {
627627 }
628628 match &e.node {
629629 ExprKind::Lit(l) => render_literal(l),
630+ ExprKind::NameOf(inner) => format!("nameOf {}", render_expr_inline(inner, Prec::Atom)),
630631 // `yield` is refused by the parser's `can_start_atom` in application
631632 // argument position (it would be ambiguous with do-block yields), so
632633 // a Var named `yield` must keep its parens there: `f (yield)`.
crates/knot/src/lexer.rs+4
@@ -27,6 +27,7 @@ pub enum TokenKind {
2727 With,
2828 Refine,
2929 Forall,
30+ NameOf,
3031
3132 // Delimiters
3233 LParen,
@@ -108,6 +109,7 @@ impl TokenKind {
108109 TokenKind::With => "'with'",
109110 TokenKind::Refine => "'refine'",
110111 TokenKind::Forall => "'forall'",
112+ TokenKind::NameOf => "'nameOf'",
111113 TokenKind::LParen => "'('",
112114 TokenKind::RParen => "')'",
113115 TokenKind::LBrace => "'{'",
@@ -163,6 +165,7 @@ impl TokenKind {
163165 TokenKind::With => Some("with"),
164166 TokenKind::Refine => Some("refine"),
165167 TokenKind::Forall => Some("forall"),
168+ TokenKind::NameOf => Some("nameOf"),
166169 _ => None,
167170 }
168171 }
@@ -550,6 +553,7 @@ impl<'src> Lexer<'src> {
550553 "with" => return TokenKind::With,
551554 "refine" => return TokenKind::Refine,
552555 "forall" => return TokenKind::Forall,
556+ "nameOf" => return TokenKind::NameOf,
553557 _ => {}
554558 }
555559 TokenKind::Lower(text.to_owned())
crates/knot/src/parser.rs+10
@@ -1690,10 +1690,20 @@ impl Parser {
16901690 let lit = self.parse_postfix_from(lit)?;
16911691 self.parse_application_from(lit)
16921692 }
1693+ TokenKind::NameOf => self.parse_nameof(),
16931694 _ => self.parse_application(),
16941695 }
16951696 }
16961697
1698+ /// `nameOf x` — the qualified path of the value `x` resolves to.
1699+ fn parse_nameof(&mut self) -> Option<Expr> {
1700+ let start = self.span();
1701+ self.advance(); // nameOf
1702+ let operand = self.parse_unary()?;
1703+ let span = Span::new(start.start, operand.span.end);
1704+ Some(Spanned::new(ExprKind::NameOf(Box::new(operand)), span))
1705+ }
1706+
16971707 fn parse_application(&mut self) -> Option<Expr> {
16981708 let func = self.parse_postfix()?;
16991709 self.parse_application_from(func)