Skip to content

Commit 831091c

Browse files
committed
Move match_cfg to a possible extension; clarify axioms
1 parent 78fc8ac commit 831091c

File tree

1 file changed

+82
-62
lines changed

1 file changed

+82
-62
lines changed

text/0000-portability-lint.md

Lines changed: 82 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
- Feature Name: N/A
1+
- Feature Name: nonportable
22
- Start Date: 2016-11-15
33
- RFC PR: (leave this empty)
44
- Rust Issue: (leave this empty)
@@ -211,8 +211,10 @@ make the warning go away):
211211
well (which will flag that function as Unix-specific).
212212

213213
- Decide to use the API *in a cross-platform way*, e.g. by providing a Windows
214-
version of the same functionality. In that case you can use a new macro,
215-
`match_cfg`, to prove to the lint that you are covering all necessary cases.
214+
version of the same functionality. In that case you `allow` the lint,
215+
explicitly acknowledging that your code may involve platform-specific APIs but
216+
claiming that all platforms of the current `cfg` are handled. (See the
217+
appendix at the end for a possible extension that does more checking).
216218

217219
In code, we'd have:
218220

@@ -232,19 +234,19 @@ fn unlabeled() {
232234
////////////////////////////////////////////////////////////////////////////////
233235

234236
#[cfg(unix)]
235-
fn foo_unix() {
237+
fn foo() {
236238
// No warning: we're within code that assumes `unix`
237239
let fd = File::open("foo.txt").unwrap().as_raw_fd();
238240
}
239241

240242
#[cfg(windows)]
241-
fn foo_windows() {
243+
fn foo() {
242244
// No warning: we're within code that assumes `windows`
243245
let handle = File::open("foo.txt").unwrap().as_raw_handle();
244246
}
245247

246248
#[cfg(linux)]
247-
fn foo_linux() {
249+
fn linux_only() {
248250
// No warning: we're within code that assumes `linux`, which implies `unix`
249251
let fd = File::open("foo.txt").unwrap().as_raw_fd();
250252
}
@@ -253,14 +255,12 @@ fn foo_linux() {
253255
// Code that provides a cross-platform abstraction
254256
////////////////////////////////////////////////////////////////////////////////
255257

256-
// No `cfg` label here; it's a cross-platform function.
258+
// No `cfg` label here; it's a cross-platform function, which we claim
259+
// via the `allow`
260+
#[allow(nonportable)]
257261
fn cross_platform() {
258-
// No warning emitted, since handling both `unix` and `windows` means
259-
// this code works for all mainstream platforms.
260-
match_cfg! {
261-
unix => foo_unix(),
262-
windows => foo_windows(),
263-
}
262+
// invoke an item with a more restrictive `cfg`
263+
foo()
264264
}
265265
```
266266

@@ -277,8 +277,7 @@ speaking, items that are labeled with a given `cfg` assumption can only be used
277277
within code making that same `cfg` assumption.
278278

279279
More precisely, each item has a *portability*, consisting of all the
280-
lexically-nested uses of `cfg`, including those expressed via `match_cfg`, but
281-
excluding mentions of Cargo features. If there are multiple uses of `cfg`, the
280+
lexically-nested uses of `cfg`. If there are multiple uses of `cfg`, the
282281
portability is taken to be their *conjunction*:
283282

284283
```rust
@@ -291,6 +290,10 @@ mod foo {
291290
}
292291
```
293292

293+
The portability only considers built-in `cfg` attributes (like `target_os`),
294+
*not* Cargo features (which are treated as automatically true for the lint
295+
purposes).
296+
294297
The lint is then straightforward to define at a high level: it walks over item
295298
definitions and checks that the item's portability is *narrower* than the
296299
portability of items it references or invokes. For example, `bar` in the above
@@ -317,16 +320,16 @@ specific to our use-case. In the limit, there are also many well-known
317320
techniques for solving SAT efficiently even on very large examples that arise in
318321
real-world usage.
319322

320-
#### Known implications
323+
#### Axioms
321324

322325
Another aspect of portability comparison is the relationship between things like
323-
`unix` and `linux`. In logical terms, we want to say that `linux` implies
326+
`unix` and `linux`. In logical terms, we want to assume that `linux` implies
324327
`unix`, for example.
325328

326329
The primitive portabilities we'll be comparing are all *built in* (since we are
327-
not including Cargo features). We can thus build in implications between these
328-
built-in portabilities, which are fed to the SAT solver when comparing
329-
portabilities. The end result is that code like the following should pass the lint:
330+
not including Cargo features). The solver can thus build in a number of
331+
assumptions about these portabilities. The end result is that code like the
332+
following should pass the lint:
330333

331334
```rust
332335
#[cfg(unix)]
@@ -339,6 +342,9 @@ fn linux_only() {
339342
}
340343
```
341344

345+
The precise details of how these implications are specified---and what
346+
implications are desired---are left as implementation details.
347+
342348
### Determining the portability of referenced items
343349

344350
**How is the portability of a referenced item determined?** The lint will
@@ -373,48 +379,6 @@ fn invoke() {
373379
}
374380
```
375381

376-
#### `match_cfg`
377-
378-
The `match_cfg` macro takes a sequence of `cfg` patterns, followed by `=>` and
379-
an expression. Its syntax and semantics resembles that of `match`. However,
380-
there are some special considerations when checking portability:
381-
382-
* When descending into an arm of a `match_cfg`, the arm is checked against
383-
portability that includes the pattern for the arm.
384-
385-
* The portability for the `match_cfg` itself is understood as `any(p1, ...,
386-
p_n)` where the `match_cfg` patterns are `p1` through `p_n`.
387-
388-
Thus, for example, the following code will pass the lint:
389-
390-
```rust
391-
#[cfg(windows)]
392-
fn windows_only() { .. }
393-
394-
#[cfg(unix)]
395-
fn unix_only() { .. }
396-
397-
#[cfg(any(windows, unix))]
398-
fn portable() {
399-
// the expression here has portability `any(windows, unix)`
400-
match_cfg! {
401-
windows => {
402-
// allowed because we are within a scope with
403-
// portability `all(any(windows, unix), windows)`
404-
windows_only()
405-
}
406-
unix => {
407-
// allowed because we are within a scope with
408-
// portability `all(any(windows, unix), unix)`
409-
unix_only()
410-
}
411-
}
412-
}
413-
```
414-
415-
If you have a `match_case` that covers *all* cases (like `windows` and
416-
`not(windows)`), then it imposes *no* portability constraints on its context.
417-
418382
## The story for `std`
419383

420384
With these basic mechanisms in hand, let's sketch out how we might apply them to
@@ -568,3 +532,59 @@ approach such cases before landing the RFC.
568532

569533
To what extent does this proposal obviate the need for the `std` facade? Might
570534
it be possible to deprecate `libcore` in favor of the "subsetting `std`" approach?
535+
536+
# Appendix: possible extensions
537+
538+
## `match_cfg`
539+
540+
The original version of this RFC was more expansive, and proposed a `match_cfg`
541+
macro that provided some additional checking.
542+
543+
The `match_cfg` macro takes a sequence of `cfg` patterns, followed by `=>` and
544+
an expression. Its syntax and semantics resembles that of `match`. However,
545+
there are some special considerations when checking portability:
546+
547+
* When descending into an arm of a `match_cfg`, the arm is checked against
548+
portability that includes the pattern for the arm.
549+
550+
* The portability for the `match_cfg` itself is understood as `any(p1, ...,
551+
p_n)` where the `match_cfg` patterns are `p1` through `p_n`.
552+
553+
Thus, for example, the following code will pass the lint:
554+
555+
```rust
556+
#[cfg(windows)]
557+
fn windows_only() { .. }
558+
559+
#[cfg(unix)]
560+
fn unix_only() { .. }
561+
562+
#[cfg(any(windows, unix))]
563+
fn portable() {
564+
// the expression here has portability `any(windows, unix)`
565+
match_cfg! {
566+
windows => {
567+
// allowed because we are within a scope with
568+
// portability `all(any(windows, unix), windows)`
569+
windows_only()
570+
}
571+
unix => {
572+
// allowed because we are within a scope with
573+
// portability `all(any(windows, unix), unix)`
574+
unix_only()
575+
}
576+
}
577+
}
578+
```
579+
580+
If you have a `match_case` that covers *all* cases (like `windows` and
581+
`not(windows)`), then it imposes *no* portability constraints on its context.
582+
583+
On more reflection, though, this extension doesn't seem so worthwhile: while it
584+
provides some additional checking, the fact remains that only the
585+
currently-enabled `cfg` is fully checked, so the additional guarantee you get is
586+
somewhat mixed. It's also a rare (maybe non-existent) error to explicitly write
587+
code that's broken down by platforms, but forget one of the platforms you wish
588+
to cover.
589+
590+
We can, however, add `match_cfg` as a backwards-compatible extension at any time.

0 commit comments

Comments
 (0)