Moving `cfg(accessible)` forward by narrowing down its scope(part of RFC2523)

The cfg(accessible) has been accept as part of RFC2523, but it's not feasible to implement as is (see Tracking issue for RFC 2523, `#[cfg(accessible(::path::to::thing))]` · Issue #64797 · rust-lang/rust · GitHub for the current status).

To move this feature forward, i'd suggest an narrowed-down and implementable portion of the feature, with these modifications over the initial plan:

  • Instead of supporting any path, we should focus on items, excluding any fields or associated items. For consistency enum variants is also not supported (not an item), but this might be added later in certain form.
  • It's clear that we can only support items from dependency crates, the most important case to support is std. Also, we should avoid inferring dependency crate from the content of path, being more explicit.

Example code within the original RFC

#[cfg(accessible(::std::iter::Flatten))]
fn make_iter(limit: u8) -> impl Iterator<Item = u8> {
    (0..limit).map(move |x| (x..limit)).flatten()
}

Proposed new syntax:

#[cfg(accessible_item(extern crate std, iter::Flatten))]
fn make_iter(limit: u8) -> impl Iterator<Item = u8> {
    (0..limit).map(move |x| (x..limit)).flatten()
}

What do people think?

It's been so long, I don't even remember what the blockers are. How does your proposal address these? Especially the final point — the change in syntax seems to have no benefit (and is a change for the worse imo).

4 Likes

Post 2018-edition, ::crate always unambiguously refers to an extern crate by definition? Paths - The Rust Reference

it seems odd to tailor the syntax for 2015 edition crates that also have modules with the same name as dependency crates at their root.

6 Likes

Blocker #1: The original design tries to check whether a certain path is accessible. Realistically, only module-relative paths from other crates is accessible-deterministic. (See #Tracking issue for RFC 2523, `#[cfg(accessible(::path::to::thing))]` · Issue #64797 · rust-lang/rust · GitHub)

Solution: by specifying the dependency crate as the starting point of the resolution, we can enforce that the resolution happens within a specific crate.

Blocker #2: The original design supports type-relative paths, which: i. breaks staging ii. needs to perform late resolution within the early resolution stage iii. has interactions with features like trait aliases (See #Tracking issue for RFC 2523, `#[cfg(accessible(::path::to::thing))]` · Issue #64797 · rust-lang/rust · GitHub)

Solution: We only support item resolution in this feature, excluding all associated items, trait-based resolution complexities. This changed is reflected in the attr name change (accessible_item)

1 Like

Syntactically i don't have much strong opinion. Just don't want to give people the incorrect impression that any usual path will work there. Ideas welcome!

Wouldn't it suffice to error if the path starts with a segment that does not resolve into the extern prelude? The attribute taking a plain path looks the most reasonable (and expected) to me.

6 Likes

I fully support this, new scope perfectly fits my own use cases. Great first step forward

Besides the fake impression that any path will be supported properly(but people can learn from the diagnostics), I guess the major downside will be that this cannot support Rust 2015 edition at all, which is … acceptable, i think.

I don't see how that is the case? I didn't mean that the path has to start with :: but that its first segment needs to resolve into the extern prelude, that is std::option::Option would work just fine, so would ::std::option::Option but foo::bar would not unless foo resolves to an extern crate (so also no local re-exports). I believe that should be compatible with 2015 name resolution?

Whether or not foo in foo::bar resolves to an extern crate depends on if there is a foo item in scope.

I don't see why cfg(accessible) can't work with paths like ::std::option::Option. There is no need for macros to parse paths using the rules of the exact same edition as the crate that uses the macro. And in fact proc macros don't as there is no way to get the edition of the invoking crate.

3 Likes

Not familiar with the compiler architecture and "when" "what" happens, but i wonder if requiring fully qualified/canonical paths and not supporting trait aliases could help at all, offloading the work by saying there is no scope, as a slightly less minimal, but that much more immediately useful, minimally viable feature, thats leaves open compatibility relaxing the path restrictions.

Macro expansion must have access to what things are in scope... is cfg evaluation separate from that so it has to run even earlier? If cfg runs with the rest of macro expansion, then name resolution information should be available, no?

Macro expansion and early name resolution runs interleaved with each other as fixpoint algorithm. If a macro definition isn't available yet, the macro expansion is simply delayed. cfg(accessible) can't be delayed until after foo is accessible as that may never happen. Macro expansion delaying works under thr assumption that the thing you are delaying based on will eventually be defined and otherwise an error can be returned. With cfg(accessible) you can easily create a paradox:

#[cfg(not(accessible(Foo)))]
struct Foo;

If Foo is not defined, then the definition will not be cfg'ed out making it defined, but then it should be cfg'ed out as the conditional is false, but that again requires Foo to be defined.

3 Likes

How is that related to the extern prelude though? I think no one will disagree that the path in question needs to resolve to a dependency precisely because of the fixpoint resolution. It seems questionable to me to use this feature for local items anyways (you are aware if things are available or not already).

My argument was that we should be able to put plain paths (opposed to requiring absolute (::path) paths) just fine if they resolve into the extern prelude. Notably that implies that the first segment needs to be a name of the extern prelude crates as using local renames (like extern crate dep as thing; would run into the same fixpoint algo again). rustc should be aware of the names of dependencies before starting the fixpoint resolution no?

What if you have

#[cfg(accessible(my_dep::Foo))]
mod my_dep {}

You don't know before macro expansion if my_dep refers to an extern dependency or to the module inside the local crate that shadows the extern prelude.

Not sure this is fully workable... but would something like this be okay from both a "teachable" and implementation standpoint?

Only allow referencing items "higher in the module/crate tree"

Example:

  • crate A
    • mod part_of_a
        1. struct ItemInA
  • crate B (depends on crate A)
    • mod part_of_b
        1. struct ItemInParent
      • mod deeper_in_b
          1. struct SiblingDirect
        • #[cfg(accessible)] invocation
    • mod another_in_b
        1. struct ItemInB

Allowed in invocation of #[cfg(accessible)]

    1. ItemInA (crate A is "parent/up the tree" from the root of crate B)
    1. ItemInParent (defined in parent of invocation module)
  • 4.? ItemInB (defined "above" deeper_in_b)... might be forbidden, to make rules simpler

Forbidden in invocation

    1. SiblingDirect (defined in same module as invocation, so we know locally whether it exists (weaker motivation for using cfg), as an earlier reply mentioned above)
  • 4.? if it makes things simpler to grok, ItemInB (defined in a local crate module that is not an ancestor of this module)

Theoretically, it could optimistically resolve to the external crate name, and register a requirement for that name to not become ambiguous later. We're effectively saying that the path must resolve to an external crate name, and it's an error if it doesn't.

I think this should work similarly to macro names ambiguous with macro expanded macro names.

1 Like

Question: do we actually need cfg(accessible) to work for any path that resolves to within the current crate? Fundamentally, cfg(accessible) is for polyfills, and what polyfill authors need is the equivalent of this Python fragment:

try:
    # may or may not be available depending on environment
    import SomeClass from not_this_package
except ImportError:
    class SomeClass:
        # fallback definition here

In Python, you don't ever need to do this for imports resolving to within the package, because you control all the code within the package. You know whether every SomeClass defined by your package exists or not, you don't need to ask the interpreter. I can think of only one exception: if SomeClass is being conditionally defined based on some other environmental consideration. But the fact that you need a fallback for it means SomeClass shouldn't be conditionally defined; go fix that at the point of definition!

Similarly in Rust, I expect the normal usage pattern for cfg(accessible) to be like

#[cfg(accessible(not_this_crate::some::Struct))]
use not_this_crate::some::Struct;

#[cfg(not(accessible(not_this_crate::some::item)))]
struct Struct { /* fallback definition */ }

and I suggest that it might be Just Fine if accessible(path::within::current::crate) was a compile error, at least in the minimum viable version of the feature. We can always relax that later.

4 Likes

We could assume that my_dep must refer to an extern prelude, and then emit an error later if it would get shadowed leading to an inconsistent state.

(So, exactly what @CAD97 said. :slight_smile: )

I think the answer is no, and that's also the gist I am getting from the prior discussion. However, it's non-trivial to even figure out whether a given path resolves within the current crate or not: my_dep::foo could refer to an extern crate called my_dep, or a local module of the same name. That's the problem the last few posts have been discussing.

2 Likes

Would it make this less of an issue if (again, in the minimum viable feature) you had to write an absolute path in the argument of cfg(accessible), with the leading ::?

1 Like