diff --git a/lix/libutil/types.hh b/lix/libutil/types.hh index 89d277448..dcc3705cc 100644 --- a/lix/libutil/types.hh +++ b/lix/libutil/types.hh @@ -158,4 +158,20 @@ constexpr auto enumerate(T && iterable) template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; +/** + * marker type for things that should never be called from async code. + * add a defaulted argument of this type to a method or constructor to + * have our linter check that marked code is never called by accident. + */ +struct NeverAsync {}; + +/** + * Escape hatch to allow calling NeverAsync-marked code from functions + * that aren't themselves NeverAsync. this should only be used when no + * typelevel proof can be given for a call that's dynamically known to + * not block. using this is still forbidden in promises since blocking + * an executor, even on something known to complete, impedes progress. + */ +constexpr inline NeverAsync always_progresses; + } diff --git a/subprojects/lix-clang-tidy/LixClangTidyChecks.cc b/subprojects/lix-clang-tidy/LixClangTidyChecks.cc index bfb95fe96..4bcd5bc89 100644 --- a/subprojects/lix-clang-tidy/LixClangTidyChecks.cc +++ b/subprojects/lix-clang-tidy/LixClangTidyChecks.cc @@ -2,6 +2,7 @@ #include #include "HasPrefixSuffix.hh" #include "CharPtrCast.hh" +#include "NeverAsync.hh" namespace nix::clang_tidy { using namespace clang; @@ -12,6 +13,7 @@ class NixClangTidyChecks : public ClangTidyModule { void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override { CheckFactories.registerCheck("lix-hasprefixsuffix"); CheckFactories.registerCheck("lix-charptrcast"); + CheckFactories.registerCheck("lix-never-async"); } }; diff --git a/subprojects/lix-clang-tidy/NeverAsync.cc b/subprojects/lix-clang-tidy/NeverAsync.cc new file mode 100644 index 000000000..c28523320 --- /dev/null +++ b/subprojects/lix-clang-tidy/NeverAsync.cc @@ -0,0 +1,112 @@ +#include "NeverAsync.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nix::clang_tidy { +using namespace clang::ast_matchers; +using namespace clang; +using namespace std::literals; + +void NeverAsync::registerMatchers(ast_matchers::MatchFinder *Finder) { + auto neverAsyncT = cxxRecordDecl(hasName("nix::NeverAsync")); + auto aioRootT = cxxRecordDecl(hasName("nix::AsyncIoRoot")); + // e.g.: auto foo = someNeverAsyncValue; + // ^^^^^^^^^^^^^^^^^^^ + // e.g. someFunctionWithNeverAsyncDefaultParam() + auto neverAsync = cxxConstructExpr( + allOf(hasDeclaration(cxxConstructorDecl(isCopyConstructor())), + hasType(neverAsyncT))); + + // Any class from which you can directly get an AsyncIoRoot (since it is illegal to use one of those from async code). + // 1. class Foo1 : nix::NeverAsync { ... }; + // 2. class Foo2 { nix::AsyncIoRoot &aio; }; + // 3. class Foo3 { nix::AsyncIoRoot &aio(); }; + // 4. class Bar : Foo3 {}; + // 2. 3. and 4. exist particularly to match any of the CLI commands in lix/nix/ + auto neverAsyncClass = anyOf( + isDerivedFrom(neverAsyncT), has(fieldDecl(hasType(references(aioRootT)))), + hasMethod(returns(references(aioRootT))), + hasAnyBase( + hasType(cxxRecordDecl(hasMethod(returns(references(aioRootT))))))); + // Explicitly marked never callable from async + // e.g. void foo(nix::NeverAsync marker = {}) + auto fnMarkedNeverAsync = hasAnyParameter(hasType(neverAsyncT)); + // Functions matching the following: + // 1. void foo(nix::AioRoot &) + // 2. void foo(nix::NeverAsync) + // 3. void foo(nix::NeverAsync &) + auto fnIsNeverAsync = + anyOf(fnMarkedNeverAsync, hasAnyParameter(hasType(references(aioRootT))), + hasAnyParameter( + anyOf(hasType(cxxRecordDecl(neverAsyncClass)), + hasType(references(cxxRecordDecl(neverAsyncClass)))))); + + // Call expression that is allowed to block indefinitely (e.g. by calling lockFile or similar). + // 1. A call expr to a function like: `void foo(nix::NeverAsync marker = {})` + // 2. void foo(nix::NeverAsync marker = {}) { ... } + // call inside this context ^^^ + // 3. class Foo : nix::NeverAsync { void foo() { ... } }; + // call inside this context ^^^ + auto stmtAllowedToBlockIndefinitely = anyOf( + hasAnyArgument(neverAsync), forCallable(functionDecl(fnIsNeverAsync)), + forCallable(cxxMethodDecl(ofClass(neverAsyncClass)))); + + // e.g. any function like `kj::Promise foo()` + auto fnIsAsync = + returns(hasDeclaration(cxxRecordDecl(hasName("kj::Promise")))); + + Finder->addMatcher( + traverse(clang::TK_AsIs, + // foo() where foo() has a NeverAsync parameter + // except if it is inside a function marked as allowed to block indefinitely + invocation(hasDeclaration(functionDecl(fnMarkedNeverAsync)), + forCallable(functionDecl().bind("fn")), + unless(stmtAllowedToBlockIndefinitely)) + .bind("call")), + this); + + Finder->addMatcher( + traverse(clang::TK_AsIs, + // foo() where foo() has a nix::NeverAsync parameter inside a coroutine + invocation(hasDeclaration(functionDecl(fnMarkedNeverAsync)), + forCallable(functionDecl(fnIsAsync).bind("fn"))) + .bind("invalid-call")), + this); + + Finder->addMatcher(traverse(clang::TK_AsIs, + cxxMethodDecl(ofClass(neverAsyncClass), fnIsAsync) + .bind("bad-method")), + this); +} + +void NeverAsync::check(const ast_matchers::MatchFinder::MatchResult &Result) { + if (const auto *call = Result.Nodes.getNodeAs("call")) { + const auto *fn = Result.Nodes.getNodeAs("fn"); + assert(fn); + diag(call->getExprLoc(), + "Call to never-async function without either: the calling function having a nix::NeverAsync parameter itself (recommended) or using the nix::always_progresses escape hatch\nSee the definition of nix::NeverAsync in lix/libutil/types.h for details"); + } else if (const auto *call = Result.Nodes.getNodeAs("invalid-call")) { + const auto *fn = Result.Nodes.getNodeAs("fn"); + assert(fn); + diag(call->getExprLoc(), "Calling never-async functions inside promises is forbidden. See the definition of nix::NeverAsync in lix/libutil/types.h for details"); + } else if (const auto *fn = + Result.Nodes.getNodeAs("bad-method")) { + diag(fn->getLocation(), "Defining coroutines inside never-async classes is forbidden."); + } else { + llvm_unreachable("bad match"); + } +} +} // namespace nix::clang_tidy diff --git a/subprojects/lix-clang-tidy/NeverAsync.hh b/subprojects/lix-clang-tidy/NeverAsync.hh new file mode 100644 index 000000000..08eee68bc --- /dev/null +++ b/subprojects/lix-clang-tidy/NeverAsync.hh @@ -0,0 +1,21 @@ +#pragma once +///@file + +#include +#include +#include + +namespace nix::clang_tidy { + +using namespace clang; +using namespace clang::tidy; +using namespace llvm; + +class NeverAsync : public ClangTidyCheck { +public: + NeverAsync(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; +}; +}; // namespace nix::clang_tidy diff --git a/subprojects/lix-clang-tidy/meson.build b/subprojects/lix-clang-tidy/meson.build index 1e412e311..7b5948369 100644 --- a/subprojects/lix-clang-tidy/meson.build +++ b/subprojects/lix-clang-tidy/meson.build @@ -8,6 +8,7 @@ sources = files( 'CharPtrCast.cc', 'HasPrefixSuffix.cc', 'LixClangTidyChecks.cc', + 'NeverAsync.cc', ) lix_clang_tidy = shared_module('lix-clang-tidy', sources,