- Feature Name: fn_path_qualifier
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Add a new fn
path qualifier to the Rust language. When added
as a prefix to the path of a use
expression, it opts in to 'fn-respecting'
import behavior, which allows importing items from parent function scopes.
Motivation
When writing macros, it's often useful to generate new modules. This can
be used to prevent users from accessing auto-generated code, and also
prevents cluttering up an existing namespace with new names.
If code in a macro-generated module needs to reference existing user-defined
types, it will need to import them into the module via use super::SomeUserType
For example, an attribute macro my_attribute
might be used like this:
#[my_attribute]
struct Foo;
and expand to the following code:
struct Foo;
mod _my_mod_Foo {
use super::Foo;
struct FooWrapper(Foo);
}
Unfortunately, this attribute macro cannot be used on any struct.
If the struct is declared within a module (either at the top level of
a file, or in an inline mod
block), everything will work correctly.
However, if the struct is declared within a function:
fn my_fn() {
#[my_attribute]
struct Foo;
}
the expanded code will not compile:
fn my_fn() {
struct Foo;
mod _my_mod_foo {
use super::Foo; // ERROR
struct FooWrapper(Foo);
}
}
While my_fn
will hide Foo
from other code, it is ignored
by super
qualifiers in use
statements. That is, it is impossible
for code in_my_mod_foo
to refer to Foo
- paths staring with use self::
will resolve to items inside _my_mod_foo
, while paths starting with use super::
will refer to items outside my_fn
.
This prevents #[my_attribute]
from being applied to all structs. The macro
author must make a decision:
- Require that the macro only be used on structs declared within a module
- Avoid generating nested modules.
The first option makes the macro much less useful. The second option requires the macro author to generate sub-optimal code, when they might have had perfectly valid reasons for choosing to generate a nested module.
Guide-level explanation
When writing a path (either in a use
statement, or when directly referring to a type),
you can prefix the path with the special fn
qualifier:
use fn::super::FirstType;
use fn::crate::SecondType;
use fn::self::ThirdType
The fn
qualifier acts as a modifier, causing functions to be treated like modules when resolving paths:
struct TopLevelStruct;
fn outer() {
struct OuterStruct;
fn inner() {
enum MyEnum { MyVariant }
use fn::self::MyEnum::MyVariant;
use fn::super::OuterStruct;
use fn::super::super::TopLevelStruct;
}
}
This snippet will import OuterStruct
from its declaration inside fn outer()
,
and TopLevelStruct
from its declaration adjacent to fn outer()
.
The above snippet behaves like this code when resolving paths:
struct TopLevelStruct;
mod outer {
struct OuterStruct;
mod inner {
use super::OuterStruct;
use super::super::TopLevelStruct;
}
}
Without the fn
qualifier, function scopes will be ignored by super
and self
qualifiers. For example, the following code does not compile:
struct TopLevelModType;
mod outer_mod {
struct OuterModType;
mod inner_mod {
struct TopLevelStruct;
fn outer() {
struct OuterStruct;
fn inner() {
use super::OuterStruct; // ERROR
use super::OuterModType; // OK
use super::super::TopLevelStruct; // ERROR
use super::super::TopLevelModType; // OK
}
}
}
}
While it is still possible to import types declared within modules, any
types declared directly within a function will not by seen by the use
statement.
Here, fn outer()
and fn inner()
are invisible to the use
statement.
The fn
qualifier can also be used when referring to types.
fn foo() {
struct OuterType;
mod foo {
struct InnerType(fn::super::OuterType);
}
}
You normally won't need to use this feature when writing Rust. You will usually not declare types outside of a function, and will almost never declare modules inside of a function.
This feature is primary useful for macro authors. When writing a macro, it's often useful
to generate a module, and then refer to some type that's in scope outside of the module.
By using the fn
qualifier, your macro can be used either within a module, or within a function.
Reference-level explanation
A new fn
path qualifier is added to the language. It can be used
as a prefix for any of the existing path qualifiers (crate
, self,
super
, or $crate
),
or as a prefix for an unqualified path.
The fn
qualifier can appear at most once, at the start of a path. Is is an error
to use the fn
qualifeir more than once in a path, or to use it in a position other than the
start of the path:
use fn::fn::Foo; // ERROR - cannot use 'fn' qualifier more than once in a path
use super::fn::Foo; // ERROR - cannot use 'fn' qualifier more than once in a pth
use fn::super::Foo; // OK
use fn::Foo // OK
struct MyType(fn::crate::Foo); // OK
When applied to a path, the fn
qualifier modifies the resolution behavior of that
path. Within that path, self
and super
statements now treat function scopes
in the same way as modules.
Specifically, for the purposes of import resultion, the expression fn foo() { ... }
is considered equivalent to the expression mod _unnameable { ... }
.
_unnableable
represents an identifier that cannot be named by the user. Effectively,
functions behave like anonymous modules when resolving self
and super
in paths.
For example, the following code compiles:
fn myfn() {
struct Foo;
mod inner {
use fn::super::Foo;
}
}
In this snippet, the super
qualifier now resolves to the (anonymous)
myfn
scope. Items occuring after super
will be resolved within the context
of myfn
, which allows Foo
to be referenced and imported.
The fn
qualifier can be applied to paths that do not involve any function
scopes. In this case, it does nothing:
struct MyStruct;
mod foo {
use fn::super::MyStruct; // Equivalent to 'use super::MyStruct'
}
Note that the namspaces introcded by functions still are anonymous, and cannot be directly named. This code does not compile:
fn myfn() {
struct Hidden;
}
use myfn::Hidden // ERROR
That is, it is still impossible to refer to types declared inside a function from outside that function.
Currently, fn
is a keyword, and cannot be used in paths. This means that adding
the fn
qualifier to the language is guaranteed to not affect any existing compiling code.
Before this RFC, this code does not compile:
use fn::SomeType;
There is no possibility of referring to a module named 'fn', since the path expression itself does not compile. Therefore, this RFC will not change the meaning of any existing legal paths in Rust code.
Drawbacks
This adds an additional niche feature to the Rust language, slightly increaing the complexity of the language spec and implementation.
It may be unclear to users when they should use this new qualifier. However, users are free to ignore this qualifier unless they need to use it - there is no reason to use it unless the user encounters a compiler error when trying to import a type.
This represents another concept that users must keep in mind when reading Rust
code. However, since it only affects import resolution, it should be usually be clear
from context what type is being imported, even for users who do not know how
the fn
qualifier works.
Rationale and alternatives
-
We could choose a name other than
fn
for this qualifier. Whatever name we end up picking must not currently be allowed in paths. This ensures that we do not break any existing rust code by changing the meaning of existing paths. -
We could do nothing. This leaves macro authors without a guaranteed way to refer to types declared in the parent scope of a module.
-
We could modify the behavior of all paths, without requiring an explicit qualifier. This would almost certainly be a breaking change, as code like this current compiles:
struct OuterType;
fn foo() {
struct OuterType; // Never used
mod inner {
use super::OuterType;
}
}
This code will import the top-level OuterType
into inner
, ignoring the
OuterType
declared directly inside foo
. Any change to path resolution
would need to ensure that this code continues to compile, and to refer
to the same OuterType
.
Prior art
The author is not aware of any prior art.
Unresolved questions
-
What should the name of the qualifier be?
fn
may not be the best choice, since there's not requirement for the path to actually involve a function scope. -
Will the parser require any changes in order to distinguish a
fn
as a part of path from afn
that starts a function definition?
Future possibilities
In a future edition, it would be possible to change the default behavior of paths.
Every path could be treated as though it had an implicit fn
qualifier at the start,
and explicit fn
qualifiers could be deprecated.
Since this is a breaking change, it would have to be done in a new edition. However,
rustfix
could likely convert existing paths, by adding in super
qualifiers
whenever a function scope would have been previously skipped over. Unfortunately,
such a transformation would not be idempotent, so it would be necessary for rustfix
to mark files in some way after transforming them (possiblly by adding a comment?)