diff --git a/CLAUDE.md b/CLAUDE.md index 6ff7df61..931a6cdc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,9 @@ composition and types) layered over `pfn` (polyfills of C++23/26 vocabulary type ## Collaboration - Pushing back **and** asking questions is welcome — a challenged design beats a silently implemented flawed one. +- Both user and yourself are fallible — take review feedback seriously, encourage using other LLMs for help and review. +- Test every design change against TYPE_ALGEBRA.md (see Docs and Code): it must be expressible there without contradicting the algebra's foundations — additions are welcome, contradictions are to be challenged. +- Always consider whether change or addition might cause the user consuming this library to write unsafe code unwittingly, and challenge such changes. ## CI @@ -17,7 +20,7 @@ composition and types) layered over `pfn` (polyfills of C++23/26 vocabulary type ## Commits & GitHub text -- Trailer `Assisted-by: Claude:` (Linux-kernel convention), e.g. `claude-opus-4-8`. This replaces the harness's trailer boilerplate entirely — no `Co-Authored-By:`, no `Claude-Session:` URL. Same rule for GitHub issues, PRs and comments: `Assisted-by:` is welcome; no other footers or attribution boilerplate. +- Trailer `Assisted-by: :` ([Linux-kernel convention](https://docs.kernel.org/process/coding-assistants.html)), e.g. `Claude:claude-opus-4-8`. This replaces the harness's trailer boilerplate entirely — no `Co-Authored-By:`, no `Claude-Session:` URL. Same rule for GitHub issues, PRs and comments: `Assisted-by:` is welcome; no other footers or attribution boilerplate. - Offer commits; never commit without the user's confirmation — which may be relayed to a commit subagent by the parent that received it. Terse messages: imperative topic; body only when the change needs a *why* (the routing rule in Code names that case). - If a pre-commit hook rewrites staged files, the commit aborts — re-stage the same files and retry once (CONTRIBUTING `## Pre-commit`). - A feature or fix commit should include a test for the behaviour it changes; exceptions are allowed. The PR must contain such a test somewhere unless the behaviour is inherently untestable (for example, because of language or compiler limitations); explain the omission. @@ -39,6 +42,7 @@ composition and types) layered over `pfn` (polyfills of C++23/26 vocabulary type - Don't create `.md`/summary/planning files unless asked (memory files are exempt — see Memory). - A new file's copyright year = the year it enters the codebase (the current year; if unsure, infer from the latest commit). - In `include/` headers, anchor the standard library as `::std::`, never bare `std::` — a user's `fn::std` would otherwise win lookup inside namespace `fn`. Not needed in tests. +- Every major change or addition must be documented in TYPE_ALGEBRA.md (see Docs). Major changes are those that impact the design of code consuming this library. ## Layering @@ -78,6 +82,12 @@ composition and types) layered over `pfn` (polyfills of C++23/26 vocabulary type - Map: README.md = user-facing overview (purpose, usage, project shape, support surface; no agent directives, no internal mechanics; CI surfaced as evidence only, never mechanics); CONTRIBUTING.md = contributor facts (coding + tests standards, build environment, workflows, all CI details, mechanics of every aspect; no agent directives, no library usage); TYPE_ALGEBRA.md = the design document — the library's type algebra worked from first principles; CHANGELOG.md = design history (dated entries, newest first); docs/ = API reference (Doxygen + znai → Pages; also usage); CLAUDE.md = agent practice + guardrails pointing into the above. - Fenced C++ examples in README.md and TYPE_ALGEBRA.md are generated from sources in `examples/` by `scripts/sync_md_examples.py` (pre-commit keeps them in sync) — edit the example source, never the fence; prose edits stay outside fences. +- When updating TYPE_ALGEBRA.md, follow the guidelines in the `document-guidelines` HTML comment at the bottom of the document. - Living documents (README, CONTRIBUTING, docs/) are timeless present tense — no "now", "no longer", "previously". A change that obsoletes documented design gets a dated CHANGELOG.md entry in the same change, saying what it obsoleted and why. -- Recency-bias and wordiness defence (all human-readable text — docs, code comments, and the like; CHANGELOG.md and commit messages are exempt — change-perspective is their correct form): before keeping new text, test — (1) day-one: would this sentence exist had the feature or fix always been here? (2) effort: is detail sized by reader need, or by how hard the work was? — a hard-won bugfix earns no extra words; its history lives in `git log`/`git blame`/CHANGELOG.md; (3) placement: is it where a newcomer looks, or where your recent work pulls it? (4) economy: could fewer words say it as well? After editing, delegate a whole-file top-to-bottom reread to a subagent briefed as a first-time reader, blind to what changed — never review only your diff. Triage its findings: fix what your edit touches, surface the rest rather than rewriting unasked. +- Recency-bias and wordiness defence — applies to all prose (docs, code comments, and the like); CHANGELOG.md and commit messages are exempt, change-perspective being their correct form. Before keeping new text, apply all four tests: + - *Day-one*: would this sentence exist had the feature or fix always been here? + - *Effort*: is detail sized by reader need, or by how hard the work was? A hard-won bugfix earns no extra words; its history lives in `git log`/`git blame`/CHANGELOG.md. + - *Placement*: is it where a newcomer looks, or where your recent work pulls it? + - *Economy*: could fewer words say it as well? +- After editing prose, delegate a whole-file top-to-bottom reread to a subagent briefed as a first-time reader, blind to what changed — never review only your diff. Triage its findings: fix what your edit touches, surface the rest rather than rewriting unasked. - On memory or practice changes, check the root `.md` files for drift from reality and **offer** fixes (CLAUDE.md = practice, README/CONTRIBUTING = facts). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bd43a48..5f86a089 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,16 @@ The headers rely on C++20 only, in every default-mode build. The C++23 validatio ## Building locally -Both `fn` (`include/fn`) and `pfn` (`include/pfn`) target C++20. The unit tests and examples build in C++20 by default. If you have a recent enough compiler, use the CMake option `VALIDATE_CXX23=ON` to additionally build them in C++23 (requires `LIBFN_TESTS=ON`). This also enables `tests/pfn/expected_validation.cpp` and `tests/pfn/optional_validation.cpp`, which run the `pfn` test suites against the standard library's own `std::expected` and `std::optional` — validating the polyfills, and the tests themselves, against the real thing. +Both `fn` (`include/fn`) and `pfn` (`include/pfn`) target C++20. By default, with no options configured, the build compiles the library, the unit tests, and the examples in C++20: -The C++26 mode is separate from the standard level: defining `LIBFN_CXX26` switches the library's internal type ordering to `std::type_order` (see the feature-reliance note above) and moves `fn` into its own ABI namespace, since the two orderings may disagree on `copack`/`choice` alternative order and the layouts must never link as one. The CMake option `LIBFN_CXX26=ON` enables the mode for this build tree, and `VALIDATE_CXX26=ON` (requires `LIBFN_TESTS=ON` and `LIBFN_CXX26=ON`) additionally builds the tests and examples in C++26 with the mode enabled. +```bash +mkdir .build && cd .build +cmake .. +cmake --build . +ctest --output-on-failure +``` + +The sections below describe other build configurations. They vary that `cmake` line and assume the same build directory. `CMAKE_BUILD_TYPE` behaves as usual in `cmake`, so it is omitted from the examples unless an option constrains it. For a quick check of a single example without the full CMake/Catch2 setup: @@ -24,6 +31,85 @@ For a quick check of a single example without the full CMake/Catch2 setup: g++ -std=c++20 -Iinclude examples/polygon/main.cpp -o /tmp/polygon ``` +### C++23 and C++26 + +The unit tests and examples build in C++20 by default. If your compiler is modern enough (GCC 15; Clang 21), use the CMake option `VALIDATE_CXX23=ON` to also build them in C++23 (requires `LIBFN_TESTS=ON`). This enables `tests/pfn/expected_validation.cpp` and `tests/pfn/optional_validation.cpp`. These run the `pfn` test suites against the standard library's `std::expected` and `std::optional` to validate that the polyfills (and the tests themselves) behave exactly like the standard library implementations. + +```bash +cmake -DVALIDATE_CXX23=ON .. +``` + +Setting the CMake option `LIBFN_CXX26=ON` enables C++26 compilation mode, which switches the library's internal type ordering to use C++26's `std::type_order` (see the feature-reliance note above). Because this can change the layout of `copack` and `choice` alternatives, the library uses a distinct ABI namespace to prevent binary compatibility issues. `LIBFN_CXX26=ON` does not select a language version in the compilation options; it needs to be set separately, e.g. with `CMAKE_CXX_STANDARD`, `CXXFLAGS` or in a consumer project. + +Setting `VALIDATE_CXX26=ON` (requires `LIBFN_TESTS=ON` and `LIBFN_CXX26=ON`) additionally builds the tests and examples in C++26 mode, selecting the C++26 language version. Since `std::type_order` is only implemented by GCC 16, this requires GCC 16. If GCC 16 is not your default compiler, select it via `CMAKE_CXX_COMPILER` and `CMAKE_C_COMPILER`, or with both `CC` and `CXX` environment variables: + +```bash +cmake -DLIBFN_CXX26=ON -DVALIDATE_CXX26=ON .. +``` + +### Sanitizers + +Sanitizers require `CMAKE_BUILD_TYPE=Debug`. They are enabled by default for a top-level build when the build type is `Debug` or left unset (including the default build steps above). To build `Debug` without sanitizers: + +```bash +cmake -DLIBFN_SANITIZERS=OFF -DCMAKE_BUILD_TYPE=Debug .. +``` + +The availability of sanitizers varies by platform and compiler: + +* **Linux (GCC & Clang):** Enables Address (ASan), Leak (LSan), and Undefined Behavior (UBSan) sanitizers. +* **macOS (Apple Clang):** Enables ASan and UBSan. LSan is not supported on macOS. +* **Unsupported configurations:** The option is rejected (causes a CMake error) on: + * MSVC + * macOS with Homebrew GCC (due to `libasan` library path issues) + * macOS with Homebrew Clang (due to library mismatches that hang the process) + +### Coverage + +Setting `LIBFN_COVERAGE=ON` adds a `coverage` target that writes a `coverage.xml` report based on previously run tests. This requires `LIBFN_TESTS=ON`, a top-level build, and GCC or Clang: + +```bash +cmake -DLIBFN_COVERAGE=ON .. +cmake --build . +ctest -L 'tests_.*' +cmake --build . --target coverage +``` + +**Requirements:** + +* **gcovr 8.4+:** The coverage build uses the `--merge-lines` option, which was added in `gcovr` 8.4. Since package managers often distribute older versions, using a Python virtual environment (`venv`) is recommended as the easiest way to install a newer version. +* **Coverage tool:** The coverage tool must match your compiler: + * GCC uses `gcov`. + * Clang uses `llvm-cov gcov`. + * Apple Clang uses `llvm-cov` resolved via `xcrun -f`. + These are detected and configured automatically. + +### Documentation + +Setting `LIBFN_DOCS=ON` adds an `export_docs` target that generates the API reference in the `docs/` folder of your build directory. This requires `LIBFN_TESTS=ON` and a top-level build: + +```bash +cmake -DLIBFN_DOCS=ON .. +cmake --build . --target export_docs +``` + +**Requirements:** +The following tools must be installed and available in your `PATH`: + +* **Doxygen** 1.12.0 +* **znai** 1.91 (requires Java/OpenJDK 21 or newer) +* **Graphviz** (provides the `dot` tool used by Doxygen) + +For a reference environment with these exact tool versions, see [`ci/docs/Dockerfile`](ci/docs/Dockerfile). + +**Validation:** +The documentation build does more than just generate HTML. It also verifies that: + +* Every documented entity is present on the generated site. +* The API signatures in the documentation exactly match the C++ headers. + +The target will fail if the documentation is out-of-date or has broken references. + ## Unit tests Although we aim for 100% unit-test coverage, executing every line and branch tells only part of the story in this project. Many important guarantees are compile-time properties, such as overload resolution, conversions, constraints and `noexcept` specifications. Tests must therefore exercise an interface with combinations of dimensions, not merely execute every line. diff --git a/README.md b/README.md index d95a8f66..cdd0ac13 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,10 @@ The library features demonstrated by the code example above: The example also demonstrates how well libfn works with general programming idioms. `make` is a *smart constructor* — the only way to build a `Rational` — enforcing the type's invariants and returning `expected`: callers never need to re-check what the type guarantees. Treating *callables as values* lets operations such as `and_then` accept `make` whole, carrying its overload set. -These properties also make libfn a natural fit for asynchronous composition, such as coroutines or senders/receivers. Operations and monadic types alike are plain values: `and_then(f)` is a *description* of a step, executed only when a monad is piped into it (an input to the sequence, or the result of the preceding operation). A framework can hold the steps of a computation and apply them as results arrive, with a strongly typed error channel and no hidden control flow — exactly what such programming models need. - Beyond the example: `choice` (a monad over `copack`); the same operations over `optional` as over `expected`; simultaneous disjunction (using `operator|` to fallback-combine monadic computations) and its `fn::disjoin` fold; `fn::conjoin` for simultaneous product folds; tuple protocol in `pack` (`get(p)` or structured bindings); `pack` and `copack` are both structural types (a `constexpr` value which may be used as a template parameter); support for immovable values and callables; an extensible pipeline, where a verb defined outside the library pipes exactly like the built-in ones; and more — see [examples/](examples/) and the [API reference][docs]. +None of this is ad hoc: [TYPE_ALGEBRA.md](TYPE_ALGEBRA.md) derives the entire design from first principles — the algebra of products and sums behind `pack` and `copack`, the logic of monadic composition, and the compiler-checked laws that the library obeys. + ## How The library comes as two parts in one repository: @@ -146,13 +146,17 @@ This library requires a total ordering of types, which the standard provides fro ## Using the library -The library is header-only. The CMake package exports two targets: +The library is header-only. The CMake package exports `libfn::fn` and `libfn::pfn`: ```cmake find_package(libfn CONFIG REQUIRED) target_link_libraries(main PRIVATE libfn::fn) # or libfn::pfn for the polyfills alone ``` +A third target, `libfn::fn_cxx26`, is the same headers entered with the [`LIBFN_CXX26` mode](#implementation-note) selected; it carries both the mode and its C++26 language requirement, so a target opts in with one link line. Link exactly one of `libfn::fn` or `libfn::fn_cxx26` per target. `pfn` is mode-independent and has no such variant. + +With `libfn::fn_cxx26`, a compiler that does not implement `std::type_order` stops at the first libfn header, with an `#error` naming the feature. Mixing the two entry points in one binary stops at the linker, on an undefined reference whose type names differ from the definition's by the `_cxx26` ABI namespace. Both are loud by design: the namespaces are separate so that two layouts cannot merge unnoticed — the invariant [TYPE_ALGEBRA.md](TYPE_ALGEBRA.md) calls one normalization order per program. + Packaging is provided — and exercised by CI — for [conan](conanfile.py), [vcpkg](ports/libfn) (an in-repo port), [Nix](flake.nix) and [Bazel](MODULE.bazel); plain CMake `FetchContent` or `add_subdirectory` works as well. Until the first tagged release, consume a pinned git revision — and read [Backwards compatibility](#backwards-compatibility). Every packaging route above except Bazel also delivers the compile options the headers require. Under Bazel — and a plain copy of `include/` — these options don't arrive automatically; provide them yourself: C++20 or newer (`--cxxopt=-std=c++20` in Bazel), `-Wno-missing-braces` on clang (`fn::pack` initialization elides braces by design), and with MSVC `/permissive-` plus `_HAS_CXX23`. The authoritative set is the `INTERFACE` options in [cmake/CompilationOptions.cmake](cmake/CompilationOptions.cmake). diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index 43607af2..f55af1b0 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -13,7 +13,7 @@ The library operates on two payload types and four computation carriers: Composition operations include `transform` (mapping), `transform_error` (error mapping), `and_then` (sequential monadic binding), `or_else` (recovery), `operator&` (conjunction / simultaneous product composition), `operator|` (disjunction / simultaneous sum composition), and the n-ary folds `fn::conjoin` and `fn::disjoin`. Elimination is `apply` (multidispatch). -### Member vs. Pipeline Syntax +### Member vs. pipeline syntax Some operations are exposed in two forms: @@ -26,17 +26,23 @@ The `operator|` carries two meanings, told apart by its right operand: a pipelin Freestanding `fn::apply(f, args...)` is the general multidispatch entry point: it accepts any mix of scalars, tuple-like structures, `pack`s and `copack`s, unpacking products and dispatching over alternatives in a single call. Do not confuse with `pfn::apply`, which is a polyfill for the C++26 `std::apply`, meant for C++20 compilers. The `fn::apply` is an extension on top of `pfn::apply`. -In prose, we omit prefixes (writing `apply`, `transform`, `and_then`, `expected`, `pack`) when referring to both forms or core vocabulary types generally. +In prose, this document omits prefixes (using `apply`, `transform`, `and_then`, `expected`, `pack`) when referring to both forms or core vocabulary types generally. -### Storage Shape vs. Call Shape +### Storage shape vs. call shape Although different types can behave identically during application, they remain strictly distinct in memory. For example, `pack`, `std::tuple`, and `std::pair` all unpack into the same call shape `f(a, b)` during `apply`, but they are separate C++ types with distinct layouts. Application does not silently convert or unify types on the storage side. To illustrate these concepts, the examples in this document use a reusable set of value and error types: - ```cpp +struct Error {}; +struct OtherError {}; + +struct A {}; +struct B {}; +struct C {}; +struct D {}; + struct UserId {}; struct User {}; struct FilePath {}; @@ -78,7 +84,7 @@ auto graded_pipeline(std::string_view sv) -> void The resulting `expected` statically records that the pipeline yields a `User` on success, or fails with exactly one of `NotANumber`, `OutOfRange`, `Missing`, or `IoError`. This exact union accumulates automatically via `and_then` composition. -### What Does "Graded" Mean? +### What does "graded" mean? Standard monads are rigid: an `expected` requires every step in a pipeline to return the identical error type `E`. This forces you to define a monolithic global error union upfront. @@ -110,7 +116,7 @@ You may also use `copack` on a value side of most carriers (except for `just void > > To enforce set semantics at compile time, `libfn` defines one canonical representation and rejects any instantiation that diverges from it: > -> - **`copack`** is the core storage type. It requires its template parameters to already be flat, unique, and sorted in a strict total order over types. The order is derived from the compiler's own spelling of each type; a build targeting C++26 with `LIBFN_CXX26` set will use `std::type_order` to derive the order of types, while the default build uses a type sorting mechanism based on compiler-specific type names (since these two orders may differ, each defines a distinct ABI namespace). If you attempt to instantiate `copack` manually with out-of-order parameters (such as `copack` when `A` precedes `B` in that order) or with nested copacks (such as `copack>`), the compiler will reject the instantiation as outright ill-formed. +> - **`copack`** is the core storage type. It requires its template parameters to already be flat, unique, and sorted in a strict total order over types. The order is derived from the compiler's own spelling of each type; a build targeting C++26 with `LIBFN_CXX26` set will use `std::type_order` to derive the order of types, while the default build uses a type sorting mechanism based on compiler-specific type names (these two orders may differ; Section 15 explains why they cannot mix). If you attempt to instantiate `copack` manually with out-of-order parameters (such as `copack` when `A` precedes `B` in that order) or with nested copacks (such as `copack>`), the compiler will reject the instantiation as outright ill-formed. > > - **`copack_for`** is the user-facing type alias. It accepts any list of types (out-of-order, duplicates, nested copacks), performs the flattening, deduplication, and canonical sorting, and resolves to the validated `copack`. > @@ -234,7 +240,7 @@ To model computation and manage control flow (success, failure, alternatives, an - **`just`**: Always contains a single successful value of type `T`. - **`choice`**: Always contains one of several selected alternatives, representing the complete state space of the computation. -Because `choice` implies that an alternative is always present, `choice<>` is incomplete: an always-present selected alternative requires at least one alternative to exist. +Because `choice` always holds a selected alternative, `choice<>` is incomplete: it offers nothing to select. Additionally, the infallible state **`expected>`** (representing $T + 0 \cong T$) can never fail because `copack<>` represents the initial zero object **0** (the uninhabited type). Lacking any possible error alternatives, it acts as an infallible, graded unit context. Since it is a specialized state of `expected` rather than a unique template, it is classified under the same computation carrier. @@ -254,7 +260,7 @@ Raw algebraic constructs—such as `std::tuple` (product), `std::variant` (sum), Composing computations requires wrapping these values in computation carriers. Product composition (conjunction) and sum composition (disjunction) operate on carriers, not raw data. The carrier manages success propagation and short-circuits failures. -### Carrier Bridging: Interoperable Pipelines +### Carrier bridging: interoperable pipelines Because these carriers represent different computational contexts, pipelines often need to transition between them. `libfn` licenses explicit **cross-carrier bridging** via pipeline-scoped operations using `operator|`. @@ -430,6 +436,8 @@ The runtime semantics are exact: Unlike disjunction (Section 7), `operator&` applies to payload types. When an operand is `copack`, it performs a Cartesian distribution, yielding a `copack` of `pack`s. A `pack` on the opposite side widens each of those `pack`s. +This distribution underpins the rule in Section 4 that a `pack` cannot contain a `copack`. The disallowed nested shape—a product over a sum, (A × B) × (C + D)—is algebraically equivalent to (A × B × C) + (A × B × D), a flat `copack` of `pack`s. This is the sum-of-products normal form that the algebra preserves. No expressive power is lost by refusing the nested shape: conjoining a `pack` with a `copack` yields the equivalent flat representation directly, as the second case below shows. + ```cpp auto test_cartesian_distribution(fn::copack_for ab, fn::copack_for cd) -> void @@ -457,7 +465,7 @@ The n-ary fold `fn::conjoin(...)` operates in two modes: Mixing carriers and data in a single call is ill-formed. -### Conjunction with the Identity Cluster +### Conjunction with the identity cluster An operand from the identity cluster (Section 10) contributes a value but never a failure: @@ -522,7 +530,7 @@ The runtime semantics are exact: Unlike conjunction, disjunction has no data-level form. Neither `operator|` nor the n-ary fold `fn::disjoin(...)` accepts a `pack`, a `copack`, or a scalar. Therefore, built-in operations (such as bitwise `OR` on integers) are never confused with disjunction. -### Disjunction with the Identity Cluster +### Disjunction with the identity cluster When an operand belongs to the identity cluster (Section 10), the disjunction cannot fail: @@ -731,7 +739,7 @@ auto test_identity_cross() -> void The *bind* operation adopts the carrier family of the provided callback — a crossing only the pipeline-scoped functors are licensed to make (Section 3). -### Success-Path Bridging +### Success-path bridging Fallible carriers (excluding `expected>`) and `optional` cannot transition to infallible carriers, because doing so would risk silently discarding an active error or empty state. @@ -801,7 +809,7 @@ Monadic operations on the identity cluster: `choice` represents a computation that always succeeds by selecting one of several alternatives. Structurally, it serves as the single-layer carrier for coproduct states, avoiding the invalid nested `just>` representation (Section 3). -### Promotion via Pipeline Functors +### Promotion via pipeline functors Pipeline `fn::transform` on a `just` that returns a `copack` promotes automatically to `choice`: @@ -966,7 +974,7 @@ A reference of `libfn` operations, organized by channel and effect: - `fn::conjoin`: An n-ary fold of `operator&`, over monadic carriers or over packs, copacks and scalars — not a mix of the two. - `fn::disjoin`: An n-ary fold of `operator|` over monadic carriers, supporting total disjunction with the identity cluster. -### Key Architectural Rules of the Map +### Key architectural rules of the map - **`fail` and `recover` are duals**: `fail` transitions success to failure ($Success \implies Failure$), and `recover` transitions failure to success ($Failure \implies Success$). Neither operation widens a graded error set. - **Graded `and_then`**: Widens the error grade by introducing new error types into the pipeline. @@ -1008,10 +1016,14 @@ Other properties hold structurally: To ensure reliability, `libfn` uses compiler mechanisms to reject malformed usage and preserve performance. -### Constraints and Exhaustiveness +### Constraints and exhaustiveness Public concepts and `requires` clauses enforce correctness before instantiation. Operations are protected by public applicability concepts (such as `fn::applicable_transform` and `fn::applicable_and_then`) that evaluate to `false` for invalid calls rather than triggering deep compiler errors. This underpins the compile-time exhaustiveness guarantees of `apply` and monadic operations established in Sections 4 and 12. +### One normalization order per program + +The `copack` normal form rests on a strict total order over types, and the default and `LIBFN_CXX26` modes may derive different orders (Section 2). Each mode therefore places `fn` in its own ABI namespace: the mode becomes part of every `fn` symbol, so the two modes never link as one, and `copack`s whose layouts disagree cannot merge unnoticed. (`pfn` carries no such split — it has no normal form to protect.) + ### C++ value properties The library respects C++ value mechanics: @@ -1077,3 +1089,50 @@ For formal validation of the algebraic structures modeled in `libfn`, refer to: 1. Orchard and Petricek, [“Embedding effect systems in Haskell”](https://www.doc.ic.ac.uk/~dorchard/publ/haskell14-effects.pdf) (for effect sets, union, and subeffecting). 2. Orchard, Wadler, and Eades, [“Unifying graded and parameterised monads”](https://arxiv.org/pdf/2001.10274) specifically Definition 21 (for the graded-monad interpretation). 3. McDermott and Uustalu, [“Flexibly Graded Monads and Graded Algebras”](https://dylanm.org/flexibly-graded-monads.pdf) _Note: `libfn` does not claim to fully implement their flexibly graded construction, but the work contextualizes graded structures._ + + diff --git a/examples/type_algebra/main.cpp b/examples/type_algebra/main.cpp index c0aca402..dca67560 100644 --- a/examples/type_algebra/main.cpp +++ b/examples/type_algebra/main.cpp @@ -19,6 +19,7 @@ #include #include +// sync-example-types-def struct Error {}; struct OtherError {}; @@ -27,7 +28,6 @@ struct B {}; struct C {}; struct D {}; -// sync-example-types-def struct UserId {}; struct User {}; struct FilePath {}; diff --git a/scripts/stage_docs_source.py b/scripts/stage_docs_source.py index 89b7ce72..903b0547 100644 --- a/scripts/stage_docs_source.py +++ b/scripts/stage_docs_source.py @@ -8,6 +8,9 @@ a page carries the name alone, and the prose's `Section N` cross-references become links to the pages they name. Repo-relative links, which znai would otherwise resolve as page references and reject, become links to the sibling section or to the file on GitHub. + +The rewrites recognize the markdown these documents use, conservatively: an ambiguous or +unrecognized construct fails the staging rather than guessing its way onto the site. """ from __future__ import annotations @@ -48,6 +51,26 @@ HEADING = re.compile(r"^(#{3,})(?=\s)") CODE_SPAN = re.compile(r"`+[^`]*`+") +# GitHub spells an admonition as a blockquote opening with an alert kind; znai spells it as a +# fenced attention block. znai builds a block only for the types below — `attention-tip` is not +# among them, and an unknown type falls through to a code snippet that prints the markdown +# verbatim, so an unmapped kind fails the staging instead of reaching the site looking like that. +ALERT = re.compile(r"^ {0,3}>\s*\[!([A-Z]+)\]\s*$") +QUOTED = re.compile(r"^ {0,3}> ?") +ATTENTION = {"NOTE": "note", "TIP": "note", "WARNING": "warning"} +# Markers stand in for the block between the two rewrites, so that the alert's own body is +# rewritten as ordinary prose and only then fenced. +ATTENTION_OPEN = "" +ATTENTION_CLOSE = "" +MARKED = re.compile(r"^$(.*?)^$", + re.MULTILINE | re.DOTALL) +# Unquoted openings that end a blockquote rather than continue its paragraph: a heading, a list +# item, a fence and a thematic break. Markdown allows a block three spaces of indent, here as +# above, and a fourth would make the line code rather than any of these. +ENDS_QUOTE = re.compile(r"^ {0,3}(#{1,6}\s|[-*+]\s|\d+[.)]\s|```|~~~|(-{3,}|\*{3,}|_{3,})\s*$)") +FENCE = re.compile(r"^\s*(`{3,}|~{3,})") +BACKTICKS = re.compile(r"^\s*(`{3,})", re.MULTILINE) + def fail(message: str) -> None: sys.stderr.write(f"Error: {message}\n") @@ -61,13 +84,19 @@ def slug(text: str) -> str: def outside_fences(text: str): """Yield (index, line, inside_fence) so no rule fires on fenced content.""" - fence = False + opening = None for index, line in enumerate(text.splitlines()): - if line.lstrip().startswith("```"): - fence = not fence + run = FENCE.match(line) + # A fence closes on its own character, a run no shorter than the one that opened it, and + # nothing after the run — a closer carries no info string — so a fence may quote a shorter + # fence of either kind, or an opening fence of any length, without ending itself. + if run and (opening is None + or (run.group(1)[0] == opening[0] and len(run.group(1)) >= len(opening) + and not line[run.end():].strip())): + opening = None if opening else run.group(1) yield index, line, True continue - yield index, line, fence + yield index, line, opening is not None def split(text: str, where: str) -> tuple[str, str, list[tuple[str, str]]]: @@ -135,6 +164,58 @@ def outside_code(line: str, rewrite) -> str: return "".join(parts) + rewrite(line[last:]) +def unquote_alerts(body: str) -> str: + """Mark GitHub's alert blockquotes and strip the quoting from their bodies. + + The body is freed of its `>` here, ahead of the rewrites below, so that a heading or a link + inside an alert is treated exactly like one outside it; the marker becomes a fence only once + those rewrites have run, since a fenced body is left alone. + """ + lines: list[str] = [] + alert = ALERT.match("") + for _, line, fenced in outside_fences(body): + if fenced: + # A fence may quote alert syntax as an example of it; that is code, not an alert. + if alert is not None: + lines.append(ATTENTION_CLOSE) + alert = None + lines.append(line) + continue + if alert is None and (alert := ALERT.match(line)): + kind = alert.group(1) + if kind not in ATTENTION: + fail(f"alert [!{kind}] has no znai attention block; " + f"known kinds are {', '.join(sorted(ATTENTION))}") + lines.append(ATTENTION_OPEN.format(ATTENTION[kind])) + continue + if alert is not None: + if quoted := QUOTED.match(line): + lines.append(line[quoted.end():]) + continue + # A blockquote also swallows an unquoted line that merely continues its paragraph, and + # only a block of its own ends it. Guessing which this is would silently split the + # alert, so anything but a new block is refused and the author quotes it. + if line.strip() and not ENDS_QUOTE.match(line): + fail(f"alert body continues into an unquoted line: {line.strip()[:60]!r}; " + "prefix it with `>` or separate it with a blank line") + lines.append(ATTENTION_CLOSE) + alert = None + lines.append(line) + if alert is not None: + lines.append(ATTENTION_CLOSE) + return "\n".join(lines) + + +def fence_alerts(body: str) -> str: + """Turn each marked alert into an attention block, fenced longer than anything inside it.""" + def block(match: re.Match[str]) -> str: + inner = match.group(2).strip("\n") + ticks = "`" * max(3, *(len(m.group(1)) + 1 for m in BACKTICKS.finditer(inner)), 3) + return f"{ticks}attention-{match.group(1)}\n{inner}\n{ticks}" + + return MARKED.sub(block, body) + + def render(body: str, page: dict[str, str] | None, number: dict[str, tuple[str, str]], chapter: str | None, repo: pathlib.Path) -> str: """Rewrite a body for the site: heading depth, links, cross-references.""" @@ -149,7 +230,7 @@ def prose(text: str) -> str: else fail(f"reference to Section {m.group(1)}, which {chapter} does not have"), text) lines = [] - for _, line, fenced in outside_fences(body): + for _, line, fenced in outside_fences(unquote_alerts(body)): if not fenced: if BADGE.match(line.strip()): continue @@ -159,7 +240,7 @@ def prose(text: str) -> str: line = HEADING.sub(lambda m: m.group(1)[1:], line) line = outside_code(line, prose) lines.append(line) - return re.sub(r"\n{3,}", "\n\n", "\n".join(lines)) + return fence_alerts(re.sub(r"\n{3,}", "\n\n", "\n".join(lines))) def write(path: pathlib.Path, title: str, body: str) -> None: