New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item-level blocks (was: Item-level *scopes*) #2377

Closed
wants to merge 3 commits into
base: master
from

Conversation

@vext01
Copy link

vext01 commented Mar 29, 2018

Hi,

This is a draft PR to address #2312.

rendered

This is my first contribution to this repo, so I may require some guidance WRT the process.

Fixes #2312

Thanks

@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Mar 30, 2018

I think this is either a subset of #2128 or a small extension to it. Not quite sure which.

@clarfon

This comment has been minimized.

Copy link
Contributor

clarfon commented Mar 30, 2018

I like this idea, if scoped modules have an actual scope, and that the items within have the visibility they provide, excluding 100% private items (i.e. anything not pub(super)).

I think that item scope should be elaborated outside of attributes, because imho a solution which works exactly like cfg-if isn't worth having in the language.

Simple use case:

#[cfg(thing)] {
    use some::stuff;

    pub(super) struct UsefulForThisFile;
    impl UsefulForThisFile {
        pub(super) fn method() {
            stuff();
        }
    }
}

Scopes are useful here because the use only applies to the inner scope, while UsefulForThisFile can still be used in the outer scope. This actually helps cfg-if as a crate because the else and else-if functionality is still useful, but you don't get confusing bugs like:

use common::thing;
cfg_if! { if #[cfg(feature = "thing")] {
    use better::thing; // shadows common version
    pub fn stuff() { ... }
} }

while shadowing works if it's in a more specific scope, if those two things have the same scope, it'll error.

@Centril Centril added the T-lang label Mar 30, 2018

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented Apr 3, 2018

@vext01 it can be useful to add a link to the rendered RFC in the first post:

[rendered](https://github.com/vext01/rfcs/blob/item-level-scopes/text/0000-item-level-scopes.md)
@Nemo157
Copy link
Contributor

Nemo157 left a comment

Despite my individual comments, I think actual item-level scopes would be super useful in proc-macro Derive implementations, e.g. serde uses a dummy const to introduce a scope for its extern crate declaration and avoid polluting the parent module. I've recently had to do something similar in my own proc-macro.

#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const #dummy_const: () = {
    extern crate serde as _serde;
    #impl_block
};

This seems to be a very different use of the word "scope" than what this RFC proposes though.

use c;
use d;
}
```

This comment has been minimized.

@Nemo157

Nemo157 Apr 3, 2018

Contributor

This example suggests that there won't be any actual scoping happening, it appears to be expecting the "scoped" use statements will be exported to the parent scope.

For example, would this compile or be an error:

{
    use a;
}

fn foo() {
    a::foo();
}

If it is actually introducing scopes then I think an actual use of the uses should be added to the examples to make them more like real code.

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

Agreed.

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

Actually wait. I think the idea is that things inside the {} are visible at the top-level.

So this should be possible:

#[cfg(target_os = "linux")] {
    use a, b, c, d;
}
...
#[cfg(target_os = "linux")]
fn do_linux_things() {
    a::blah();
}

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

But I do see that there is an inconsistency. In regular Rust code, uses are scoped. So this doesn't work:

fn f () {
    let _ = Path::new("lalala");
}

fn main() {
    use std::path::Path;
    f();
}

This comment has been minimized.

@Nemo157

Nemo157 Apr 11, 2018

Contributor

Yep, I think the new example on the other thread clarifies that this should be auto-exported to the parent scope. I wonder if there's some alternative phrasing for this that will reduce confusion on whether the items defined in the "item-level scope" are scoped to that block or available in the parent scope.

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

The only other idea I have (which is less pretty) is:

#[cfg(...)]
parent! {
    ...
}

Where the purpose of parent! is to make it explicit that the new scope is "injected" into the parent scope. But then that raises questions about whether such an interface is general:

fn f() {
    {
        parent! {
            let x = 1;
        }
        println!("{}", x);
    }
}

Should work?!

I don't see any need for such a use-case, so I'd favour not going down that path, but instead documenting that item-level scopes re-export to the top-level (module) scope. Full stop.

This comment has been minimized.

@Nemo157

Nemo157 Apr 11, 2018

Contributor

By phrasing I was mostly referring to the documentation/naming of the feature. Mostly about removing the word "scope" from the name since visibility and scope are very tightly coupled terms. One alternative would be to focus on "block" instead, maybe "conditional item blocks"? Blocks are associated with visibility, but less so than scope.

}
```

Which seems overly verbose and complicated.

This comment has been minimized.

@Nemo157

Nemo157 Apr 3, 2018

Contributor

A better example of a currently usable alternative is:

#[cfg(debug_assertions)]
use ::{a, b, c, d};

which I think is definitely good enough for this specific use case.

This comment has been minimized.

@Ixrec

Ixrec Apr 3, 2018

Contributor

Strongly agreed, this is what I was trying to say with my previous comment linking to #2128. If the only motivation for "item-level scopes" is to apply an attribute to several uses, then your syntax as enabled by that RFC is the better solution.

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

It depends if people would like to (for example), batch a load of function defs and/or data structures into a guarded scope. Do you think that might be useful? If not, then perhaps this RFC is superfluous?

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

I can imagine one such scenario: for platform abstractions, where each platform might have it's own data structures and functions.

#[cfg(target_platform = "linux")] {
    use something::only::linux::uses;
    use more::linux::stuff;

    const SOME_SPECIFIC_PATH: &'static str = "/sys/blah/flibble/linux_specific123";

    struct LinuxThingy1 {
       ...
    }

    impl LinuxThingy1 {
         ...
    }

    struct LinuxThingy2 {
        ...
    }

    impl LinuxThingy2 {
         ...
    }
}

#[cfg(target_platform = "windows")] {
    ... lots of completely different imports and data structures...
}

#[cfg(target_platform = "freebsd")] {
    ... lots of yet different imports and data structures...
}

This comment has been minimized.

@Nemo157

Nemo157 Apr 11, 2018

Contributor

That's a much better example, another relevant example could be replacing the if_std! macro used in futures.

This comment has been minimized.

@vext01

vext01 Apr 11, 2018

Author

Yes, that would help there too.

But I'm a little spooked now, because as you said above, I didn't intend to introduce a "real scope" but users may expect one because of the "{ ... }".

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented Apr 3, 2018

@Nemo157

serde uses a dummy const to introduce a scope for its extern crate declaration and avoid polluting the parent module.

This is something that is supposed to be done through macro hygiene and not module-level blocks.

@fknorr

This comment has been minimized.

Copy link

fknorr commented Apr 6, 2018

Regarding the syntax, it might be a good idea to sell item-level scopes as "anonymous modules":

#[cfg(debug_assertions)]
mod {
    // ...
}

This clarifies it as an item-level scope and allows easier visual recognition of the block start, since #[attribute] {} is uncommon syntax.

It also has precedent with C++'s concept of anonymous namespaces, where namespace {} introduces an unnameable scope with the parent automatically importing all definitions.

@vext01

This comment has been minimized.

Copy link
Author

vext01 commented Apr 11, 2018

@fknorr I'm not sure about spinning it as an anonymous module. The items inside a item-level scope should be visible at the top-level of the module, whereas a sub-module would have its own name-space. So your suggestion implies that we special case item-level scopes?

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Apr 12, 2018

@vext01

This is my first contribution to this repo, so I may require some guidance WRT the process.

Welcome and thank you for contributing to the design of Rust!

With respect to process, various people will comment, etc.
Try to work in their concerns wrt. possible clarifications, drawbacks and proposed alternatives as best you can and add commits to the PR as the discussion develops.

Currently, the language team is primarily focused on urgent RFCs that need to be settled before the new edition.

Eventually however, the language team will discuss this RFC in a meeting and they will ask for changes or someone in the team will propose to merge / postpone / close the RFC.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

An Item-level scope is allowed wherever an Item may appear. Item-level scopes

This comment has been minimized.

@Centril

Centril Apr 12, 2018

Contributor

I believe as currently framed, this RFC constitutes a breaking change.
Consider for example the following snippet which is valid today:

fn main () {
    #[cfg(debug_assertions)] {
        struct X;
    }
    #[cfg(debug_assertions)] {
        struct X;
    }
}

Here, the struct Xs are items, but my understanding of this RFC is that struct X would both be exported to the parent scope (fn main() { .. }) and thus cause a conflict for existing code which the compiler doesn't know how to resolve.

The mod { .. } idea does not have this problem tho.

This comment has been minimized.

@vext01

vext01 Apr 17, 2018

Author

The blocks that I'm proposing appear only at the top level of a module. The semantics of your above example should be untouched.

This comment has been minimized.

@Centril

Centril Apr 19, 2018

Contributor

That's true; However, it may cause confusion between the semantics of top level items and items inside functions. Meanwhile, mod { .. } as anonymous modules do not cause such confusion.

@fknorr

This comment has been minimized.

Copy link

fknorr commented Apr 14, 2018

@vext01 exactly, but I can see where that might cause confusion with 'real' modules.

It might be surprising though that inside a function, { fn foo(); } would put foo in its own namespace where on the item level the same code doesn't. That's not enough for me to disagree with your proposal, just a heads up.

OTOH, it's not really consistent how curly braces interact with scoping anyway. When defining an item or inside a function they introduce a namespace, but then we have extern "C" {} where they don't.

@vext01

This comment has been minimized.

Copy link
Author

vext01 commented Apr 17, 2018

Another piece of code I have in one of my projects is:

#[cfg(perf_pt)]                                          
pub mod perf_pt;                                                        
#[cfg(perf_pt)]                                                                    
pub use self::perf_pt::PerfPTTracer;

Is anyone aware of a more concise way to write this with current Rust, or is this another good use-case for this RFC?

@vext01 vext01 changed the title Item-level scopes. Item-level blocks (was: Item-level *scopes*) Apr 27, 2018

@vext01

This comment has been minimized.

Copy link
Author

vext01 commented Apr 27, 2018

Thanks for everyones input.

I've revised the RFC to include your comments.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented May 3, 2018

I feel a bit nervous about introducing blocks -- there is definitely the potential for confusion about the scoping. My inclination for the scenarios covered in this RFC is usually to make a module -- one could also imagine writing a macro (e.g., cfg_all! { configuration => item* }) that applies a configuration to all items. In short, I can imagine in any case that making a module gets awkward but is it worth extending the language for it? It'd be good to see some more concrete examples (e.g., pointers to code) where this becomes a problem.

Perhaps the @rust-lang/libs team would like to weigh in with their experiences from libstd.

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented May 3, 2018

I have a lot of experience from writing no_std compatible libraries (failure and futures) where the current best practice is to have a if_std! macro that applies a cfg to every item inside the scope.

I think better than the status quo but also possibly better than just introducing arbitrary blocks is adding a built-in (someday a macro with the two-block syntax) like if_cfg!(feature = "std") { ... }.

@Nemo157

This comment has been minimized.

Copy link
Contributor

Nemo157 commented May 3, 2018

One thing I just remembered is there is one place that the if_std! macro pattern falls down: conditional methods in impl blocks, such as these methods in futures.

I don't think this RFC as written could be used in that position, I'm not really sure whether the scope inside an impl is "item-level" or not, but maybe it could be extended to work there as well.

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Jun 4, 2018

In the interests of moving this forward:

It sounds like the primary concern here is whether people will expect things declared inside a { block } to get scoped as though declared one block out. After all, that doesn't occur for blocks inside a function.

On the flip side, I do think the ability to have a block and apply attributes to it (starting with cfg attributes) makes sense and significantly improves the readability of conditionally compiled code.

Given that, I do think it might make sense to wrap this in a built-in macro, taking the attribute (using standard attribute syntax) and the block as arguments.

@alercah

This comment has been minimized.

Copy link
Contributor

alercah commented Jul 3, 2018

I would like the solution to allow scoping use to the interior of the block, so that things imported inside the block are not visible inside it. This can allow, for instance, glob imports of restricted scope, which are especially useful with macro-based libraries that may require additional imports that you have to fight through error messages to get to:

{
  use some_crate::*;
  fn i_use_tons_of_stuff() { ... }
}

fn i_dont() { /* can't accidentally refer to some_crate here */ }

C++ provides us a little precedent here with anonymous namespaces, which are used largely for visibility, and provide an effect similar to implicitly declaring them at a previous level. Things declared inside the anonymous namespace, though not accessible from other translation units, act similarly to how they would if they were declared at the top-level. That said, C++ also now has inline namespaces, where inline namespace ns { ... } brings declarations up a level, so that a library can version ABI compatibility changes without breaking source compatibility; it's possible that they would require the inline keyword for namespaces today.

However, bringing declarations up a level would also mean that you can't scope declarations to the block alone; they would necessarily be visible from the rest of the module (and thus from children). This feels a little bit unclean. To throw another bike at the shed, what if any item declared in an anonymous module with a pub specifier of any kind was treated as if it was declared in the outer scope? So:

mod {
  pub(self) fn f() {}
  fn h() {}
}

fn g() { f() } // ok
fn f() {} // not ok, f is already declared in this scope
fn i() { h() } // not ok, h is not visible

The downside is the boilerplate, of course; since Rust has no way to explicitly make something less public, this becomes tricky.

This was referenced Oct 7, 2018

@Centril Centril added the I-nominated label Nov 29, 2018

@joshtriplett

This comment has been minimized.

Copy link
Member

joshtriplett commented Dec 20, 2018

Based on discussions among the language team, we mostly agreed that we'd like a way to reduce repetition of the same cfg attributes, or for that matter other attributes. However, the use of braces (with or without mod) produce confusion about what scope the items appear in.

For that reason:
@rfcbot close

However, we'd love to see a proposal that does not use braces, and still eliminates repetition, such as an attribute alias system, or some other syntax.

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Dec 20, 2018

Team member @joshtriplett has proposed to close this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Dec 20, 2018

I'm not sure I agree with the sentiment that mod { ... } leads to confusion; but I agree that this current proposal does.

@graydon

This comment has been minimized.

Copy link

graydon commented Jan 12, 2019

Oppose. This is what inline mods are there for.

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Jan 13, 2019

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Jan 23, 2019

The final comment period, with a disposition to close, as per the review above, is now complete.

By the power vested in me by Rust, I hereby close this RFC.

@rfcbot rfcbot added closed and removed disposition-close labels Jan 23, 2019

@rfcbot rfcbot closed this Jan 23, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment