Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
# Patternia clang-tidy configuration.
#
# Scope: correctness and performance findings in library headers.
# Style/readability checks stay with clang-format; two bugprone
# checks are disabled because they conflict with deliberate design
# idioms (see comments inline).
Checks: >
bugprone-*,
clang-analyzer-*,
performance-*,
portability-*,
concurrency-*,
-bugprone-crtp-constructor-accessibility,
-bugprone-branch-clone,
-bugprone-easily-swappable-parameters,
-bugprone-exception-escape

# Only library headers are checked; test/sample code and
# third-party headers are out of scope.
HeaderFilterRegex: 'include/ptn/'

# CI treats every warning as an error.
WarningsAsErrors: '*'

# Rationale for disabled checks:
# - bugprone-crtp-constructor-accessibility: pattern_base and
# guard_operator are intentional public CRTP mixins.
# - bugprone-branch-clone: repeated branch bodies are a normal
# consequence of constexpr/if-constexpr metaprogramming.
# - bugprone-easily-swappable-parameters: fires pervasively on
# forwarding-reference machinery where parameter order is part
# of the DSL contract.
# - bugprone-exception-escape: noisy on executable mains (samples),
# irrelevant for a header-only library.
46 changes: 46 additions & 0 deletions .github/workflows/clang-tidy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: clang-tidy

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
clang-tidy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install clang-tidy
run: |
sudo apt-get update
sudo apt-get install -y clang clang-tidy
clang-tidy --version

- name: Configure (compile_commands.json)
run: |
cmake -S . -B build \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DPTN_BUILD_TESTS=ON

- name: Run clang-tidy on tests and samples
shell: bash
run: |
set -euo pipefail
# WarningsAsErrors is set in .clang-tidy, so any finding
# fails the step. json_dispatch samples are excluded:
# they need nlohmann/json which is not part of the build.
for f in tests/*.cpp samples/*.cpp; do
echo "=== $f"
clang-tidy -p build "$f"
done
59 changes: 59 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: fuzz

on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
# Weekly deep run, Monday 03:00 UTC.
- cron: '0 3 * * 1'

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fuzz:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install clang
run: |
sudo apt-get update
sudo apt-get install -y clang

- name: Build fuzz target
run: |
cmake -S . -B build \
-DCMAKE_CXX_COMPILER=clang++ \
-DPTN_BUILD_FUZZ=ON
cmake --build build --target ptn_fuzz_match

- name: Fuzz (oracle + ASan/UBSan)
shell: bash
run: |
set -euo pipefail
TIME=180
if [ "${{ github.event_name }}" = "schedule" ]; then
TIME=900
fi
mkdir -p fuzz_corpus artifacts
./build/fuzz/ptn_fuzz_match fuzz_corpus \
-max_total_time="$TIME" \
-rss_limit_mb=2048 \
-artifact_prefix=artifacts/ \
-print_final_stats=1

- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crash-${{ github.sha }}
path: artifacts/
if-no-files-found: ignore
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ option(PTN_INSTALL "Generate install/export targets" ${PTN_MASTER_PROJECT})
option(PTN_BUILD_TESTS "Build unit tests" ${PTN_MASTER_PROJECT})
option(PTN_BUILD_BENCHMARKS "Build benchmarks" ${PTN_MASTER_PROJECT})
option(PTN_BUILD_SAMPLES "Build sample programs" ${PTN_MASTER_PROJECT})
option(PTN_BUILD_FUZZ "Build fuzz targets (requires Clang)" OFF)
option(PTN_DEV_INDEX "Create local indexable target for IDEs (not installed)" ON)
option(PTN_SKIP_COMPILER_CHECK "Skip compiler version check" OFF)

Expand Down Expand Up @@ -182,6 +183,14 @@ if(PTN_BUILD_TESTS)
endif()
endif()

if(PTN_BUILD_FUZZ)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR
"PTN_BUILD_FUZZ requires Clang (libFuzzer entry point).")
endif()
add_subdirectory(fuzz)
endif()

if(PTN_BUILD_BENCHMARKS)
add_subdirectory(bench)
endif()
Expand Down
33 changes: 33 additions & 0 deletions fuzz/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Fuzz targets (libFuzzer). Built only when PTN_BUILD_FUZZ=ON.
#
# Sanitizer set is platform-dependent: Windows supports fuzzer and
# address reliably; Linux additionally gets undefined.

if(WIN32)
set(PTN_FUZZ_SANITIZERS "fuzzer,address")
else()
set(PTN_FUZZ_SANITIZERS "fuzzer,address,undefined")
endif()

add_executable(ptn_fuzz_match fuzz_match.cpp)
target_link_libraries(ptn_fuzz_match PRIVATE patternia)
target_compile_features(ptn_fuzz_match PRIVATE cxx_std_17)
target_compile_options(ptn_fuzz_match PRIVATE
-fsanitize=${PTN_FUZZ_SANITIZERS} -g -O1
)
target_link_options(ptn_fuzz_match PRIVATE
-fsanitize=${PTN_FUZZ_SANITIZERS}
)

if(WIN32)
# clang_rt.fuzzer ships as MT_StaticRelease. Pin the fuzz target
# to the static release CRT (otherwise Debug trees produce
# msvcrtd objects and /failifmismatch rejects the link), and
# disable iterator debugging for the same reason.
set_target_properties(ptn_fuzz_match PROPERTIES
MSVC_RUNTIME_LIBRARY "MultiThreaded"
)
target_compile_options(ptn_fuzz_match PRIVATE
-U_DEBUG -DNDEBUG -D_ITERATOR_DEBUG_LEVEL=0
)
endif()
177 changes: 177 additions & 0 deletions fuzz/fuzz_match.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Fuzz target for match evaluation.
//
// Strategy: oracle comparison. Every battery evaluates the same
// subject through patternia and through a hand-written reference
// implementation; any divergence traps. ASan/UBSan reports and
// divergences both count as failures.

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <variant>

#include <ptn/patternia.hpp>

using namespace ptn;

namespace {

struct Point {
int x;
int y;
};

PTN_BIND(Point, x, y);

[[noreturn]] void diverged(const char *battery) {
// Print then trap: libFuzzer reports the input on abnormal
// termination.
std::fprintf(stderr, "oracle divergence in %s\n", battery);
std::abort();
}

// --- literal dispatch battery ---

int lit_ref(int v) {
if (v == 0)
return 10;
if (v == 1)
return 11;
if (v == 2)
return 12;
if (v == 3)
return 13;
return -1;
}

int lit_match(int v) {
return match(v)
| on( //
lit(0) >> 10, //
lit(1) >> 11, //
val<2> >> 12, //
val<3> >> 13, //
_ >> -1);
}

// --- guard battery ---

int guard_ref(int v) {
if (v < 0)
return 1;
if (v >= 0 && v <= 9)
return 2;
if (v >= 10 && v < 100)
return 3;
return 4;
}

int guard_match(int v) {
return match(v)
| on( //
$[_ < 0] >> 1, //
$[rng(0, 9)] >> 2, //
$[_ >= 10 && _ < 100] >> 3, //
_ >> 4);
}

// --- structural battery ---

int struct_ref(const Point &p) {
if (p.x * p.x + p.y * p.y == 25)
return p.x + p.y;
if (p.x == 0 && p.y == 0)
return 0;
return -1;
}

int struct_match(const Point &p) {
return match(p)
| on( //
$(has<&Point::x, &Point::y>)[x * x + y * y == 25] >>
[](int a, int b) { return a + b; },
has<&Point::x, &Point::y>[x == 0 && y == 0] >> 0,
_ >> -1);
}

// --- variant battery ---

using Value = std::variant<int, std::string>;

std::string variant_ref(const Value &v) {
if (std::holds_alternative<int>(v))
return "i:" + std::to_string(std::get<int>(v));
return "s:" + std::get<std::string>(v);
}

std::string variant_match(const Value &v) {
return match(v)
| on( //
$(is<int>) >>
[](int i) { return "i:" + std::to_string(i); },
$(is<std::string>) >>
[](const std::string &s) { return "s:" + s; },
_ >> [] { return std::string("?"); });
}

// --- combinator battery ---

int comb_ref(int v) {
if (v == 1 || v == 2 || v == 3)
return 1;
if (v != 0 && v % 2 == 0)
return 2;
return 3;
}

int comb_match(int v) {
auto even = [](int n) { return n % 2 == 0; };
return match(v)
| on( //
(lit(1) || lit(2) || lit(3)) >> 1, //
(!lit(0) && pred(even)) >> 2, //
_ >> 3);
}

// Reads sizeof(int) bytes starting at off (wrapping) as a
// big-endian int.
int read_int(const uint8_t *data, size_t size, size_t off) {
int v = 0;
for (size_t i = 0; i < sizeof(int); ++i) {
v = (v << 8) | data[(off + i) % size];
}
return v;
}

} // namespace

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data,
size_t size) {
if (size < 8 || size > 4096)
return 0;

const int a = read_int(data, size, 0);
const int b = read_int(data, size, 4);

if (lit_match(a) != lit_ref(a))
diverged("literal");
if (guard_match(a) != guard_ref(a))
diverged("guard");
if (comb_match(b) != comb_ref(b))
diverged("combinator");

const Point p{a & 0xFF, b & 0xFF};
if (struct_match(p) != struct_ref(p))
diverged("structural");

const size_t slen = static_cast<size_t>(b & 0x3F) % (size + 1);
const Value v = (a & 1) ? Value{a}
: Value{std::string(
reinterpret_cast<const char *>(data),
slen)};
if (variant_match(v) != variant_ref(v))
diverged("variant");

return 0;
}
4 changes: 3 additions & 1 deletion include/ptn/core/common/common_traits.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <cstdint>

// Core type traits used by the matching engine.
//
// This header provides fundamental type traits and utilities for
Expand Down Expand Up @@ -68,7 +70,7 @@ namespace ptn::core::traits {

// Fallback Semantics (pattern-level vs match-level)

enum class fallback_level {
enum class fallback_level : std::uint8_t {
none,
pattern, // e.g. wildcard '_'
match // e.g. otherwise(...)
Expand Down
Loading
Loading