Idea: make assert!() a keyword - REJECTED

Can you elaborate on this? You can have macros at global scope or at local scope, as the crate I linked earlier already does. (You were unhappy about it requiring const, but didn't mention anything about the fact that the macros already work in both places.)

Where's the other scope that you'd like to have these?

You're right, I wasn't being specific enough when I said 'where macros aren't allowed.' What I meant to say was that there are places where a macro is allowed, but since the particular macro doesn't evaluate to something permitted in that position, it causes an error. E.g.:

assert!(true);

fn main() {
    println!("This code doesn't compile.");
}

In this particular case, the static_assertions crate that you referenced earlier can solve the problem. But this introduces friction; you need to know what the macro will evaluate to, and whether or not that will be appropriate at that position. I want to get rid of that issue entirely.

In my ideal world, I could replace the assert with assert_constraint!(), which the compiler could then either evaluate, or if it had to be evaluated at run time, place the constraint test in a block that dominates all other executable code (e.g., a new block that is at the start of main(), or a private function that every public function in a library calls on entry to the public function).

I hope that clears up what I meant to say.

Yes, I did. I still don't see how a macro is limiting your abilities. Your writing is lacking a lot of technical details with regards to the limits of what optimization is capable of and what (unsafe) tools the Rust language already provides to strongly hint additional external knowledge to supplement what the internal optimization engine of llvm is unable to proof. I supoose that a lot of the guarantees that a proof checker can deduce could be encoded for full exploitation (read: removal of runtime checks) by llvm's codegen by marking particular code paths with unchreachable_unchecked or assume. This also does not go beyond the capabilities of a proc-macro instrumenting functions. Do you suppose the macro would be lacking some input?

My apologies, I hope I didn't offend you.

You're right, and I apologize about that. First, I expect this to be usable from safe code. Requiring the use of unsafe intrinsics is something I'd like to avoid as it requires some sophistication from the end user, which shifts effort from the compiler, which really understands what Rust is, to end users who may not understand Rust well enough to use unsafe intrinsics correctly.

Second, my understanding is that while LLVM is quite powerful in it's own right, the reason rustc has its own intermediate representations is because when code is lowered to LLVM, some amount of information is lost, which reduces what LLVM is able to do. I do not have any examples or proof of this statement, it's just something that I understand from reading posts that others have made over the years. If I'm wrong about this, please correct me.

Finally, is there a way of writing a macro that creates global assertions? E.g., is there a way of doing something like the following using macros today?:

// In the root `lib.rs` or `main.rs` file
assert_constraint!(Foo::a != Bar::b);

// In the `foo.rs` file within the crate.
pub struct Foo{
   a: u8,
}

// In the `bar.rs` file within the crate.
pub struct Bar {
    b: u8
}

// In the `utility_functions.rs` file
pub fn baz(alpha: &Foo, beta: &Bar) {
    assert_constraint!(alpha::a == beta::b);
}
1 Like

<lang-hat>Not speaking for the team here, but the bar for a new keyword is generally extremely high, even one that looks like a macro. I suspect that "there are two macros (or two macro forms)" here would be considered a better solution than adding a keyword</lang-hat>

Note that Rust generally tries to avoid life before main, so I suspect "a new block that is at the start of main()" wouldn't happen. (Among other reasons, because what would that mean for something compiled as a library and might be used somewhere without a Rust main.)

That said, if we had something like Idea: global static variables extendable at compile-time (which has a chance, if someone's interested in driving it) could do the "extensible collection of bits of code" that could be called manually from entry-points. Which I agree would be a nice use of this, and mean that the assertion macros here would just be macros.

I agree that the bar should be high; I'm not sure of the best way of handling this though. I don't think that pure macros are powerful enough in all situations, which is why I wanted to have the option of turning this into a keyword by reserving those macro names ASAP.

Yes, and I agree with that idea. The global form of assert_constraint!() would be sugar for putting assert_constraint!() at the start of each block that is a public entry point into the crate. You could do that by hand, but the global form would be easier and less error prone. Do you know of a method of doing this using macros today?

What does this actually mean? "Of all the Foo structures and Bar structures in the program at any moment it's impossible to find a Foo and a Bar that have equal contents"? Or is the restriction limited to the explicit arguments of every function? Or every Foo and Bar that function can access without unsafe? Or what?

1 Like

The idea right now is not about assigning meaning, it's about carving a space in the language for constraints such that external libraries can't override the meaning. I can't redefine the meaning of the keyword while1, but I can redefine assert!() if I so chose. That means that the compiler and any other tools that could benefit from have a built-in meaning for assert!() can't do so; they are required to treat it as a macro. By carving out assert_constraint!() and try_constraint!() as objects that only the compiler/language can define, there is space in the language to assign a meaning to the example I've given above.

If there is this space available, then following RFCs will define meaning for these macros that go beyond what a macro is actually capable of right now (turning them into 'real' keywords). Those RFCs will be debated by the language team, analyzed to make sure they don't break rust, etc.

1 r#while doesn't count; the r# proves that. What I mean by the above statement is that let while = 1; doesn't work, and will never work.

1 Like

Why would you need that? If crates require a specific functionality, they can import it. The benefit of keywords over macros is not that their behavior can't be changed, it is ergonomics and native support. By adding new keywords before actually offering any of the other two, you only limit the language more.

What if I define assert_constraint!() in my library crate such that it never panics? Others now start using my crate in their code; where does the original definition apply, and where does my definition apply? Do external tools need to parse my crate and all of my dependencies to decide what assert_constraint!() means? Does that mean that I as an end user now need to analyze all crates I depend on to make sure that they haven't redefined what assert_constraint!() means? What happens if different crates each redefine it?

If crates redefine assert_constraint! with a different meaning, then they are not defining constraints in the sense you mean. If you want to define constraints in your described sense, then you need to import the right crate and make sure to use the correct one, as usual.

If you want to have global verifiability of constraints on data values, for all rust crates, then I would suggest to work out the meaning first, before reserving a keyword for something that is not even designed yet.

Crates import other crates, which may import even more crates. This turns into a DAG, and figuring out which crates are silently in conflict with one another would require auditing all crates in the entire DAG, both when the project is created, and every time any project is updated. That is untenable.

I understand the reasoning behind this (avoiding holding things in reserve indefinitely), but I disagree on the conclusion.

I view what we're doing similar to being a city planner. When a city is a tiny town, there is plenty of space to put in a new highway, so everyone complains when the city reserves wide lanes as a waste of space. If the city waits until the population is large enough that a full highway is needed, there is no space to put it anywhere, and everyone complains about the traffic and the inability to widen the roads.

Likewise, while Rust is growing rapidly, it is still a small town with plenty of space around it so reserving keywords seems to be a waste of space that others could use. However, once Rust is a large city, there won't be as much room left to put in new keywords.

Honestly, if I could start over with designing Rust, I'd probably create a namespace for keywords (e.g., all words starting with 'r') so that we wouldn't even have to have this discussion.

1 Like

This is a general problem and is definitely not resolved by adding any new crates as a built in keyword to the compiler. I am missing a good reason as to why these keywords should be added to the compiler now, when there is not even a prototype. I suggest we work out an API first, which demonstrates this functionality, and then promote it to a keyword eventually.

I doubt that a keyword assert_constraint that is unrelated to this feature request will be added to Rust anytime soon, as new keywords are extremely rare. And if anything, they start out as basic crates anyway, so you will likely have an opportunity to influence its development there.

I don't think the analogy between city streets and Rust keywords is very accurate. Streets are nothing new, they have been done before and don't really need to be designed over. The only question is how much street to put where. Rust features have fundamental qualitative differences and the question is not just which keywords are needed, but also how they should work. And that question influences the answers to "which keywords do we need?". That is why my argument was not only about avoiding holding things in reserve forever, but actually about putting the meaning first, and then adding a keyword, following the meaning.

Another solution would be to put the assert_constraint! macro into the std library, I don't think this is the issue that we should worry about right now. There will be a place to put such a macro, once it exists.

I agree about working out the API, and even where it can be used. I just want to make sure we have some place to put it once the API is designed. Aside from reserving a keyword, is there any mechanism in place to do this? Is there a guaranteed namespace for keywords?

That said, I can see that many people really do not want to reserve a keyword without having full meaning first. I don't think that's a good idea, but if the only way we can move forwards is to leave the keyword part out and hope that there is a place to plug this in later on, I'll accept that in order to move forwards right now. For right now, I'm going to continue using assert_constraint!() and try_constraint!() as if they were reserved keywords with the understanding that the actual names (if eventually used) could be different. Is that acceptable to everyone?

1 Like

The ability of one crate to locally shadow the assert_constraint! name would not impact the semantics of other crates using the standard macro with that name, any more than the current ability to define a custom panic! macro changes the semantics of code that calls the standard macro.

Note that even if one crate shadows the name within its local namespace, the original macro is still callable at its full path (e.g., std::panic or std::assert_constraint) even within that namespace, and the shadowing doesn't “leak” to other namespaces. In any case, the compiler and optimizer’s capabilities are defined by the types and semantics of the code, not by which names it uses.

The idea of a keyword to prevent name shadowing is a red herring. The important thing is to define the semantics at the levels below names, for example in MIR. (I still strongly suggest looking into existing literature and research on typestate, since the proposal here seems to be indepedently reinventing parts of it.)

7 Likes

Using 2015 here, just to make things clear:

#[macro_use]
use crate1::*;

#[macro_use]
use crate2::*;

#[macro_use]
use crate3::*;

Assume that each of those attempted to redefine the same macro; what happens?

Sure, but means that we have to be more defensive about our programming, which I'm concerned about (glob imports are easy/fast, so I suspect are more common than they probably should be; I know I'm guilty of them in my own code!).

I did briefly look at it, but I'll look into it again.

Gee*, if only there were some way to test:

$ cargo new testit
$ cd testit
$ echo 'assert2 = "0.3.3"' >> Cargo.toml
$ echo -e "use std::*;\nuse assert2::*;\nfn main() {\n  assert!(1 == 2);\n}" >! src/main.rs
$ cargo check
error[E0659]: `assert` is ambiguous (glob import vs glob import in the same module)
 --> src/main.rs:4:3
  |
4 |   assert!(1 == 2);
  |   ^^^^^^ ambiguous name
  |
note: `assert` could refer to the macro imported here
 --> src/main.rs:1:5
  |
1 | use std::*;
  |     ^^^^^^
  = help: consider adding an explicit import of `assert` to disambiguate
note: `assert` could also refer to the macro imported here
 --> src/main.rs:2:5
  |
2 | use assert2::*;
  |     ^^^^^^^^^^
  = help: consider adding an explicit import of `assert` to disambiguate

warning: unused import: `assert2::*`
 --> src/main.rs:2:5
  |
2 | use assert2::*;
  |     ^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error: aborting due to previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0659`.
error: could not compile `testit`.

To learn more, run the command again with --verbose.

*Sorry for the snark :slight_smile:

2 Likes

Then you'll get a friendly error message at compile time when you try to use that macro, with instructions for how to disambiguate it at either the import site or the call site.

Rust already prevents glob imports from ever shadowing names, so I'm not sure how this example demonstrates the danger of shadowing.


Edit: If you use Rust 2015-style extern crate imports, then there is indeed some danger of shadowing macro names, though you will at least get a warning at compile time in this particular case.

OK, I deserved that. :sweat_smile: Thank you for posting the test results.

By the way, std could define it's assert_constraint! macro as __builtin_unnamable_keyword_assert_constraint for the "it has language semantics" part. Any of the constraint solving wants to run after macro expansion, anyway.

2 Likes