This is an archive of the discontinued LLVM Phabricator instance.

[Clang][ModuleMap] Add conditional parsing via requires block declaration
Needs RevisionPublic

Authored by Bigcheese on Jan 26 2022, 8:32 PM.

Details

Summary

Add a new form of requires called a requires block declaration.

A requires block declaration allows conditional inclusion of other
declarations within a module map.

The syntax is the same as a requires-declaration, except that it is
followed by a block. If the feature-list isn’t satisfied, then the
contents of the block is ignored and skipped over. If the feature-list
is satisfied, then the contents of the requires block are parsed as if
the requires block was not present.

This differs from a requires-declaration in that it is not an error to
import a module from within an unsatisfied requires-block-declaration
as long as another declaration of the module exists.

Diff Detail

Event Timeline

Bigcheese requested review of this revision.Jan 26 2022, 8:32 PM
Bigcheese created this revision.
Herald added a project: Restricted Project. · View Herald TranscriptJan 26 2022, 8:32 PM
Herald added a subscriber: cfe-commits. · View Herald Transcript
iana added a subscriber: iana.Jan 27 2022, 3:16 PM
iana added inline comments.
clang/docs/Modules.rst
651

Is there any kind of else syntax here? Or do you just use !whatever for the else? Is something like this valid?

requires cplusplus11 {
  ...
}
requires cplusplus && !cplusplus11 {
  ...
}

Or would you nest the requires? If you wanted OR, would you just have to duplicate the requires block body?

Bigcheese added inline comments.Jan 27 2022, 3:24 PM
clang/docs/Modules.rst
651

There's currently no else syntax, although I don't think it would be hard to add. Nesting is allowed. The syntax in your example would be:

requires cplusplus11 {
  ...
}
requires cplusplus, !cplusplus11 {
  ...
}

OR currently requires duplication. The idea is that the feature list expressions just aren't that complex in practice.

iana added inline comments.Jan 27 2022, 3:27 PM
clang/docs/Modules.rst
651

Seems like you don't strictly need else then, probably fine without it as long as there are enough examples in the documentation?

Haven't really checked the code, at the moment thinking about various failure modes.

Cases that aren't tested but I suspect are valid ones:

  • empty block, i.e., requires cplusplus {}
  • nested blocks.

Is it possible to reference external module map from requires block? I mean that in one context the module has some extra requirements but in a different context doesn't have them.

It would be nice to have some mechanism to notify developers that includes are still performed regardless of requires. For example, in A.h we have

#include <A_cpp_feature.h>

and in module map

module A {
  header "A.h"
  requires cplusplus {
    header "A_cpp_feature.h"
  }
  export *
}

It doesn't mean the header A_cpp_feature.h is used only with cplusplus feature which is not obvious and with big headers can be hard to notice. But I feel like this request goes beyond the scope of your change, so not insisting on it.

Also it can be nice to diagnose strange conditions like requires cplusplus, !cplusplus but that's orthogonal to this feature.

clang/include/clang/Basic/DiagnosticLexKinds.td
722

s/rquires/requires/

Would it be useful to put requires into some quotation marks to show it's not a part of the sentence but used verbatim?

Is it possible to reference external module map from requires block? I mean that in one context the module has some extra requirements but in a different context doesn't have them.

Can you provide an example where this would cause issues?

It would be nice to have some mechanism to notify developers that includes are still performed regardless of requires.

This would be nice, but is rather complicated to do given how the module map parser works right now. We would need to actually record what gets skipped by a requires block without including it in the Module. With the current design that's not easy to do.

clang/include/clang/Basic/DiagnosticLexKinds.td
722

Possibly, but I don't think we do that anywhere else. '' is always used to refer to user identifiers, "" is only used when referring to headers or strings, and I don't see any usage of ``. I have added a note so it shows up now as:

requires-block-errors.m.tmp/missing-opening-brace/module.modulemap:3:1: error: expected '{' to start requires block
module Pony {
^
requires-block-errors.m.tmp/missing-opening-brace/module.modulemap:2:1: note: for requires block declared here
requires !cplusplus
^

Which makes it clear.

Bigcheese updated this revision to Diff 405133.Feb 1 2022, 5:25 PM

Add testing of empty blocks and nested blocks and make the missing { error clearer.

Drive-by comment on the docs; otherwise this sounds awesome; as long as else is easy to add later this SGTM (I'll let others do the code review).

(Although, if else {} and else requires {} would be easy to add now/soon, I feel like it'd be worth it. Modelling an else-requires chain by hand would be quite error-prone, and it might be annoying to stage the adoption separately...)

clang/docs/Modules.rst
473

Should this be *requires-block-declaration*? I'm not seeing the definition of *requires-block* anywhere.

iana added inline comments.Feb 1 2022, 5:34 PM
clang/include/clang/Basic/Module.h
249

Is static correct? (Sorry I know very little about C++)

dexonsmith added inline comments.Feb 1 2022, 5:41 PM
clang/include/clang/Basic/DiagnosticLexKinds.td
722

The updated text looks better; maybe good enough; but I wonder if it'd be more clear to diagnose as a requires-declaration at global scope. E.g., something like:

error: invalid requires declaration outside of a module
note: did you mean to add a '{' to open a block?

(maybe my wording isn't great but I hope it indicates the direction I'm suggesting)

(I don't feel strongly either way)

Drive-by comment on the docs; otherwise this sounds awesome; as long as else is easy to add later this SGTM (I'll let others do the code review).
(Although, if else {} and else requires {} would be easy to add now/soon, I feel like it'd be worth it. Modelling an else-requires chain by hand would be quite error-prone, and it might be annoying to stage the adoption separately...)

I don't really expect users to actually need that, but adding it is pretty trivial.

clang/docs/Modules.rst
473

Yes.

clang/include/clang/Basic/DiagnosticLexKinds.td
722

That does seem better, although I'm not sure how difficult it is to emit a fixit from the modulemap parser, but I can take a look.

clang/include/clang/Basic/Module.h
249

Yeah, you don't need an instance of a Module to query this.

Bigcheese updated this revision to Diff 405482.Feb 2 2022, 4:02 PM
  • Fixed documentation typo.
  • Made missing '{' diagnostic more clear.
  • Use ModuleMapParser::skipUntil.

Emitting an actual fixup is kind of difficult from ModuleMapParser because the way it handles tokens makes it hard to get the source location of the end of the line containing the requires feature list.

In my testing I was able to find a case where we go around requires like

// module.modulemap
module Main {
  header "main.h"
  export *

  extern module A "extra.modulemap"
  requires nonsense {
    extern module B "extra.modulemap"
  }
}
// extra.modulemap
module Main.A {}
module Main.B {}

In this case we can do @import Main.A and @import Main.B despite unsatisfied requires in the module.modulemap. I'm not sure it is a bug but I'm not really in a position to predict the expectations of other developers, especially those not deeply familiar with module maps.

Other than this example, I haven't found any other strange requires/extern interactions. For example, attributes like [extern_c] are communicated through module/sub-module relationship and not through extern parsing location, so block requires does not affect the attributes (as far as I can tell).

clang/lib/Lex/ModuleMap.cpp
2315–2320

You can try using all_of for this computation. But I don't know without trying if it would improve readability, to be honest. If it looks clunky, I'm perfectly happy to keep the "for" loop.

3059

/*TopLevel=*/ would help understand the meaning of true.

Also I'm not sure if parseRequiresDecl parameter should have default value as we can update all the call places. But I don't have a strong opinion about that and leave the decision to you.

clang/test/Modules/requires-block.m
8

After reading the code it seems more like testing skipUntil, so I'm not sure it is a useful test. I was thinking about testing the closing brace as a token, i.e.,

requires checkClosingBraceNotAsSeparateToken {
  //}
}

If you think it doesn't add extra value, no need to add it.

bruno requested changes to this revision.Mar 3 2022, 4:04 PM

It would be nice to have some mechanism to notify developers that includes are still performed regardless of requires

I'd like to see a module remark for whenever a match fails -Rmodule-requires-fail or something like that. I've been bitten before by a regular submodule requires not triggering, just to find out some specific subfeature was missing in the compiler invocation - it can take a while to spot that. Doesn't need to be a blocker for getting this in though.

clang/docs/Modules.rst
657

Nice doc and examples!

clang/include/clang/Basic/DiagnosticLexKinds.td
723

I'm not really too worried about a fixit here, but it would be nice if the "did you mean..." part came out of a note instead.

clang/lib/Lex/ModuleMap.cpp
2355

Too many curly braces?

This revision now requires changes to proceed.Mar 3 2022, 4:04 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 3 2022, 4:04 PM
Bigcheese added inline comments.Mar 8 2022, 5:13 PM
clang/lib/Lex/ModuleMap.cpp
2355

This is correct. It closes the block on line 2353.