diff --git a/.emacs.d/init.el b/.emacs.d/init.el new file mode 100644 index 0000000..ebf3d9c --- /dev/null +++ b/.emacs.d/init.el @@ -0,0 +1,247 @@ +;; Save any custom set variable in exordium-custom-file rather than at the end of init.el: +(setq custom-file (locate-user-emacs-file "custom.el")) + +(require 'package) +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(setq package-user-dir + (locate-user-emacs-file (concat "elpa-" emacs-version))) + +(when (fboundp 'native-comp-available-p) + (setq package-native-compile (native-comp-available-p))) +(package-initialize) + +;; Load the packages we need if they are not installed already +(let ((package-pinned-packages (append + '((use-package . "melpa") + (diminish . "melpa") + (bind-key . "melpa")))) + (has-refreshed nil)) + + (defun update-package (p has-refreshed) + (unless (package-installed-p p) + (unless has-refreshed + (message "Refreshing package database...") + (package-refresh-contents) + (setq has-refreshed t) + (message "Done.")) + (package-install p))) + + (dolist (pkg package-pinned-packages) + (let ((p (car pkg))) + (update-package p has-refreshed)))) + +;; This is only needed once, near the top of the file +(eval-when-compile + ;; Following line is not needed if use-package.el is in ~/.emacs.d + (require 'use-package)) + +(require 'use-package-ensure) +(setq use-package-always-ensure t) +(setq use-package-compute-statistics t) +;;; remove a package from the builtin list so it can be upgraded +(defun wg21org-ignore-builtin (pkg) + (assq-delete-all pkg package--builtins) + (assq-delete-all pkg package--builtin-versions)) + + +;;; Org mode + +(use-package org + :commands (org-mode) + :mode (("\\.org\\'" . org-mode)) + :after (flycheck) + :bind + (:map org-mode-map + ([remap org-toggle-comment] . iedit-mode)) + :custom + (org-confirm-babel-evaluate t) + (org-fontify-quote-and-verse-blocks t) + (org-fontify-whole-heading-line t) + (org-log-into-drawer t) + (org-src-fontify-natively t) + (org-src-preserve-indentation t) + (org-startup-folded nil) + (org-startup-indented t) + (org-startup-truncated nil) + (org-startup-with-inline-images t) + (org-support-shift-select :always) + (org-use-sub-superscripts "{}") + + ;; Edit settings + (org-auto-align-tags nil) + (org-tags-column 0) + (org-catch-invisible-edits 'show-and-error) + (org-special-ctrl-a/e t) + (org-insert-heading-respect-content t) + + ;; Org styling, hide markup etc. + (org-hide-emphasis-markers nil) + (org-pretty-entities t) + + ;; ;;;; code blocks + (org-confirm-babel-evaluate nil) + (org-src-window-setup 'reorganize-frame) + (org-edit-src-persistent-message t) + (org-src-fontify-natively t) + (org-src-preserve-indentation t) + (org-src-tab-acts-natively t) + (org-edit-src-content-indentation 0) + + ;; ;;;; export + (org-export-with-toc t) + (org-export-headline-levels 8) + (org-export-dispatch-use-expert-ui nil) + (org-html-htmlize-output-type 'css) + (org-html-head-include-default-style t) + (org-html-head-include-scripts t) + + ;;; visual line mode + (visual-line-mode 1) + + :hook ((org-mode . variable-pitch-mode)) + + :init + (add-hook 'org-src-mode-hool #'(lambda () + (add-to-list 'flycheck-disabled-checkers 'emacs-lisp-checkdoc))) + + :config + ;; Enable org-babel for perl, ruby, sh, python, emacs-lisp, C, C++, etc + ;; TODO: add extra languages configurable by user + (org-babel-do-load-languages + 'org-babel-load-languages + `((perl . t) + (ruby . t) + (shell . t) + (python . t) + (emacs-lisp . t) + (C . t) + (dot . t) + (sql . t)))) + +(use-package htmlize + :ensure t) + + +(use-package graphviz-dot-mode + :config + (setq graphviz-dot-indent-width 4)) + +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((dot . t)))) + + +(setq plantuml-jar-path "/usr/share/plantuml/plantuml.jar") +(setq plantuml-default-exec-mode 'jar) + +(setq org-plantuml-jar-path (expand-file-name "/usr/share/plantuml/plantuml.jar")) +(add-to-list 'org-src-lang-modes '("plantuml" . plantuml)) +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((plantuml . t)))) + +(org-babel-do-load-languages + 'org-babel-load-languages + (append org-babel-load-languages + '((ditaa . t)))) + +(setq org-ditaa-jar-path "/usr/share/ditaa/ditaa.jar") + +(setq org-support-shift-select 'always) + + +;; Reveal.js + Org mode +(use-package org-re-reveal + :config + (setq org-re-reveal-root "file:////home/sdowney/bld/reveal.js")) + + +(use-package org-transclusion + :after org + :bind (:map + org-mode-map + ("C-c C-x T" . org-transclusion-mode)) + :config + (org-transclusion-mode 1) + ) + + +(use-package ox-gfm + :after org) + +(use-package with-editor) + +(use-package citeproc :ensure t :after org) + +;; Export orgit links as GitHub source browser URLs +(defvar orgit-base-url "https://github.com/steve-downey/expected/blob/main/" + "Base URL for exporting orgit links.") + +(org-link-set-parameters + "orgit" + :export (lambda (path desc backend) + (let* ((parts (split-string path "::")) + (filepath (if (> (length parts) 1) (cadr parts) path)) + (url (concat orgit-base-url filepath))) + (cond + ((or (eq 'md backend) (eq 'gfm backend)) + (format "[`%s`](%s)" (or desc filepath) url)) + ((eq 'html backend) + (format "%s" url (or desc filepath))) + (t url))))) + + + + + + + + + +;; Allow org-transclusion to resolve orgit links to the local file system +(defun org-transclusion-add-orgit (link plist) + "Resolve orgit links into file links in-place." + (when (string= "orgit" (org-element-property :type link)) + (let* ((full-path (org-element-property :path link)) + (parts (split-string full-path "::")) + (repo-dir (car parts)) + (inner-file (cadr parts)) + (search-uuid (if (> (length parts) 2) (caddr parts) nil)) + (actual-file (expand-file-name inner-file repo-dir)) + (raw-link (concat "file:" actual-file))) + + (when search-uuid + (setq raw-link (concat raw-link "::" search-uuid))) + + ;; CRITICAL: Mutate the original link in-place so downstream plugins + ;; (like org-transclusion-src-lines) see the correct file path! + (org-element-put-property link :type "file") + (org-element-put-property link :path actual-file) + (org-element-put-property link :raw-link raw-link) + (if search-uuid + (org-element-put-property link :search-option search-uuid) + (org-element-put-property link :search-option nil)))) + ;; ALWAYS return nil so the NEXT functions (like org-transclusion-add-src-lines) can handle it! + nil) + +(require 'org-transclusion) +(add-hook 'org-transclusion-add-functions 'org-transclusion-add-orgit) + +;; Pinned transclusion for the epistolary blog posts. +;; +;; Two document categories, two policies (docs/epistolary-pinning-plan.md): +;; - living docs (docs/compiler_architecture.org) transclude via `file:' / +;; `orgit:' and roll forward with the worktree, which is the point of them; +;; - blog posts transclude via `orgit-file:' pinned to a `blog/phase-NN' tag, +;; so a later refactor cannot rewrite the code inside a published entry. +;; +;; Both adapters live on `org-transclusion-add-functions' and coexist; the link +;; type selects the policy. See docs/blog/pins.md for the post-to-tag mapping. +(add-to-list 'load-path + (expand-file-name + "lisp" (or (and load-file-name + (file-name-directory load-file-name)) + user-emacs-directory))) +(require 'orgit-file-transclusion) diff --git a/.emacs.d/lisp/orgit-file-transclusion.el b/.emacs.d/lisp/orgit-file-transclusion.el new file mode 100644 index 0000000..86056be --- /dev/null +++ b/.emacs.d/lisp/orgit-file-transclusion.el @@ -0,0 +1,146 @@ +;;; orgit-file-transclusion.el --- transclude UUID-anchored regions at a git rev -*- lexical-binding: t; -*- +;; SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +;; Provenance: docs/epistolary-pinning-plan.md (section 3), shared across +;; compile-time-scheme sibling repositories by copy, not by load-path coupling. +;; +;; Link form: [[orgit-file:REPO::REV::PATH::UUID]] +;; Used with: :lines 2- :src LANG :end "UUID end" (unchanged conventions) +;; +;; Where the sibling `orgit:' adapter in init.el resolves PATH against the +;; worktree -- showing the file as of now -- this one resolves it against REV, +;; so an epistolary post keeps showing the code its prose was written about. +;; REV is normally an annotated `blog/phase-NN' tag; see docs/blog/pins.md. +;; +;; No magit dependency: the blob comes from a `git show' subprocess, so batch +;; export stays light. The interactive `orgit-file' package (gggion/orgit-file) +;; uses the same link syntax, so installing it makes these links followable in +;; a live Emacs, but nothing here requires it. + +;;; Commentary: +;; +;; Implementation note -- why this extracts a blob to a file instead of +;; slicing the region itself: +;; +;; `org-transclusion-add-src-lines' already implements the exact semantics the +;; posts depend on, and they are fiddlier than they look. `:end' is EXCLUSIVE +;; of the line holding the end marker; `:lines 2-' counts forward from the +;; anchor line found by the search option, not from the top of the file; and +;; `:src cpp' wraps the result in a fenced block. Reimplementing that slicing +;; would duplicate three behaviours that must not drift. +;; +;; So this adapter mirrors `org-transclusion-add-orgit': it materialises the +;; pinned blob, rewrites the link in place into a plain `file:' link with the +;; same `::UUID' search option, and returns nil so the standard src-lines +;; handler does the work. Post attributes keep meaning exactly what they +;; meant under worktree resolution -- by construction, not by imitation. +;; +;; The blob is written under a path that preserves the original repo-relative +;; path, so `find-file-noselect' picks the same major mode it would have for +;; the real file. `org-link-search', which resolves the `::UUID' anchor, is +;; mode-sensitive, so this is what keeps anchor resolution identical. + +;;; Code: + +(require 'org-transclusion) +(require 'subr-x) + +(defgroup orgit-file-transclusion nil + "Transclude UUID-anchored source regions pinned to a git revision." + :group 'org-transclusion) + +(defcustom orgit-file-transclusion-cache-dir + (expand-file-name "orgit-file-transclusion" temporary-file-directory) + "Directory holding blobs extracted from git. +Contents are disposable: each blob is cached under its resolved commit +SHA, so a stale entry cannot be served for a different revision." + :type 'directory + :group 'orgit-file-transclusion) + +(defcustom orgit-file-transclusion-base-url + "https://github.com/steve-downey/expected/blob/" + "Base URL for exporting `orgit-file' links, with a trailing slash. +The pinned REV is appended, so an exported link is a permalink to the +revision the post was written against rather than to a moving branch." + :type 'string + :group 'orgit-file-transclusion) + +(defun orgit-file-transclusion--parse (raw) + "Split RAW into (REPO REV PATH UUID); signal on any other shape." + (let ((parts (split-string raw "::"))) + (unless (= 4 (length parts)) + (error "orgit-file link needs REPO::REV::PATH::UUID, got: %s" raw)) + parts)) + +(defun orgit-file-transclusion--rev-parse (repo rev) + "Resolve REV to a full commit SHA in REPO." + (with-temp-buffer + (unless (zerop (call-process "git" nil t nil + "-C" (expand-file-name repo) + "rev-parse" (concat rev "^{commit}"))) + (error "orgit-file: cannot resolve rev %s in %s (fetch tags?): %s" + rev repo (string-trim (buffer-string)))) + (string-trim (buffer-string)))) + +(defun orgit-file-transclusion--blob-file (repo rev path) + "Materialise PATH at REV in REPO as a local file; return its name. +The returned path ends in PATH, so the buffer gets the major mode it +would have had for the real file." + (let* ((sha (orgit-file-transclusion--rev-parse repo rev)) + (cached (expand-file-name + path (expand-file-name sha orgit-file-transclusion-cache-dir)))) + (unless (file-exists-p cached) + (make-directory (file-name-directory cached) t) + ;; Write via a temp name and rename, so a failed `git show' can never + ;; leave a truncated blob behind for a later run to trust. + (let ((tmp (concat cached ".partial"))) + (with-temp-buffer + (unless (zerop (call-process "git" nil t nil + "-C" (expand-file-name repo) + "show" (format "%s:%s" sha path))) + (error "orgit-file: git show %s:%s failed in %s: %s" + rev path repo (string-trim (buffer-string)))) + (let ((coding-system-for-write 'no-conversion)) + (write-region (point-min) (point-max) tmp nil 'quiet))) + (rename-file tmp cached t))) + cached)) + +(defun orgit-file-transclusion-add (link _plist) + "Resolve an `orgit-file' LINK into a `file' link on the pinned blob. +Mutates LINK in place and returns nil, so `org-transclusion-add-src-lines' +applies the post's :lines/:src/:end attributes exactly as it does for a +worktree-resolved link." + (when (string= "orgit-file" (org-element-property :type link)) + (pcase-let* ((`(,repo ,rev ,path ,uuid) + (orgit-file-transclusion--parse + (org-element-property :path link))) + (blob (orgit-file-transclusion--blob-file repo rev path))) + ;; Mutate in place so downstream handlers see the resolved file path. + (org-element-put-property link :type "file") + (org-element-put-property link :path blob) + (org-element-put-property link :raw-link (concat "file:" blob "::" uuid)) + (org-element-put-property link :search-option uuid))) + ;; Always nil: let the next add-function build the payload. + nil) + +(add-hook 'org-transclusion-add-functions #'orgit-file-transclusion-add) + +;; Export `orgit-file' links as forge permalinks at the pinned revision. +;; Posts currently carry these links only on `#+transclude:' lines, which +;; org-transclusion consumes before export, so this is for future inline use. +(org-link-set-parameters + "orgit-file" + :export + (lambda (path desc backend) + (let* ((parts (split-string path "::")) + (rev (nth 1 parts)) + (filepath (or (nth 2 parts) path)) + (url (concat orgit-file-transclusion-base-url rev "/" filepath))) + (cond + ((memq backend '(md gfm)) (format "[`%s`](%s)" (or desc filepath) url)) + ((eq backend 'html) + (format "%s" url (or desc filepath))) + (t url))))) + +(provide 'orgit-file-transclusion) +;;; orgit-file-transclusion.el ends here diff --git a/.gitignore b/.gitignore index 0e68d18..8f0e1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ /uv.lock .build /.claude/ + +# ignore emacs package cache and generated blog artifacts +/.emacs.d/eln-cache/ +/.emacs.d/elpa-*/ +/.emacs.d/elpa/ +/.emacs.d/custom.el +*.deps diff --git a/Makefile b/Makefile index 90a682b..a75cb86 100755 --- a/Makefile +++ b/Makefile @@ -264,6 +264,86 @@ endif install-uv: ## install uv via `pipx install uv` $(install_uv_cmd) +# ------------------------------------------------------------------------------ +# Blog: org-mode -> GFM markdown, with UUID-anchored source transclusion. +# +# docs/blog/*.org posts pull code out of the tree with org-transclusion, +# resolved by the elisp in .emacs.d/. orgit-file: links are pinned to a +# committed git rev, so a published post keeps showing the code its prose was +# written about. See docs/blog/pins.md for the post-to-rev mapping. +# ------------------------------------------------------------------------------ +EMACS := $(shell command -v emacs 2> /dev/null) + +ORGFILES := $(wildcard *.org) + +%.html : %.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(org-export-to-file 'html \"$@\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(ORGFILES:%.org=%.html.deps)) + +%-slides.html : %.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(org-export-to-file 're-reveal \"$@\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(ORGFILES:%.org=%-slides.html.deps)) + +BLOG_ORGFILES := $(wildcard docs/blog/*.org) + +docs/blog/%.md : docs/blog/%.org + $(EMACS) --init-directory=.emacs.d/ \ + --batch --load .emacs.d/init.el \ + -f package-initialize \ + --eval "(setq enable-local-variables :all)" \ + --visit $< \ + --eval "(org-transclusion-mode t)" \ + --eval "(require 'ox-gfm)" \ + --eval "(org-export-to-file 'gfm \"$(abspath $@)\")" + echo $@ : \\ > $@.deps + echo " $<" \\ >> $@.deps + sed -n \ + -e "s/^.*\[\[file:\(\S*\)::.*$$/\1/p" \ + -e "s/^.*\[\[orgit:[^:]*::\([^:]*\)::.*$$/\1/p" \ + < $< | sort -u | xargs printf " %s \\\\\\n" >> $@.deps + +-include $(wildcard $(BLOG_ORGFILES:.org=.md.deps)) + +.PHONY: blog-md +blog-md: $(BLOG_ORGFILES:.org=.md) ## convert docs/blog/*.org to GFM markdown + +.PHONY: clean-blog-md +clean-blog-md: + -rm -f $(BLOG_ORGFILES:.org=.md) $(BLOG_ORGFILES:.org=.md.deps) +clean: clean-blog-md + + +.PHONY: clean-emacs.d +clean-emacs.d: + -rm -rf .emacs.d/eln-cache + -rm -rf .emacs.d/elpa* + +realclean: clean-emacs.d + +.PHONY: clean-org-deps +clean-org-deps: + -rm $(ORGFILES:%.org=%.org.deps) + # Help target .PHONY: help help: ## Show this help. diff --git a/cmake/gcc-16-toolchain.cmake b/cmake/gcc-16-toolchain.cmake index ae1bb01..b7310d3 100644 --- a/cmake/gcc-16-toolchain.cmake +++ b/cmake/gcc-16-toolchain.cmake @@ -15,3 +15,17 @@ set(CMAKE_CXX_FLAGS_ASAN "C++ ASAN Flags" FORCE ) + +# Reflection (P2996). This belongs here rather than in gcc-flags.cmake, which +# is shared with gcc-12 through gcc-15: -freflection is a gcc-16 feature, and +# gcc rejects it outright below -std=c++26 rather than warning. gcc-flags.cmake +# already pins -std=gnu++26, so the requirement is met. +# +# Enabling it selects the reflection implementation of +# tests/beman/expected/testing/type_name.hpp, which is otherwise dormant. +set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -freflection" + CACHE STRING + "CXX_FLAGS" + FORCE +) diff --git a/docs/blog/pins.md b/docs/blog/pins.md new file mode 100644 index 0000000..ed9301c --- /dev/null +++ b/docs/blog/pins.md @@ -0,0 +1,23 @@ +# Blog transclusion pins + +Each post that transcludes live code is pinned to one annotated tag. +`#+transclude:` links resolve against that tag's tree, not against the +worktree, so a later refactor cannot rewrite the code inside an already +published entry. The transclusion machinery is the copy of `.emacs.d/` and the +`blog-md` Makefile target carried over from the `compile-time-scheme` +repository; see `.emacs.d/lisp/orgit-file-transclusion.el`. + +## The mapping + +| Post | Tag | Basis | +|---|---|---| +| `scrap-your-static_assert.org` | `blog/scrap-static-assert` | commit adding the UUID anchors and the post | + +## Notes + +`orgit-file:` links pin to a tag, so a pinned post's `.md.deps` names only its +own `.org` and not the transcluded sources — that is correct, not a bug to +repair. The code comes from an immutable tag, so there is no worktree +dependency to track; rebuilding the post when the working tree changes would be +the defect. The `file:`/`orgit:` dependency extraction in the Makefile stays +useful only for any living document that still resolves against the worktree. diff --git a/docs/blog/scrap-your-static_assert.md b/docs/blog/scrap-your-static_assert.md new file mode 100644 index 0000000..7523930 --- /dev/null +++ b/docs/blog/scrap-your-static_assert.md @@ -0,0 +1,85 @@ +The obvious way to test a compile-time fact is `static_assert`. It's right there, it needs no framework, and for a fact that has to hold it's the right tool. As a *test*, though, it has one bad property: a wrong answer is a translation failure. The build stops at the first one, you get a compiler diagnostic instead of a test result, and every other test in the file goes unrun. The xUnit report is empty. You learn that something is wrong, once, and nothing about the rest. + +There's a second, smaller problem. Even when you write the check as a runtime `CHECK` so that it gets reported, a bare trait doesn't report anything you can use: + +```C++ +CHECK(std::is_same_v); // FAILED: CHECK( false ) +``` + +The expansion is the word `false`. You already knew the two types differed; the framework won't tell you what either of them was. + +Converting the `expected` tests off `static_assert` came down to two header-only components that fix these two problems. Neither is clever. (The title owes Lämmel and Peyton Jones; the debt stops at the title.) + + +# Type identity as a value + +The fix for the second problem is to compare type *identities* that carry their spelling for diagnostics, instead of comparing a bool. See [`type\_name.hpp`](https://github.com/steve-downey/expected/blob/main/tests/beman/expected/testing/type_name.hpp). The comparison is still `std::is_same_v`, so the verdict is exact and a false pass isn't possible: + +```cpp +template +constexpr bool beman::expected::testing::operator==(type_name_t, type_name_t) { + return std::is_same_v; +} +``` + +The spelling is consulted only after a comparison has already failed and the framework needs to explain it. So a failing check explains itself: + +```text +FAILED: CHECK( type_name() == type_name() ) +with expansion: const int& == int& +``` + +And the tests read like the trait they replaced: + +```cpp +TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); +} +``` + + +# Reporting a compile-time value at runtime + +The fix for the first problem is to split the two questions a constexpr test actually asks. "Can this be constant-evaluated at all?" is a property of the code; it stays a hard translation failure, which is correct, because that's a fact that has to hold. "Does it produce the right answer?" is a property of a value, and there's no reason a wrong value should stop the build. + +[`constant\_eval.hpp`](https://github.com/steve-downey/expected/blob/main/tests/beman/expected/testing/constant_eval.hpp) is `consteval`, so a call to it is evaluated during translation. If the probe body isn't usable in a constant expression the program is ill-formed, and the first question is answered by the call itself, with no `static_assert` needed. The result then behaves as an ordinary prvalue, free to be handed to `CHECK`. The whole thing is a one-line wrapper: + +```cpp +template +consteval auto beman::expected::testing::constant_eval(Probe probe) { + return probe(); +} +``` + +A probe is a plain lambda that reduces what it observes to a literal aggregate: + +```cpp +TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { + constexpr auto probe = [] { + constexpr expt::expected e; + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 0}); + CHECK(probe() == int_state{true, 0}); +} +``` + +Because the probe is a plain lambda and not a `consteval` one, the same body runs in both evaluation modes. Constant evaluation and ordinary evaluation can take different paths through a union-based type like `expected`, so running both earns its second line: + +```C++ +CHECK(constant_eval(probe) == expect); // constant evaluation +CHECK(probe() == expect); // ordinary evaluation +``` + +Give the returned aggregate an `operator<<`. Without one, Catch2 prints `{?} == {?}` and you're back where `static_assert` left you. + + +# What it buys + +Two things. The reporting is better: a mismatch names both types, or prints both states, instead of expanding to `false` or stopping at a diagnostic before it can say anything. And a wrong answer is no longer a compile failure that blocks everything behind it. The suite builds, runs, and reports every case; a broken trait shows up as one red line among the green, with the rest of the run intact. + +None of this abolishes `static_assert`. The genuinely ill-formed cases stay ill-formed, checked in their own negative-compilation files. What moved to runtime is only the part that was a test wearing an assertion's clothes. diff --git a/docs/blog/scrap-your-static_assert.org b/docs/blog/scrap-your-static_assert.org new file mode 100644 index 0000000..2308540 --- /dev/null +++ b/docs/blog/scrap-your-static_assert.org @@ -0,0 +1,95 @@ +#+title: Scrap your ~static_assert~ +#+date: <2026-08-10> +#+author: Steve Downey +#+OPTIONS: toc:nil num:nil ^:nil + +The obvious way to test a compile-time fact is ~static_assert~. It's right +there, it needs no framework, and for a fact that has to hold it's the right +tool. As a /test/, though, it has one bad property: a wrong answer is a +translation failure. The build stops at the first one, you get a compiler +diagnostic instead of a test result, and every other test in the file goes +unrun. The xUnit report is empty. You learn that something is wrong, once, and +nothing about the rest. + +There's a second, smaller problem. Even when you write the check as a runtime +~CHECK~ so that it gets reported, a bare trait doesn't report anything you can +use: + +#+begin_src C++ +CHECK(std::is_same_v); // FAILED: CHECK( false ) +#+end_src + +The expansion is the word ~false~. You already knew the two types differed; the +framework won't tell you what either of them was. + +Converting the =expected= tests off ~static_assert~ came down to two +header-only components that fix these two problems. Neither is clever. +(The title owes Lämmel and Peyton Jones; the debt stops at the title.) + +* Type identity as a value + +The fix for the second problem is to compare type /identities/ that carry their +spelling for diagnostics, instead of comparing a bool. See +[[orgit:~/src/steve-downey/expected/scrap-static-assert::tests/beman/expected/testing/type_name.hpp][type_name.hpp]]. The +comparison is still ~std::is_same_v~, so the verdict is exact and a false pass +isn't possible: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/testing/type_name.hpp::d1c7602e-a42f-46ab-bd53-7adef5646545]] :lines 2- :src cpp :end "d1c7602e-a42f-46ab-bd53-7adef5646545 end" + +The spelling is consulted only after a comparison has already failed and the +framework needs to explain it. So a failing check explains itself: + +#+begin_src text +FAILED: CHECK( type_name() == type_name() ) +with expansion: const int& == int& +#+end_src + +And the tests read like the trait they replaced: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/expected.test.cpp::52697d22-633f-4e07-b364-5b5db41ca2a7]] :lines 2- :src cpp :end "52697d22-633f-4e07-b364-5b5db41ca2a7 end" + +* Reporting a compile-time value at runtime + +The fix for the first problem is to split the two questions a constexpr test +actually asks. "Can this be constant-evaluated at all?" is a property of the +code; it stays a hard translation failure, which is correct, because that's a +fact that has to hold. "Does it produce the right answer?" is a property of a +value, and there's no reason a wrong value should stop the build. + +[[orgit:~/src/steve-downey/expected/scrap-static-assert::tests/beman/expected/testing/constant_eval.hpp][constant_eval.hpp]] +is ~consteval~, so a call to it is evaluated during translation. If the probe +body isn't usable in a constant expression the program is ill-formed, and the +first question is answered by the call itself, with no ~static_assert~ needed. +The result then behaves as an ordinary prvalue, free to be handed to ~CHECK~. +The whole thing is a one-line wrapper: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/testing/constant_eval.hpp::f0b2e22a-6ab4-4112-b826-297a776f01f1]] :lines 2- :src cpp :end "f0b2e22a-6ab4-4112-b826-297a776f01f1 end" + +A probe is a plain lambda that reduces what it observes to a literal aggregate: + +#+transclude: [[orgit-file:~/src/steve-downey/expected/scrap-static-assert::blog/scrap-static-assert::tests/beman/expected/expected.test.cpp::a525eae8-2bb0-4cae-aeaf-b134cbffef1c]] :lines 2- :src cpp :end "a525eae8-2bb0-4cae-aeaf-b134cbffef1c end" + +Because the probe is a plain lambda and not a ~consteval~ one, the same body +runs in both evaluation modes. Constant evaluation and ordinary evaluation can +take different paths through a union-based type like ~expected~, so running both +earns its second line: + +#+begin_src C++ +CHECK(constant_eval(probe) == expect); // constant evaluation +CHECK(probe() == expect); // ordinary evaluation +#+end_src + +Give the returned aggregate an ~operator<<~. Without one, Catch2 prints ~{?} == +{?}~ and you're back where ~static_assert~ left you. + +* What it buys + +Two things. The reporting is better: a mismatch names both types, or prints both +states, instead of expanding to ~false~ or stopping at a diagnostic before it +can say anything. And a wrong answer is no longer a compile failure that blocks +everything behind it. The suite builds, runs, and reports every case; a broken +trait shows up as one red line among the green, with the rest of the run intact. + +None of this abolishes ~static_assert~. The genuinely ill-formed cases stay +ill-formed, checked in their own negative-compilation files. What moved to +runtime is only the part that was a test wearing an assertion's clothes. diff --git a/tests/beman/expected/bad_expected_access.test.cpp b/tests/beman/expected/bad_expected_access.test.cpp index 3b9c010..c8eef7a 100644 --- a/tests/beman/expected/bad_expected_access.test.cpp +++ b/tests/beman/expected/bad_expected_access.test.cpp @@ -5,26 +5,41 @@ #include +#include + #include #include +#include #include namespace expt = test_ns; +using beman::expected::testing::type_name; + // ============================================================================= -// [expected.bad.void] and [expected.bad] — type-level assertions +// [expected.bad.void] and [expected.bad] — type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing the compiler's spelling of +// the two types, so a mismatch prints what was deduced next to what was +// wanted rather than the bare word `false`. // ============================================================================= -// Inheritance chain -static_assert(std::is_base_of_v>); -static_assert(std::is_base_of_v, expt::bad_expected_access>); -static_assert(std::is_base_of_v>); +TEST_CASE("bad_expected_access: inheritance chain", "[BadExpectedAccessTest]") { + CHECK(std::is_base_of_v>); + CHECK(std::is_base_of_v, expt::bad_expected_access>); + CHECK(std::is_base_of_v>); +} -// error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("bad_expected_access: error() ref-qualification return types", "[BadExpectedAccessTest]") { + using bad_access_t = expt::bad_expected_access; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} TEST_CASE("bad_expected_access: breathing", "[BadExpectedAccessTest]") {} diff --git a/tests/beman/expected/expected.test.cpp b/tests/beman/expected/expected.test.cpp index 8b4a0e0..544ae0e 100644 --- a/tests/beman/expected/expected.test.cpp +++ b/tests/beman/expected/expected.test.cpp @@ -1,12 +1,15 @@ // beman/expected/expected.test.cpp -*-C++-*- // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -#include "test_expected.hpp" +#include #include -#include "testing/types.hpp" +#include +#include +#include +#include #include #include #include @@ -15,8 +18,11 @@ namespace expt = test_ns; +using beman::expected::testing::constant_eval; +using beman::expected::testing::type_name; + // ============================================================================= -// Helper types at namespace scope (needed for static_assert outside functions) +// Helper types, at namespace scope because more than one test case uses them // ============================================================================= struct NoDefault { @@ -42,51 +48,66 @@ struct MightThrow { }; // ============================================================================= -// [expected.object.general] para 2-3 — type-level static assertions +// [expected.object.general] para 2-3 — type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing the compiler's spelling of +// the two types, so a mismatch prints what was deduced next to what was +// wanted rather than the bare word `false`. // ============================================================================= // Ill-formed T: reference type — tested via negative compile file expected_t_ref_fail.cpp // Ill-formed E: reference type — tested via negative compile file expected_e_ref_fail.cpp // Ill-formed T: array type — tested via negative compile file expected_t_array_fail.cpp -// Default constructor: requires is_default_constructible_v -static_assert(!std::is_default_constructible_v>); +TEST_CASE("expected: special member availability and noexcept", "[ExpectedTest]") { + // Default constructor: requires is_default_constructible_v + CHECK_FALSE(std::is_default_constructible_v>); -// Copy constructor: not present when T is not copy-constructible -static_assert(!std::is_copy_constructible_v>); + // Copy constructor: not present when T is not copy-constructible + CHECK_FALSE(std::is_copy_constructible_v>); -// Destructor: trivially destructible when T and E are -static_assert(std::is_trivially_destructible_v>); + // Destructor: trivially destructible when T and E are + CHECK(std::is_trivially_destructible_v>); -// Move constructor: noexcept when T and E are nothrow-move-constructible -static_assert(std::is_nothrow_move_constructible_v>); -static_assert(!std::is_nothrow_move_constructible_v>); + // Move constructor: noexcept when T and E are nothrow-move-constructible + CHECK(std::is_nothrow_move_constructible_v>); + CHECK_FALSE(std::is_nothrow_move_constructible_v>); -// Move assignment: noexcept when all four noexcept conditions hold -static_assert(std::is_nothrow_move_assignable_v>); + // Move assignment: noexcept when all four noexcept conditions hold + CHECK(std::is_nothrow_move_assignable_v>); +} -// operator* ref-qualification return types -static_assert(std::is_same_v&>()), int&>); -static_assert(std::is_same_v&>()), const int&>); -static_assert(std::is_same_v&&>()), int&&>); -static_assert(std::is_same_v&&>()), const int&&>); +// 52697d22-633f-4e07-b364-5b5db41ca2a7 +TEST_CASE("expected: operator* ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); +} +// 52697d22-633f-4e07-b364-5b5db41ca2a7 end -// error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("expected: error() ref-qualification return types", "[ExpectedTest]") { + using expected_t = expt::expected; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} // ============================================================================= // Type aliases // ============================================================================= TEST_CASE("expected: type aliases", "[ExpectedTest]") { - static_assert(std::is_same_v::value_type, int>); - static_assert(std::is_same_v::error_type, std::string>); - static_assert(std::is_same_v::unexpected_type, expt::unexpected>); - static_assert( - std::is_same_v::rebind, expt::expected>); + using expected_t = expt::expected; + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name>()); + CHECK(type_name>() == type_name>()); } // ============================================================================= @@ -697,30 +718,75 @@ TEST_CASE("expected: cross-type equality error", "[ExpectedTest]") { // ============================================================================= // Constexpr usage +// +// Each of these declares the expected as a constexpr object inside a +// self-contained probe lambda and reduces its state to a literal aggregate. The +// constexpr declaration keeps "is this usable as a constexpr variable?" answered +// by the compiler; `constant_eval` runs the probe during translation — so "is +// this usable in a constant expression?" is answered too — and hands back the +// result as an ordinary value, +// so "did it produce the right state?" is answered by a reported CHECK. +// Calling the same probe directly runs the identical body at runtime, which +// is worth doing separately: constant evaluation and ordinary evaluation take +// different paths through a union-based type. // ============================================================================= +namespace { +// The observable state of an expected, reduced to literal types. +// Streamable so that a mismatch prints both states; without an inserter +// Catch2 reports `{?} == {?}` and the reporting benefit is lost. +struct int_state { + bool has_value; + int observed; // *e when has_value is true, e.error() otherwise + + // HIDDEN FRIENDS + friend constexpr bool operator==(const int_state&, const int_state&) = default; + friend std::ostream& operator<<(std::ostream& os, const int_state& s) { + return os << (s.has_value ? "{ value " : "{ error ") << s.observed << " }"; + } +}; +} // namespace + +// a525eae8-2bb0-4cae-aeaf-b134cbffef1c TEST_CASE("expected: constexpr default construction", "[ExpectedTest]") { - constexpr expt::expected e; - static_assert(e.has_value()); - static_assert(*e == 0); + constexpr auto probe = [] { + constexpr expt::expected e; + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 0}); + CHECK(probe() == int_state{true, 0}); } +// a525eae8-2bb0-4cae-aeaf-b134cbffef1c end TEST_CASE("expected: constexpr value construction", "[ExpectedTest]") { - constexpr expt::expected e(42); - static_assert(e.has_value()); - static_assert(*e == 42); + constexpr auto probe = [] { + constexpr expt::expected e(42); + return int_state{e.has_value(), *e}; + }; + CHECK(constant_eval(probe) == int_state{true, 42}); + CHECK(probe() == int_state{true, 42}); } TEST_CASE("expected: constexpr error construction", "[ExpectedTest]") { - constexpr expt::expected e(expt::unexpect, 7); - static_assert(!e.has_value()); - static_assert(e.error() == 7); + constexpr auto probe = [] { + constexpr expt::expected e(expt::unexpect, 7); + return int_state{e.has_value(), e.error()}; + }; + CHECK(constant_eval(probe) == int_state{false, 7}); + CHECK(probe() == int_state{false, 7}); } TEST_CASE("expected: constexpr equality", "[ExpectedTest]") { - constexpr expt::expected a(42); - constexpr expt::expected b(42); - static_assert(a == b); + // The answer here is a bare bool, so there is no richer value to report; + // the win is that a wrong answer is still a reported failure rather than + // a build break, and the rest of the file still runs. + constexpr auto probe = [] { + constexpr expt::expected a(42); + constexpr expt::expected b(42); + return a == b; + }; + CHECK(constant_eval(probe)); + CHECK(probe()); } // ============================================================================= @@ -796,7 +862,7 @@ TEST_CASE("expected: operator-> returns address of value", "[ExpectedTest]") { TEST_CASE("expected: emplace with nothrow-constructible type", "[ExpectedTest]") { // int is nothrow constructible — emplace must be available - static_assert(std::is_nothrow_constructible_v); + CHECK(std::is_nothrow_constructible_v); expt::expected e(expt::unexpect, "err"); int& r = e.emplace(99); CHECK(r == 99); @@ -809,10 +875,11 @@ TEST_CASE("expected: emplace with nothrow-constructible type", "[ExpectedTest]") // ============================================================================= TEST_CASE("expected: value() ref-qualification return types", "[ExpectedTest]") { - static_assert(std::is_same_v&>().value()), int&>); - static_assert(std::is_same_v&>().value()), const int&>); - static_assert(std::is_same_v&&>().value()), int&&>); - static_assert(std::is_same_v&&>().value()), const int&&>); + using expected_t = expt::expected; + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); } // ============================================================================= @@ -1120,14 +1187,14 @@ TEST_CASE("cross-eq: expected == expected", "[ExpectedTest][ // --------------------------------------------------------------------------- TEST_CASE("constraint: from_expected is derived from expected", "[ExpectedTest][constraint]") { - static_assert(std::is_base_of_v, from_expected>); + CHECK(std::is_base_of_v, from_expected>); from_expected fe(42); CHECK(fe.has_value()); CHECK(*fe == 42); } TEST_CASE("constraint: from_unexpected is derived from unexpected", "[ExpectedTest][constraint]") { - static_assert(std::is_base_of_v, from_unexpected>); + CHECK(std::is_base_of_v, from_unexpected>); from_unexpected fu(7); CHECK(fu.error() == 7); } diff --git a/tests/beman/expected/expected_constraints.test.cpp b/tests/beman/expected/expected_constraints.test.cpp index ca53c5f..ccdea59 100644 --- a/tests/beman/expected/expected_constraints.test.cpp +++ b/tests/beman/expected/expected_constraints.test.cpp @@ -11,6 +11,18 @@ using namespace beman::expected; +// ============================================================================= +// How these constraints are checked +// +// Each constraint below reduces to a trait, so it is a plain bool. They are +// checked at runtime rather than at translation time so that a constraint that +// has drifted is reported by the test run, naming the type and the polarity +// responsible, instead of stopping the build at the first failure and +// reporting nothing about the rest. A constraint that must exclude an overload +// is checked with CHECK_FALSE; the explanatory text each check used to carry is +// kept as an INFO, so it is printed when that check is the one that fails. +// ============================================================================= + // --------------------------------------------------------------------------- // Converting constructor: bool exemption (constraint 18.3) // @@ -33,9 +45,10 @@ TEST_CASE("converting ctor: expected from expected error path CHECK(dst.error() == 7); } -// expected IS constructible from expected (converting ctor selected) -static_assert(std::is_constructible_v, const expected&>, - "expected must be constructible from expected via converting ctor"); +TEST_CASE("converting ctor: expected is constructible from expected", "[constraints]") { + INFO("expected must be constructible from expected via converting ctor"); + CHECK(std::is_constructible_v, const expected&>); +} // --------------------------------------------------------------------------- // Value constructor: unexpected guard (constraint 23.4) @@ -69,10 +82,12 @@ TEST_CASE("value ctor: unexpected blocked as value even when T is construct // Value ctor is blocked for U = unexpected: is_constructible via value ctor // path requires U not be an unexpected specialization. Verify by checking that -// the overall construction resolves correctly (above test covers behavior; -// the static_assert below checks the trait): -static_assert(std::is_constructible_v, unexpected>, - "construction must still work — via unexpected ctor, not value ctor"); +// the overall construction resolves correctly (the test above covers the +// behavior; the check below covers the trait): +TEST_CASE("value ctor: expected is still constructible from unexpected", "[constraints]") { + INFO("construction must still work — via unexpected ctor, not value ctor"); + CHECK(std::is_constructible_v, unexpected>); +} // --------------------------------------------------------------------------- // Value assignment: unexpected goes to unexpected overload (constraint 11.2) @@ -96,17 +111,30 @@ struct ThrowingMove { ThrowingMove& operator=(ThrowingMove&&) = default; }; -// Both T and E are throwing-move: move assignment must be deleted -static_assert(!std::is_move_assignable_v>, - "move assignment must be deleted when neither T nor E is nothrow move constructible"); - -// At least one nothrow-move: move assignment must exist -static_assert(std::is_move_assignable_v>, - "move assignment must be available when T is nothrow move constructible"); -static_assert(std::is_move_assignable_v>, - "move assignment must be available when E is nothrow move constructible"); -static_assert(std::is_move_assignable_v>, - "move assignment must be available when both are nothrow move constructible"); +TEST_CASE("move assignment: availability follows the nothrow-move-constructible condition", "[constraints]") { + // Each check is braced so that its INFO is scoped to it alone, and only the + // explanation belonging to a failing check is printed. + + // Both T and E are throwing-move: move assignment must be deleted + { + INFO("move assignment must be deleted when neither T nor E is nothrow move constructible"); + CHECK_FALSE(std::is_move_assignable_v>); + } + + // At least one nothrow-move: move assignment must exist + { + INFO("move assignment must be available when T is nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } + { + INFO("move assignment must be available when E is nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } + { + INFO("move assignment must be available when both are nothrow move constructible"); + CHECK(std::is_move_assignable_v>); + } +} // --------------------------------------------------------------------------- // operator==(expected, T2): T2 must not be an expected specialization @@ -131,8 +159,11 @@ TEST_CASE("operator==: expected compared to int uses value overload", "[constrai // The T2 value overload must NOT fire when T2 is itself an expected specialization. // is_constructible check: operator==(expected, expected) must // resolve via the expected friend, not the T2 value friend. -// (Behavioral coverage above; static check: ensure T2=expected doesn't pick value overload.) -static_assert( - !std:: - is_invocable_r_v, expected>, - "sanity: plain int equality lambda cannot be called with expected args"); +// (Behavioral coverage above; the check below ensures T2=expected doesn't pick +// the value overload.) +using int_equality_t = decltype([](int x, int y) { return x == y; }); + +TEST_CASE("operator==: a plain int equality callable is not invocable with expected arguments", "[constraints]") { + INFO("sanity: plain int equality lambda cannot be called with expected args"); + CHECK_FALSE((std::is_invocable_r_v, expected>)); +} diff --git a/tests/beman/expected/expected_hardened.test.cpp b/tests/beman/expected/expected_hardened.test.cpp index 1560d89..34f30e7 100644 --- a/tests/beman/expected/expected_hardened.test.cpp +++ b/tests/beman/expected/expected_hardened.test.cpp @@ -9,6 +9,7 @@ #include #include +#include using namespace beman::expected; @@ -91,6 +92,12 @@ TEST_CASE("hardened: error() on error-state expected", "[hardened]") { // --------------------------------------------------------------------------- // unexpected friend swap: constraint check (beman-only) +// +// The constraint is checked at runtime rather than with static_assert so that a +// violation is reported by the test run, with the responsible type named, +// instead of stopping the build and reporting nothing. Querying the trait at +// all is still the point: an unconstrained hidden-friend swap would make +// is_swappable_v a hard error rather than a well-formed `false`. // --------------------------------------------------------------------------- struct NonSwappable { @@ -100,5 +107,8 @@ struct NonSwappable { NonSwappable& operator=(const NonSwappable&) = delete; NonSwappable& operator=(NonSwappable&&) = delete; }; -static_assert(!std::is_swappable_v); -static_assert(!std::is_swappable_v>); + +TEST_CASE("hardened: unexpected is not swappable when E is not", "[hardened]") { + CHECK_FALSE(std::is_swappable_v); + CHECK_FALSE(std::is_swappable_v>); +} diff --git a/tests/beman/expected/expected_monadic.test.cpp b/tests/beman/expected/expected_monadic.test.cpp index a81565e..dd05df7 100644 --- a/tests/beman/expected/expected_monadic.test.cpp +++ b/tests/beman/expected/expected_monadic.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,6 +15,8 @@ using namespace test_ns; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- // and_then // --------------------------------------------------------------------------- @@ -144,7 +148,7 @@ TEST_CASE("or_else: value passes through chain", "[expected_monadic]") { TEST_CASE("transform: has value - transforms", "[expected_monadic]") { expected e(6); auto r = e.transform([](int v) { return v * 7; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -165,7 +169,7 @@ TEST_CASE("transform: void return type", "[expected_monadic]") { expected e(1); int count = 0; auto r = e.transform([&](int) { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(count == 1); } @@ -174,7 +178,7 @@ TEST_CASE("transform: void return - error state does not call F", "[expected_mon expected e(unexpect, "no"); int count = 0; auto r = e.transform([&](int) { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(count == 0); CHECK(r.error() == "no"); @@ -183,7 +187,7 @@ TEST_CASE("transform: void return - error state does not call F", "[expected_mon TEST_CASE("transform: type change", "[expected_monadic]") { expected e(42); auto r = e.transform([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == "42"); } @@ -216,7 +220,7 @@ TEST_CASE("transform: const rvalue overload", "[expected_monadic]") { TEST_CASE("transform_error: has error - transforms", "[expected_monadic]") { expected e(unexpect, 3); auto r = e.transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } diff --git a/tests/beman/expected/expected_monadic_constraints.test.cpp b/tests/beman/expected/expected_monadic_constraints.test.cpp index f9aa5bb..359d3a7 100644 --- a/tests/beman/expected/expected_monadic_constraints.test.cpp +++ b/tests/beman/expected/expected_monadic_constraints.test.cpp @@ -13,6 +13,19 @@ using namespace beman::expected; +// ============================================================================= +// How these constraints are checked +// +// Every check below is the satisfaction of a detector concept, so it is a +// plain bool. They are checked at runtime rather than with static_assert so +// that a constraint that has drifted is reported by the test run, naming the +// operation and the value category responsible, instead of stopping the build +// at the first failure and reporting nothing about the rest. +// +// The polarity matters as much as the value: an operation that must be +// constrained *out* is checked with CHECK_FALSE. +// ============================================================================= + // A type that is not constructible from lvalue ref (only move-constructible) struct MoveOnly { MoveOnly() = default; @@ -41,65 +54,77 @@ concept has_transform_error = requires(F f) { std::declval().transform_error( // MoveOnly as E: lvalue overloads (&, const&) are constrained out because // E is not copy-constructible, so is_constructible_v is false. -using MoveOnlyErr = expected; -[[maybe_unused]] auto dummy_and_then = [](int) { return expected(); }; - -static_assert(!has_and_then); -static_assert(has_and_then); -static_assert(!has_and_then); - +// +// The aliases and callables stay at namespace scope, next to the section they +// belong to: naming them there keeps each detector-concept argument short +// enough to read, and each case is then a list of the constraints alone. +using MoveOnlyErr = expected; +[[maybe_unused]] auto dummy_and_then = [](int) { return expected(); }; [[maybe_unused]] auto dummy_transform = [](int) { return 0; }; -static_assert(!has_transform); -static_assert(has_transform); -static_assert(!has_transform); +TEST_CASE("monadic constraints: and_then / transform on expected", "[monadic][constraints]") { + // Lvalue overloads require is_constructible_v, which fails for a + // move-only E, so only the rvalue overload survives. + CHECK_FALSE(has_and_then); + CHECK(has_and_then); + CHECK_FALSE(has_and_then); + + CHECK_FALSE(has_transform); + CHECK(has_transform); + CHECK_FALSE(has_transform); +} // --------------------------------------------------------------------------- // Primary template: or_else / transform_error need T constructible from *this // --------------------------------------------------------------------------- -using MoveOnlyVal = expected; -[[maybe_unused]] auto dummy_or_else = [](int) { return expected(); }; - -static_assert(!has_or_else); -static_assert(has_or_else); -static_assert(!has_or_else); - +using MoveOnlyVal = expected; +[[maybe_unused]] auto dummy_or_else = [](int) { return expected(); }; [[maybe_unused]] auto dummy_transform_error = [](int) { return 0; }; -static_assert(!has_transform_error); -static_assert(has_transform_error); -static_assert(!has_transform_error); +TEST_CASE("monadic constraints: or_else / transform_error on expected", "[monadic][constraints]") { + // Lvalue overloads require is_constructible_v, which fails for a + // move-only T, so only the rvalue overload survives. + CHECK_FALSE(has_or_else); + CHECK(has_or_else); + CHECK_FALSE(has_or_else); + + CHECK_FALSE(has_transform_error); + CHECK(has_transform_error); + CHECK_FALSE(has_transform_error); +} // --------------------------------------------------------------------------- // Void specialization: and_then / transform need E constructible from error() // --------------------------------------------------------------------------- -using VoidMoveOnlyErr = expected; -[[maybe_unused]] auto void_dummy_and_then = []() { return expected(); }; - -static_assert(!has_and_then); -static_assert(has_and_then); -static_assert(!has_and_then); - +using VoidMoveOnlyErr = expected; +[[maybe_unused]] auto void_dummy_and_then = []() { return expected(); }; [[maybe_unused]] auto void_dummy_transform = []() { return 0; }; -static_assert(!has_transform); -static_assert(has_transform); -static_assert(!has_transform); +TEST_CASE("monadic constraints: and_then / transform on expected", "[monadic][constraints]") { + CHECK_FALSE(has_and_then); + CHECK(has_and_then); + CHECK_FALSE(has_and_then); + + CHECK_FALSE(has_transform); + CHECK(has_transform); + CHECK_FALSE(has_transform); +} // --------------------------------------------------------------------------- // Void specialization: or_else / transform_error have NO constraints // --------------------------------------------------------------------------- -// or_else and transform_error on void specialization should always be available -[[maybe_unused]] auto void_dummy_or_else = [](MoveOnly&&) { return expected(); }; - -static_assert(has_or_else); - +[[maybe_unused]] auto void_dummy_or_else = [](MoveOnly&&) { return expected(); }; [[maybe_unused]] auto void_dummy_transform_error = [](MoveOnly&&) { return 0; }; -static_assert(has_transform_error); +TEST_CASE("monadic constraints: void or_else / transform_error are unconstrained", "[monadic][constraints]") { + // Nothing is required of T, because there is no T to reconstruct, so these + // are available even for an error type that is only move-constructible. + CHECK(has_or_else); + CHECK(has_transform_error); +} // --------------------------------------------------------------------------- // Normal types: all operations remain available @@ -111,14 +136,16 @@ using NormalExpected = expected; [[maybe_unused]] auto normal_transform = [](int) { return 42; }; [[maybe_unused]] auto normal_transform_err = [](int) { return 42; }; -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); +TEST_CASE("monadic constraints: every operation available for expected", "[monadic][constraints]") { + CHECK(has_and_then); + CHECK(has_and_then); + CHECK(has_and_then); + CHECK(has_and_then); -static_assert(has_or_else); -static_assert(has_transform); -static_assert(has_transform_error); + CHECK(has_or_else); + CHECK(has_transform); + CHECK(has_transform_error); +} TEST_CASE("monadic constraints: rvalue and_then works with move-only error", "[monadic][constraints]") { expected e(42); diff --git a/tests/beman/expected/expected_ref.test.cpp b/tests/beman/expected/expected_ref.test.cpp index e014aaf..088a38f 100644 --- a/tests/beman/expected/expected_ref.test.cpp +++ b/tests/beman/expected/expected_ref.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,36 +15,59 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared through `type_name`, whose comparison is +// `std::is_same_v` — exactly as strict as the static_assert it replaces — so +// a mismatch prints what was deduced next to what was wanted rather than the +// bare word `false`. // ============================================================================= -static_assert(std::is_constructible_v, int&>); -static_assert(!std::is_default_constructible_v>); -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); - -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>()), int&>); -static_assert(std::is_same_v>().value()), int&>); - -// const expected still returns T* / T& (shallow const) -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>()), int&>); - -// Triviality: when E is trivial, copy/move/assign/destroy should be trivial -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); - -// Non-trivial E: still constructible/assignable but not trivially -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_destructible_v>); +TEST_CASE("expected: special member availability", "[expected_ref]") { + using expected_t = expected; + CHECK((std::is_constructible_v)); + CHECK_FALSE(std::is_default_constructible_v); + CHECK(std::is_copy_constructible_v); + CHECK(std::is_move_constructible_v); +} + +TEST_CASE("expected: observer return types", "[expected_ref]") { + using expected_t = expected; + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name())>() == type_name()); + CHECK(type_name().value())>() == type_name()); +} + +TEST_CASE("expected: const expected still returns T* / T& (shallow const)", "[expected_ref]") { + using expected_t = expected; + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name())>() == type_name()); +} + +TEST_CASE("expected: triviality when E is trivial", "[expected_ref]") { + // When E is trivial, copy/move/assign/destroy should be trivial + using expected_t = expected; + CHECK(std::is_trivially_copy_constructible_v); + CHECK(std::is_trivially_move_constructible_v); + CHECK(std::is_trivially_copy_assignable_v); + CHECK(std::is_trivially_move_assignable_v); + CHECK(std::is_trivially_destructible_v); +} + +TEST_CASE("expected: non-trivial E is still constructible, but not trivially", "[expected_ref]") { + using expected_t = expected; + CHECK(std::is_copy_constructible_v); + CHECK(std::is_move_constructible_v); + CHECK_FALSE(std::is_trivially_copy_constructible_v); + CHECK_FALSE(std::is_trivially_move_constructible_v); + CHECK_FALSE(std::is_trivially_destructible_v); +} // ============================================================================= // Construction @@ -234,7 +259,7 @@ TEST_CASE("expected: shallow const allows mutation of referent", "[expected_ TEST_CASE("expected: operator-> on const returns T*", "[expected_ref]") { int x = 5; const expected e(x); - static_assert(std::is_same_v()), int*>); + CHECK(type_name())>() == type_name()); *e.operator->() = 99; CHECK(x == 99); } @@ -246,7 +271,7 @@ TEST_CASE("expected: operator-> on const returns T*", "[expected_ref]") { TEST_CASE("expected: operator* returns T&", "[expected_ref]") { int x = 42; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); *e = 99; CHECK(x == 99); } @@ -265,7 +290,7 @@ TEST_CASE("expected: operator-> returns T*", "[expected_ref]") { TEST_CASE("expected: value() returns T& or throws", "[expected_ref]") { int x = 1; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(e.value() == 1); e.value() = 2; CHECK(x == 2); diff --git a/tests/beman/expected/expected_ref_both.test.cpp b/tests/beman/expected/expected_ref_both.test.cpp index d228edf..ea1f35b 100644 --- a/tests/beman/expected/expected_ref_both.test.cpp +++ b/tests/beman/expected/expected_ref_both.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,61 +15,83 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is checked by comparing type_name, which compares by +// std::is_same_v — exactly as strict as the original — with the compiler's +// spellings used only to explain a failure. // ============================================================================= -// No default constructor — T& cannot be default-initialized -static_assert(!std::is_default_constructible_v>); +TEST_CASE("expected: special member availability", "[expected_ref_both]") { + // No default constructor — T& cannot be default-initialized + CHECK_FALSE(std::is_default_constructible_v>); -// Constructible from lvalue (value side) -static_assert(std::is_constructible_v, int&>); + // Constructible from lvalue (value side) + CHECK(std::is_constructible_v, int&>); -// Copy/move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); + // Copy/move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); +} + +TEST_CASE("expected: fully trivial — both sides are pointers", "[expected_ref_both]") { + // Trivially copyable/movable/destructible + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} -// Fully trivial: both sides are pointers — trivially copyable/movable/destructible -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("expected: assignable even though const E& is not", "[expected_ref_both]") { + // Finding 1: copy/move assignment must be available for const-reference E, where E + // itself is not assignable (is_copy_assignable_v is false) but the + // stored unexpected rebinds via pointer assignment. + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} -// Finding 1: copy/move assignment must be available for const-reference E, where E -// itself is not assignable (is_copy_assignable_v is false) but the -// stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); +TEST_CASE("expected: observer return types are shallow-const", "[expected_ref_both]") { + using expected_t = expected; -// operator-> returns T* (shallow const) -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>().operator->()), int*>); + // operator-> returns T* (shallow const) + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name().operator->())>() == type_name()); -// operator* returns T& (shallow const) -static_assert(std::is_same_v>()), int&>); -static_assert(std::is_same_v>()), int&>); + // operator* returns T& (shallow const) + CHECK(type_name())>() == type_name()); + CHECK(type_name())>() == type_name()); -// value() returns T& (shallow const) -static_assert(std::is_same_v>().value()), int&>); -static_assert(std::is_same_v>().value()), int&>); + // value() returns T& (shallow const) + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); -// error() returns E& (shallow const) -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); + // error() returns E& (shallow const) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} -// Cannot construct from temporary value (T& rvalue deleted) -static_assert(!std::is_constructible_v, int&&>); +TEST_CASE("expected: temporaries cannot be bound", "[expected_ref_both]") { + // Cannot construct from temporary value (T& rvalue deleted) + CHECK_FALSE(std::is_constructible_v, int&&>); -// Cannot construct from temporary error (rvalue or any type creating a temp E) -static_assert(!std::is_constructible_v, unexpect_t, int&&>); -// Cross-type temporary: float would create a temp double when binding const double& -static_assert(!std::is_constructible_v, unexpect_t, float>); -// Lvalue of same type is fine -static_assert(std::is_constructible_v, unexpect_t, const double&>); + // Cannot construct from temporary error (rvalue or any type creating a temp E) + CHECK_FALSE(std::is_constructible_v, unexpect_t, int&&>); + // Cross-type temporary: float would create a temp double when binding const double& + CHECK_FALSE(std::is_constructible_v, unexpect_t, float>); + // Lvalue of same type is fine + CHECK(std::is_constructible_v, unexpect_t, const double&>); +} -// Converting construction from expected -static_assert(std::is_constructible_v, const expected&>); +TEST_CASE("expected: converting construction from expected is available", "[expected_ref_both]") { + CHECK(std::is_constructible_v, const expected&>); +} // ============================================================================= // Construction — value side @@ -287,7 +311,7 @@ TEST_CASE("expected: operator*() returns T& (mutation visible)", "[expect TEST_CASE("expected: value() returns T& (throws on error)", "[expected_ref_both]") { int x = 42; expected e(x); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.value() == &x); } @@ -300,7 +324,7 @@ TEST_CASE("expected: value() throws bad_expected_access on error", "[expe TEST_CASE("expected: error() returns E& (mutation visible)", "[expected_ref_both]") { int err = 7; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); e.error() = 99; CHECK(err == 99); } diff --git a/tests/beman/expected/expected_ref_constraints.test.cpp b/tests/beman/expected/expected_ref_constraints.test.cpp index 9805c73..c667c24 100644 --- a/tests/beman/expected/expected_ref_constraints.test.cpp +++ b/tests/beman/expected/expected_ref_constraints.test.cpp @@ -2,6 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // Beman-only: tests SFINAE behavior of constraints on expected. +// +// Every constraint below is checked at runtime rather than with static_assert +// so that a violated constraint is reported by the test run, naming the +// responsible type, instead of stopping the build at the first failure and +// reporting nothing at all. Each check is a boolean trait or concept, so a +// plain CHECK / CHECK_FALSE is exactly as strict as the static_assert it +// replaces; the polarity of the original is preserved. #include #include @@ -63,24 +70,47 @@ concept has_value_or = requires(X x, U u) { x.value_or(u); }; // C2/C3: Copy/move constructors require E to be copy/move constructible // =========================================================================== -static_assert(!std::is_copy_constructible_v>, - "expected must not be copy-constructible"); -static_assert(std::is_move_constructible_v>, - "expected must be move-constructible"); - -static_assert(std::is_copy_constructible_v>, "expected must be copy-constructible"); -static_assert(std::is_move_constructible_v>, "expected must be move-constructible"); +TEST_CASE("ref constraint: copy/move ctor track E copy/move constructibility", "[ref_constraints]") { + { + INFO("expected must not be copy-constructible"); + CHECK_FALSE((std::is_copy_constructible_v>)); + } + { + INFO("expected must be move-constructible"); + CHECK((std::is_move_constructible_v>)); + } + { + INFO("expected must be copy-constructible"); + CHECK((std::is_copy_constructible_v>)); + } + { + INFO("expected must be move-constructible"); + CHECK((std::is_move_constructible_v>)); + } +} // =========================================================================== // A1/A2: Copy/move assignment require E to be copy/move constructible+assignable // =========================================================================== -static_assert(!std::is_copy_assignable_v>, - "expected must not be copy-assignable"); -static_assert(std::is_move_assignable_v>, "expected must be move-assignable"); - -static_assert(std::is_copy_assignable_v>, "expected must be copy-assignable"); -static_assert(std::is_move_assignable_v>, "expected must be move-assignable"); +TEST_CASE("ref constraint: copy/move assignment track E copy/move assignability", "[ref_constraints]") { + { + INFO("expected must not be copy-assignable"); + CHECK_FALSE((std::is_copy_assignable_v>)); + } + { + INFO("expected must be move-assignable"); + CHECK((std::is_move_assignable_v>)); + } + { + INFO("expected must be copy-assignable"); + CHECK((std::is_copy_assignable_v>)); + } + { + INFO("expected must be move-assignable"); + CHECK((std::is_move_assignable_v>)); + } +} // =========================================================================== // C5/C6: Value ctor excludes expected self-type and unexpected specializations @@ -99,81 +129,118 @@ TEST_CASE("ref constraint: unexpected routes to unexpected ctor", "[ref_constrai // C7: Value ctor requires is_constructible_v // =========================================================================== -// Cannot construct expected from a string — int& is not bindable to string -static_assert(!std::is_constructible_v, std::string&>, - "expected must not be constructible from string&"); - -// Can construct expected from int& -static_assert(std::is_constructible_v, int&>, - "expected must be constructible from int&"); +TEST_CASE("ref constraint: value ctor requires is_constructible_v", "[ref_constraints]") { + // Cannot construct expected from a string — int& is not + // bindable to string + { + INFO("expected must not be constructible from string&"); + CHECK_FALSE((std::is_constructible_v, std::string&>)); + } + // Can construct expected from int& + { + INFO("expected must be constructible from int&"); + CHECK((std::is_constructible_v, int&>)); + } +} // =========================================================================== // C9/C10: Converting ctor from expected // =========================================================================== -// Cannot convert expected to expected (string& not bindable to int&) -static_assert(!std::is_constructible_v, const expected&>, - "expected must not be constructible from expected"); - -// Can convert expected to expected (same types) -static_assert(std::is_constructible_v, const expected&>, - "expected must be constructible from expected"); - // Cannot convert when E is not constructible from G struct Unconstructible { Unconstructible() = default; Unconstructible(int) = delete; }; -static_assert(!std::is_constructible_v, const expected&>, - "expected not constructible from expected — E(const G&) fails"); +TEST_CASE("ref constraint: converting ctor from expected", "[ref_constraints]") { + // Cannot convert expected to expected (string& + // not bindable to int&) + { + INFO("expected must not be constructible from expected"); + CHECK_FALSE((std::is_constructible_v, const expected&>)); + } + // Can convert expected to expected (same types) + { + INFO("expected must be constructible from expected"); + CHECK((std::is_constructible_v, const expected&>)); + } + { + INFO("expected not constructible from expected — E(const G&) fails"); + CHECK_FALSE((std::is_constructible_v, const expected&>)); + } +} // =========================================================================== // A3/A4/A5: Value assignment constraints // =========================================================================== -// Value assignment from int& works -static_assert(std::is_assignable_v&, int&>, "expected must be assignable from int&"); - -// Value assignment from unrelated type that can't bind to int& is blocked -static_assert(!std::is_assignable_v&, std::string&>, - "expected must not be assignable from string&"); +TEST_CASE("ref constraint: value assignment constraints", "[ref_constraints]") { + // Value assignment from int& works + { + INFO("expected must be assignable from int&"); + CHECK((std::is_assignable_v&, int&>)); + } + // Value assignment from unrelated type that can't bind to int& is blocked + { + INFO("expected must not be assignable from string&"); + CHECK_FALSE((std::is_assignable_v&, std::string&>)); + } +} // =========================================================================== // A6: unexpected assignment requires constructible+assignable E // =========================================================================== -static_assert(std::is_assignable_v&, unexpected>, - "expected must be assignable from unexpected"); +TEST_CASE("ref constraint: unexpected assignment requires constructible+assignable E", "[ref_constraints]") { + INFO("expected must be assignable from unexpected"); + CHECK((std::is_assignable_v&, unexpected>)); +} // =========================================================================== // S1: swap requires E swappable and move-constructible // =========================================================================== -static_assert(has_swap>, "expected must be swappable"); - -static_assert(!has_swap>, "expected must not be swappable"); +TEST_CASE("ref constraint: swap requires E swappable and move-constructible", "[ref_constraints]") { + { + INFO("expected must be swappable"); + CHECK((has_swap>)); + } + { + INFO("expected must not be swappable"); + CHECK_FALSE((has_swap>)); + } +} // =========================================================================== // E1: emplace requires constructible and no dangling // =========================================================================== -static_assert(has_emplace, int&>, "expected must support emplace from int&"); - -// emplace from an rvalue int — should be blocked (can't bind int& to rvalue) -static_assert(!has_emplace_from, int>, - "expected must not support emplace from rvalue int"); +TEST_CASE("ref constraint: emplace requires constructible and no dangling", "[ref_constraints]") { + { + INFO("expected must support emplace from int&"); + CHECK((has_emplace, int&>)); + } + // emplace from an rvalue int — should be blocked (can't bind int& to rvalue) + { + INFO("expected must not support emplace from rvalue int"); + CHECK_FALSE((has_emplace_from, int>)); + } +} // =========================================================================== // V1: value_or requires is_object_v && !is_array_v (LWG4304) // =========================================================================== -static_assert(has_value_or, int>, "expected must support value_or"); +TEST_CASE("ref constraint: value_or requires object type (LWG4304)", "[ref_constraints]") { + INFO("expected must support value_or"); + CHECK((has_value_or, int>)); -// Function type: int(int) is not an object type -// Note: expected would require T = int(int), which is a function type. -// We can't easily form expected due to other constraints, so we verify -// the positive case works and trust the requires clause. + // Function type: int(int) is not an object type + // Note: expected would require T = int(int), which is a function type. + // We can't easily form expected due to other constraints, so we verify + // the positive case works and trust the requires clause. +} // =========================================================================== // MC1/MC2: and_then/transform constrained by E constructibility @@ -182,21 +249,34 @@ static_assert(has_value_or, int>, "expected using RefMoveOnlyErr = expected; [[maybe_unused]] auto ref_dummy_and_then = [](int&) { return expected(); }; -static_assert(!has_and_then, - "and_then lvalue must be constrained out when E is move-only"); -static_assert(has_and_then, - "and_then rvalue must be available when E is move-constructible"); -static_assert(!has_and_then, - "and_then const lvalue must be constrained out when E is move-only"); - [[maybe_unused]] auto ref_dummy_transform = [](int&) { return 0; }; -static_assert(!has_transform, - "transform lvalue must be constrained out when E is move-only"); -static_assert(has_transform, - "transform rvalue must be available when E is move-constructible"); -static_assert(!has_transform, - "transform const lvalue must be constrained out when E is move-only"); +TEST_CASE("ref constraint: and_then/transform constrained by E constructibility", "[ref_constraints]") { + { + INFO("and_then lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_and_then)); + } + { + INFO("and_then rvalue must be available when E is move-constructible"); + CHECK((has_and_then)); + } + { + INFO("and_then const lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_and_then)); + } + { + INFO("transform lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_transform)); + } + { + INFO("transform rvalue must be available when E is move-constructible"); + CHECK((has_transform)); + } + { + INFO("transform const lvalue must be constrained out when E is move-only"); + CHECK_FALSE((has_transform)); + } +} // =========================================================================== // MC3/MC4: or_else/transform_error have no constraints — always available @@ -207,13 +287,18 @@ static_assert(!has_transform(x); }; -static_assert(has_or_else, - "or_else must be available — no constraints on value constructibility for T&"); - [[maybe_unused]] auto ref_dummy_transform_error = [](MoveOnly&&) { return 0; }; -static_assert(has_transform_error, - "transform_error must be available — no constraints for T& specialization"); +TEST_CASE("ref constraint: or_else/transform_error are unconstrained", "[ref_constraints]") { + { + INFO("or_else must be available — no constraints on value constructibility for T&"); + CHECK((has_or_else)); + } + { + INFO("transform_error must be available — no constraints for T& specialization"); + CHECK((has_transform_error)); + } +} // =========================================================================== // Positive: all operations available for normal types @@ -229,14 +314,16 @@ using NormalRef = expected; [[maybe_unused]] auto ref_normal_transform = [](int&) { return 42; }; [[maybe_unused]] auto ref_normal_transform_err = [](int) { return 42; }; -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); -static_assert(has_and_then); +TEST_CASE("ref constraint: all monadic operations available for normal types", "[ref_constraints]") { + CHECK((has_and_then)); + CHECK((has_and_then)); + CHECK((has_and_then)); + CHECK((has_and_then)); -static_assert(has_or_else); -static_assert(has_transform); -static_assert(has_transform_error); + CHECK((has_or_else)); + CHECK((has_transform)); + CHECK((has_transform_error)); +} // =========================================================================== // Runtime tests for constraint-gated paths diff --git a/tests/beman/expected/expected_ref_e.test.cpp b/tests/beman/expected/expected_ref_e.test.cpp index 0af4751..77be6a6 100644 --- a/tests/beman/expected/expected_ref_e.test.cpp +++ b/tests/beman/expected/expected_ref_e.test.cpp @@ -6,6 +6,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -13,82 +15,116 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // ============================================================================= // Finding 5: feature-test macro for the reference-E / reference-T extensions // ============================================================================= +// Presence of the macro is still a hard translation failure: without it there +// is nothing for a test to inspect, and `#if` is the only tool that can ask. #ifndef __cpp_lib_expected_ref #error "__cpp_lib_expected_ref must be defined by " #endif -static_assert(__cpp_lib_expected_ref > 0); + +TEST_CASE("expected: feature-test macro has a positive value", "[expected_ref_e]") { + CHECK(__cpp_lib_expected_ref > 0); +} // ============================================================================= // Finding 4: guarded delete-with-message macro (falls back to plain `delete` // pre-C++26; either way, the deleted overload stays deleted). // ============================================================================= -static_assert(!std::is_constructible_v, int&&>, - "unexpected dangling-temporary ctor must stay deleted regardless of " - "BEMAN_EXPECTED_DELETE_MSG's expansion"); -static_assert(!std::is_default_constructible_v>, - "expected must stay non-default-constructible regardless of " - "BEMAN_EXPECTED_DELETE_MSG's expansion"); +TEST_CASE("expected: BEMAN_EXPECTED_DELETE_MSG leaves deleted overloads deleted", "[expected_ref_e]") { + { + INFO("unexpected dangling-temporary ctor must stay deleted regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); + CHECK_FALSE(std::is_constructible_v, int&&>); + } + { + INFO("expected must stay non-default-constructible regardless of " + "BEMAN_EXPECTED_DELETE_MSG's expansion"); + CHECK_FALSE(std::is_default_constructible_v>); + } +} // ============================================================================= -// Type-level static assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared by `type_name`, which decides by +// std::is_same_v and carries the compiler's spelling only so that a mismatch +// prints what was deduced next to what was wanted rather than the bare word +// `false`. // ============================================================================= -// expected is a valid specialization — default constructible (value side) -static_assert(std::is_default_constructible_v>); -static_assert(std::is_constructible_v, std::in_place_t, int>); +TEST_CASE("expected: special member availability", "[expected_ref_e]") { + // expected is a valid specialization — default constructible (value side) + CHECK(std::is_default_constructible_v>); + CHECK(std::is_constructible_v, std::in_place_t, int>); + + // Copy/move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); +} -// error() returns E& (shallow const — const expected still returns E&, not const E&) -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); +TEST_CASE("expected: observer return types", "[expected_ref_e]") { + using expected_t = expected; -// value() returns T& (non-const) / const T& (const) -static_assert(std::is_same_v&>().value()), int&>); -static_assert(std::is_same_v&>().value()), const int&>); + // error() returns E& (shallow const — const expected still returns E&, not const E&) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); -// operator-> returns T* / const T* -static_assert(std::is_same_v>().operator->()), int*>); -static_assert(std::is_same_v>().operator->()), const int*>); + // value() returns T& (non-const) / const T& (const) + CHECK(type_name().value())>() == type_name()); + CHECK(type_name().value())>() == type_name()); -// Copy/move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); + // operator-> returns T* / const T* + CHECK(type_name().operator->())>() == type_name()); + CHECK(type_name().operator->())>() == type_name()); +} // Finding 1: copy/move assignment must be available for reference E, including // const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); - -// Triviality: when T is trivial, copy/move/assign/destroy should be trivial -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); - -// Non-trivial T: still constructible/assignable but not trivially -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_destructible_v>); - -// Cannot construct from temporary error (rvalue deleted, or any type creating a temp E) -static_assert(!std::is_constructible_v, unexpect_t, int&&>); -// Cross-type temporary: float would create a temp double when binding const double& -static_assert(!std::is_constructible_v, unexpect_t, float>); -// Lvalue of same type is fine -static_assert(std::is_constructible_v, unexpect_t, const double&>); +TEST_CASE("expected: copy/move assignment available for reference E", "[expected_ref_e]") { + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} -// Converting construction from expected -static_assert(std::is_constructible_v, const expected&>); +TEST_CASE("expected: triviality follows T", "[expected_ref_e]") { + // Triviality: when T is trivial, copy/move/assign/destroy should be trivial + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); + + // Non-trivial T: still constructible/assignable but not trivially + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_move_constructible_v>); + CHECK_FALSE(std::is_trivially_destructible_v>); +} + +TEST_CASE("expected: error construction rejects temporaries", "[expected_ref_e]") { + // Cannot construct from temporary error (rvalue deleted, or any type creating a temp E) + CHECK_FALSE(std::is_constructible_v, unexpect_t, int&&>); + // Cross-type temporary: float would create a temp double when binding const double& + CHECK_FALSE(std::is_constructible_v, unexpect_t, float>); + // Lvalue of same type is fine + CHECK(std::is_constructible_v, unexpect_t, const double&>); +} + +TEST_CASE("expected: converting construction from expected is available", "[expected_ref_e]") { + CHECK(std::is_constructible_v, const expected&>); +} // ============================================================================= // Construction — value side (same as primary template) @@ -236,7 +272,7 @@ TEST_CASE("expected: operator*() and operator->() work normally", "[expect TEST_CASE("expected: value() returns T& (owned)", "[expected_ref_e]") { expected e(42); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); e.value() = 99; CHECK(*e == 99); } @@ -250,7 +286,7 @@ TEST_CASE("expected: value() throws on error", "[expected_ref_e]") { TEST_CASE("expected: error() returns E&", "[expected_ref_e]") { int err = 7; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.error() == &err); } diff --git a/tests/beman/expected/expected_review_corrections.test.cpp b/tests/beman/expected/expected_review_corrections.test.cpp index 39acb99..29fea32 100644 --- a/tests/beman/expected/expected_review_corrections.test.cpp +++ b/tests/beman/expected/expected_review_corrections.test.cpp @@ -22,26 +22,31 @@ using namespace beman::expected; // type (referent lives inside the wrapper) or would drop const. // ============================================================================= -// Allowed: reference G, across all three specializations. -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); - -// Allowed: binding a more-const reference (const int& <- int&). -static_assert(std::is_constructible_v, unexpected>); -static_assert(std::is_constructible_v, unexpected>); - -// Deleted: value G would dangle once a temporary unexpected is destroyed. -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); - -// Deleted: dropping const (int& <- const int&) is not constructible. -static_assert(!std::is_constructible_v, unexpected>); -static_assert(!std::is_constructible_v, unexpected>); - -// Value-E construction is unaffected. -static_assert(std::is_constructible_v, unexpected>); +// Checked at runtime rather than with static_assert so that a violated rule is +// reported by the test run, with the offending specialization named, instead of +// stopping the build at the first failure and reporting nothing. +TEST_CASE("F6: construction from unexpected is allowed only for reference G", "[ref][unexpected]") { + // Allowed: reference G, across all three specializations. + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + + // Allowed: binding a more-const reference (const int& <- int&). + CHECK(std::is_constructible_v, unexpected>); + CHECK(std::is_constructible_v, unexpected>); + + // Deleted: value G would dangle once a temporary unexpected is destroyed. + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + + // Deleted: dropping const (int& <- const int&) is not constructible. + CHECK_FALSE(std::is_constructible_v, unexpected>); + CHECK_FALSE(std::is_constructible_v, unexpected>); + + // Value-E construction is unaffected. + CHECK(std::is_constructible_v, unexpected>); +} TEST_CASE("reference-error construction from unexpected binds an external object", "[ref][unexpected]") { int err = 41; @@ -62,14 +67,16 @@ TEST_CASE("reference-error construction from unexpected binds an external ob } // Rebinding assignment from unexpected — allowed for reference E only when G is a reference -// (rebinds the error pointer to an external object; never dangles). Value G and const-drop are -// statically rejected, mirroring construction. -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); -static_assert(std::is_assignable_v&, unexpected>); // more-const OK -static_assert(!std::is_assignable_v&, unexpected>); // value G: deleted -static_assert(!std::is_assignable_v&, unexpected>); // const drop: no overload +// (rebinds the error pointer to an external object; never dangles). Value G and const-drop make +// the assignment ill-formed; that trait is checked at runtime here, mirroring construction. +TEST_CASE("F6: rebinding assignment from unexpected is allowed only for reference G", "[ref][unexpected][assign]") { + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); + CHECK(std::is_assignable_v&, unexpected>); // more-const OK + CHECK_FALSE(std::is_assignable_v&, unexpected>); // value G: deleted + CHECK_FALSE(std::is_assignable_v&, unexpected>); // const drop: no overload +} TEST_CASE("rebinding assignment from unexpected repoints the error reference", "[ref][unexpected][assign]") { int g1 = 1, g2 = 2; @@ -114,8 +121,10 @@ struct ThrowingRef { }; } // namespace -static_assert(std::is_nothrow_constructible_v, int&>); -static_assert(!std::is_nothrow_constructible_v, ThrowingRef>); +TEST_CASE("F4/F5: value construction is noexcept only when the reference bind cannot throw", "[ref][noexcept]") { + CHECK(std::is_nothrow_constructible_v, int&>); + CHECK_FALSE(std::is_nothrow_constructible_v, ThrowingRef>); +} TEST_CASE("value constructor propagates a throwing reference conversion", "[ref][noexcept]") { int caught = 0; @@ -187,8 +196,10 @@ concept has_error_or = requires(Ex e, Arg a) { e.error_or(a); }; struct NotConvertible {}; } // namespace -static_assert(has_error_or&, const char*>); -static_assert(!has_error_or&, NotConvertible>); +TEST_CASE("F7: error_or is SFINAE-friendly", "[ref][error_or]") { + CHECK(has_error_or&, const char*>); + CHECK_FALSE(has_error_or&, NotConvertible>); +} // ============================================================================= // Shallow conversion — converting from an rvalue reference-holding expected to a diff --git a/tests/beman/expected/expected_smf_regressions.test.cpp b/tests/beman/expected/expected_smf_regressions.test.cpp index b1875f3..856ed30 100644 --- a/tests/beman/expected/expected_smf_regressions.test.cpp +++ b/tests/beman/expected/expected_smf_regressions.test.cpp @@ -60,18 +60,31 @@ struct NoexceptNonTrivial { template inline constexpr bool swap_query_is_wellformed = (static_cast(std::is_swappable_v), true); -static_assert(swap_query_is_wellformed>); -static_assert(swap_query_is_wellformed>); +} // namespace -// Bug 2: non-trivial-but-noexcept copy => nothrow copy construct/assign. -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_constructible_v>); -static_assert(std::is_nothrow_copy_assignable_v>); -static_assert(std::is_nothrow_copy_assignable_v>); -static_assert(std::is_nothrow_copy_assignable_v>); +// The trait properties below are checked at runtime rather than with +// static_assert so that a regression is reported by the test run, with the +// responsible type named, instead of stopping the build at the first failure +// and reporting nothing. Well-formedness is still a translation-time question: +// naming `swap_query_is_wellformed` instantiates it, so a hard error in +// `is_swappable_v` remains a build failure — what the CHECK adds is that a +// *wrong answer* is reported instead. -} // namespace +TEST_CASE("querying is_swappable on a move-restricted error type is well-formed") { + // Bug 1: previously a hard error via the unconstrained hidden-friend swap. + CHECK(swap_query_is_wellformed>); + CHECK(swap_query_is_wellformed>); +} + +TEST_CASE("non-trivial but noexcept copy gives a nothrow copy constructor and assignment") { + // Bug 2: non-trivial-but-noexcept copy => nothrow copy construct/assign. + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_constructible_v>); + CHECK(std::is_nothrow_copy_assignable_v>); + CHECK(std::is_nothrow_copy_assignable_v>); + CHECK(std::is_nothrow_copy_assignable_v>); +} TEST_CASE("swap is well-formed and works for a move-restricted error type") { // Previously a hard error via the unconstrained hidden-friend swap. diff --git a/tests/beman/expected/expected_std_equivalence.test.cpp b/tests/beman/expected/expected_std_equivalence.test.cpp index 5e87a42..9711163 100644 --- a/tests/beman/expected/expected_std_equivalence.test.cpp +++ b/tests/beman/expected/expected_std_equivalence.test.cpp @@ -3,7 +3,7 @@ // Observable-equivalence gate: beman::expected holds the exposition-only member // unexpected, but every observable special-member property is keyed on E. -// This asserts, across a battery of adversarial error types, that the resulting +// This checks, across a battery of adversarial error types, that the resulting // type traits match std::expected exactly -- i.e. that the unexpected member // is behaviourally inert against a T/E rendering. See D4280: ABI stability for // existing implementations is a design goal, and any divergence found here is a @@ -14,6 +14,12 @@ // non-trivial assignment is a specification accident). Those traits are checked // as "beman is at least as trivial as std". // +// The comparisons are made at runtime rather than with static_assert so that a +// divergence is reported by the test run -- naming the trait and the pair of +// types it disagreed on, and reporting *every* divergence -- instead of +// stopping the build at the first one and reporting nothing. The traits +// themselves are still compile-time facts; only the verdict is reported. +// // Compiled against std::expected requires C++23. Skipped on libc++, whose // std::expected has its own quirks for pathological types -- see the std-parity // gate in CMakeLists.txt and docs/std-parity.md. @@ -22,12 +28,16 @@ #include +#include + #include #include #include namespace bx = beman::expected; +using beman::expected::testing::display_name; + namespace { struct Plain { @@ -99,11 +109,25 @@ inline constexpr bool skip_trivial_parity = false; template <> inline constexpr bool skip_trivial_parity = true; +// Every trait parity for one (E, T) pair, each one reported. `Bm` and `St` are +// named in the failure output so a divergence says which pair of types it was +// found on; the trait's own name is carried by the CHECK expression and by the +// scoped message, which is what the static_assert message used to say. template -constexpr bool parity() { +void check_parity() { using Bm = bx::expected; using St = std::expected; -#define BEMAN_PARITY(TRAIT) static_assert(std::TRAIT##_v == std::TRAIT##_v, #TRAIT " parity") + + constexpr auto bm_name = display_name(); + constexpr auto st_name = display_name(); + INFO("beman: " << bm_name); + INFO("std: " << st_name); + +#define BEMAN_PARITY(TRAIT) \ + do { \ + INFO(#TRAIT " parity"); \ + CHECK(std::TRAIT##_v == std::TRAIT##_v); \ + } while (false) BEMAN_PARITY(is_copy_constructible); BEMAN_PARITY(is_move_constructible); BEMAN_PARITY(is_nothrow_copy_constructible); @@ -121,34 +145,41 @@ constexpr bool parity() { BEMAN_PARITY(is_swappable); BEMAN_PARITY(is_nothrow_swappable); #undef BEMAN_PARITY + // Drive-by fix: beman is at least as trivially copyable/assignable as std. + // Each is an implication, so it is one bool rather than a comparison; the + // extra parentheses keep Catch2 from trying to decompose the `||`. if constexpr (!skip_trivial_parity) { - static_assert(!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v); - static_assert(!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v); - static_assert(!std::is_trivially_copyable_v || std::is_trivially_copyable_v); + INFO("beman is at least as trivial as std"); + CHECK((!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v)); + CHECK((!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v)); + CHECK((!std::is_trivially_copyable_v || std::is_trivially_copyable_v)); } - return true; } -#define BEMAN_RUN(E) static_assert(parity() && parity()) -BEMAN_RUN(Plain); -BEMAN_RUN(DelMoveOkCopy); -BEMAN_RUN(MoveOnly); -BEMAN_RUN(ThrowMoveNoexceptCopy); -BEMAN_RUN(NonTrivialDtor); -BEMAN_RUN(NoexceptMoveOnly); -BEMAN_RUN(Immovable); -BEMAN_RUN(NoexceptNonTrivial); -BEMAN_RUN(int); -BEMAN_RUN(std::string); -#undef BEMAN_RUN - -// Lock in the drive-by fix itself: beman is trivially copyable for trivial T,E -// (std::expected is not, by specification accident). -static_assert(std::is_trivially_copyable_v>); - } // namespace TEST_CASE("beman::expected type traits match std::expected") { - SUCCEED("all parity checks are static_asserts evaluated at compile time"); +#define BEMAN_RUN(E) \ + do { \ + check_parity(); \ + check_parity(); \ + } while (false) + BEMAN_RUN(Plain); + BEMAN_RUN(DelMoveOkCopy); + BEMAN_RUN(MoveOnly); + BEMAN_RUN(ThrowMoveNoexceptCopy); + BEMAN_RUN(NonTrivialDtor); + BEMAN_RUN(NoexceptMoveOnly); + BEMAN_RUN(Immovable); + BEMAN_RUN(NoexceptNonTrivial); + BEMAN_RUN(int); + BEMAN_RUN(std::string); +#undef BEMAN_RUN +} + +TEST_CASE("beman::expected is trivially copyable") { + // Lock in the drive-by fix itself: beman is trivially copyable for trivial + // T,E (std::expected is not, by specification accident). + CHECK(std::is_trivially_copyable_v>); } diff --git a/tests/beman/expected/expected_trivial.test.cpp b/tests/beman/expected/expected_trivial.test.cpp index a454889..ce85ec3 100644 --- a/tests/beman/expected/expected_trivial.test.cpp +++ b/tests/beman/expected/expected_trivial.test.cpp @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // Beman-only: triviality of special member functions is implementation quality. -// libstdc++ expected may or may not match these static_asserts. +// libstdc++ expected may or may not match these checks. +// +// The triviality properties are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. #include @@ -17,33 +22,41 @@ using namespace beman::expected; // Primary template: trivial when T and E are trivial // --------------------------------------------------------------------------- -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("trivial SMFs: expected special members are trivial", "[trivial]") { + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} // --------------------------------------------------------------------------- // Void specialization: trivial when E is trivial // --------------------------------------------------------------------------- -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_copy_assignable_v>); -static_assert(std::is_trivially_move_assignable_v>); -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("trivial SMFs: expected special members are trivial", "[trivial]") { + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_copy_assignable_v>); + CHECK(std::is_trivially_move_assignable_v>); + CHECK(std::is_trivially_destructible_v>); +} // --------------------------------------------------------------------------- // Non-trivial when T or E is non-trivial // --------------------------------------------------------------------------- -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_move_constructible_v>); -static_assert(!std::is_trivially_copy_assignable_v>); -static_assert(!std::is_trivially_move_assignable_v>); +TEST_CASE("trivial SMFs: a non-trivial T makes the special members non-trivial", "[trivial]") { + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_move_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_assignable_v>); + CHECK_FALSE(std::is_trivially_move_assignable_v>); +} -static_assert(!std::is_trivially_copy_constructible_v>); -static_assert(!std::is_trivially_copy_constructible_v>); +TEST_CASE("trivial SMFs: a non-trivial E makes the special members non-trivial", "[trivial]") { + CHECK_FALSE(std::is_trivially_copy_constructible_v>); + CHECK_FALSE(std::is_trivially_copy_constructible_v>); +} TEST_CASE("trivial SMFs: expected is trivially copyable", "[trivial]") { expected a(42); diff --git a/tests/beman/expected/expected_void.test.cpp b/tests/beman/expected/expected_void.test.cpp index 576a026..00899df 100644 --- a/tests/beman/expected/expected_void.test.cpp +++ b/tests/beman/expected/expected_void.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -17,6 +19,8 @@ using test_ns::expected; using test_ns::unexpect; using test_ns::unexpected; +using beman::expected::testing::type_name; + // ============================================================================= // [expected.void.general] Ill-formed instantiation constraints // ============================================================================= @@ -24,6 +28,13 @@ using test_ns::unexpected; // not a reference, not an array, not cv-qualified, not unexpected. // These are enforced by static_asserts inside the class body; verified by // negative compile tests (expected_void_ref_fail.cpp, expected_void_array_fail.cpp). +// +// The type-level properties below are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. Type identity is checked by comparing the +// compiler's spelling of the two types, so a mismatch prints what was deduced +// next to what was wanted rather than the bare word `false`. // ============================================================================= // [expected.void.cons] Constructors @@ -34,7 +45,7 @@ using test_ns::unexpected; TEST_CASE("expected: default construct", "[expected_void]") { expected e; CHECK(e.has_value()); - static_assert(std::is_nothrow_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); } // --- Copy constructor --- @@ -44,8 +55,10 @@ struct NoCopyE { NoCopyE() = default; }; -static_assert(!std::is_copy_constructible_v>); -static_assert(std::is_trivially_copy_constructible_v>); +TEST_CASE("expected: copy constructor availability and triviality", "[expected_void]") { + CHECK_FALSE(std::is_copy_constructible_v>); + CHECK(std::is_trivially_copy_constructible_v>); +} TEST_CASE("expected: copy construct with value", "[expected_void]") { expected a; @@ -62,7 +75,9 @@ TEST_CASE("expected: copy construct with error", "[expected_void]") { // --- Move constructor --- -static_assert(std::is_nothrow_move_constructible_v>); +TEST_CASE("expected: move constructor is noexcept", "[expected_void]") { + CHECK(std::is_nothrow_move_constructible_v>); +} TEST_CASE("expected: move construct with error", "[expected_void]") { expected a(unexpect, "err"); @@ -73,8 +88,10 @@ TEST_CASE("expected: move construct with error", "[expected_void]") { // --- Converting constructor from expected where is_void_v --- -// Constraint: U must be void — cannot convert from expected -static_assert(!std::is_constructible_v, expected>); +TEST_CASE("expected: converting constructor requires void U", "[expected_void]") { + // Constraint: U must be void — cannot convert from expected + CHECK_FALSE(std::is_constructible_v, expected>); +} TEST_CASE("expected: convert from expected with value", "[expected_void]") { expected src; @@ -108,7 +125,7 @@ TEST_CASE("expected: construct from unexpected&&", "[expected_void]") { TEST_CASE("expected: in_place_t constructor", "[expected_void]") { expected e(std::in_place); CHECK(e.has_value()); - static_assert(noexcept(expected(std::in_place))); + CHECK(noexcept(expected(std::in_place))); } // --- unexpect_t constructors --- @@ -129,7 +146,9 @@ TEST_CASE("expected: unexpect_t ilist constructor", "[expected_void]") { // [expected.void.dtor] Destructor // ============================================================================= -static_assert(std::is_trivially_destructible_v>); +TEST_CASE("expected: trivially destructible when E is", "[expected_void]") { + CHECK(std::is_trivially_destructible_v>); +} TEST_CASE("expected: destructor destroys error", "[expected_void]") { int destroyed = 0; @@ -178,7 +197,9 @@ TEST_CASE("expected: copy assign error-to-error", "[expected_void]") { // --- Move assignment --- -static_assert(std::is_nothrow_move_assignable_v>); +TEST_CASE("expected: move assignment is noexcept", "[expected_void]") { + CHECK(std::is_nothrow_move_assignable_v>); +} TEST_CASE("expected: move assign value-to-error", "[expected_void]") { expected a, b(unexpect, "err"); @@ -208,7 +229,7 @@ TEST_CASE("expected: emplace from error state", "[expected_void]") { expected e(unexpect, 5); e.emplace(); CHECK(e.has_value()); - static_assert(noexcept(e.emplace())); + CHECK(noexcept(e.emplace())); } TEST_CASE("expected: emplace from value state (no-op)", "[expected_void]") { @@ -269,7 +290,7 @@ TEST_CASE("expected: has_value and bool", "[expected_void]") { TEST_CASE("expected: operator* is void", "[expected_void]") { expected e; - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); *e; // compiles and does nothing } @@ -296,9 +317,9 @@ TEST_CASE("expected: rvalue value() throws on error", "[expected_void]") { TEST_CASE("expected: error() all ref qualifications", "[expected_void]") { expected e(unexpect, 99); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); + CHECK(type_name() == type_name()); CHECK(e.error() == 99); } diff --git a/tests/beman/expected/expected_void_monadic.test.cpp b/tests/beman/expected/expected_void_monadic.test.cpp index fa2a414..3007e6e 100644 --- a/tests/beman/expected/expected_void_monadic.test.cpp +++ b/tests/beman/expected/expected_void_monadic.test.cpp @@ -5,6 +5,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -12,6 +14,8 @@ using namespace test_ns; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- // and_then - F called with no args when void // --------------------------------------------------------------------------- @@ -50,7 +54,7 @@ TEST_CASE("and_then void: rvalue overload propagates error by move", "[expected_ TEST_CASE("and_then void: return void expected", "[expected_void_monadic]") { expected e; auto r = e.and_then([]() -> expected { return {}; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } @@ -101,7 +105,7 @@ TEST_CASE("or_else void: error propagated through lambda", "[expected_void_monad TEST_CASE("transform void: has value - calls F, returns expected", "[expected_void_monadic]") { expected e; auto r = e.transform([]() { return 42; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -122,7 +126,7 @@ TEST_CASE("transform void: F returns void - expected()", "[expected_voi expected e; int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); CHECK(count == 1); } @@ -131,7 +135,7 @@ TEST_CASE("transform void: has error, F returns void - propagates", "[expected_v expected e(unexpect, 5); int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(count == 0); REQUIRE(!r.has_value()); CHECK(r.error() == 5); @@ -151,7 +155,7 @@ TEST_CASE("transform void: rvalue overload", "[expected_void_monadic]") { TEST_CASE("transform_error void: has error - transforms error", "[expected_void_monadic]") { expected e(unexpect, 3); auto r = e.transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } @@ -164,7 +168,7 @@ TEST_CASE("transform_error void: has value - returns expected()", "[exp return ""; }); CHECK(!called); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } @@ -177,7 +181,7 @@ TEST_CASE("void monadic chaining: and_then -> transform_error", "[expected_void_ auto r = e.and_then([]() -> expected { return {}; }).transform_error([](int v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); } diff --git a/tests/beman/expected/expected_void_ref_e.test.cpp b/tests/beman/expected/expected_void_ref_e.test.cpp index a9c6dac..19cca7c 100644 --- a/tests/beman/expected/expected_void_ref_e.test.cpp +++ b/tests/beman/expected/expected_void_ref_e.test.cpp @@ -4,6 +4,8 @@ #include +#include + #include "testing/types.hpp" #include @@ -11,49 +13,72 @@ using namespace beman::expected; +using beman::expected::testing::type_name; + // --------------------------------------------------------------------------- -// Type-level assertions +// Type-level properties +// +// These are checked at runtime rather than with static_assert so that a +// violated property is reported by the test run, with the responsible type +// named, instead of stopping the build at the first failure and reporting +// nothing. Type identity is compared by `type_name`, which decides by +// std::is_same_v and carries the compiler's spelling only so that a mismatch +// prints what was deduced next to what was wanted rather than the bare word +// `false`. // --------------------------------------------------------------------------- -static_assert(std::is_default_constructible_v>); -static_assert(std::is_nothrow_default_constructible_v>); +TEST_CASE("expected: special member availability and triviality", "[expected_void_ref_e]") { + CHECK(std::is_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); + + CHECK(std::is_trivially_copy_constructible_v>); + CHECK(std::is_trivially_move_constructible_v>); + CHECK(std::is_trivially_destructible_v>); + CHECK(std::is_trivially_copyable_v>); +} -static_assert(std::is_trivially_copy_constructible_v>); -static_assert(std::is_trivially_move_constructible_v>); -static_assert(std::is_trivially_destructible_v>); -static_assert(std::is_trivially_copyable_v>); +TEST_CASE("expected: observer return types", "[expected_void_ref_e]") { + using expected_t = expected; -static_assert(std::is_same_v>().error()), int&>); -static_assert(std::is_same_v>().error()), int&>); + // error() returns E& (shallow const — const expected still returns E&, not const E&) + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); -static_assert(std::is_void_v>())>); + CHECK(std::is_void_v())>); +} // absence of operator-> and value_or tested by _fail.cpp negative compile tests // Finding 1: copy/move assignment must be available for reference E, including // const-reference E, where E itself is not assignable (is_copy_assignable_v is false) but the stored unexpected rebinds via pointer assignment. -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); -static_assert(std::is_copy_assignable_v>); -static_assert(std::is_move_assignable_v>); +TEST_CASE("expected: copy/move assignment available for reference E", "[expected_void_ref_e]") { + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); + CHECK(std::is_copy_assignable_v>); + CHECK(std::is_move_assignable_v>); +} // Finding 2: the general (value-G) void converting constructors must be gated on // !is_reference_v — for reference E they are unsound: the lvalue form would bind into // the source's owned error (dangling once the source is gone), and the rvalue form hard-errors // by selecting a deleted unexpected constructor deep in the body instead of being excluded // from overload resolution. is_constructible_v must report false for both, matching reality. -static_assert(!std::is_constructible_v, const expected&>); -static_assert(!std::is_constructible_v, expected&&>); -static_assert(!std::is_constructible_v, const expected&>); -static_assert(!std::is_constructible_v, expected&&>); +TEST_CASE("expected: value-G converting constructors excluded for reference E", "[expected_void_ref_e]") { + CHECK_FALSE(std::is_constructible_v, const expected&>); + CHECK_FALSE(std::is_constructible_v, expected&&>); + CHECK_FALSE(std::is_constructible_v, const expected&>); + CHECK_FALSE(std::is_constructible_v, expected&&>); +} // The dedicated reference-E path (source error type is itself a reference) remains available, // for both lvalue and rvalue sources. -static_assert(std::is_constructible_v, const expected&>); -static_assert(std::is_constructible_v, expected&&>); -static_assert(std::is_constructible_v, const expected&>); -static_assert(std::is_constructible_v, expected&&>); +TEST_CASE("expected: reference-G converting constructors remain available", "[expected_void_ref_e]") { + CHECK(std::is_constructible_v, const expected&>); + CHECK(std::is_constructible_v, expected&&>); + CHECK(std::is_constructible_v, const expected&>); + CHECK(std::is_constructible_v, expected&&>); +} // --------------------------------------------------------------------------- // Construction @@ -62,7 +87,7 @@ static_assert(std::is_constructible_v, expected: default construct has value", "[expected_void_ref_e]") { expected e; REQUIRE(e.has_value()); - static_assert(std::is_nothrow_default_constructible_v>); + CHECK(std::is_nothrow_default_constructible_v>); } TEST_CASE("expected: construct from unexpect+ref binds E&", "[expected_void_ref_e]") { @@ -76,7 +101,7 @@ TEST_CASE("expected: construct from unexpect+ref binds E&", "[expected_ TEST_CASE("expected: in_place_t constructor", "[expected_void_ref_e]") { expected e(std::in_place); CHECK(e.has_value()); - static_assert(noexcept(expected(std::in_place))); + CHECK(noexcept(expected(std::in_place))); } TEST_CASE("expected: copy construct from value state", "[expected_void_ref_e]") { @@ -211,7 +236,7 @@ TEST_CASE("expected: emplace from error state sets has_value", "[expect expected e(unexpect, err); e.emplace(); CHECK(e.has_value()); - static_assert(noexcept(e.emplace())); + CHECK(noexcept(e.emplace())); } TEST_CASE("expected: emplace from value state is no-op", "[expected_void_ref_e]") { @@ -236,7 +261,7 @@ TEST_CASE("expected: operator bool and has_value", "[expected_void_ref_ TEST_CASE("expected: operator*() is void no-op", "[expected_void_ref_e]") { expected e; - static_assert(std::is_void_v); + CHECK(std::is_void_v); *e; } @@ -260,7 +285,7 @@ TEST_CASE("expected: rvalue value() throws on error", "[expected_void_r TEST_CASE("expected: error() returns E& with correct address", "[expected_void_ref_e]") { int err = 99; expected e(unexpect, err); - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); CHECK(&e.error() == &err); } @@ -407,7 +432,7 @@ TEST_CASE("expected: or_else short-circuits on success", "[expected_voi TEST_CASE("expected: transform calls F with no args", "[expected_void_ref_e]") { expected e; auto r = e.transform([]() { return 42; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(r.has_value()); CHECK(*r == 42); } @@ -416,7 +441,7 @@ TEST_CASE("expected: transform with void-returning F", "[expected_void_ expected e; int count = 0; auto r = e.transform([&]() { ++count; }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(r.has_value()); CHECK(count == 1); } @@ -438,7 +463,7 @@ TEST_CASE("expected: transform_error transforms E& to new type", "[expe int err = 3; expected e(unexpect, err); auto r = e.transform_error([](int& v) -> std::string { return std::to_string(v); }); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); REQUIRE(!r.has_value()); CHECK(r.error() == "3"); } @@ -479,8 +504,8 @@ TEST_CASE("expected: monadic chaining error path", "[expected_void_ref_ // --------------------------------------------------------------------------- TEST_CASE("expected: trivial operations", "[expected_void_ref_e]") { - static_assert(std::is_trivially_copyable_v>); - static_assert(std::is_trivially_destructible_v>); + CHECK(std::is_trivially_copyable_v>); + CHECK(std::is_trivially_destructible_v>); } // ============================================================================= diff --git a/tests/beman/expected/testing/constant_eval.hpp b/tests/beman/expected/testing/constant_eval.hpp new file mode 100644 index 0000000..aba5f40 --- /dev/null +++ b/tests/beman/expected/testing/constant_eval.hpp @@ -0,0 +1,74 @@ +// tests/beman/expected/testing/constant_eval.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Reporting compile-time facts through the runtime test framework. +// +// The usual way to test a constexpr contract is `static_assert`. That has one +// bad property as a *test*: a wrong answer is a translation failure, so the +// only thing anyone ever sees is a compiler diagnostic. The test run reports +// nothing, the xUnit output is empty, and because the build stops at the +// first failing assertion no other test in the file is exercised at all. +// +// `constant_eval` separates the two questions a constexpr test actually asks: +// +// 1. "can this be constant-evaluated at all?" — still a hard translation +// failure, because that is a property of the code, not of a value. +// 2. "does it produce the right answer?" — an ordinary runtime comparison +// inside a TEST_CASE, so a wrong answer is *reported*, with the actual +// and expected values, alongside every other test in the run. +// +// `constant_eval` is `consteval`, so a call to it is an immediate invocation: +// the probe is evaluated during translation and its result is required to be +// a constant expression. Question 1 is therefore answered by the call itself, +// with no `static_assert` needed — if the probe body is not usable in a +// constant expression, the program is ill-formed. The result then behaves as +// an ordinary prvalue, free to be compared by CHECK. +// +// TEST_CASE("expected: constexpr error construction") { +// constexpr auto probe = [] { +// expt::expected e(expt::unexpect, 7); +// return int_state{e.has_value(), e.error()}; +// }; +// CHECK(constant_eval(probe) == int_state{false, 7}); +// } +// +// A probe is a plain lambda, not a `consteval` one, so the same probe body +// can be run in both evaluation modes — constant evaluation and ordinary +// evaluation can take different paths through a union-based type: +// +// CHECK(constant_eval(probe) == expect); // constant evaluation +// CHECK(probe() == expect); // ordinary evaluation +// +// Two constraints on a probe follow from the above, and neither is arbitrary: +// +// - It takes no arguments and captures nothing. A closure over a runtime +// value is not a constant expression. Construct whatever the probe +// observes inside the probe. +// - It returns a literal type by value — a scalar, or a small aggregate of +// scalars — never a reference to something it created, and never a type +// whose value cannot escape constant evaluation (`std::string`, +// `std::vector`). Reduce such state to scalars inside the probe. +// +// Give the returned aggregate an `operator<<` so that a mismatch prints both +// states. Without one, Catch2 reports `{?} == {?}` and the reporting benefit +// this component exists for is lost. + +#ifndef BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP +#define BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP + +namespace beman::expected::testing { + +// Constant-evaluate `probe` and return its result as an ordinary value. +template +consteval auto constant_eval(Probe probe); + +} // namespace beman::expected::testing + +// f0b2e22a-6ab4-4112-b826-297a776f01f1 +template +consteval auto beman::expected::testing::constant_eval(Probe probe) { + return probe(); +} +// f0b2e22a-6ab4-4112-b826-297a776f01f1 end + +#endif // BEMAN_EXPECTED_TESTING_CONSTANT_EVAL_HPP diff --git a/tests/beman/expected/testing/type_name.hpp b/tests/beman/expected/testing/type_name.hpp new file mode 100644 index 0000000..23bbde4 --- /dev/null +++ b/tests/beman/expected/testing/type_name.hpp @@ -0,0 +1,223 @@ +// tests/beman/expected/testing/type_name.hpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Type identity as a value, reported by name. +// +// A type-identity check written as `static_assert(std::is_same_v)` is a +// translation failure that names neither type usefully; written as +// `CHECK(std::is_same_v)` it is reported, but the expansion is the bare +// word `false`. `type_name` gives both halves: +// +// FAILED: CHECK( type_name() == type_name() ) +// with expansion: const int& == int& +// +// `type_name()` returns the *identity* of T, not its spelling. Comparison +// is `std::is_same_v`, so the verdict is exact and a false pass is not +// possible. The spelling is consulted only when a comparison has already +// failed and the test framework needs to explain it. Two distinct types that +// the compiler happened to print identically would therefore be reported as +// `foo == foo` — a confusing explanation of a correct verdict, rather than +// the silent false pass that comparing spellings would have given. +// +// That collision is close to unreachable in practice — gcc distinguishes even +// unnamed-namespace and function-local classes by their enclosing scope — so +// this is exactness by construction, not a fix for an observed defect. The +// point is that the guarantee no longer depends on how good any particular +// compiler's names happen to be. +// +// `display_name()` is the spelling on its own, for when a test wants the +// string rather than the comparison. +// +// The spelling is implementation-defined under either implementation below — +// `int&` on one compiler, `int &` on another — so never compare a +// `display_name` against a string literal. +// +// There are two implementations of the spelling, selected automatically. Both +// satisfy the same contract — a non-empty spelling that distinguishes +// distinct types — and the self-checks at the bottom of this header hold each +// of them to it. Which one is in use never changes what a test looks like. +// +// Reflection (P2996), when the compiler offers it: +// +// std::meta::display_string_of(^^T) +// +// This is what reflection is for. It asks the compiler for the name directly +// instead of recovering it from a diagnostic string, so there is no parsing +// and nothing to calibrate. Note that `display_string_of` is itself specified +// as implementation-defined, so this buys robustness, not a canonical +// spelling. No `dealias` is needed: substituting a template argument already +// resolves an alias, so `display_name()` names the aliased type. +// +// Without reflection, the spelling is recovered from +// `std::source_location::function_name()` by calibration: the signatures of +// `signature` and `signature` differ only where the template +// argument is named, so the common prefix and common suffix of the two give +// the offsets at which any type's name sits. Nothing about any compiler's +// format is hard-coded. +// +// Define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION to 0 or 1 to force +// one implementation, which is how the unselected one gets exercised. + +#ifndef BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP +#define BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP + +// `__cpp_impl_reflection` is predefined by the compiler, but +// `__cpp_lib_reflection` is a library macro: without first it is +// never defined here and the reflection path silently never activates. +#include + +#ifndef BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + #if defined(__cpp_lib_reflection) && defined(__cpp_impl_reflection) + #define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION 1 + #else + #define BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION 0 + #endif +#endif + +#include +#include +#include + +#if BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + + #include + +#else + + #include + #include + +namespace beman::expected::testing::detail { + +// The compiler's spelling of this function, template argument included. +template +consteval std::string_view signature(); + +// Length of the longest common prefix of `a` and `b`. +consteval std::size_t common_prefix(std::string_view a, std::string_view b); + +// Length of the longest common suffix of `a` and `b`. +consteval std::size_t common_suffix(std::string_view a, std::string_view b); + +} // namespace beman::expected::testing::detail + +#endif + +namespace beman::expected::testing { + +// The compiler's spelling of `T`. +template +consteval std::string_view display_name(); + +// The identity of `T`, carrying its spelling for diagnostics. Tests do not +// name this type; they compare the results of `type_name`. +template +struct type_name_t { + // HIDDEN FRIENDS + // + // Reached only once a comparison has failed and the test framework is + // explaining it. Nothing about the verdict depends on this text. + friend std::ostream& operator<<(std::ostream& os, type_name_t) { + constexpr std::string_view name = display_name(); + return os << name; + } +}; + +// Exact type identity. The spelling is never consulted. +template +constexpr bool operator==(type_name_t, type_name_t); + +// The identity of `T`, for comparison in a test. +template +consteval type_name_t type_name(); + +} // namespace beman::expected::testing + +// d1c7602e-a42f-46ab-bd53-7adef5646545 +template +constexpr bool beman::expected::testing::operator==(type_name_t, type_name_t) { + return std::is_same_v; +} +// d1c7602e-a42f-46ab-bd53-7adef5646545 end + +template +consteval beman::expected::testing::type_name_t beman::expected::testing::type_name() { + return {}; +} + +#if BEMAN_EXPECTED_TESTING_TYPE_NAME_USE_REFLECTION + +template +consteval std::string_view beman::expected::testing::display_name() { + return std::meta::display_string_of(^^T); +} + +#else + +template +consteval std::string_view beman::expected::testing::detail::signature() { + return std::source_location::current().function_name(); +} + +consteval std::size_t beman::expected::testing::detail::common_prefix(std::string_view a, std::string_view b) { + std::size_t n = 0; + while (n < a.size() && n < b.size() && a[n] == b[n]) + ++n; + return n; +} + +consteval std::size_t beman::expected::testing::detail::common_suffix(std::string_view a, std::string_view b) { + std::size_t n = 0; + while (n < a.size() && n < b.size() && a[a.size() - 1 - n] == b[b.size() - 1 - n]) + ++n; + return n; +} + +template +consteval std::string_view beman::expected::testing::display_name() { + // `bool` and `char` share no leading and no trailing character, so + // neither contributes to the common prefix or the common suffix. Pairs + // that look equally good can fail: gcc spells `long` as `long int`, whose + // trailing `int` is also the whole of `int`, so `int`/`long` would fold + // part of the name itself into the common suffix. + constexpr std::string_view as_bool = detail::signature(); + constexpr std::string_view as_char = detail::signature(); + constexpr std::size_t prefix = detail::common_prefix(as_bool, as_char); + constexpr std::size_t suffix = detail::common_suffix(as_bool, as_char); + static_assert(prefix + suffix < as_bool.size(), + "display_name: this compiler's function_name() does not name the template argument"); + + const std::string_view signature = detail::signature(); + return signature.substr(prefix, signature.size() - prefix - suffix); +} + +#endif + +namespace beman::expected::testing { + +// Self-check on whichever spelling implementation was selected: a recovered +// name must be non-empty, and must not have had a cv-qualifier, a reference, +// or a pointer trimmed off either end. +static_assert(!display_name().empty()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); +static_assert(display_name() != display_name()); + +// Self-check on comparison. These cannot distinguish identity from spelling, +// because no pair of types can be written here where the two disagree: gcc +// qualifies even unnamed-namespace and function-local classes distinctly, so +// a spelling collision is not constructible within one translation unit. +// Exactness is a property of how the comparison is defined, not something +// these assertions can demonstrate. +static_assert(type_name() == type_name()); +static_assert(type_name() == type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); +static_assert(type_name() != type_name()); + +} // namespace beman::expected::testing + +#endif // BEMAN_EXPECTED_TESTING_TYPE_NAME_HPP diff --git a/tests/beman/expected/unexpected.test.cpp b/tests/beman/expected/unexpected.test.cpp index d315bc2..aa96af1 100644 --- a/tests/beman/expected/unexpected.test.cpp +++ b/tests/beman/expected/unexpected.test.cpp @@ -5,6 +5,9 @@ #include +#include +#include + #include #include #include @@ -12,32 +15,49 @@ namespace expt = test_ns; +using beman::expected::testing::constant_eval; +using beman::expected::testing::type_name; + // ============================================================================= // [expected.un.general] para 2 — ill-formed instantiation constraints // (actual ill-formed cases tested via negative compile files) +// +// These type-level properties are checked at runtime rather than with +// static_assert so that a violated property is reported by the test run, with +// the responsible type named, instead of stopping the build at the first +// failure and reporting nothing. Type identity is checked by comparing the +// compiler's spelling of the two types, so a mismatch prints what was deduced +// next to what was wanted rather than the bare word `false`. // ============================================================================= -// [expected.un.cons] Constraint 1.3: is_constructible_v must be true -static_assert(std::is_constructible_v, int>); -static_assert(std::is_constructible_v, const char*>); -static_assert(!std::is_constructible_v, std::string>); +TEST_CASE("unexpected: construction constraints", "[UnexpectedTest]") { + // [expected.un.cons] Constraint 1.3: is_constructible_v must be true + CHECK(std::is_constructible_v, int>); + CHECK(std::is_constructible_v, const char*>); + CHECK_FALSE(std::is_constructible_v, std::string>); -// [expected.un.cons] Constraint 1.2: the *converting* ctor excludes in_place_t as Err, -// routing it to the in-place constructor instead. Both work: -static_assert(std::is_constructible_v, std::in_place_t>); // in-place ctor + // [expected.un.cons] Constraint 1.2: the *converting* ctor excludes in_place_t as Err, + // routing it to the in-place constructor instead. Both work: + CHECK(std::is_constructible_v, std::in_place_t>); // in-place ctor +} -// Copy and move constructible -static_assert(std::is_copy_constructible_v>); -static_assert(std::is_move_constructible_v>); +TEST_CASE("unexpected: copy, move and swap availability", "[UnexpectedTest]") { + // Copy and move constructible + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); -// [expected.un.swap] Constraint: is_swappable_v -static_assert(std::is_swappable_v>); + // [expected.un.swap] Constraint: is_swappable_v + CHECK(std::is_swappable_v>); +} -// [expected.un.obs] error() ref-qualification return types -static_assert(std::is_same_v&>().error()), int&>); -static_assert(std::is_same_v&>().error()), const int&>); -static_assert(std::is_same_v&&>().error()), int&&>); -static_assert(std::is_same_v&&>().error()), const int&&>); +TEST_CASE("unexpected: error() ref-qualification return types", "[UnexpectedTest]") { + // [expected.un.obs] error() ref-qualification return types + using unexpected_t = expt::unexpected; + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); + CHECK(type_name().error())>() == type_name()); +} TEST_CASE("unexpected: construct from int", "[UnexpectedTest]") { expt::unexpected u(42); @@ -147,29 +167,37 @@ TEST_CASE("unexpected: equality different types", "[UnexpectedTest]") { TEST_CASE("unexpected: CTAD from int", "[UnexpectedTest]") { expt::unexpected u(42); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(u.error() == 42); } TEST_CASE("unexpected: CTAD from string", "[UnexpectedTest]") { std::string s("deduced"); expt::unexpected u(s); - static_assert(std::is_same_v>); + CHECK(type_name() == type_name>()); CHECK(u.error() == "deduced"); } TEST_CASE("unexpected: copy and move constructible", "[UnexpectedTest]") { - static_assert(std::is_copy_constructible_v>); - static_assert(std::is_move_constructible_v>); + CHECK(std::is_copy_constructible_v>); + CHECK(std::is_move_constructible_v>); } TEST_CASE("unexpected: unexpect_t tag type", "[UnexpectedTest]") { - static_assert(std::is_same_v); + CHECK(type_name() == type_name()); } TEST_CASE("unexpected: constexpr basic usage", "[UnexpectedTest]") { - constexpr expt::unexpected u(123); - static_assert(u.error() == 123); + // The probe declares the constexpr variable itself, so "is unexpected + // usable as a constexpr variable?" is still answered by the compiler; + // `constant_eval` then hands the observed error back as an ordinary value + // so that a wrong answer is reported rather than breaking the build. + constexpr auto probe = [] { + constexpr expt::unexpected u(123); + return u.error(); + }; + CHECK(constant_eval(probe) == 123); + CHECK(probe() == 123); } TEST_CASE("unexpected: inequality operator (synthesized)", "[UnexpectedTest]") { @@ -180,6 +208,5 @@ TEST_CASE("unexpected: inequality operator (synthesized)", "[UnexpectedTest]") { TEST_CASE("unexpected: in-place ilist constraint: is_constructible from ilist", "[UnexpectedTest]") { // is_constructible_v&, Args...> must hold - static_assert( - std::is_constructible_v>, std::in_place_t, std::initializer_list>); + CHECK(std::is_constructible_v>, std::in_place_t, std::initializer_list>); }