You can't really do this in a language where the way you use libraries is by textual inclusion of headers which can include arbitrary code (no one uses modules).
You can't really do this in a language where the way you use libraries is by textual inclusion of headers which can include arbitrary code (no one uses modules).
It would have been extremely difficult to do this for say C++ 20 even if the other work was abandoned (which IMO would have been sensible) and I feel confident it is now impossible.
One possible design is to assign editions to all preprocessed tokens. This implies the edition definition should be some sort of pragma (say, `#pragma STDCXX EDITION 2023`) and preprocessors should add pragmas themselves if tokens with a different edition are transcluded into other tokens. Implementations with integrated preprocessor and parser may also use a simpler internal representation to convey editions as well. Now `_` would be a normal identifier in past editions and a placeholder in a new edition, however they are passed through macros. For example:
#pragma STDCXX EDITION 2023
// Hereafter `_` is parsed as `_2023` internally
#define GUARD() auto _ = make_guard()
#pragma STDCXX EDITION 2020
// Hereafter `_` is parsed as `_2020` internally
{
GUARD();
GUARD(); // Okay, equivalent to `auto _2023 = make_guard();`
foo(&_); // Up to the proposal author, but P2169 suggests that
// this `_` should refer to the second `_2023`
}
auto _ = make_guard(); // Okay, equivalent to `auto _2020 = make_guard();`
auto _ = make_guard(); // Nope, `_2020` can't be defined more than once in the same scope
Of course there are more questions to answer. For example what would `#` and `##` do to editions? Should the current edition reset after `#include` finishes? (I think so.) How would we compute the edition for specific syntax from constituent tokens? (A systematic approach would be to collect a set of all editions used in syntactic tokens, and if not all editions in that set result in the same result, to issue an error.)But I still argue that this design is simple, checks all checkboxes as long as open questions are decided, and can be independently deployed. And this token-level tracking is not something alien, it's required to give more accurate diagnostics anyway---and other languages like Rust have even used it to implement things like macro hygiene.
You're handwaving a ton of complexity. You don't even need token pasting for this to become a confusing mess, just consider what happens if GUARD takes the name as an argument, or if an expression is constructed by putting two macros with different editions next to each other. You're also missing the biggest issue, which is 'what happens with headers that don't care about editions, and only begrudgingly even include an extern "C" for C++ users?'.
> (A systematic approach would be to collect a set of all editions used in syntactic tokens, and if not all editions in that set result in the same result, to issue an error.)
This does not work in general. Suppose you wanted to remove most vexing parse in a new edition: changing the result of `Foo bar(std::array<int, PAGE_SIZE>())` is the goal!
The name token will be assigned the edition of the originating position.
#pragma STDCXX EDITION 2023
#define GUARD(var) auto var = make_guard()
#pragma STDCXX EDITION 2020
GUARD(_);
GUARD(_); // `_` is a normal identifier, so this will probably error.
> or if an expression is constructed by putting two macros with different editions next to each other.Let's replicate the removal of implicit conversions from the Epochs proposal then.
#pragma STDCXX EDITION 2020
#define MOVE move
#define LPAREN (
#define RPAREN )
#define COMMA ,
#define P p
#define LIT_3_42 3.42
#define LIT_2_49 2.49
#pragma STDCXX EDITION 2023
void move(Particle&, float x, float y);
void foo() {
Particle p{};
// error, all tokens are in the 2023 edition
move(p, 3.42, 2.49);
// depending on the proposal, at least one of them
// should work because all constituent tokens for
// the call expression are in the 2020 edition.
// proposal authors would then be responsible for
// which tokens are considered "constituent" here.
move LPAREN p, 3.42, 2.49 RPAREN;
move LPAREN p COMMA 3.42 COMMA 2.49 RPAREN;
MOVE LPAREN p COMMA 3.42 COMMA 2.49 RPAREN;
MOVE LPAREN P COMMA LIT_3_42 COMMA LIT_2_49 RPAREN;
// assuming the call only regards `(` and `)` as
// constituent tokens, the following call has
// ambiguous editions and will fail to compile.
move(p, 3.42, 2.49 RPAREN;
}
So my chain of logic is that, we ideally want to put editions to the AST, but the existence of textual inclusion means that edition should also exist in tokens and the AST node's edition will have to be computed from them. Not a big deal for existing compilers (they already track spans).> what happens with headers that don't care about editions, and only begrudgingly even include an extern "C" for C++ users?
For this reason I believe the edition should reset across files, reverting to the default edition.
And in fact this feature will benefit both C and C++, and thus ideally be harmonized. In this way we will no longer have to special-case some files to be implicitly in the `extern "C"` block! (Yes, this exists, see for example -internal-externc-isystem in clang cc1.)
> Suppose you wanted to remove most vexing parse in a new edition: changing the result of `Foo bar(std::array<int, PAGE_SIZE>())` is the goal!
Now this is probably a better question, because we have tons of options for how existing parsers should behave. Note that I've intentionally omitted this part because those options are only useful when the ambiguity arises, and I think they can be determined in the case-by-case basis.
But if you indeed want to remove the most vexing parse, it's mostly up to the proposal author's decision. I should note the following quote from ISO C++:
If the statement cannot syntactically be a declaration, there is no ambiguity, so this rule does not apply. The whole statement might need to be examined to determine whether this is the case. [1]
So the parser should have already produced two possible ASTs for the same token sequence in the first place and picked one of them, in principle. The proposal therefore will determine which token affects this decision---for example that may be `(` and `)` as in the call, or the whole `std::array<int, PAGE_SIZE>()` sequence that can be parsed as a type-id. In any case the proposal will give a concrete and compatible algorithm to remove the most vexing parse in a new edition. You can't remove it in existing editions, and this is by design.
(Alternatively and probably more commonly, the parser may use a prioritized choice, i.e. trying to parse type-id first then initializer-clause if it fails. In this case parsing type-id may have to signal that some constituent tokens are of a new edition and backtracking might be needed. The proposal author would want to experiment with different choices of constituent tokens to ease the implementation. But again, parser should have already implemented backtracking so this should be not a huge cost.)
[1] https://timsong-cpp.github.io/cppwp/n4868/stmt.ambig (this is not exactly about the most vexing parse but both cases are using the same wording)