The model
resolveresolves one unit with the default (lexical, Kraken-style) policy: the lazy path.- A
Policyis a language's scoping rules as data. Presets:kraken,php,python. - A
Resolverresolves one unit with a policy, anEnv(names outside the program: other packages, the standard library, primitive types), and aBudget. - A
Programresolves several units against each other; a unit given a root name is reachable from every unit asname::item. - The
Resolutionholds the resolved units, the **Diagnostic**s, and theIndex.
What it guarantees, and how each guarantee is checked:
| Guarantee | How it is held |
|---|---|
| Lexical resolution is right: the innermost visible binder or item wins, hoisting and frames included. | Property tests compare it with a separate reference resolver (a stack of scopes searched innermost-out) on random programs of nested blocks, items, closures, parameters, generic parameters, let_place aliases, keyed yield and yield from, and union and intersection type annotations, under scope hoisting and under declare-before-use. |
| Imports reach the least fixpoint of their rules, whatever the order. | Property tests compare it with a naive fixpoint over random module graphs of definitions, aliased imports, and globs, public and private, with cycles and ambiguity; a second property permutes the modules and checks that nothing changes. A third compares relative imports whose first segment arrives only through a glob (or falls back to the enclosing module) with a naive fixpoint over per-table slots. |
| PHP's name tables: functions, methods, and classes fold ASCII case; constants keep case in a table of their own. | A property test compares PHP resolution with a reference made of plain maps on random programs of functions, constants, class members, and calls spelled in random case. |
PHP classes: a class name works as a value (new Foo, instanceof Foo, Foo::class), classes inherit their interfaces' constants, and trait member tables follow PHP's copy rules. |
Integration tests resolve the exact operand shape bcgen-lang compiles; a property compares Class::CONST with a reference of PHP's lookup order on random class and interface hierarchies; a property compares every member of every class with a model of zend_traits_copy_functions on random mixin graphs (aliases, insteadof, visibility rules, nesting, abstract methods). |
| Several bases follow C3 (Python's MRO); a class's unqualified names see inherited members. | Property tests compare member lookup and lexical inheritance with a textbook C3 reference on random hierarchies with public, protected, and private members; inconsistent hierarchies are counted and must be reported. |
| Every reference ends resolved, or with exactly one diagnostic. | Checked by the property tests on every random program; access errors keep their resolution and carry one diagnostic. |
The result is a valid Hir. |
Every resolution goes through Hir::resolve_partial; a refusal becomes Res::Err and a diagnostic. The tests validate the result again. |
| The index is consistent: a definition lists exactly the references that point at it. | Checked by the property tests on every random program. |
| No recursion over input; deep or wide programs never overflow the stack. | Tests resolve 100,000 nested blocks, 25,000 nested closures, 20,000 classes nested through method bodies, and a 50,000-item module. |
| Untrusted input cannot drive unbounded work. | Glob propagation, inheritance walks, and did-you-mean comparisons each run against an explicit Budget; exceeding the first two fails the run cleanly, the third only stops suggesting. |
Installation
[]
= "0.4"
= "0.4"
= "1"
Without the standard library:
[]
= { = "0.4", = false }
Quick start
Build a unit with hir-lang's builder, resolve it, read the result:
use ;
use Interner;
// fn length(n) { n } fn main() { lenght }
let mut names = new;
let n = new;
let mut b = new;
let = b.local_param;
let use_n = b.name_expr;
let body = b.block;
let length = b.func;
let typo = b.name_expr;
let main_body = b.block;
let main = b.func;
let root = b.module;
let hir = b.finish?;
let unit = hir.unit;
let res = resolve?;
let hir = res.hir.unwrap;
let Path = *hir.expr else ;
assert_eq!;
assert_eq!;
# Ok::
A language's policy and environment
use ;
use Interner;
use ;
let mut names = new;
let int = new;
// The host provides primitive type names.
let env = new.with_prelude;
// type Count = int
let mut b = new;
let path = b.name_path;
let ty = b.ty;
let alias = b.item;
let root = b.module;
let res = new.with_env.resolve?;
assert_eq!;
# Ok::
Several units, and the index
use ;
use Interner;
use ;
let mut names = new;
let = ;
// Unit 1, reachable as `util`: pub fn helper() {}
let mut b = for_unit;
let body = b.block;
let f = b.item;
let root = b.module;
let util_hir = b.finish?;
// Unit 2: fn main() { util::helper }
let mut b = for_unit;
let segs = ;
let segs = b.list;
let path = b.path;
let call = b.expr;
let body = b.block;
let main = b.func;
let root = b.module;
let app_hir = b.finish?;
let mut program = new;
program.add_unit;
program.add_unit;
let res = program.resolve?;
assert!;
// Find references: `util::helper` is one reference to `helper`.
let index = res.index;
let def = index.def_of.unwrap;
assert_eq!;
assert_eq!;
# Ok::
PHP's tables and Python's MRO
use ;
use Interner;
use ;
// function strLen() {} const config = 1; function config() {}
// function main() { STRLEN(); config(); config; }
let mut names = new;
let mut b = new;
let body = b.block;
let strlen = b.func;
let one = b.int;
let config = new;
let k = b.item;
let body = b.block;
let f = b.func;
let c1 = b.name_expr;
let call1 = b.call;
let c2 = b.name_expr;
let call2 = b.call;
let value = b.name_expr;
let list = b.list;
let t = b.expr;
let main_body = b.block;
let main = b.func;
let root = b.module;
let hir = b.finish?;
let unit = hir.unit;
let res = new.resolve?;
// No duplicate (a function and a constant may share a name), nothing unresolved.
assert!;
let hir = res.hir.unwrap;
let target = ;
let res_of = ;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
Under Policy::python(), a class C(A, B) looks members up in C3 order
(C, A, B, …), and a hierarchy with no consistent order is reported, as Python
raises TypeError; see the php_python example.
More in examples/: basic (the lazy path), multi_unit (two units importing each other), lsp (go-to-definition, references, rename, outline), php_python (PHP's tables and case folding, Python's C3 order).
What resolution covers
- Binders through
Hir::lookup_localand the walk'sBind/Scope/Frameevents: the HIR's own scope rules, never re-derived. A local behind a frame it cannot cross (a nested function) is reported, not silently skipped. - Items by namespace and hoisting: visible in their whole scope, from their declaration on, or (PHP) in their whole module wherever declared.
- Case folding per table: PHP 8's ASCII rule for functions, methods, classes, and namespaces, through every lookup; names stay as written in diagnostics and the index, and a rename covers every case variant.
- Constants apart from functions (PHP): a call's callee searches functions first, any other value constants first, so a method and a class constant may share a name.
- Paths:
a::b::cthrough modules, sum variants, and the environment's containers; partial resolution where the rest is type-directed (Vec::new,T::Item,<T as Tr>::Out); roots::,self::,super::;self::/parent::/static::early or late per policy. - Imports: single, aliased, glob, re-exported, private, across units, in cycles; ambiguity between globs reported where the name is used.
- HIR 0.4 forms: a
let_placealias is visible in its body only and crosses no frame (a closure naming it is reported), while its place's operands resolve outside it; a template's alias carries its mark, so user names never reach it. Keyedyieldandyield fromoperands, and every type path in a union or intersection, resolve like any other; union members hir-lang's normalization dropped are left alone. - Patterns: a bare identifier pattern matches a constant, unit variant, or unit record of that name, and binds otherwise.
- Classes: member tables with mixin expansion (
insteadof,as, matched case-insensitively under PHP), inherited lookups in C3 order (Python's MRO), private and protected access; under lexical class scoping, unqualified names see inherited members before enclosing scopes. - Diagnostics: unresolved names (with a suggestion within a small edit distance), ambiguous globs, private access, duplicates, uncapturable locals, wrong kinds, import cycles, mixin conflicts, inconsistent method resolution orders.
Performance
Measured with cargo bench --bench bench on a desktop x86_64 machine (Windows, release profile), resolution only (building the HIR is excluded):
| Benchmark | Size | Time |
|---|---|---|
| One unit, functions with locals and calls | 124,000 nodes | 22.3 ms |
| One unit, functions with locals and calls | 1,240,000 nodes | 219 ms |
The same unit under Policy::php(), every call found through case folding |
1,240,000 nodes | 215 ms |
| 1,000 modules in a glob ring, aliased imports | 53,000 nodes | 12.1 ms |
| Eight units, each calling into the next | 992,000 nodes | 160 ms |
| 10,000 classes with three bases each (C3), inherited members named unqualified and through the class | 219,000 nodes | 97 ms |
| Generator functions with four let_place aliases each, keyed yield, yield from, and union-typed parameters (hir-lang 0.4 forms) | 192,000 nodes | 31 ms |
These are library numbers: the time includes building the index and writing every resolution into the Hir. The rows above the last were measured for 0.3.0; for 0.4.0 the 0.3.0 and 0.4.0 crates were run back to back on the same machine under the same load (124k- and 1.24M-node units, eight units, index queries), and 0.4.0 was within noise or faster on each. The last row was measured on a loaded machine, so it is an upper bound.
Testing
The property tests in tests/properties.rs, tests/import_graph.rs, tests/import_corner.rs, tests/case_fold.rs, and tests/inheritance.rs hold the resolver to independent references written in the tests. Every rust example in this README and in docs/API.md is compiled and run as a doctest.
Cross-platform support
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)
The crate uses no operating-system facilities and no platform-specific code.
Contributing
See REPS.md for the engineering standards every change is held to, dev/DIRECTIVES.md for the definition of done, and dev/ROADMAP.md for the plan. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.