From 7034cf70ce390885db05b01c6499cf273b5229ed Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Sun, 19 May 2024 12:01:01 +0200 Subject: [PATCH] docs(c_test): Add a poc regarding results in c --- c_test/.ccls | 1 + c_test/.envrc | 10 ++ c_test/.gitignore | 7 ++ c_test/flake.lock | 129 ++++++++++++++++++++++ c_test/flake.nix | 87 +++++++++++++++ c_test/makefile | 66 +++++++++++ c_test/result.h | 269 +++++++++++++++++++++++++++++++++++++++++++++ c_test/src/main.c | 101 +++++++++++++++++ c_test/treefmt.nix | 73 ++++++++++++ 9 files changed, 743 insertions(+) create mode 100644 c_test/.ccls create mode 100644 c_test/.envrc create mode 100644 c_test/.gitignore create mode 100644 c_test/flake.lock create mode 100644 c_test/flake.nix create mode 100644 c_test/makefile create mode 100644 c_test/result.h create mode 100644 c_test/src/main.c create mode 100644 c_test/treefmt.nix diff --git a/c_test/.ccls b/c_test/.ccls new file mode 100644 index 0000000..90584dd --- /dev/null +++ b/c_test/.ccls @@ -0,0 +1 @@ +gcc diff --git a/c_test/.envrc b/c_test/.envrc new file mode 100644 index 0000000..8f9d5d7 --- /dev/null +++ b/c_test/.envrc @@ -0,0 +1,10 @@ +use flake || use nix +watch_file flake.nix + +PATH_add ./target +PATH_add ./scripts + +if on_git_branch; then + echo && git status --short --branch && + echo && git fetch --verbose +fi diff --git a/c_test/.gitignore b/c_test/.gitignore new file mode 100644 index 0000000..8dad36b --- /dev/null +++ b/c_test/.gitignore @@ -0,0 +1,7 @@ +# build +/target +/result + +# dev env +.direnv +.ccls-cache diff --git a/c_test/flake.lock b/c_test/flake.lock new file mode 100644 index 0000000..544cbdf --- /dev/null +++ b/c_test/flake.lock @@ -0,0 +1,129 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake_version_update": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1685288691, + "narHash": "sha256-oP6h34oJ8rm6KlUpyZrX+ww3hnoWny2ecrEXxkU7F3c=", + "ref": "refs/heads/prime", + "rev": "e9a97e01eca780bd16e1dbdbd8856b59558f4959", + "revCount": 5, + "type": "git", + "url": "https://codeberg.org/soispha/flake_version_update.git" + }, + "original": { + "type": "git", + "url": "https://codeberg.org/soispha/flake_version_update.git" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711715736, + "narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "807c549feabce7eddbf259dbdcec9e0600a0660d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "flake_version_update": "flake_version_update", + "nixpkgs": "nixpkgs", + "systems": "systems", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1680978846, + "narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=", + "owner": "nix-systems", + "repo": "x86_64-linux", + "rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "x86_64-linux", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711803027, + "narHash": "sha256-Qic3OvsVLpetchzaIe2hJqgliWXACq2Oee6mBXa/IZQ=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "1810d51a015c1730f2fe05a255258649799df416", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/c_test/flake.nix b/c_test/flake.nix new file mode 100644 index 0000000..96e6f25 --- /dev/null +++ b/c_test/flake.nix @@ -0,0 +1,87 @@ +{ + description = "[can be empty]"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs = { + nixpkgs.follows = "nixpkgs"; + }; + }; + + flake_version_update = { + url = "git+https://codeberg.org/soispha/flake_version_update.git"; + inputs = { + systems.follows = "systems"; + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + + # inputs for following + systems = { + url = "github:nix-systems/x86_64-linux"; # only evaluate for this system + }; + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + inputs = { + systems.follows = "systems"; + }; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + treefmt-nix, + flake_version_update, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages."${system}"; + + nativeBuildInputs = with pkgs; [valgrind pandoc]; + treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;}; + + # This version is set automatically on `cog bump --auto`; + version = "v0.1.0"; # GUIDING VERSION STRING + pname = "c_test"; + + build = pkgs.stdenv.mkDerivation { + inherit pname version; + + src = ./.; + makeFlags = ["PREFIX=${placeholder "out"}" "BIN_VERSION=${version}"]; + inherit nativeBuildInputs; + }; + in { + packages.default = build; + checks = { + inherit build; + formatting = treefmtEval.config.build.check self; + }; + formatter = treefmtEval.config.build.wrapper; + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + cocogitto + flake_version_update.packages."${system}".default + + licensure + ]; + inherit nativeBuildInputs; + env = { + GCC_COLORS = "error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01"; + }; + }; + }); +} +# vim: ts=2 + diff --git a/c_test/makefile b/c_test/makefile new file mode 100644 index 0000000..d460739 --- /dev/null +++ b/c_test/makefile @@ -0,0 +1,66 @@ +PREFIX := /usr/local +BINPREFIX := $(DESTDIR)$(PREFIX)/bin +MANPREFIX := $(DESTDIR)$(PREFIX)/share/man/man1 + +BIN_NAME := c_test +# This version is set automatically on `cog bump --auto`; +BIN_VERSION := "v0.1.0" # GUIDING VERSION STRING + +# The trailing slash is important +BUILD_DIR := ./target/ +BIN_PATH := $(BUILD_DIR)$(BIN_NAME) + + +SRC := $(wildcard src/*.c) +OBJ := $(SRC:.c=.o) +DEP := $(OBJ:.o=.d) + +LIBS := + +ALL_CFLAGS := -O3 -MMD -Wall -Wextra -Wno-unused-parameter $(CFLAGS) $(CPPFLAGS) +ALL_LDFLAGS := $(addprefix -l,$(LIBS)) -L $(LD_LIBRARY_PATH) $(LDFLAGS) + +default: all + +all: $(BIN_NAME) + +$(BIN_NAME): $(OBJ) + $(CC) $(addprefix $(BUILD_DIR),$(notdir $(OBJ))) -o $(addprefix $(BUILD_DIR),$(notdir $(BIN_NAME))) $(ALL_CFLAGS) $(ALL_LDFLAGS) + +$(OBJ): $(SRC) + mkdir --parents $(BUILD_DIR) + $(CC) -c $< -o $(addprefix $(BUILD_DIR),$(notdir $(OBJ))) $(ALL_CFLAGS) + +manual: + mkdir --parents $(BUILD_DIR)docs + pandoc "./docs/$(BIN_NAME).1.md" -s -t man > $(BUILD_DIR)docs/$(BIN_NAME).1 + +.PHONY : clean options install memory_leak_test +options: + @echo "PREFIX = $(PREFIX)" + @echo "BINPREFIX = $(BINPREFIX)" + @echo "MANPREFIX = $(MANPREFIX)" + @echo "" + @echo "BIN_NAME = $(BIN_NAME)" + @echo "BUILD_DIR = $(BUILD_DIR)" + @echo "BIN_PATH = $(BIN_PATH)" + @echo "" + @echo "SRC = $(SRC)" + @echo "OBJ = $(OBJ)" + @echo "DEP = $(DEP)" + @echo "" + @echo "LIBS = $(LIBS)" + @echo "" + @echo "ALL_CFLAGS = $(ALL_CFLAGS)" + @echo "ALL_LDFLAGS = $(ALL_LDFLAGS)" + @echo "" + +clean : + rm --recursive $(BUILD_DIR) + +install: $(BIN_NAME) manual + install -D $(BUILD_DIR)docs/$(BIN_NAME).1 $(MANPREFIX)/$(BIN_NAME); + install -D $(BUILD_DIR)$(BIN_NAME) $(BINPREFIX)/$(BIN_NAME); + +memory_leak_test: + sh ./scripts/valgrind_test.sh $(BIN_NAME) diff --git a/c_test/result.h b/c_test/result.h new file mode 100644 index 0000000..465fbea --- /dev/null +++ b/c_test/result.h @@ -0,0 +1,269 @@ +#include +#include + +/** + * @file result.h + * Provides an alternative to exceptions, whose semantics are slightly better + * than exceptions + * + * C does not support exceptions. Many C functions instead return a status code + * reporting what happened. Alternatively, many C libraries allow a callback to + * be logged in order to report a detailed description of any errors that + * occurred. However, the programming language known as Rust deliberately left + * out exceptions; instead they opted for return values to report errors, with + * a `RESULT` type. This type contains the actual value if there is one, or + * some error otherwise. + * + * This is the approach that this file defines. + */ + +/** + * @brief A macro which gives the value of a `RESULT` if present, else returns + * the result. + * @details Do not use this twice on the same line. + * + * Example Usage: + * @code + * RESULT(int) someResultFromFn = ...; + * + * int value; + * + * RESULT_M_TRY(int, value, someResultFromFn); + * @endcode + * + * @param type The type of the `RESULT(type)` + * @param dest The location to write the new result into + * @param result The old result + * @return nothing. (wrapped in `do-while`) + * + * @see RESULT_M_TRY_DECL + */ +#define RESULT_M_TRY(type, dest, result) \ + do \ + { \ + RESULT (type) _result_##type##_try_at_##__LINE__ = result; \ + if (_result_##type##_try_at_##__LINE__.err) \ + { \ + return _result_##type##_try_at_##__LINE__; \ + } \ + dest = _result_##type##_try_at_##__LINE__.value; \ + } \ + while (0) + +/** + * @brief A macro which gives the value of a `RESULT` if present, else returns + * the result. + * @details Do not use this twice on the same line. + * + * Example Usage: + * @code + * RESULT(int) myFn(int someParam) { + * .... + * } + * + * RESULT(int) function() { + * RESULT_M_TRY_DECL(int, val, myFn(1)); + * // equivalent to: + * RESULT(int) r = myFn(1); + * if (!RESULT_IS_OK(int)(r)) return r; + * int val = RESULT_UNWRAP(r); + * } + * @endcode + * + * @param type The type of the `RESULT(type)` + * @param dest The location to write the new result into + * @param result The old result + * @return nothing. (wrapped in `do-while`) + * + * @see RESULT_M_TRY + */ +#define RESULT_M_TRY_DECL(type, dest, result) \ + type dest; \ + RESULT_M_TRY (type, dest, result) + +/** + * @brief Template for a RESULT class (similar to the one that the language + * Rust has). This is the typename. + * @details This macro expands to a unique name per type, so `RESULT(int)` is + * roughly equivalent to the C++ `RESULT`. Note that + * `RESULT(int*)` will not compile; if you want to use something along + * those lines, you must typedef it first. + * + * For the error strings, you must use string literals or something + * with storage duration, so the pointer doesn't become invalidated. + * + * @param The template parameter + * @return `Result_ type`, so `RESULT(int)` is `Result_int` Each macro of this + * sort follows that convention, and you can rely on it + */ +#define RESULT(type) Result_##type + +// Class functions +/** + * @brief Creates a result which holds a value of `type` + * @details Indicates that no error occurred, and that the computed value from + * the function can be retrieved via `RESULT_UNWRAP`. + * + * @param type value + * @return Returns a `RESULT(type)` containing a `type` + */ +#define RESULT_OK(type) Result_##type##_Ok + +/** + * @brief Creates a result which indicates that an error occurred. + * @details Indicates that an error occurred in the function. Similar to an + * exception in other languages. + * + * @param const char * error message + * @return Returns a `RESULT(type)` that does not contain a `type` + */ +#define RESULT_ERR(type) Result_##type##_Err + +// methods +/** + * @brief Tests if the `RESULT` actually contains a `type` (else it was an + * error) + * + * @return `true` if calling `RESULT_UNWRAP` is safe, else `false` + */ +#define RESULT_IS_OK(type) Result_##type##_is_ok + +/** + * @brief Tests if the `RESULT` doesn't contain a `type` (else it was ok) + * + * @return `true` if calling `RESULT_UNWRAP_ERR` is safe, else `false` + */ +#define RESULT_IS_ERR(type) Result_##type##_is_err + +/** + * @brief Unwraps the `RESULT` into a `type`, panicking if impossible + * @details If `RESULT_IS_OK`, this returns the `type` value of the `RESULT`. + * Otherwise, this panics. + * + * @return `type` that is contained in the `RESULT`, or could `exit()` + * + * @see RESULT_EXPECT + */ +#define RESULT_UNWRAP(type) Result_##type##_unwrap + +/** + * @brief Unwraps the `RESULT` into a `type`, panicking with the error message + * if impossible + * @details Similar to `RESULT_UNWRAP`, except this has a specialized error + * message + * + * @param const char * the error message on failure + * @return `type` contained in `RESULT`, else terminates the program + * + * @see RESULT_UNWRAP + */ +#define RESULT_EXPECT(type) Result_##type##_expect + +/** + * @brief Unwraps the `RESULT` into a `type`, returning the supplied + * alternative otherwise + * @details If `RESULT_IS_OK`, this returns the `type` value of the `RESULT`. + * Otherwise, this returns the alternative supplied in the function + * arguments. + * + * @param type or_else The value returned if the unwrap failed + * @return `type` contained in `RESULT`, or the alternative + * + * @see RESULT_UNWRAP + * @see RESULT_EXPECT + */ +#define RESULT_UNWRAP_OR(type) Result_##type##_unwrap_or + +/** + * @brief Unwraps the `RESULT` into an error string, panicking if impossible + * @details If `RESULT_IS_ERR`, this returns error message of the `RESULT`. + * Otherwise, this panics. + * + * @param const char * the error string + * @return error string in the RESULT, or could terminate + * + * @see RESULT_EXPECT + */ +#define RESULT_UNWRAP_ERR(type) Result_##type##_unwrap_err + +/** + * @brief Declares a RESULT type for use. + * @details This declares the struct and functions for a given type. Note that + * the functions still need to be defined. The values inside the + * struct are implementation details, and may change at any time; do not use + * them. + * + * @param template parameter + * + * @see RESULT_DEFINE + */ +#define RESULT_DECLARE(type) \ + typedef struct RESULT (type) \ + { \ + type value; \ + /* string literals only */ \ + const char *err; \ + } RESULT (type); \ + RESULT (type) RESULT_OK (type) (type value); \ + RESULT (type) RESULT_ERR (type) (const char *err); \ + bool RESULT_IS_OK (type) (const RESULT (type) *); \ + bool RESULT_IS_ERR (type) (const RESULT (type) *); \ + type RESULT_UNWRAP (type) (const RESULT (type) *); \ + type RESULT_EXPECT (type) (const RESULT (type) *, const char *); \ + type RESULT_UNWRAP_OR (type) (const RESULT (type) *, type); \ + const char *RESULT_UNWRAP_ERR (type) (const RESULT (type) *); + +/** + * @brief Defines all the functions for the given `RESULT(type)` + * @details Defines each and every function necessary to use the `RESULT`. + * + * @param template parameter + * + * @see RESULT_DECLARE + */ +#define RESULT_DEFINE(type) \ + RESULT (type) RESULT_OK (type) (type value) \ + { \ + RESULT (type) res = { .value = value, .err = NULL }; \ + return res; \ + } \ + RESULT (type) RESULT_ERR (type) (const char *err) \ + { \ + RESULT (type) res = { .err = err }; \ + return res; \ + } \ + bool RESULT_IS_OK (type) (const RESULT (type) * res) \ + { \ + return res->err == NULL; \ + } \ + bool RESULT_IS_ERR (type) (const RESULT (type) * res) \ + { \ + return res->err != NULL; \ + } \ + type RESULT_UNWRAP (type) (const RESULT (type) * res) \ + { \ + if (RESULT_IS_ERR (type) (res)) \ + panicf ("Attempted to unwrap empty Result of type " #type \ + ". Instead had error: %s", \ + res->err); \ + return res->value; \ + } \ + type RESULT_EXPECT (type) (const RESULT (type) * res, \ + const char *message_on_err) \ + { \ + if (RESULT_IS_ERR (type) (res)) \ + panic (message_on_err); \ + return res->value; \ + } \ + type RESULT_UNWRAP_OR (type) (const RESULT (type) * res, type else_val) \ + { \ + if (RESULT_IS_ERR (type) (res)) \ + return else_val; \ + return res->value; \ + } \ + const char *RESULT_UNWRAP_ERR (type) (const RESULT (type) * res) \ + { \ + if (RESULT_IS_OK (type) (res)) \ + panic ("Result was not an error; type: " #type); \ + return res->err; \ + } diff --git a/c_test/src/main.c b/c_test/src/main.c new file mode 100644 index 0000000..924e15b --- /dev/null +++ b/c_test/src/main.c @@ -0,0 +1,101 @@ +#include +#include + +#include + +#define Result(ok, err) struct result_##ok##_##err + +typedef const char *string; + +enum ResultTag +{ + OK, + ERR, +}; + +Result (u_int32_t, string) +{ + enum ResultTag tag; + union + { + u_int32_t ok; + string err; + } value; +}; + +Result (string, ResultTag) +{ + enum ResultTag tag; + union + { + string ok; + enum ResultTag err; + } value; +}; + +Result (u_int32_t, string) get_a () +{ + int r = rand (); // Returns a pseudo-random integer between 0 and RAND_MAX. + + Result (u_int32_t, string) result; + if (r % 2 == 0) + { + result.value.ok = 32; + result.tag = OK; + } + else + { + result.value.err = "Can't access a! Have you run the foo?"; + result.tag = ERR; + } + + return result; +} + +Result (string, ResultTag) get_b () +{ + int r = rand (); // Returns a pseudo-random integer between 0 and RAND_MAX. + + Result (string, ResultTag) result; + if (r % 2 == 0) + { + result.value.ok = "Wow could defudge the foo!"; + result.tag = OK; + } + else + { + result.value.err = ERR; + result.tag = ERR; + } + + return result; +} + +int +main () +{ + srand (time (NULL)); // Initialization, should only be called once. + printf ("Hello World!\n"); + + Result (u_int32_t, string) result = get_a (); + if (result.tag == OK) + { + printf ("Result is ok with value: '%d'\n", result.value.ok); + } + else + { + printf ("Result is err with value: '%s'\n", result.value.err); + } + + Result (string, ResultTag) result_b = get_b (); + if (result.tag == OK) + { + printf ("Result B is ok with value: '%s'\n", result_b.value.ok); + } + else + { + printf ("Result B is err with value: '%i'\n", result_b.value.err); + } + + return EXIT_SUCCESS; +} diff --git a/c_test/treefmt.nix b/c_test/treefmt.nix new file mode 100644 index 0000000..1cbab40 --- /dev/null +++ b/c_test/treefmt.nix @@ -0,0 +1,73 @@ +{ + treefmt-nix, + pkgs, +}: +treefmt-nix.lib.evalModule pkgs ( + {pkgs, ...}: { + # Used to find the project root + projectRootFile = "flake.nix"; + + programs = { + alejandra.enable = true; + rustfmt.enable = true; + clang-format.enable = true; + mdformat.enable = true; + shfmt = { + enable = true; + indent_size = 4; + }; + shellcheck.enable = true; + prettier = { + settings = { + arrowParens = "always"; + bracketSameLine = false; + bracketSpacing = true; + editorconfig = true; + embeddedLanguageFormatting = "auto"; + endOfLine = "lf"; + # experimentalTernaries = false; + htmlWhitespaceSensitivity = "css"; + insertPragma = false; + jsxSingleQuote = true; + printWidth = 80; + proseWrap = "always"; + quoteProps = "consistent"; + requirePragma = false; + semi = true; + singleAttributePerLine = true; + singleQuote = true; + trailingComma = "all"; + useTabs = false; + vueIndentScriptAndStyle = false; + + tabWidth = 4; + overrides = { + files = ["*.js"]; + options.tabwidth = 2; + }; + }; + }; + stylua.enable = true; + ruff = { + enable = true; + format = true; + }; + taplo.enable = true; + }; + + settings = { + global.excludes = [ + "CHANGELOG.md" + "NEWS.md" + ]; + formatter = { + clang-format = { + options = ["--style" "GNU"]; + }; + shfmt = { + includes = ["*.bash"]; + }; + }; + }; + } +)