crates/mimir-schema/src/moniker.rs
//! THE SCIP moniker formatter — the one writer (LAW 5).
//!
//! Every Mímir node id is a SCIP symbol string in the shape `rust-analyzer scip`
//! emits: `rust-analyzer cargo <package> <version> <descriptors>`. This module is
//! the ONLY place that string is assembled. `nornir::coderoom` (syntactic, world
//! ring) and `mimir-driver` (resolved, rustc) both call into it, so a definition
//! written by either producer gets the same bytes.
//!
//! Descriptor grammar (SCIP `format_symbol_with`):
//!
//! | suffix | form | used for |
//! |-----------------|-------------|--------------------------------------------|
//! | `Namespace` | `name/` | modules (and the crate root, `crate/`) |
//! | `Type` | `name#` | struct · enum · union · trait · type alias · variant |
//! | `Term` | `name.` | const · static · field |
//! | `Method` | `name().` | fn · method |
//! | `TypeParameter` | `[name]` | generic params, the impl self/trait slots |
//! | `Parameter` | `(name)` | fn params |
//! | `Meta` | `name:` | derive / attribute macros, non-lib target prefix (`` `bin=x`: ``), mono instances, cargo features (`` `feature=x`: ``), compiled-out items' predicate |
//! | `Macro` | `name!` | bang macros (derive / attribute macros are `Meta`, `Debug:`) |
//!
//! ## Targets of one package
//!
//! A package's LIB target (and the `--test` harness of that lib) has
//! rust-analyzer's ids, byte for byte: `… lgz 0.3.14 crate/`, `… lgz 0.3.14
//! chunk/split().`. rust-analyzer has no answer for the OTHER targets of the
//! package — it gives a bin's `crate/` and `main().` the lib's ids (and warns
//! about its own duplicate symbols) — so every node of a non-lib target
//! starts with one `Meta` descriptor naming the target, [`target_prefix`]:
//! `` `bin=lgz`: ``, `` `test=roundtrip`: ``, `` `bench=inflate`: ``,
//! `` `example=demo`: `` (cargo's target kind `=` the target's crate name).
//! The kind is part of it because cargo lets a package have a test, a bench,
//! an example and a bin of ONE name, and a bin named like the lib is the
//! default layout (`src/lib.rs` + `src/main.rs`). A target's own `--test`
//! harness keeps the target's ids.
//!
//! Impl scopes are rust-analyzer's: `impl#[Self]` for an inherent impl and
//! `impl#[Self][Trait]` for a trait impl, then the item — `impl#[Point]new().`,
//! `impl#[Dog][Greet]greet().`, ``impl#[i32][`Add<Self>`]add().`` (observed from
//! `rust-analyzer scip`, rust-analyzer 1.101.0-nightly 2822155, pinned in
//! tests/schema_and_moniker.rs). See [`push_impl`].
/// The SCIP scheme every Mímir moniker uses — rust-analyzer's, so a driver row
/// and an `rust-analyzer scip` row for the same item are the same key.
pub const SCHEME: &str = "rust-analyzer";
/// The SCIP package manager field.
pub const MANAGER: &str = "cargo";
/// Descriptor of a crate's ROOT node. A module literally named `crate` cannot
/// exist (keyword), so this can never collide with a real module.
pub const CRATE_ROOT_DESCRIPTOR: &str = "crate/";
/// One SCIP descriptor suffix.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Suffix {
Namespace,
Type,
Term,
Method,
TypeParameter,
Parameter,
Meta,
Macro,
}
/// SCIP name escaping: a name that is not a simple identifier is backtick-quoted
/// with `` ` `` doubled — verbatim the rule in `scip::symbol::escape_name`. Rust
/// idents never trip it, but the moniker must stay parseable by the CANONICAL
/// parser for the round-trip oracle to mean anything.
pub fn escape_name(name: &str) -> String {
if is_simple_identifier(name) {
name.to_string()
} else {
format!("`{}`", name.replace('`', "``"))
}
}
fn is_simple_identifier(name: &str) -> bool {
name.chars()
.all(|c| c == '_' || c == '+' || c == '-' || c == '$' || c.is_ascii_alphanumeric())
}
/// Append ONE descriptor (escaped name + suffix) to `out`.
pub fn push_descriptor(out: &mut String, suffix: Suffix, name: &str) {
let n = escape_name(name);
match suffix {
Suffix::Namespace => {
out.push_str(&n);
out.push('/');
}
Suffix::Type => {
out.push_str(&n);
out.push('#');
}
Suffix::Term => {
out.push_str(&n);
out.push('.');
}
Suffix::Method => {
out.push_str(&n);
out.push_str("().");
}
Suffix::TypeParameter => {
out.push('[');
out.push_str(&n);
out.push(']');
}
Suffix::Parameter => {
out.push('(');
out.push_str(&n);
out.push(')');
}
Suffix::Meta => {
out.push_str(&n);
out.push(':');
}
Suffix::Macro => {
out.push_str(&n);
out.push('!');
}
}
}
/// rust-analyzer's impl scope: `impl#[Self]` (inherent) or `impl#[Self][Trait]`
/// (trait impl). The impl's items follow directly: `impl#[Dog][Greet]greet().`.
pub fn push_impl(out: &mut String, self_name: &str, trait_name: Option<&str>) {
push_descriptor(out, Suffix::Type, "impl");
push_descriptor(out, Suffix::TypeParameter, self_name);
if let Some(t) = trait_name {
push_descriptor(out, Suffix::TypeParameter, t);
}
}
/// [`push_impl`] as an owned string.
pub fn impl_descriptor(self_name: &str, trait_name: Option<&str>) -> String {
let mut s = String::new();
push_impl(&mut s, self_name, trait_name);
s
}
/// rust-analyzer's version field for the toolchain's own crates (std, core,
/// alloc, proc_macro, test …): not a semver, the library's source URL.
pub fn lang_version(crate_name: &str) -> String {
format!("https://github.com/rust-lang/rust/library/{crate_name}")
}
/// One descriptor as an owned string.
pub fn descriptor(suffix: Suffix, name: &str) -> String {
let mut s = String::new();
push_descriptor(&mut s, suffix, name);
s
}
/// The suffix for one item kind in the `symbol_facts.item_kind` vocabulary
/// (`fn` / `method` / `struct` / `enum` / `trait` / `type` / `const` / `static`
/// / `macro` / `mod`) — the vocabulary `nornir::coderoom` writes. `None` for a
/// kind SCIP has no descriptor for.
pub fn suffix_for_kind(kind: &str) -> Option<Suffix> {
Some(match kind {
"mod" => Suffix::Namespace,
"struct" | "enum" | "trait" | "type" => Suffix::Type,
"fn" | "method" => Suffix::Method,
"const" | "static" => Suffix::Term,
"macro" => Suffix::Macro,
_ => return None,
})
}
/// The SCIP descriptor for one item of a `symbol_facts.item_kind` — the grammar
/// `scip::symbol::format_symbol_with` emits. `None` for a kind SCIP has no
/// descriptor for.
pub fn descriptor_for_kind(kind: &str, name: &str) -> Option<String> {
suffix_for_kind(kind).map(|s| descriptor(s, name))
}
/// **The moniker** — `rust-analyzer cargo <package> <version> <descriptors>`.
/// `descriptors` is the already-suffixed descriptor chain (`m/B#go().`).
pub fn moniker(package: &str, version: &str, descriptors: &str) -> String {
let mut s = String::with_capacity(
SCHEME.len() + MANAGER.len() + package.len() + version.len() + descriptors.len() + 4,
);
s.push_str(SCHEME);
s.push(' ');
s.push_str(MANAGER);
s.push(' ');
s.push_str(package);
s.push(' ');
s.push_str(version);
s.push(' ');
s.push_str(descriptors);
s
}
/// Cargo target kinds whose nodes carry rust-analyzer's ids unprefixed: the
/// package's library in any of its crate types.
pub const LIB_TARGET_KINDS: [&str; 6] = ["lib", "rlib", "dylib", "cdylib", "staticlib", "proc-macro"];
/// The descriptor prefix of every node of ONE cargo target of a package:
/// empty for the lib target ([`LIB_TARGET_KINDS`]), and for any other target a
/// `Meta` descriptor `` `<kind>=<crate_name>`: `` — `kind` as cargo names it
/// (`bin`, `example`, `test`, `bench`), `crate_name` the target's crate name
/// (`-` → `_`). So two targets of one package never share an id, whatever
/// their names; `=` cannot occur in a Rust identifier, so the descriptor can
/// never be an item's own (a macro called `test`, a module called `bin`).
pub fn target_prefix(kind: &str, crate_name: &str) -> String {
if LIB_TARGET_KINDS.contains(&kind) {
return String::new();
}
descriptor(Suffix::Meta, &format!("{kind}={crate_name}"))
}
/// The moniker of a crate's ROOT node (`… <package> <version> crate/`).
pub fn crate_moniker(package: &str, version: &str) -> String {
moniker(package, version, CRATE_ROOT_DESCRIPTOR)
}
/// The node id of ONE monomorphised instance of a generic item: the generic
/// item's moniker + a `Meta` descriptor holding the instance as rustc names it
/// (the hash-free demangled symbol, e.g. `a::ident::<u32>`). SCIP has no
/// instance concept; the meta descriptor keeps the id parseable, ties it to
/// its generic item by prefix, and is deterministic per instance.
pub fn mono_instance(generic_moniker: &str, instance: &str) -> String {
let mut s = String::with_capacity(generic_moniker.len() + instance.len() + 3);
s.push_str(generic_moniker);
push_descriptor(&mut s, Suffix::Meta, instance);
s
}
/// The node id of ONE cargo feature of `package@version`: a `Meta` descriptor
/// named `feature=<name>`. `=` cannot occur in a Rust identifier, so the name
/// is always backtick-escaped and can never be the id of an item — a module or
/// macro called `feature` included. One per `crate@version`: the same feature
/// name in two versions of a crate is two nodes.
pub fn feature(package: &str, version: &str, name: &str) -> String {
moniker(package, version, &descriptor(Suffix::Meta, &format!("feature={name}")))
}
/// The `package` field of every table node id: not a cargo package (`@`
/// cannot start one), the same convention as the log's `@universe`.
pub const TABLE_PACKAGE: &str = "@warehouse";
/// The `version` field of a table node id: SCIP's placeholder for "none".
pub const TABLE_VERSION: &str = ".";
/// The node id of ONE warehouse table (node kind `table`): a `Meta`
/// descriptor named `table=<name>` under [`TABLE_PACKAGE`]. No crate, no
/// version, no repo is in it — the id is the table's NAME and nothing else,
/// so every crate of every repo that touches the table points at the same
/// node. `=` keeps it apart from any item id, as with [`feature`].
///
/// The namespace / catalog the table lives in is NOT part of the id: it is a
/// runtime value at nearly every access site. Two tables of one name in two
/// catalogs are therefore one node.
pub fn table(name: &str) -> String {
moniker(TABLE_PACKAGE, TABLE_VERSION, &descriptor(Suffix::Meta, &format!("table={name}")))
}
/// The `package` field of a BUILT PRODUCT's node id (node kind `artifact`):
/// not a cargo package, like [`TABLE_PACKAGE`].
pub const ARTIFACT_PACKAGE: &str = "@build-thing";
/// The `package` field of a COMMIT node id (node kind `commit`).
pub const COMMIT_PACKAGE: &str = "@git";
/// The node id of ONE built product: a `Meta` descriptor `artifact=<id>` under
/// [`ARTIFACT_PACKAGE`], where `id` is the artifact's content hash (sha256) —
/// the same bytes built twice are one node.
pub fn artifact(content_id: &str) -> String {
moniker(ARTIFACT_PACKAGE, TABLE_VERSION, &descriptor(Suffix::Meta, &format!("artifact={content_id}")))
}
/// The node id of commit `sha` of `repo` (`commit=<repo>@<sha>` under
/// [`COMMIT_PACKAGE`]).
pub fn commit(repo: &str, sha: &str) -> String {
moniker(COMMIT_PACKAGE, TABLE_VERSION, &descriptor(Suffix::Meta, &format!("commit={repo}@{sha}")))
}
/// The `package` field of every SYSTEM CALL node id (node kind `syscall`)
/// and of the boundary producer's own crate nodes: not a cargo package.
pub const SYSCALL_PACKAGE: &str = "@syscall";
/// The `package` field of every KERNEL node id (types and functions read
/// from the kernel's BTF); the version field is the kernel release.
pub const KERNEL_PACKAGE: &str = "@kernel";
/// The node id of system call `name` of architecture `arch`
/// (`… @syscall . x86_64/openat().`): no version, no kernel release — the
/// call's NAME on its architecture. The ONE node for a system call whose
/// number is chosen at run time is `syscall(arch, "<indirect>")`.
pub fn syscall(arch: &str, name: &str) -> String {
let mut d = descriptor(Suffix::Namespace, arch);
push_descriptor(&mut d, Suffix::Method, name);
moniker(SYSCALL_PACKAGE, TABLE_VERSION, &d)
}
/// The module node holding one architecture's system calls (`… @syscall . x86_64/`).
pub fn syscall_arch(arch: &str) -> String {
moniker(SYSCALL_PACKAGE, TABLE_VERSION, &descriptor(Suffix::Namespace, arch))
}
/// The crate node every system call table hangs under (`… @syscall . crate/`).
pub fn syscall_crate() -> String {
crate_moniker(SYSCALL_PACKAGE, TABLE_VERSION)
}
/// The crate node of the boundary MAP of one dependency-universe owner
/// (`owner` = `libc@0.2.177:lib`) for one libc (`gnu` / `musl`): the
/// SYSCALLS edges out of that owner's functions hang under it, and the
/// owner's crate DEPENDS_ON it.
pub fn syscall_map_crate(owner: &str, libc: &str) -> String {
let mut d = descriptor(Suffix::Meta, &format!("map={owner}/{libc}"));
d.push_str(CRATE_ROOT_DESCRIPTOR);
moniker(SYSCALL_PACKAGE, TABLE_VERSION, &d)
}
/// The crate node of one kernel read from BTF (`… @kernel <release> crate/`).
pub fn kernel_crate(release: &str) -> String {
crate_moniker(KERNEL_PACKAGE, release)
}
/// The module node of one C name space of a kernel (`struct`, `union`,
/// `enum`, `typedef`, `func`): `… @kernel <release> struct/`.
pub fn kernel_ns(release: &str, ns: &str) -> String {
moniker(KERNEL_PACKAGE, release, &descriptor(Suffix::Namespace, ns))
}
/// The node id of one kernel item read from BTF: `struct/task_struct#`,
/// `typedef/pid_t#`, `func/__x64_sys_read().`. A SECOND definition of one
/// name in one name space (BTF keeps differing layouts apart) carries a
/// `` `btf=<type id>`: `` meta descriptor.
pub fn kernel_item(release: &str, ns: &str, name: &str, dup: Option<u32>) -> String {
let mut d = descriptor(Suffix::Namespace, ns);
push_descriptor(&mut d, if ns == "func" { Suffix::Method } else { Suffix::Type }, name);
if let Some(id) = dup {
push_descriptor(&mut d, Suffix::Meta, &format!("btf={id}"));
}
moniker(KERNEL_PACKAGE, release, &d)
}
/// The table name back out of a [`table`] id; `None` for any other id.
pub fn table_name(id: &str) -> Option<String> {
let (package, version, d) = parse(id)?;
if package != TABLE_PACKAGE || version != TABLE_VERSION {
return None;
}
let quoted = d.strip_suffix(':')?;
let inner = quoted.strip_prefix('`')?.strip_suffix('`')?.replace("``", "`");
inner.strip_prefix("table=").map(str::to_string)
}
/// The scope a node's CHILDREN are named in, as a moniker prefix: the node's
/// own id, except for a crate root, whose children start empty (`crate/` is
/// the root's node id only — `… a 0.1.0 crate/` → `… a 0.1.0 `, and a
/// non-lib target's ``… b 0.2.0 `bin=bctl`:crate/`` → ``… b 0.2.0 `bin=bctl`:``).
pub fn child_scope(node_id: &str) -> &str {
match parse(node_id) {
Some((_, _, d)) if d == CRATE_ROOT_DESCRIPTOR || d.ends_with(&format!(":{CRATE_ROOT_DESCRIPTOR}")) => {
&node_id[..node_id.len() - CRATE_ROOT_DESCRIPTOR.len()]
}
_ => node_id,
}
}
/// The node id of an item the build compiled OUT (node kind `cfg_gated_out`):
/// `scope` ([`child_scope`] of the enclosing node, plus the descriptors of any
/// compiled-out containers in between) + the item's own descriptor + a `Meta`
/// descriptor holding the predicate that gates it, e.g.
/// ``… a 0.1.0 store/open().`cfg(feature = "s3")`:``.
///
/// It is deliberately NOT the id the item has when it is compiled in: the
/// predicate is part of the identity, so the usual pair `#[cfg(feature = "x")]
/// fn f` / `#[cfg(not(feature = "x"))] fn f` is two ids (one resolved, one
/// stub), and nobody can mistake the stub for a resolved item. Same shape as
/// [`mono_instance`] (item id + a meta descriptor); the meta name always
/// starts with `cfg(`.
pub fn cfg_gated_out(scope: &str, item_descriptor: &str, predicate: &str) -> String {
let mut s = String::with_capacity(scope.len() + item_descriptor.len() + predicate.len() + 3);
s.push_str(scope);
s.push_str(item_descriptor);
push_descriptor(&mut s, Suffix::Meta, predicate);
s
}
/// The `(package, version, descriptors)` triple back out of a moniker this
/// module wrote. `None` for anything not in the `rust-analyzer cargo` shape.
pub fn parse(moniker: &str) -> Option<(&str, &str, &str)> {
let rest = moniker.strip_prefix(SCHEME)?.strip_prefix(' ')?;
let rest = rest.strip_prefix(MANAGER)?.strip_prefix(' ')?;
let (package, rest) = rest.split_once(' ')?;
let (version, descriptors) = rest.split_once(' ')?;
Some((package, version, descriptors))
}
/// The version field a [`stable`] id carries in place of the real one.
pub const STABLE_VERSION: &str = "*";
/// **The stable id** — the moniker with its version replaced by
/// [`STABLE_VERSION`], so the same item at two dependency (or own) versions
/// has ONE identity in the log. The real version stays in the stored row;
/// only identity (delta and fold keying) uses this form. Anything not in the
/// `rust-analyzer cargo` shape — table, artifact and commit ids, foreign
/// symbols — is returned unchanged.
pub fn stable(id: &str) -> std::borrow::Cow<'_, str> {
match parse(id) {
Some((_, STABLE_VERSION, _)) | None => std::borrow::Cow::Borrowed(id),
Some((package, _, descriptors)) => std::borrow::Cow::Owned(moniker(package, STABLE_VERSION, descriptors)),
}
}