Compile-time checked version of `unimplemented!()`


#1

I posted this idea as an RFC issue in #1911, but figured I’d post here as well to get some feedback.

The linked issue has some further technical details, but the basic idea is to introduce a construct to indicate that a piece of code needs to be filled in (i.e., the program should not compile while it is present), but that the developer wants to postpone coding up until later. This came about following a rather large refactor I had to do; part of the work I had to do was replace all Foos and Bars with Bazs. But while doing so, there were times when I realized some other change would need to be made (like Baz requiring some additional information, but that information not being readily available in a particular segment of the code). I wanted to first finish translating all the Foos and Bars, and only after that deal with the corner cases.

Normally in this case, I add a // TODO, or an unimplemented!(). However, this approach has some drawbacks. In particular, these both still let the code compile when present. Thus, after I finished my “first pass” I had to remember to also grep for all the unimplemented()s and // TODOs (and filter out any that aren’t relevant to the refactor in question). What I really wanted was a way to tell the compiler “don’t let the code compile while this is unimplemented”. But crucially, I still want to check if my code type-checks and if the borrow checker is satisfied. The compiler should only remind me of those locations if the code would otherwise have compiled.

I realize that this feature is kind of weird — it’s a construct that will never be present in any distributed code (because, by definition, it prevents compilation). But I believe it would be extremely useful during development, especially of large passes like refactors, building a new, big feature, or writing a bunch of code from scratch.

Thoughts?


#2

Isn’t that just a very weak version of “typed holes” that - for example - Idris has?

Also, why would you not want to run it? This allows you to test finished branches while others are still I’m construction.


#3

It’s similar, but not quite the same if I understand Typed Holes correctly. A typed hole is an instruction to the compiler that you don’t know what type you want in a particular location, and that you’d like it to tell you what can. Whereas this is telling the compiler I haven’t completed this code yet, and that you simply want it to warn you if you try to compile the code with that segment still unfilled.

If the hole is something that you expect not to be encountered, or to only be encountered infrequently, using unimplemented!() and having it panic at runtime would be fine. The kind of uses I envision for this is when there are key pieces of logic missing, that you know the program can’t do without. And running without those implemented is pretty much guaranteed to not work, so your program should simply not compile.


#4
struct Unimplemented;

Then create an Unimplemented everywhere you want it not to compile? This should surely be a type error.


#5

Ah, but that would prevent, say, the borrow checking stage from running. And it will cause all sorts of downstream errors in that code because the variable in question will be of the wrong type.


#6

One could write a clippy lint for unimplemented! and deny it?


#7

Except that I actually want unimplemented!() in my code too. I use that whenever there’s some weird corner case that I don’t know how to deal with yet, that I think will never be run into in practice, or that is just particularly finicky. incomplete!() would be used during development for things I know must be completed for anything to work.


#8

Then write a lint for incomplete!? Or use panic!("unimplemented") for the ones you don’t want to trigger a lint?

You can’t have a type error and then do borrowck on it, after all. A lint that gets translated into a hard error afterwards is done is the only way to do this.


#9

Well, ideally I’d want it to not be a type error (see for example @cuviper’s suggestion here). You are of course right that this could be implemented using a clippy lint, but I feel like this is in a very similar spirit to unreachable!() and unimplemented!(), and would benefit from being a built-in. Furthermore, making it a clippy lint would mean a) that your code needs to be clippy clean for it to be visible, and crucially b) that you must run clippy every time you compile…


#10

It’s easy to skip clippy lints except ones you want:

#![allow(clippy)]
#![deny(unimplemented)]

Or cargo clippy -- -A clippy -D unimplemented. And if you don’t want to manually invoke cargo clippy, you could do #![plugin(clippy)] so that clippy is run everytime you compile. I’d like a built-in, allow-by-default lint for unimplemented!() though.


#11

It sounds like a call to a non-existant external function, which would fail at link time, would be close to what you want.


#12

Yeah, @cuviper brought that up in the RFC issue, but the drawback is that at the linking stage we’ve lost the location information for the incomplete!() that triggered it.

I’ve made a crate that abuses an existing compiler lint to get something close to what I want, but it’s not quite as good as a compiler-supported version would be


#13

The link error does point to the referring function, at least when linking with gcc: playground

extern { fn foo(); }
fn main() { unsafe { foo() } }
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" [...]
  = note: ./out.0.o: In function `rust_out::main':
/tmp/<anon>:2: undefined reference to `foo'
collect2: error: ld returned 1 exit status

One advantage here is that incomplete!() will still compile if that code is never actually used at all.


#14

Why not just use a syntax error with a custom error message behind a macro?