Pre-RFC: allow macros become `println{...}` rather than `println!(...)`

The idea is mainly come from Exclamation ! mark ! for ! macros ! feels ! kinda wrong? - The Rust Programming Language Forum, it might be better to discuss about the future syntax of macros.

The reason I prefer println{...} is three fold:

Firstly, it would save a !, and typing println{} won't be more difficult than typing println().

Secondly, {} could provide more than what ! provides, ! could only suggests that is a macro, but {} would further suggests things in this bracket is a new grammar.

A famous grammar in the curly brackets is, default constructor:

Foo{field:42}

Here, we use :, rather than =, as the assigment expression, which might be regard as a new grammar. If we unify the macro's keyword{} and struct's StructName{}, we could regard the latter one as a macro, and people could wrote new StructName macros to expand the function of old macros.

Here, we could make a new expand macro like:

expand_macro_rules! exist_macro potential_new_macro_name {
    // new rules
}

if potential_new_macro_name is not provided, expand_macro_rules would override (actually expand) the current exist macro in current crate. when old macro failed to match, it would try matching new rules.

If we have the new macro system, we could introduce more initialize methods more than Foo{field:42,..Default::default()}

Last but the most important, curly brackets allow us expand Rust's syntax directly. For example, suppose we are in Rust 2015 and async is not a keyword yet, when we want to add async syntax, we could define a macro async, and wrote

async{...}

directly.

further, we could further allow macro combinations, which could allow us write macros like

do {
    body
} while {cond}

If we allow write extra parameters after macro keyword (rather than type { immeditely, in this case, we might want an additional !(which is what currently macro_rules!'s grammar)), we could wrote

while! cond {
    body1
} else {
    body2
}
the latter code could be translate to
loop {
    if cond {
        body1
    } else {
        break body2
    }
}
directly.

Is this idea worth discussion?

Is there any disadvantages introduce the new syntax?


update:

for naming confliction, there might be several solutions:

  1. add macro keyword when necessary:
macro 宏 {...} // for languages could not perform snake_case convertion, add a macro keyword
println {...} // it compiles, but it is not very necessary to add the macro hint, since `println` is already  snake_case.
macro MacroWithUnusualNaming {} // this seems to be a struct, but if it is indeed a macro, use `macro` keyword.
  1. add :: or <> for constructors
Vec::{iter:my_iter}
Vec::<_>{iter:my_iter}

most cases, add macro keyword when necessary is enough, and people may forgot add macro keyword since there is no need to add this keyword in most of cases.

But if someone really want to argue about macro keyword, the turbofish symbol might be a good alternative. Since we could not construct structs with a private field with grammar Struct{...} now, add some extra symbol might not cost a lot.

What's more, we could just type ::{...} and let the compiler guessing the correct type.

struct Foo{};
impl Foo {
    fn new()->Self {
        ::{} // Foo{}
    }
}
1 Like

As a matter of fact, on a German keyboard it is more difficult, since { (and }) is pretty nasty. Though ease of typing is rarely a good argument for or against some syntax (except in extreme cases), which also means the “saves a !” doesn't have much of any value itself either.

22 Likes

They're different namespaces, so that would be a breaking change.

For example, this works fine (in rust 2015):

struct try { x: i32 };
let x = 4;
let y: try = try{x};

https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=45a3e09fff1bfef583d46f0aa73e6c02

The ! is important for knowing in which namespace to lookup the name.

8 Likes

Firstly, you must type {} in rust, and if you are using println! macros, typing {} inside the format string is unavoidable.

Secondly, I tried search German Keyboard Layout, found that, one could type {} easily if it has no less than 3 fingers and one hand.

At least I could type 7 and 0 with 2 fingers while right Alt key pressed by my thumb.

What's more, if Germany thought press AltGr is harder, they should wrote code like:

let a=vec!(0;5); // equivlent to `let a=vec![0;5];`, which compiles at least now.

But until now, I have not seen such code.

sorry for the rule I've forgotten, I'd wrote it later...
If you wrote a macro using a constructor's syntax, you should create a struct. But if you just heppened to wrote a struct with the same name, the expand rule works fine.

In your case,

struct try { x: i32 }; // definately a struct
let x = 4; // that's OK.
let y: try = try{x}; // try{x} is not a legal constructor (at least for now), thus it could be fall back to try macro, which might provide a try struct.

Currently, StructName{field_value} is not legal, but if we allow using macros, we could make StructName{field_value} expand to StructName{field_name:field_value}. And if you want StructName{things} to be a macro, it is indeed a macro.

Further, the novel macro syntax may help for writting new constructors:

let a=Vec{ptr:...,len:0,cap:0}; // constructor
let a=Vec{0;5}; // macro, since it is not a legal constructor grammar.

With help of the novel macro syntax, we could wrote such macros:

let map=HashMap{
    ("key","val"),
    "delim"=>"could be either comma or semicolon";
    ("or","just")("omit","the delimiter")
} // currently there is no hashmap macros like `vec!`, but we could provide it in the future.

If we have procedure macros, we might even wrote

let map=HashMap{iter:my_iterator}; // or `let map=HashMap{iterator};`

In this case, we may save a lot of works for naming constructors.

This is why I want to unify the namespace.

After namespace is unified, there is no problem about "knowing which namespace to lookup"

Uh, yes it is - MyStruct { fieldname } works fine.

Also println! { "...." } is already valid syntax, so all you're really proposing is invoking macros without !.

I think making macros obvious is a pretty nice aspect of Rust, and using ! for this is fine. I'd prefer to add a not keyword to eliminate prefix-!, since assert!(!badness()) is not great.

19 Likes

If you want a 2021 example,

#[derive(Debug)]
struct vec { x: i32 };
let x = 2;
dbg!(vec!{x});
dbg!(vec{x});

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5dc11205eae1a3700e15b2953ea46f5f

4 Likes

IMHO, in this case, the question would be, should both vec exists?

Since struct name should be UpperCamelCase and macros should have a snake_case name, you may never be bothered by the naming conflictions.

In my RFC, when we wrote Vec{...}, it is a (macro) constructor, and when we wrote println{...}, it is definately a macro.

Thus, no conflict generates.

We already have a lot of macro suggestions, there is no need to add an extra !

In the very rare case, if one does not using English, they could add ! as part of identifiers

fn 恒等映射(变量:i32)->i32 { 变量 } // using identifier which could not perform snake_case convertion
macro_rules! 中文! { /* 
    currently, `!` with both half width and full width are treated specially,
    thus we could not add this character and make the program compiles.
    but we could remove the limitation about `!` to allow this special mark occurs for identifiers which could not perform snake_case convertion.
*/ }

"Should" is not "must". Also, some_macro!{} is not always a struct; it is typically the most natural choice for top-level items (lazy_static! for instance). This proposal will break existing code (the "any disadvantages" asked for) and the justification for such things is very high; I'm not seeing that here myself.

13 Likes

type T and macro T coexisting is quite common as well. The most common case is that #[derive(T)] uses T from the macro namespace. Perhaps you're willing to replace "cannot construct trait T with an object literal" errors with "no macro T in scope," but this is a downside.

Also, while the most common, it's not the only case where macro and type name deliberately collide. When writing a macro which expands to a type name, it's typical to case the macro name as a type. All notable cases I can think of off the top of my head don't actually use the same name (e.g. tstr has TS!/TStr, frunk has Coprod!/Coproduct), but I know I've seen it done.

Then there's the human ambiguity to consider. It doesn't matter all that much whether the compiler can figure out how to parse a code snippet — code is structured first for the human. When I see nonkeyword { … } today, I know that this is a struct literal for a type nonkeyword. If it could be a macro, I have to determine where the name nonkeyword comes from (which could even be hidden by a use crate::glob::* import) to determine if this is a struct literal or a macro with potentially arbitrary semantics.

Rust has a very hardline stance forbidding breaking changes in all but exceptional cases. Removing ! from arbitrary macro calls doesn't get anywhere close. The better option is to provide less powerful options that don't need the ! warning of arbitrary syntax, but it's quite contentious where exactly the line for requiring ! should be drawn. (I've argued the line should be based on control flow, but the permissive type inference proposed there is enough for several people to say it should require the ! and all the other permissions that come with it.)

5 Likes

We will have Rust 2024, in this situation, "break existing code" might not a disadvantage, since Rust already minimize the loss

Currently, it seems that derive macro could never be called

error: expected macro, found derive macro `std::fmt::Debug`
 --> test.rs:8:48
  |
8 |     println!("{} {} {}",a.out(),X::<1>().out(),std::fmt::Debug!(a))
  |                                                ^^^^^^^^^^^^^^^ not a macro

Thus no collision happens.


for human ambiguity, UpperCamelCase macros are macros for struct with the same name, and snake_case macros are just macros. There is no conflict between them.

In this situation, if you wrote NonKeyword{...}, it is definately a struct (although it might not a literal) for a type nonkeyword. Since the macro expand rule is expand, if the original crate support Vec{iter:iterator}, then no matter how many crate you are import, Vec{iter:iterator} should only generates the correct vector you want(since it matches the rule in the original crate)

In the very rare case that a struct have the same name with macro, we could rename one of them, or add some extra characters, like s#struct and m#Macro. But, the wise choice might be choosing a different name.

1 Like

I don't think name lookup rules are something that is compatible with an edition barrier as it can hide symbols and make them unavailable for use if things come from a crate using an older edition. Editions are not "we get to reinvent the language" mechanisms. If you cannot use lazy_static! in Rust 2024, whatever rule makes that impossible is what needs to go.

Currently, Rust could handle different version in a same crate (use a very simple macro, sorry I forgot what name the macro was).

There might be no difficult to allow lazy_static! works in 2021 and lazy_static{} works in 2024

1 Like

I quite like this idea, but I don't think it is a good fit for Rust, because one of Rust's goals is to be explicit, and currently when you see a ! you know all bets are off about what might happen to the tokens inside the macro invocation. If you lose that I can imaging getting more confused when things don't work, scratching my head for ages and then realizing that some function call isn't 'just' a function call.

Slightly OT: I think there's an opportunity for people to experiment with other front-ends for rustc, playing with different syntactic choices like this one. Or alternatively one could codegen rust code.

3 Likes

It is explicit, macros are written with snake_case and structs with CamelCase.

1 Like

Notice that you can allread use println!{...} if you want to. Macros do not distinglish what kind of outer brackets they use

The trouble I have with this proposial is that ! immeditatly followring an identify is allread a syntax extention marker. {...} is used in valid syntax for the declaration of structs, aggregate expressions, and block expressions, so to turn it into a custom syntax bracket, you would have to change the syntax of all these constructs as well (good luck with that). Otherwise removing the ! would make the location of macro invocations invisible.

You suggested while! macro does, in theory looks nice, but it is bad from a parsing point of view and is also hard to interpret by humans, who have to figure out where you macro, and the custom syntax it contains, ends.

I do not see any value in defining async as a macro, an async block uses standard Rust syntax and using a keyword indicates this. New keywords could be relatively easy be added with a k#async syntax nowadays (this syntax not implemented, as it isn't used so far, but it could be added anytime if needed), before being simplifed to async in the next edition. The macro solution does look nicer in the short run, but this is payed heavily by the fact that it has to be parsed as a macro first, taking extra compilation time, even long after implementation.

2 Likes

What about the following grammar? This is why I asked for a macro chain.

try {
...
} catch err : io::Error {
retry! // use ! if it is needed.
} catch err : Box<dyn std::error::Error> {
...
} finally {
...
}

Currently,

try! {
...
} catch! err : io::Error {
retry! // use ! if it is needed.
} catch! err : Box<dyn std::error::Error> {
...
} finally! {
...
}

is a try macro, 2 catch macro, a retry macro and a finally macro, all of them could not be combined together, and try macro must end up with the first block it parsed, thus require a combination of mutiple macro seems not possible.

This is just an example to show the flexibility of my proposed grammar.

I haven't seen the 2nd version either, where did you find that one? The macro version I would expect looks like the one implemented here: try_catch - Rust, aka one overall block containing custom syntax.

Nothing enforces this. Is this a proposal to enforce such things? If so, what do we do about u8, str and friends which do not have CamelCase type names?

I means, if we want to achieve such grammar with macros. Currently we must use a whole marco which might introduce additional indentation. Although it is a choice, it might not the best one.

u8 and str are primitives, they are treated specially. Besides, you may not wrote macro as a generator that yield primitives.

for those who could not perform CamelCase, maybe additional symbol is required. I prefer a macro symbol:

println{"This is a macro"} // macro
macro println{"This is a macro"} // macro with hints, it is legal but not necessary to write a macro prefix here.
let c=Vec{iter:my_iter} // constructor which is expanded by a macro.
// `let c=macro Vec{iter:my_iter};` should generate an error to show that
// `Vec{iter:...}` here is a constructor (that yield a struct), not a "pure" macro although we use macro to expand the default constructor.
macro 宏 {...} // since "宏" is not CamelCase form, a macro is needed.
let c:结构体=结构体 {...} // no macro keyword even if “结构体 {...}” is a macro that generates a “结构体 {...}”

and Rust might generate warnings for macros (which not produce struct with the same name) without CamelCase. (we might not enforce this since compiler could recognize which macro is macro and which struct constructor is constructor, but a warning is enough for all cases.)

1 Like

Rust isn't going to add semantic case distinction[1].

Rust's identifier grammar is that defined by the Unicode Consortium as an appropriate identifier grammar for the purpose of a programming language by UAX #31: Unicode Identifier and Pattern Syntax.

While §5.2 Case Stability of the technical report contains suggestions for the purpose of implementing a semantic case distinction in a programming language, the document is clear that not doing so is preferred, as a case distinction unfairly biases against allowed scripts without a concept of case distinction.

This is, in significant part, an appeal to authority. However, the Unicode Consortium is in fact the reference for any technical properties of internationalization and is maintained by many experts in the space. Any proposal to go against the recommendations of the Unicode Consortium needs to at a minimum have a champion who fluently speaks and actually uses in code[2] a language/script without a casing distinction.


  1. To be precise, semantically (let alone grammatically) differentiating between identifier classes based on casing. Wherever an identifier is allowed, any (nonkeyword) identifier is allowed. Convention lints are allowed and in fact encouraged by the Unicode Consortium after the embedded RTL/LTR override trojan source technique was publicized. ↩︎

  2. Unfortunately many software developers, even multilingual ones, are far too quick to dismiss the prospect of developing with a noneuropean language as the primary used identifier language. In today's day and age, this is quite frankly just bigotry, though often and hopefully from a place of ignorance rather than malice. It doesn't take but a quick visit to any of the nonenglish StackOverflow variants to show the assertion that everyone develops with English-language APIs patently false. ↩︎

4 Likes