diff --git a/.clang-format b/.clang-format index 42912fae..0a5bfb88 100644 --- a/.clang-format +++ b/.clang-format @@ -2,19 +2,29 @@ Language: Cpp ColumnLimit: 110 IndentPPDirectives: BeforeHash -AlwaysBreakTemplateDeclarations : true -PackConstructorInitializers : CurrentLine +AlwaysBreakTemplateDeclarations: Yes +BreakAfterAttributes: Always +PackConstructorInitializers: CurrentLine AccessModifierOffset: -1 -IndentCaseLabels : true +IndentCaseLabels: true AllowShortLambdasOnASingleLine: Empty RequiresExpressionIndentation: OuterScope -BinPackArguments : false -BinPackParameters : false -LambdaBodyIndentation : Signature -PenaltyReturnTypeOnItsOwnLine : 1 +BinPackArguments: false +LambdaBodyIndentation: Signature +PenaltyReturnTypeOnItsOwnLine: 1 + +Macros: + - LF_TRY=if + - LF_CATCH_ALL=else + - LF_HOF(x)={x;} + - LF_HOF(x,y)={x,y;} + - LF_HOF(x,y,z)={x,y,z;} + - LF_HOF(x,y,z,w)={x,y,z,w;} + - LF_HOF(a,b,c,d,e)={a;} + - LF_HOF(a,b,c,d,e,f)={a,b,c,d,e,f;} SpaceBeforeParens: Custom SpaceBeforeParensOptions: - AfterRequiresInClause: true - AfterRequiresInExpression : true + AfterRequiresInClause: true + AfterRequiresInExpression: true ... diff --git a/.clang-tidy b/.clang-tidy index 5d813fd5..d0568116 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,148 +10,150 @@ Checks: "*,\ -llvm-header-guard,\ -llvm-include-order,\ -llvmlibc-*,\ - -modernize-use-nodiscard,\ + -readability-identifier-length,\ -misc-non-private-member-variables-in-classes" -WarningsAsErrors: '' +WarningsAsErrors: "" CheckOptions: - key: readability-function-cognitive-complexity.IgnoreMacros - value: 'true' - - key: 'bugprone-argument-comment.StrictMode' - value: 'true' -# Prefer using enum classes with 2 values for parameters instead of bools - - key: 'bugprone-argument-comment.CommentBoolLiterals' - value: 'true' - - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts' - value: 'true' - - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression' - value: 'true' - - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison' - value: 'true' - - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn' - value: 'true' - - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment' - value: 'true' - - key: 'readability-uniqueptr-delete-release.PreferResetCall' - value: 'true' - - key: 'cppcoreguidelines-init-variables.MathHeader' - value: '' - - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode' - value: 'true' - - key: 'readability-else-after-return.WarnOnUnfixable' - value: 'true' - - key: 'readability-else-after-return.WarnOnConditionVariables' - value: 'true' - - key: 'readability-inconsistent-declaration-parameter-name.Strict' - value: 'true' - - key: 'readability-qualified-auto.AddConstToQualified' - value: 'true' - - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration' - value: 'true' -# These seem to be the most common identifier styles - - key: 'readability-identifier-naming.AbstractClassCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ClassCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ClassConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ClassMemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ClassMethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstantMemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstantParameterCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstantPointerParameterCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstexprFunctionCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstexprMethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ConstexprVariableCase' - value: 'lower_case' - - key: 'readability-identifier-naming.EnumCase' - value: 'lower_case' - - key: 'readability-identifier-naming.EnumConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.FunctionCase' - value: 'lower_case' - - key: 'readability-identifier-naming.GlobalConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.GlobalConstantPointerCase' - value: 'lower_case' - - key: 'readability-identifier-naming.GlobalFunctionCase' - value: 'lower_case' - - key: 'readability-identifier-naming.GlobalPointerCase' - value: 'lower_case' - - key: 'readability-identifier-naming.GlobalVariableCase' - value: 'lower_case' - - key: 'readability-identifier-naming.InlineNamespaceCase' - value: 'lower_case' - - key: 'readability-identifier-naming.LocalConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.LocalConstantPointerCase' - value: 'lower_case' - - key: 'readability-identifier-naming.LocalPointerCase' - value: 'lower_case' - - key: 'readability-identifier-naming.LocalVariableCase' - value: 'lower_case' - - key: 'readability-identifier-naming.MacroDefinitionCase' - value: 'UPPER_CASE' - - key: 'readability-identifier-naming.MemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.MethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.NamespaceCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ParameterCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ParameterPackCase' - value: 'lower_case' - - key: 'readability-identifier-naming.PointerParameterCase' - value: 'lower_case' - - key: 'readability-identifier-naming.PrivateMemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.PrivateMemberPrefix' - value: 'm_' - - key: 'readability-identifier-naming.PrivateMethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ProtectedMemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ProtectedMemberPrefix' - value: 'm_' - - key: 'readability-identifier-naming.ProtectedMethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.PublicMemberCase' - value: 'lower_case' - - key: 'readability-identifier-naming.PublicMethodCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ScopedEnumConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.StaticConstantCase' - value: 'lower_case' - - key: 'readability-identifier-naming.StaticVariableCase' - value: 'lower_case' - - key: 'readability-identifier-naming.StructCase' - value: 'lower_case' - - key: 'readability-identifier-naming.TemplateParameterCase' - value: 'CamelCase' - - key: 'readability-identifier-naming.TemplateTemplateParameterCase' - value: 'CamelCase' - - key: 'readability-identifier-naming.TypeAliasCase' - value: 'lower_case' - - key: 'readability-identifier-naming.TypedefCase' - value: 'lower_case' - - key: 'readability-identifier-naming.TypeTemplateParameterCase' - value: 'CamelCase' - - key: 'readability-identifier-naming.UnionCase' - value: 'lower_case' - - key: 'readability-identifier-naming.ValueTemplateParameterCase' - value: 'CamelCase' - - key: 'readability-identifier-naming.VariableCase' - value: 'lower_case' - - key: 'readability-identifier-naming.VirtualMethodCase' - value: 'lower_case' + value: "true" + - key: "cppcoreguidelines-avoid-do-while.IgnoreMacros" + value: "true" + - key: "bugprone-argument-comment.StrictMode" + value: "true" + # Prefer using enum classes with 2 values for parameters instead of bools + - key: "bugprone-argument-comment.CommentBoolLiterals" + value: "true" + - key: "bugprone-misplaced-widening-cast.CheckImplicitCasts" + value: "true" + - key: "bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression" + value: "true" + - key: "bugprone-suspicious-string-compare.WarnOnLogicalNotComparison" + value: "true" + - key: "readability-simplify-boolean-expr.ChainedConditionalReturn" + value: "true" + - key: "readability-simplify-boolean-expr.ChainedConditionalAssignment" + value: "true" + - key: "readability-uniqueptr-delete-release.PreferResetCall" + value: "true" + - key: "cppcoreguidelines-init-variables.MathHeader" + value: "" + - key: "cppcoreguidelines-narrowing-conversions.PedanticMode" + value: "true" + - key: "readability-else-after-return.WarnOnUnfixable" + value: "true" + - key: "readability-else-after-return.WarnOnConditionVariables" + value: "true" + - key: "readability-inconsistent-declaration-parameter-name.Strict" + value: "true" + - key: "readability-qualified-auto.AddConstToQualified" + value: "true" + - key: "readability-redundant-access-specifiers.CheckFirstDeclaration" + value: "true" + # These seem to be the most common identifier styles + - key: "readability-identifier-naming.AbstractClassCase" + value: "lower_case" + - key: "readability-identifier-naming.ClassCase" + value: "lower_case" + - key: "readability-identifier-naming.ClassConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.ClassMemberCase" + value: "lower_case" + - key: "readability-identifier-naming.ClassMethodCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstantMemberCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstantParameterCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstantPointerParameterCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstexprFunctionCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstexprMethodCase" + value: "lower_case" + - key: "readability-identifier-naming.ConstexprVariableCase" + value: "lower_case" + - key: "readability-identifier-naming.EnumCase" + value: "lower_case" + - key: "readability-identifier-naming.EnumConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.FunctionCase" + value: "lower_case" + - key: "readability-identifier-naming.GlobalConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.GlobalConstantPointerCase" + value: "lower_case" + - key: "readability-identifier-naming.GlobalFunctionCase" + value: "lower_case" + - key: "readability-identifier-naming.GlobalPointerCase" + value: "lower_case" + - key: "readability-identifier-naming.GlobalVariableCase" + value: "lower_case" + - key: "readability-identifier-naming.InlineNamespaceCase" + value: "lower_case" + - key: "readability-identifier-naming.LocalConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.LocalConstantPointerCase" + value: "lower_case" + - key: "readability-identifier-naming.LocalPointerCase" + value: "lower_case" + - key: "readability-identifier-naming.LocalVariableCase" + value: "lower_case" + - key: "readability-identifier-naming.MacroDefinitionCase" + value: "UPPER_CASE" + - key: "readability-identifier-naming.MemberCase" + value: "lower_case" + - key: "readability-identifier-naming.MethodCase" + value: "lower_case" + - key: "readability-identifier-naming.NamespaceCase" + value: "lower_case" + - key: "readability-identifier-naming.ParameterCase" + value: "lower_case" + - key: "readability-identifier-naming.ParameterPackCase" + value: "lower_case" + - key: "readability-identifier-naming.PointerParameterCase" + value: "lower_case" + - key: "readability-identifier-naming.PrivateMemberCase" + value: "lower_case" + - key: "readability-identifier-naming.PrivateMemberPrefix" + value: "m_" + - key: "readability-identifier-naming.PrivateMethodCase" + value: "lower_case" + - key: "readability-identifier-naming.ProtectedMemberCase" + value: "lower_case" + - key: "readability-identifier-naming.ProtectedMemberPrefix" + value: "m_" + - key: "readability-identifier-naming.ProtectedMethodCase" + value: "lower_case" + - key: "readability-identifier-naming.PublicMemberCase" + value: "lower_case" + - key: "readability-identifier-naming.PublicMethodCase" + value: "lower_case" + - key: "readability-identifier-naming.ScopedEnumConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.StaticConstantCase" + value: "lower_case" + - key: "readability-identifier-naming.StaticVariableCase" + value: "lower_case" + - key: "readability-identifier-naming.StructCase" + value: "lower_case" + - key: "readability-identifier-naming.TemplateParameterCase" + value: "CamelCase" + - key: "readability-identifier-naming.TemplateTemplateParameterCase" + value: "CamelCase" + - key: "readability-identifier-naming.TypeAliasCase" + value: "lower_case" + - key: "readability-identifier-naming.TypedefCase" + value: "lower_case" + - key: "readability-identifier-naming.TypeTemplateParameterCase" + value: "CamelCase" + - key: "readability-identifier-naming.UnionCase" + value: "lower_case" + - key: "readability-identifier-naming.ValueTemplateParameterCase" + value: "CamelCase" + - key: "readability-identifier-naming.VariableCase" + value: "lower_case" + - key: "readability-identifier-naming.VirtualMethodCase" + value: "lower_case" ... diff --git a/.clangd b/.clangd index ef86cb6b..fd3d2a8f 100644 --- a/.clangd +++ b/.clangd @@ -1,2 +1,2 @@ CompileFlags: - CompilationDatabase: build/dev \ No newline at end of file + CompilationDatabase: build/dev diff --git a/.codespellrc b/.codespellrc index c3920f35..c6cf5dfe 100644 --- a/.codespellrc +++ b/.codespellrc @@ -3,5 +3,5 @@ builtin = clear,rare,en-GB_to_en-US,names,informal,code check-filenames = check-hidden = ignore-words-list = deque,warmup,stdio,copyable,combinate -skip = */.git,*/build,*/prefix,*/vcpkg,*/_build,*/bench +skip = */.git,*/build,*/.legacy quiet-level = 2 diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000..b8dce87f --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,8 @@ +{ + "context": { + "fileName": "AGENTS.md" + }, + "ui": { + "hideBanner": true + } +} \ No newline at end of file diff --git a/.github/workflows/linear.yml b/.github/workflows/linear.yml new file mode 100644 index 00000000..108e2a4d --- /dev/null +++ b/.github/workflows/linear.yml @@ -0,0 +1,34 @@ +name: Linear History + +on: + pull_request: + branches: ["modules"] + workflow_dispatch: + +jobs: + check-linear-history: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + + - name: Check for merge commits + run: | + BASE_REF=${{ github.base_ref || 'modules' }} + echo "Comparing against base: $BASE_REF" + git fetch origin $BASE_REF:$BASE_REF + MERGE_COMMITS=$(git rev-list --merges $BASE_REF..HEAD) + if [ -n "$MERGE_COMMITS" ]; then + echo "Error: Merge commits detected. libfork requires a linear history." + echo "Please rebase your branch onto $BASE_REF to remove merge commits." + echo "" + echo "Merge commits found:" + git log --merges --oneline $BASE_REF..HEAD + exit 1 + else + echo "No merge commits detected. Linear history check passed." + fi + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..4f9e9a90 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: ["modules"] + pull_request: + branches: ["modules"] + workflow_dispatch: + +jobs: + lint: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Install Dependencies + run: brew install clang-format codespell + + - name: Run codespell + run: codespell + + - name: Run clang-format + run: | + find src include test benchmark/src -name "*.cpp" -o -name "*.hpp" -o -name "*.cxx" | xargs clang-format --dry-run --Werror diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 00000000..149bbd6b --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,34 @@ +name: Linux + +on: + push: + branches: ["modules"] + pull_request: + branches: ["modules"] + workflow_dispatch: + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + preset: [ci-hardened, ci-release, ci-no-except-rtti] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Install Dependencies + run: brew install cmake ninja gcc binutils catch2 google-benchmark + + - name: Configure + run: cmake --preset ${{ matrix.preset }} -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-brew-toolchain.cmake + + - name: Build + run: cmake --build --preset ${{ matrix.preset }} + + - name: Test + run: ctest --preset ${{ matrix.preset }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 00000000..5893b55f --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,34 @@ +name: MacOS + +on: + push: + branches: ["modules"] + pull_request: + branches: ["modules"] + workflow_dispatch: + +jobs: + build-and-test: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + preset: [ci-hardened, ci-release, ci-no-except-rtti, ci-sanitize] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Homebrew + uses: Homebrew/actions/setup-homebrew@master + + - name: Install Dependencies + run: brew install cmake ninja llvm catch2 google-benchmark + + - name: Configure + run: cmake --preset ${{ matrix.preset }} -DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake + + - name: Build + run: cmake --build --preset ${{ matrix.preset }} + + - name: Test + run: ctest --preset ${{ matrix.preset }} diff --git a/.gitignore b/.gitignore index 47a6b5d9..41a66b83 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,3 @@ output.png **/.DS_Store compile_commands.json -CMakeLists.txt.user -CMakeUserPresets.json diff --git a/.legacy/include/libfork/core/macro.hpp b/.legacy/include/libfork/core/macro.hpp index 0944c164..e42fcfc1 100644 --- a/.legacy/include/libfork/core/macro.hpp +++ b/.legacy/include/libfork/core/macro.hpp @@ -61,17 +61,6 @@ #define LF_STATIC_CONST const #endif -// clang-format off - -/** - * @brief Use like `BOOST_HOF_RETURNS` to define a function/lambda with all the noexcept/requires/decltype specifiers. - * - * This macro is not truly variadic but the ``...`` allows commas in the macro argument. - */ -#define LF_HOF_RETURNS(...) noexcept(noexcept(__VA_ARGS__)) -> decltype(__VA_ARGS__) requires requires { __VA_ARGS__; } { return __VA_ARGS__;} - -// clang-format on - /** * @brief __[public]__ Detects if the compiler has exceptions enabled. * @@ -192,28 +181,6 @@ using std::unreachable; #define LF_ASSERT(expr) LF_ASSUME(expr) #endif -/** - * @brief Macro to prevent a function to be inlined. - */ -#if !defined(LF_NOINLINE) - #if defined(_MSC_VER) && !defined(__clang__) - #define LF_NOINLINE __declspec(noinline) - #elif defined(__GNUC__) && __GNUC__ > 3 - // Clang also defines __GNUC__ (as 4) - #if defined(__CUDACC__) - // nvcc doesn't always parse __noinline__, see: https://svn.boost.org/trac/boost/ticket/9392 - #define LF_NOINLINE __attribute__((noinline)) - #elif defined(__HIP__) - // See https://github.com/boostorg/config/issues/392 - #define LF_NOINLINE __attribute__((noinline)) - #else - #define LF_NOINLINE __attribute__((__noinline__)) - #endif - #else - #define LF_NOINLINE - #endif -#endif - /** * @brief Force no-inline for clang, works-around https://github.com/llvm/llvm-project/issues/63022. * @@ -229,28 +196,6 @@ using std::unreachable; #define LF_CLANG_TLS_NOINLINE #endif -/** - * @brief Macro to use next to 'inline' to force a function to be inlined. - * - * \rst - * - * .. note:: - * - * This does not imply the c++'s `inline` keyword which also has an effect on linkage. - * - * \endrst - */ -#if !defined(LF_FORCEINLINE) - #if defined(_MSC_VER) && !defined(__clang__) - #define LF_FORCEINLINE __forceinline - #elif defined(__GNUC__) && __GNUC__ > 3 - // Clang also defines __GNUC__ (as 4) - #define LF_FORCEINLINE __attribute__((__always_inline__)) - #else - #define LF_FORCEINLINE - #endif -#endif - #if defined(__clang__) && defined(__has_attribute) /** * @brief Compiler specific attribute. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..c3ee7803 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,228 @@ +# Libfork Copilot Instructions + +## Project Overview + +**libfork** is a continuation-stealing coroutine-tasking library implementing +strict fork-join parallelism using C++20 coroutines. + +- **Type**: C++ library with module/`import std` support +- **Languages**: C++26 + +## Critical Build Requirements + +### Compiler & Module Support + +This project **requires C++23's `import std`** and **MUST** use the appropriate +toolchain file: + +- **MacOS**: Use `-DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake` +- **Linux**: Use `-DCMAKE_TOOLCHAIN_FILE=cmake/gcc-brew-toolchain.cmake` + +**Common Error**: Without the toolchain file, CMake will fail. + +**Always include the toolchain file** in configure commands. + +### Dependencies (Homebrew) + +Make sure Homebrew is installed and `brew` is in your `PATH`: + +```bash +brew --version +``` + +**Required for building/testing:** + +- `cmake` +- `ninja` +- `catch2` +- `google-benchmark` +- `clang-format` +- `codespell` + +If on MacOS, also require: + +- `llvm` + +If on Linux, also require: + +- `gcc` +- `binutils` + +Install all at once (MacOS): + +```bash +brew install cmake ninja catch2 google-benchmark clang-format codespell llvm +``` + +Install all at once (Linux): + +```bash +brew install cmake ninja catch2 google-benchmark clang-format codespell gcc binutils +``` + +## Build & Test Workflow + +### 1. Configure + +Always use presets with the toolchain file: + +```bash +cmake --preset -DCMAKE_TOOLCHAIN_FILE=cmake/.cmake +``` + +**Relevant available presets** (from `CMakePresets.json`): + +- `ci-hardened` - Debug build with warnings and hardening flags +- `ci-release` - Optimized release build + +All presets enable developer mode (`libfork_DEV_MODE=ON`) and use Ninja generator. + +You should use the `ci-hardened` preset for development/testing and +`ci-release` for benchmarking. + +### 2. Build + +```bash +cmake --build --preset +``` + +**Build warnings** (expected and safe): + +- "It is recommended to build benchmarks in Release mode" - only relevant for `ci-hardened` +- CMake experimental `import std;` warning - expected for C++23's `import std` + +### 3. Test + +```bash +ctest --preset +``` + +All tests should pass. If tests fail, check that: + +- Configuration used the correct toolchain file +- Build completed without errors +- Any changes you have made are correct + +## Linting & Validation + +The CI runs two linting tools that you should run before committing: + +### codespell (spelling) + +```bash +codespell +``` + +Config: `.codespellrc` (ignores: build/, .git/, etc.) +Should produce no output if passing. + +### clang-format (code formatting) + +```bash +find src include test benchmark/src -name "*.cpp" -o -name "*.hpp" -o -name "*.cxx" | xargs clang-format --dry-run --Werror +``` + +Config: `.clang-format` (110 column limit, specific style) +Should produce no output if passing. + +**To auto-fix formatting**: + +```bash +find src include test benchmark/src -name "*.cpp" -o -name "*.hpp" -o -name "*.cxx" | xargs clang-format -i +``` + +## Project Structure + +### Source Layout + +```sh +libfork/ +├── cmake/ # CMake utilities +├── include/libfork/**/*.hpp # Public headers +├── src/**/ # Module and source files +│ ├── *.cxx # Module files +│ └── *.cpp # Source files +├── test/src/**/ # Test suite (Catch2) +│ └── *.cpp # Test source files +├── benchmark/src/ # Benchmarking suite (google-benchmark) +│ └── libfork_benchmark/ # Merged source/header files for benchmarks +│ └── fib/ # Each benchmark in its own sub-directory +│ ├── *.hpp # Benchmark header files +│ └── *.cpp # Benchmark source files +├── .github/workflows/ # CI workflows +│ ├── linux.yml # Linux builds +│ ├── macos.yml # MacOS builds +│ ├── lint.yml # Linting +│ └── linear.yml # Enforces linear history (no merge commits) +└── CMakeLists.txt # Main build configuration +``` + +## Workflows + +### Workflow Command Pattern + +All workflows follow this pattern: + +```yaml +- Install Dependencies: brew install ... +- Configure: cmake --preset -DCMAKE_TOOLCHAIN_FILE=.cmake +- Build: cmake --build --preset +- Test: ctest --preset +``` + +## Common Development Tasks + +### Making Code Changes + +1. **Modify source files** in `src/`, `include/`, `test/`, or `benchmark/` +2. **Rebuild**: `cmake --build --preset ` +3. **Test**: `ctest --preset ` +4. **Lint**: Run codespell and clang-format checks + +#### Adding/removing files from `src/` or `include/` + +- Update the root `CMakeLists.txt` with new/removed files. + +#### Adding/removing files from `benchmark/src/` + +- Update `benchmark/CMakeLists.txt` with new/removed files. + +### Adding Tests + +Strive to add tests for new features/bug fixes. + +- Add `.cpp` files to `test/src/` +- Tests auto-discovered by CMake (GLOB_RECURSE) +- Links against `libfork::libfork` and `Catch2::Catch2WithMain` + +### Modifying Build Configuration + +**Warning**: Module-related changes are complex. Test thoroughly with clean builds. + +## Troubleshooting + +### Build Failures + +**Problem**: Configuration/Build fails after adding/removing files or modifying CMakeLists.txt +**Solution**: Try a clean build directory: + +```bash +rm -rf build/ +``` + +**Problem**: "compiler does not provide a way to discover the import graph" +**Solution**: Add `-DCMAKE_TOOLCHAIN_FILE=cmake/llvm-brew-toolchain.cmake` to configure + +**Problem**: "Could not find 'brew' executable" +**Solution**: Install Homebrew + +**Problem**: "Could not automatically find libc++.modules.json" +**Solution**: Ensure LLVM is installed via Homebrew; toolchain auto-detects the path + +### Linting Failures + +**Problem**: clang-format errors +**Solution**: Run fix command above to auto-format code + +**Problem**: codespell errors +**Solution**: Fix typos or add to ignore list in `.codespellrc` if false positive diff --git a/CMakeLists.txt b/CMakeLists.txt index d193bc87..7743ca6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,34 +5,83 @@ set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") include(cmake/read_version.cmake) -read_version(${CMAKE_CURRENT_SOURCE_DIR}/include/libfork/core/macro.hpp) +read_version(${CMAKE_CURRENT_SOURCE_DIR}/include/libfork/version.hpp) project( libfork VERSION ${version_major}.${version_minor}.${version_patch} - DESCRIPTION "A bleeding-edge, lock-free, wait-free, continuation-stealing fork-join library built on C++20's coroutines." LANGUAGES CXX ) +# ---- Project options ---- + +option(libfork_DEV_MODE "Enable developer build (tests/benchmarks/etc) for libfork" OFF) + +# ---- System dependencies ---- + +find_package(Threads REQUIRED) + +# =========================== + # Tell CMake that we explicitly want `import std`. This will initialize the # property on all targets declared after this to 1 # TODO: set property per target set(CMAKE_CXX_MODULE_STD 1) -# Make a library. -add_library(uses_std STATIC) +add_library(libfork_libfork) +add_library(libfork::libfork ALIAS libfork_libfork) + +# target_link_libraries(libfork_libfork PRIVATE Threads::Threads) -# Add sources. -target_sources(uses_std PRIVATE uses_std.cxx) +set_property(TARGET libfork_libfork PROPERTY EXPORT_NAME libfork) -# Tell CMake we're using C++23 but only C++20 is needed to consume it. -target_compile_features(uses_std INTERFACE cxx_std_23) +target_compile_features(libfork_libfork PUBLIC cxx_std_26) -# Make an executable. -add_executable(main) +# Public headers +target_sources(libfork_libfork + PUBLIC + FILE_SET HEADERS FILES + include/libfork/version.hpp + include/libfork/__impl/compiler.hpp + include/libfork/__impl/exception.hpp + include/libfork/__impl/utils.hpp + include/libfork/__impl/assume.hpp + BASE_DIRS + include +) -target_sources(main PRIVATE main.cxx) -target_link_libraries(main PRIVATE uses_std) +# Add the module files to the library, must be public because +# consumers will need bo build the BMI +target_sources(libfork_libfork + PUBLIC + FILE_SET CXX_MODULES FILES + src/core/core.cxx + src/core/promise.cxx + src/core/concepts.cxx + src/core/utility.cxx + src/core/frame.cxx + src/core/constants.cxx + src/core/tuple.cxx + src/core/ops.cxx + src/core/context.cxx + src/core/deque.cxx + PRIVATE + src/exception.cpp +) + +# ====================== + +if(libfork_DEV_MODE) + + include(CTest) # Enables the BUILD_TESTING option + + if(BUILD_TESTING) + add_subdirectory(test) + endif() + + add_subdirectory(benchmark) + +endif() # list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # @@ -43,9 +92,6 @@ target_link_libraries(main PRIVATE uses_std) # # message(STATUS "CMAKE_BUILD_TYPE is set to '${CMAKE_BUILD_TYPE}'") # -# # ---- System dependencies ---- -# -# find_package(Threads REQUIRED) # # # ------ Declare library ------ # diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..c51d90f0 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,126 @@ +{ + "version": 10, + "configurePresets": [ + { + "name": "cmake-pedantic", + "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "deprecated": true + } + }, + { + "name": "ci-base", + "inherits": "cmake-pedantic", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "libfork_DEV_MODE": "ON" + } + }, + { + "name": "ci-hardened", + "inherits": "ci-base", + "displayName": "Debug with warnings and hardening", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O2 -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast -Werror=format-security -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -fstrict-flex-arrays=3 -fstack-protector-strong -Wno-missing-braces -Wno-missing-field-initializers" + } + }, + { + "name": "ci-release", + "inherits": "ci-base", + "displayName": "Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG -flto=auto -march=native" + } + }, + { + "name": "ci-no-except-rtti", + "inherits": "ci-base", + "displayName": "Release no RTTI or exceptions", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG -flto=auto -march=native -fno-exceptions -fno-rtti" + } + }, + { + "name": "ci-sanitize", + "inherits": "ci-base", + "displayName": "Debug with sanitizers", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Sanitize", + "CMAKE_CXX_FLAGS": "-O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS" + } + } + ], + "buildPresets": [ + { + "name": "ci-hardened", + "configurePreset": "ci-hardened" + }, + { + "name": "ci-release", + "configurePreset": "ci-release" + }, + { + "name": "ci-no-except-rtti", + "configurePreset": "ci-no-except-rtti" + }, + { + "name": "ci-sanitize", + "configurePreset": "ci-sanitize" + } + ], + "testPresets": [ + { + "name": "ci-hardened", + "configurePreset": "ci-hardened", + "output": { + "outputOnFailure": true + }, + "execution": { + "stopOnFailure": true + } + }, + { + "name": "ci-release", + "configurePreset": "ci-release", + "output": { + "outputOnFailure": true + }, + "execution": { + "stopOnFailure": true + } + }, + { + "name": "ci-no-except-rtti", + "configurePreset": "ci-no-except-rtti", + "output": { + "outputOnFailure": true + }, + "execution": { + "stopOnFailure": true + } + }, + { + "name": "ci-sanitize", + "configurePreset": "ci-sanitize", + "output": { + "outputOnFailure": true + }, + "execution": { + "stopOnFailure": true + } + } + ] +} diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json new file mode 100644 index 00000000..26e65016 --- /dev/null +++ b/CMakeUserPresets.json @@ -0,0 +1,79 @@ +{ + "version": 10, + "configurePresets": [ + { + "name": "dev", + "inherits": "ci-hardened", + "displayName": "Hardened development build", + "toolchainFile": "${sourceDir}/cmake/llvm-brew-toolchain.cmake", + "cacheVariables": { + "CMAKE_COLOR_DIAGNOSTICS": "ON" + } + }, + { + "name": "bench", + "inherits": "ci-release", + "displayName": "Release build for benchmarks", + "toolchainFile": "${sourceDir}/cmake/llvm-brew-toolchain.cmake", + "cacheVariables": { + "CMAKE_COLOR_DIAGNOSTICS": "ON" + } + } + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev" + }, + { + "name": "bench", + "configurePreset": "bench" + } + ], + "testPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "output": { + "outputOnFailure": true + }, + "execution": { + "stopOnFailure": true + } + } + ], + "workflowPresets": [ + { + "name": "dev", + "displayName": "Development Debug Hardened Workflow", + "steps": [ + { + "type": "configure", + "name": "dev" + }, + { + "type": "build", + "name": "dev" + }, + { + "type": "test", + "name": "dev" + } + ] + }, + { + "name": "bench", + "displayName": "Release Build (including Benchmarks)", + "steps": [ + { + "type": "configure", + "name": "bench" + }, + { + "type": "build", + "name": "bench" + } + ] + } + ] +} diff --git a/.legacy/LICENSE.md b/LICENSE.md similarity index 100% rename from .legacy/LICENSE.md rename to LICENSE.md diff --git a/actions/setup/action.yaml b/actions/setup/action.yaml deleted file mode 100644 index cd451a14..00000000 --- a/actions/setup/action.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: 'setup' -description: 'setup vcpkg/cmake/ninja and caching' - -runs: - using: "composite" - - steps: - # Set env vars needed for vcpkg to leverage the GitHub Action cache as a storage for Binary Caching. - - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - uses: actions/checkout@v3 - with: - submodules: true - - name: "Create directory '${{ env.VCPKG_DEFAULT_BINARY_CACHE }}'" - run: mkdir -p $VCPKG_DEFAULT_BINARY_CACHE - shell: bash - - # Setup the build machine with the most recent versions of CMake and Ninja. - # Both are cached if not already: on subsequent runs both will be quickly restored from GitHub cache service. - - uses: lukka/get-cmake@latest - - # Restore vcpkg from the GitHub Action cache service. - # Note that packages are restored by vcpkg's binary caching when it is being run afterwards by CMake. - - name: Restore vcpkg - uses: actions/cache@v3 - with: - # The first path is the location of vcpkg: it contains the vcpkg executable and data files, as long as the - # built package archives (aka binary cache) which are located by VCPKG_DEFAULT_BINARY_CACHE env var. - # The other paths starting with '!' are exclusions: they contain temporary files generated - # during the build of the installed packages. - path: | - ${{ env.VCPKG_ROOT_DIR }} - !${{ env.VCPKG_ROOT_DIR }}/buildtrees - !${{ env.VCPKG_ROOT_DIR }}/packages - !${{ env.VCPKG_ROOT_DIR }}/downloads - !${{ env.VCPKG_ROOT_DIR }}/installed - # The key is composed in a way that it gets properly invalidated whenever a different version of vcpkg is being used. - key: | - ${{ hashFiles( '.git/modules/vcpkg/HEAD' )}} - - # On Windows runners, let's ensure to have the Developer Command Prompt environment setup correctly. - # As used here the Developer Command Prompt created is targeting x64 and using the default the Windows SDK. - - uses: ilammy/msvc-dev-cmd@v1 - - - name: Setup xcode - if: matrix.os == 'macos-13' - shell: bash - run: sudo xcode-select --switch /Applications/Xcode_15.0.app/Contents/Developer \ No newline at end of file diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt new file mode 100644 index 00000000..25574d70 --- /dev/null +++ b/benchmark/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 4.2.1 FATAL_ERROR) + +project(libfork_benchmark LANGUAGES CXX) + +if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") + message(WARNING "It is recommended to build benchmarks in Release mode for accurate results.") +endif() + +# ---- Benchmarks ---- + +find_package(benchmark REQUIRED) + +add_executable(libfork_benchmark) + +target_link_libraries(libfork_benchmark + PRIVATE + benchmark::benchmark_main +) + +# Headers (shared between all benchmarks) +target_sources(libfork_benchmark + PRIVATE + FILE_SET HEADERS FILES + src/libfork_benchmark/common.hpp + src/libfork_benchmark/fib/fib.hpp + BASE_DIRS + src +) + +if(BUILD_TESTING) + add_test(NAME Benchmark + COMMAND + libfork_benchmark --benchmark_dry_run --benchmark_filter=^test/ + ) +endif() + +# ---- Serial ---- + +target_sources(libfork_benchmark + PRIVATE + src/libfork_benchmark/fib/serial.cpp + src/libfork_benchmark/fib/serial_return.cpp +) + +# ---- Baremetal ---- + +target_sources(libfork_benchmark + PRIVATE + src/libfork_benchmark/fib/baremetal.cpp +) + +# ---- Libfork ---- + +target_sources(libfork_benchmark + PRIVATE + src/libfork_benchmark/fib/lf_parts.cpp +) + +target_link_libraries(libfork_benchmark + PRIVATE + libfork::libfork +) diff --git a/benchmark/src/libfork_benchmark/common.hpp b/benchmark/src/libfork_benchmark/common.hpp new file mode 100644 index 00000000..d886c22f --- /dev/null +++ b/benchmark/src/libfork_benchmark/common.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include "libfork/__impl/exception.hpp" + +struct incorrect_result : public std::runtime_error { + using std::runtime_error::runtime_error; +}; + +#define CHECK_RESULT(result, expected) \ + do { \ + auto &&lf_check_result_val = (result); \ + auto &&lf_check_expected_val = (expected); \ + if (lf_check_result_val != lf_check_expected_val) { \ + LF_THROW(incorrect_result( \ + std::format("{}={} != {}={}", #expected, lf_check_expected_val, #result, lf_check_result_val))); \ + } \ + } while (0) diff --git a/benchmark/src/libfork_benchmark/fib/baremetal.cpp b/benchmark/src/libfork_benchmark/fib/baremetal.cpp new file mode 100644 index 00000000..e02bcac9 --- /dev/null +++ b/benchmark/src/libfork_benchmark/fib/baremetal.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include + +#include + +#include "libfork_benchmark/fib/fib.hpp" + +// === Coroutine + +namespace { + +// ==== Allocators ==== // + +constinit inline thread_local std::byte *tls_bump_ptr = nullptr; + +struct task { + struct promise_type { + + static auto operator new(std::size_t sz) -> void * { + auto *prev = tls_bump_ptr; + tls_bump_ptr += fib_align_size(sz); + return prev; + } + + static auto operator delete(void *p, [[maybe_unused]] std::size_t sz) noexcept -> void { + tls_bump_ptr = std::bit_cast(p); + } + + auto get_return_object() -> task { return {std::coroutine_handle::from_promise(*this)}; } + + auto initial_suspend() -> std::suspend_always { return {}; } + + auto final_suspend() noexcept { + struct final_awaitable : std::suspend_always { + auto await_suspend(std::coroutine_handle h) noexcept -> std::coroutine_handle<> { + + std::coroutine_handle<> cont = h.promise().continuation; + + h.destroy(); + + if (cont) { + return cont; + } + + return std::noop_coroutine(); + } + }; + + return final_awaitable{}; + } + + void return_value(std::int64_t val) { *value = val; } + void unhandled_exception() { std::terminate(); } + + std::int64_t *value = nullptr; + std::coroutine_handle<> continuation = nullptr; + }; + + std::coroutine_handle coro; + + auto set(std::int64_t &out) -> task & { + coro.promise().value = &out; + return *this; + } + + auto await_ready() noexcept -> bool { return false; } + + auto await_suspend(std::coroutine_handle<> h) -> std::coroutine_handle { + coro.promise().continuation = h; + return coro; + } + + void await_resume() noexcept {} +}; + +auto fib(std::int64_t n) -> task { + if (n <= 1) { + co_return n; + } + std::int64_t a = 0; + std::int64_t b = 0; + co_await fib(n - 1).set(a); + co_await fib(n - 2).set(b); + co_return a + b; +} + +void fib_coro_no_queue(benchmark::State &state) { + + std::int64_t n = state.range(0); + std::int64_t expect = fib_ref(n); + + state.counters["n"] = static_cast(n); + + // 8MB stack + std::unique_ptr buffer = std::make_unique(1024 * 1024 * 8); + tls_bump_ptr = buffer.get(); + + for (auto _ : state) { + benchmark::DoNotOptimize(n); + std::int64_t result = 0; + fib(n).set(result).coro.resume(); + CHECK_RESULT(result, expect); + benchmark::DoNotOptimize(result); + } + + if (tls_bump_ptr != buffer.get()) { + std::terminate(); // Stack leak + } +} + +// === Recursive with Deque overhead + +constinit inline thread_local lf::deque *tls_deque = nullptr; + +LF_NO_INLINE +auto deque() -> lf::deque & { return *tls_deque; } + +auto fib_recursive_deque_impl(std::int64_t n) -> std::int64_t { + if (n <= 1) { + return n; + } + + // Emulate work item creation/scheduling overhead + deque().push(n); + std::int64_t a = fib_recursive_deque_impl(n - 1); + deque().pop(); + + std::int64_t b = fib_recursive_deque_impl(n - 2); + + return a + b; +} + +void fib_recursive_deque(benchmark::State &state) { + + std::int64_t n = state.range(0); + std::int64_t expect = fib_ref(n); + + state.counters["n"] = static_cast(n); + + lf::deque deque; + tls_deque = &deque; + + for (auto _ : state) { + benchmark::DoNotOptimize(n); + std::int64_t result = fib_recursive_deque_impl(n); + CHECK_RESULT(result, expect); + benchmark::DoNotOptimize(result); + } + + tls_deque = nullptr; +} + +} // namespace + +// Minimal coroutine, bump allocated (thread-local) stack +BENCHMARK(fib_coro_no_queue)->Name("test/baremetal/fib/coro")->Arg(fib_test); +BENCHMARK(fib_coro_no_queue)->Name("base/baremetal/fib/coro")->Arg(fib_base); + +BENCHMARK(fib_recursive_deque)->Name("test/baremetal/fib/deque")->Arg(fib_test); +BENCHMARK(fib_recursive_deque)->Name("base/baremetal/fib/deque")->Arg(fib_base); diff --git a/benchmark/src/libfork_benchmark/fib/fib.hpp b/benchmark/src/libfork_benchmark/fib/fib.hpp new file mode 100644 index 00000000..58238376 --- /dev/null +++ b/benchmark/src/libfork_benchmark/fib/fib.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include "libfork/__impl/compiler.hpp" + +#include "libfork_benchmark/common.hpp" + +import libfork.core; + +inline constexpr int fib_test = 3; +inline constexpr int fib_base = 37; + +/** + * @brief Non-recursive Fibonacci calculation + */ +constexpr auto fib_ref(std::int64_t n) -> std::int64_t { + + if (n < 2) { + return n; + } + + std::int64_t prev = 0; + std::int64_t curr = 1; + + for (std::int64_t i = 2; i <= n; ++i) { + std::int64_t next = prev + curr; + prev = curr; + curr = next; + } + + return curr; +} + +[[nodiscard]] +inline auto fib_align_size(std::size_t n) -> std::size_t { + return (n + lf::k_new_align - 1) & ~(lf::k_new_align - 1); +} diff --git a/benchmark/src/libfork_benchmark/fib/lf_parts.cpp b/benchmark/src/libfork_benchmark/fib/lf_parts.cpp new file mode 100644 index 00000000..20cded7e --- /dev/null +++ b/benchmark/src/libfork_benchmark/fib/lf_parts.cpp @@ -0,0 +1,309 @@ +#include +#include +#include + +#include + +#include "libfork/__impl/assume.hpp" + +#include "libfork_benchmark/fib/fib.hpp" + +import libfork.core; + +namespace { + +struct global_allocator { + + struct empty { + auto operator==(empty const &) const -> bool = default; + }; + + constexpr static auto push(std::size_t sz) -> void * { return ::operator new(sz); } + constexpr static auto pop(void *p, std::size_t sz) noexcept -> void { ::operator delete(p, sz); } + constexpr static auto checkpoint() noexcept -> empty { return {}; } + constexpr static auto release() noexcept -> void {} + constexpr static auto acquire(empty) noexcept -> void {} +}; + +static_assert(lf::stack_allocator); + +struct linear_allocator { + + std::unique_ptr data = std::make_unique(1024 * 1024); + std::byte *ptr = data.get(); + + constexpr auto push(std::size_t sz) -> void * { + auto *prev = ptr; + ptr += fib_align_size(sz); + return prev; + } + constexpr auto pop(void *p, std::size_t) noexcept -> void { ptr = static_cast(p); } + + constexpr auto checkpoint() noexcept -> std::byte * { return data.get(); } + constexpr auto release() noexcept -> void {} + constexpr auto acquire(std::byte *) noexcept -> void {} +}; + +static_assert(lf::stack_allocator); + +template +struct vector_ctx { + + using handle_type = lf::frame_handle; + + std::vector work; + Alloc allocator; + + vector_ctx() { work.reserve(1024); } + + auto alloc() noexcept -> Alloc & { return allocator; } + + // TODO: try LF_NO_INLINE for final allocator + LF_NO_INLINE + void push(handle_type handle) { work.push_back(handle); } + + auto pop() noexcept -> handle_type { + auto handle = work.back(); + work.pop_back(); + return handle; + } +}; + +template +struct deque_ctx { + + using handle_type = lf::frame_handle; + + lf::deque work; + Alloc allocator; + + auto alloc() noexcept -> Alloc & { return allocator; } + + // TODO: try LF_NO_INLINE for final allocator + LF_NO_INLINE + void push(handle_type handle) { work.push(handle); } + + auto pop() noexcept -> handle_type { + return work.pop([] static -> handle_type { + return {}; + }); + } +}; + +template +struct poly_vector_ctx final : lf::polymorphic_context { + + using handle_type = lf::frame_handle>; + + std::vector work; + + poly_vector_ctx() { work.reserve(1024); } + + void push(handle_type handle) override { work.push_back(handle); } + + auto pop() noexcept -> handle_type override { + auto handle = work.back(); + work.pop_back(); + return handle; + } +}; + +struct poly_deque_ctx final : lf::polymorphic_context { + + using handle_type = lf::frame_handle>; + + lf::deque work; + + void push(handle_type handle) override { work.push(handle); } + + auto pop() noexcept -> handle_type override { + return work.pop([] static -> handle_type { + return {}; + }); + } +}; + +using lf::task; + +template +constexpr auto no_await = [](this auto fib, std::int64_t *ret, std::int64_t n) -> task { + if (n < 2) { + *ret = n; + co_return; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + auto t1 = fib(&lhs, n - 1); + t1.promise->frame.kind = lf::category::root; + t1.promise->frame.stack_ckpt = lf::thread_context->alloc().checkpoint(); + t1.promise->frame.cancel = nullptr; + t1.promise->handle().resume(); + + auto t2 = fib(&rhs, n - 2); + t2.promise->frame.kind = lf::category::root; + t2.promise->frame.stack_ckpt = lf::thread_context->alloc().checkpoint(); + t2.promise->frame.cancel = nullptr; + t2.promise->handle().resume(); + + *ret = lhs + rhs; +}; + +template +constexpr auto await = [](this auto fib, std::int64_t *ret, std::int64_t n) -> lf::task { + if (n < 2) { + *ret = n; + co_return; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + co_await lf::call(fib, &lhs, n - 1); + co_await lf::call(fib, &rhs, n - 2); + + *ret = lhs + rhs; +}; + +template +constexpr auto ret = [](this auto fib, std::int64_t n) -> lf::task { + if (n < 2) { + co_return n; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + co_await lf::call(&lhs, fib, n - 1); + co_await lf::call(&rhs, fib, n - 2); + + co_return lhs + rhs; +}; + +template +constexpr auto fork_call = [](this auto fib, std::int64_t n) -> lf::task { + if (n < 2) { + co_return n; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + co_await lf::fork(&rhs, fib, n - 2); + co_await lf::call(&lhs, fib, n - 1); + + if constexpr (Join) { + co_await lf::join(); + } + + co_return lhs + rhs; +}; + +using global_alloc = vector_ctx; +using linear_alloc = vector_ctx; + +template +void fib(benchmark::State &state) { + + std::int64_t n = state.range(0); + std::int64_t expect = fib_ref(n); + + state.counters["n"] = static_cast(n); + + T context; + + lf::thread_context = static_cast(&context); + + lf::defer _ = [] static noexcept { + lf::thread_context = nullptr; + }; + + for (auto _ : state) { + benchmark::DoNotOptimize(n); + std::int64_t result = 0; + + if constexpr (requires { Fn(&result, n); }) { + auto task = Fn(&result, n); + task.promise->frame.kind = lf::category::root; + task.promise->frame.cancel = nullptr; + task.promise->frame.stack_ckpt = lf::thread_context->alloc().checkpoint(); + task.promise->handle().resume(); + } else { + auto task = Fn(n); + task.promise->frame.kind = lf::category::root; + task.promise->frame.cancel = nullptr; + task.promise->return_address = &result; + task.promise->frame.stack_ckpt = lf::thread_context->alloc().checkpoint(); + task.promise->handle().resume(); + } + + CHECK_RESULT(result, expect); + benchmark::DoNotOptimize(result); + } +} + +} // namespace + +static_assert(lf::worker_context); + +// Return by ref-arg, test direct root, no co-await, direct resumes, uses new/delete for alloc +BENCHMARK(fib, global_alloc>)->Name("test/libfork/fib/heap/no_await")->Arg(fib_test); +BENCHMARK(fib, global_alloc>)->Name("base/libfork/fib/heap/no_await")->Arg(fib_base); + +// Same as above but uses bump allocator +BENCHMARK(fib, linear_alloc>)->Name("test/libfork/fib/bump/no_await")->Arg(fib_test); +BENCHMARK(fib, linear_alloc>)->Name("base/libfork/fib/bump/no_await")->Arg(fib_base); + +// TODO: no_await with segmented stack allocator? + +// Return by ref-arg, libfork call/call with co-await, uses new/delete for alloc +BENCHMARK(fib, global_alloc>)->Name("test/libfork/fib/heap/await")->Arg(fib_test); +BENCHMARK(fib, global_alloc>)->Name("base/libfork/fib/heap/await")->Arg(fib_base); + +// // Same as above but uses bump allocator +BENCHMARK(fib, linear_alloc>)->Name("test/libfork/fib/bump/await")->Arg(fib_test); +BENCHMARK(fib, linear_alloc>)->Name("base/libfork/fib/bump/await")->Arg(fib_base); + +// Return by value +// libfork call/call with co-await +BENCHMARK(fib, linear_alloc>)->Name("test/libfork/fib/bump/return")->Arg(fib_test); +BENCHMARK(fib, linear_alloc>)->Name("base/libfork/fib/bump/return")->Arg(fib_base); + +// Return by value +// libfork call/fork (no join) +// Non-polymorphic vector-backed context +BENCHMARK(fib, linear_alloc>)->Name("test/libfork/fib/vector_ctx")->Arg(fib_test); +BENCHMARK(fib, linear_alloc>)->Name("base/libfork/fib/vector_ctx")->Arg(fib_base); + +// Same as above but with join. +BENCHMARK(fib, linear_alloc>) + ->Name("test/libfork/fib/vector_ctx/join") + ->Arg(fib_test); +BENCHMARK(fib, linear_alloc>) + ->Name("base/libfork/fib/vector_ctx/join") + ->Arg(fib_base); + +using A = poly_vector_ctx; +using B = lf::polymorphic_context; + +// Same as above but with polymorphic contexts. +BENCHMARK(fib, A, B>)->Name("test/libfork/fib/poly_vector_ctx")->Arg(fib_test); +BENCHMARK(fib, A, B>)->Name("base/libfork/fib/poly_vector_ctx")->Arg(fib_base); + +// Same as above but with join. +BENCHMARK(fib, A, B>)->Name("test/libfork/fib/poly_vector_ctx/join")->Arg(fib_test); +BENCHMARK(fib, A, B>)->Name("base/libfork/fib/poly_vector_ctx/join")->Arg(fib_base); + +using C = poly_deque_ctx; +using D = deque_ctx; + +// Return by value, +// Libfork call/join/fork with co-await, +// Deque-backed context +BENCHMARK(fib, D, D>)->Name("test/libfork/fib/deque_ctx/join")->Arg(fib_test); +BENCHMARK(fib, D, D>)->Name("base/libfork/fib/deque_ctx/join")->Arg(fib_base); + +// Same as above but polymorphic +BENCHMARK(fib, C, B>)->Name("test/libfork/fib/poly_deque_ctx/join")->Arg(fib_test); +BENCHMARK(fib, C, B>)->Name("base/libfork/fib/poly_deque_ctx/join")->Arg(fib_base); diff --git a/benchmark/src/libfork_benchmark/fib/serial.cpp b/benchmark/src/libfork_benchmark/fib/serial.cpp new file mode 100644 index 00000000..4a50f5e9 --- /dev/null +++ b/benchmark/src/libfork_benchmark/fib/serial.cpp @@ -0,0 +1,43 @@ +#include + +#include "libfork/__impl/compiler.hpp" + +#include "libfork_benchmark/fib/fib.hpp" + +namespace { + +LF_NO_INLINE auto fib(std::int64_t &ret, std::int64_t n) -> void { + if (n < 2) { + ret = n; + return; + } + + std::int64_t lhs = 0; + std::int64_t rhs = 0; + + fib(lhs, n - 1); + fib(rhs, n - 2); + + ret = lhs + rhs; +} + +void fib_serial(benchmark::State &state) { + + std::int64_t n = state.range(0); + std::int64_t expect = fib_ref(n); + + state.counters["n"] = static_cast(n); + + for (auto _ : state) { + benchmark::DoNotOptimize(n); + std::int64_t result = 0; + fib(result, n); + CHECK_RESULT(result, expect); + benchmark::DoNotOptimize(result); + } +} + +} // namespace + +BENCHMARK(fib_serial)->Name("test/serial/fib")->Arg(fib_test); +BENCHMARK(fib_serial)->Name("base/serial/fib")->Arg(fib_base); diff --git a/benchmark/src/libfork_benchmark/fib/serial_return.cpp b/benchmark/src/libfork_benchmark/fib/serial_return.cpp new file mode 100644 index 00000000..976e967d --- /dev/null +++ b/benchmark/src/libfork_benchmark/fib/serial_return.cpp @@ -0,0 +1,38 @@ +#include + +#include "libfork/__impl/compiler.hpp" + +#include "libfork_benchmark/fib/fib.hpp" + +namespace { + +LF_NO_INLINE auto fib(std::int64_t n) -> std::int64_t { + if (n < 2) { + return n; + } + + std::int64_t lhs = fib(n - 1); + std::int64_t rhs = fib(n - 2); + + return lhs + rhs; +} + +void fib_serial_return(benchmark::State &state) { + + std::int64_t n = state.range(0); + std::int64_t expect = fib_ref(n); + + state.counters["n"] = static_cast(n); + + for (auto _ : state) { + benchmark::DoNotOptimize(n); + std::int64_t result = fib(n); + CHECK_RESULT(result, expect); + benchmark::DoNotOptimize(result); + } +} + +} // namespace + +BENCHMARK(fib_serial_return)->Name("test/serial/fib/return")->Arg(fib_test); +BENCHMARK(fib_serial_return)->Name("base/serial/fib/return")->Arg(fib_base); diff --git a/cmake/gcc-brew-toolchain.cmake b/cmake/gcc-brew-toolchain.cmake new file mode 100644 index 00000000..2423087a --- /dev/null +++ b/cmake/gcc-brew-toolchain.cmake @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 4.2.1) + +# Set up Homebrew GCC@15 & Ninja toolchain for CMake + +find_program(BREW_EXE brew) + +if(NOT BREW_EXE) + message(FATAL_ERROR "Could not find 'brew' executable. Please install Homebrew.") +endif() + +# --- Ninja + +execute_process( + COMMAND ${BREW_EXE} --prefix ninja + OUTPUT_VARIABLE NINJA_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) + +find_program(CMAKE_MAKE_PROGRAM + NAMES ninja + HINTS "${NINJA_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +# --- GCC + +execute_process( + COMMAND ${BREW_EXE} --prefix gcc + OUTPUT_VARIABLE GCC_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) + +find_program(CMAKE_C_COMPILER + NAMES gcc-15 + HINTS "${GCC_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_CXX_COMPILER + NAMES g++-15 + HINTS "${GCC_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_AR + NAMES gcc-ar-15 + HINTS "${GCC_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_RANLIB + NAMES gcc-ranlib-15 + HINTS "${GCC_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_NM + NAMES gcc-nm-15 + HINTS "${GCC_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +# --- Binutils + +execute_process( + COMMAND ${BREW_EXE} --prefix binutils + OUTPUT_VARIABLE BINUTILS_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -B${BINUTILS_PREFIX}/bin/" CACHE STRING "" FORCE) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -B${BINUTILS_PREFIX}/bin/" CACHE STRING "" FORCE) + + +# Get macOS SDK path (only on macOS) +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) +endif() diff --git a/cmake/llvm-brew-toolchain.cmake b/cmake/llvm-brew-toolchain.cmake new file mode 100644 index 00000000..199bdae3 --- /dev/null +++ b/cmake/llvm-brew-toolchain.cmake @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 4.2.1) + +# Set up Homebrew LLVM & Ninja toolchain for CMake + +find_program(BREW_EXE brew) + +if(NOT BREW_EXE) + message(FATAL_ERROR "Could not find 'brew' executable. Please install Homebrew.") +endif() + +# --- Ninja + +execute_process( + COMMAND ${BREW_EXE} --prefix ninja + OUTPUT_VARIABLE NINJA_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) + +find_program(CMAKE_MAKE_PROGRAM + NAMES ninja + HINTS "${NINJA_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +# --- LLVM + +execute_process( + COMMAND ${BREW_EXE} --prefix llvm + OUTPUT_VARIABLE LLVM_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) + +find_program(CMAKE_C_COMPILER + NAMES clang + HINTS "${LLVM_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_CXX_COMPILER + NAMES clang++ + HINTS "${LLVM_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_AR + NAMES llvm-ar + HINTS "${LLVM_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_RANLIB + NAMES llvm-ranlib + HINTS "${LLVM_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +find_program(CMAKE_NM + NAMES llvm-nm + HINTS "${LLVM_PREFIX}/bin" + NO_DEFAULT_PATH + REQUIRED +) + +# Dynamically find the standard library modules JSON, brew puts it in the wrong place +file(GLOB_RECURSE LIBCXX_MODULES_JSON "${LLVM_PREFIX}/lib/**/libc++.modules.json") + +if(LIBCXX_MODULES_JSON) + set(CMAKE_CXX_STDLIB_MODULES_JSON "${LIBCXX_MODULES_JSON}") +else() + message(FATAL_ERROR "Could not automatically find libc++.modules.json in ${LLVM_PREFIX}") +endif() + +# Get macOS SDK path (only on macOS) +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) +endif() diff --git a/docs/tour.md b/docs/tour.md index 4c1800dd..45b7b1fe 100644 --- a/docs/tour.md +++ b/docs/tour.md @@ -25,7 +25,7 @@ Definitions: - __Parent:__ A task that spawns other tasks. - __Child:__ A task that is spawned by another task. -The tasking/fork-join interface is designed to mirror [Cilk](https://en.wikipedia.org/wiki/Cilk) and other fork-join frameworks. The best way to learn is by example, lets start with the canonical introduction to fork-join, the recursive Fibonacci function, in regular C++ it looks like this: +The tasking/fork-join interface is designed to mirror [Cilk](https://en.wikipedia.org/wiki/Cilk) and other fork-join frameworks. The best way to learn is by example, let's start with the canonical introduction to fork-join, the recursive Fibonacci function, in regular C++ it looks like this: ```cpp auto fib(int n) -> int { diff --git a/include/libfork/__impl/assume.hpp b/include/libfork/__impl/assume.hpp new file mode 100644 index 00000000..89370bab --- /dev/null +++ b/include/libfork/__impl/assume.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "libfork/__impl/exception.hpp" + +/** + * @file assume.hpp + * + * @brief A collection of internal macros. + * + * These macros are not safe to use unless `import std` is in scope. + */ + +/** + * @brief If expr evaluates to `false`, terminates the program with an error message. + * + * This macro is always active, regardless of optimization settings or `NDEBUG`. + */ +#define LF_ENSURE(expr) \ + do { \ + if (!(expr)) { \ + LF_TERMINATE("Assumption '" #expr "' failed!"); \ + } \ + } while (false) + +/** + * @brief Invokes undefined behavior if ``expr`` evaluates to `false`. + * + * \rst + * + * .. warning:: + * + * This has different semantics than ``[[assume(expr)]]`` as it WILL evaluate the + * expression at runtime. Hence you should conservatively only use this macro + * if ``expr`` is side-effect free and cheap to evaluate. + * + * \endrst + */ +#ifdef NDEBUG + #define LF_ASSUME(expr) \ + do { \ + if (!(expr)) { \ + ::std::unreachable(); \ + } \ + } while (false) +#else + #define LF_ASSUME(expr) LF_ENSURE(expr) +#endif diff --git a/include/libfork/__impl/compiler.hpp b/include/libfork/__impl/compiler.hpp new file mode 100644 index 00000000..8e71083f --- /dev/null +++ b/include/libfork/__impl/compiler.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "libfork/__impl/exception.hpp" + +/** + * @file compiler.hpp + * + * @brief A collection of internal macros. + * + * These macros are standalone i.e. they can be used without importing/including anything else. + */ + +// =============== Inlining/optimization =============== // + +/** + * @brief Macro to use next to 'inline' to force a function to be inlined. + * + * \rst + * + * .. note:: + * + * This does not imply the c++'s `inline` keyword which also has an effect on linkage. + * + * \endrst + */ +#if !defined(LF_FORCE_INLINE) + #if defined(_MSC_VER) && !defined(__clang__) + #define LF_FORCE_INLINE __forceinline + #elif defined(__GNUC__) && __GNUC__ > 3 + // Clang also defines __GNUC__ (as 4) + #define LF_FORCE_INLINE __attribute__((__always_inline__)) + #else + #define LF_FORCE_INLINE + #endif +#endif + +/** + * @brief Macro to prevent a function to be inlined. + */ +#if !defined(LF_NO_INLINE) + #if defined(_MSC_VER) && !defined(__clang__) + #define LF_NO_INLINE __declspec(noinline) + #elif defined(__GNUC__) && __GNUC__ > 3 + // Clang also defines __GNUC__ (as 4) + #if defined(__CUDACC__) + // nvcc doesn't always parse __noinline__, see: https://svn.boost.org/trac/boost/ticket/9392 + #define LF_NO_INLINE __attribute__((noinline)) + #elif defined(__HIP__) + // See https://github.com/boostorg/config/issues/392 + #define LF_NO_INLINE __attribute__((noinline)) + #else + #define LF_NO_INLINE __attribute__((__noinline__)) + #endif + #else + #define LF_NO_INLINE + #endif +#endif diff --git a/include/libfork/__impl/exception.hpp b/include/libfork/__impl/exception.hpp new file mode 100644 index 00000000..857dbca9 --- /dev/null +++ b/include/libfork/__impl/exception.hpp @@ -0,0 +1,70 @@ +#pragma once + +/** + * @file exception.hpp + * + * @brief A collection of internal macros for exception handling. + * + * These macros are standalone i.e. they can be used without importing/including anything else. + */ + +/** + * @brief Detects if the compiler has exceptions enabled. + * + * Overridable by defining `LF_COMPILER_EXCEPTIONS` globally. + */ +#ifndef LF_COMPILER_EXCEPTIONS + #if defined(__cpp_exceptions) || (defined(_MSC_VER) && defined(_CPPUNWIND)) || defined(__EXCEPTIONS) + #define LF_COMPILER_EXCEPTIONS 1 + #else + #define LF_COMPILER_EXCEPTIONS 0 + #endif +#endif + +namespace lf::impl { + +/** + * @brief Calls `std::terminate` after printing `msg`. + */ +[[noreturn]] +void terminate_with(char const *message, char const *file, int line) noexcept; + +} // namespace lf::impl + +#define LF_TERMINATE(message) ::lf::impl::terminate_with((message), __FILE__, __LINE__) + +#if LF_COMPILER_EXCEPTIONS + /** + * @brief Expands to ``try`` if exceptions are enabled, otherwise expands to ``if constexpr (true)``. + */ + #define LF_TRY try + /** + * @brief Expands to ``catch (...)`` if exceptions are enabled, otherwise ``if constexpr (false)``. + */ + #define LF_CATCH_ALL catch (...) + /** + * @brief Expands to ``throw X`` if exceptions are enabled, otherwise terminates the program. + */ + #define LF_THROW(X) throw X + /** + * @brief Expands to ``throw`` if exceptions are enabled, otherwise terminates the program. + */ + #define LF_RETHROW throw +#else + /** + * @brief Expands to ``try`` if exceptions are enabled, otherwise expands to ``if constexpr (true)``. + */ + #define LF_TRY if constexpr (true) + /** + * @brief Expands to ``catch (...)`` if exceptions are enabled, otherwise ``if constexpr (false)``. + */ + #define LF_CATCH_ALL if constexpr (false) + /** + * @brief Expands to ``throw X`` if exceptions are enabled, otherwise terminates the program. + */ + #define LF_THROW(X) LF_TERMINATE("Tried to throw '" #X "' without compiler exceptions") + /** + * @brief Expands to ``throw`` if exceptions are enabled, otherwise terminates the program. + */ + #define LF_RETHROW LF_TERMINATE("Tried to rethrow without compiler exceptions") +#endif diff --git a/include/libfork/__impl/utils.hpp b/include/libfork/__impl/utils.hpp new file mode 100644 index 00000000..2adbf49c --- /dev/null +++ b/include/libfork/__impl/utils.hpp @@ -0,0 +1,32 @@ +#pragma once + +/** + * @file utils.hpp + * + * @brief A collection of internal utility macros. + * + * These macros are not safe to use unless `import std` is in scope. + */ + +// =============== Utility =============== // + +// clang-format off + +/** + * @brief Use like `BOOST_HOF_RETURNS` to define a function/lambda with all the noexcept/decltype specifiers. + * + * This macro is not truly variadic but the ``...`` allows commas in the macro argument. + */ +#define LF_HOF(...) noexcept(noexcept(__VA_ARGS__)) -> decltype(__VA_ARGS__) { return __VA_ARGS__;} + +// clang-format on + +/** + * @brief Use like `std::forward` to perfectly forward an expression. + */ +#define LF_FWD(...) ::std::forward(__VA_ARGS__) + +/** + * @brief Use to define a `T` that is aligned to the required alignment of `std::atomic_ref`. + */ +#define ATOMIC_ALIGN(T) alignas(std::atomic_ref::required_alignment) T diff --git a/include/libfork/version.hpp b/include/libfork/version.hpp index d048ad62..03f18744 100644 --- a/include/libfork/version.hpp +++ b/include/libfork/version.hpp @@ -1,3 +1,5 @@ +#pragma once + /** * @brief __[public]__ The major version of libfork. * diff --git a/src/core/concepts.cxx b/src/core/concepts.cxx new file mode 100644 index 00000000..5e9138ca --- /dev/null +++ b/src/core/concepts.cxx @@ -0,0 +1,150 @@ +module; +export module libfork.core:concepts; + +import std; + +namespace lf { + +// =========== Atomic related concepts =========== // + +/** + * @brief Verify a type is suitable for use with `std::atomic` + * + * This requires a `TriviallyCopyable` type satisfying both `CopyConstructible` and `CopyAssignable`. + */ +export template +concept atomicable = std::is_trivially_copyable_v && // + std::is_copy_constructible_v && // + std::is_move_constructible_v && // + std::is_copy_assignable_v && // + std::is_move_assignable_v && // + std::same_as>; // + +/** + * @brief A concept that verifies a type is lock-free when used with `std::atomic`. + */ +export template +concept lock_free = atomicable && std::atomic::is_always_lock_free; + +// ========== Specialization ========== // + +template typename Template> +struct is_specialization_of : std::false_type {}; + +template