From d7d861ee3db7988b97d55bb5ee12eba5d5391840 Mon Sep 17 00:00:00 2001 From: "Eric Vincent (arch)" Date: Wed, 22 Jul 2026 11:57:58 -0400 Subject: [PATCH 1/2] refactor(build): consume cpp-core via pinned submodule --- .gitmodules | 3 + README.md | 24 +- ci/verify_cpp_core_submodule.sh | 27 + docs/CONTRIBUTING.md | 30 + docs/Doxyfile | 1 + makefile | 35 +- src/base/core_console.cppm | 298 ---------- src/base/core_debug.h | 34 -- src/base/core_file.cppm | 129 ----- src/base/core_hash.cppm | 102 ---- src/base/core_hash_map.cppm | 996 -------------------------------- src/base/core_lexer.cppm | 478 --------------- src/base/core_lexer_coord.cppm | 232 -------- src/base/core_memory.cppm | 197 ------- src/base/core_option.cppm | 91 --- src/base/core_portability.cppm | 231 -------- src/base/core_result.cppm | 47 -- src/base/core_result_s.cppm | 60 -- src/base/core_string.cppm | 397 ------------- src/base/core_text_parse.cppm | 522 ----------------- src/base/core_types.cppm | 229 -------- src/base/core_vm.cppm | 78 --- src/puzzles/2015/y2015d00.cppm | 2 +- src/puzzles/2015/y2015d02.cppm | 2 +- src/puzzles/2015/y2015d03.cppm | 2 +- src/puzzles/2015/y2015d04.cppm | 2 +- src/puzzles/2015/y2015d05.cppm | 2 +- src/puzzles/2015/y2015d06.cppm | 2 +- src/puzzles/2015/y2015d07.cppm | 2 +- src/puzzles/2015/y2015d08.cppm | 2 +- src/puzzles/2015/y2015d09.cppm | 2 +- third_party/cpp-core | 1 + 32 files changed, 114 insertions(+), 4146 deletions(-) create mode 100644 .gitmodules create mode 100755 ci/verify_cpp_core_submodule.sh delete mode 100644 src/base/core_console.cppm delete mode 100644 src/base/core_debug.h delete mode 100644 src/base/core_file.cppm delete mode 100644 src/base/core_hash.cppm delete mode 100644 src/base/core_hash_map.cppm delete mode 100644 src/base/core_lexer.cppm delete mode 100644 src/base/core_lexer_coord.cppm delete mode 100644 src/base/core_memory.cppm delete mode 100644 src/base/core_option.cppm delete mode 100644 src/base/core_portability.cppm delete mode 100644 src/base/core_result.cppm delete mode 100644 src/base/core_result_s.cppm delete mode 100644 src/base/core_string.cppm delete mode 100644 src/base/core_text_parse.cppm delete mode 100644 src/base/core_types.cppm delete mode 100644 src/base/core_vm.cppm create mode 160000 third_party/cpp-core diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ebb3c89 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/cpp-core"] + path = third_party/cpp-core + url = https://github.com/efvincent/cpp-core.git diff --git a/README.md b/README.md index 892360c..a5c548c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # aoc-cpp -Low-level Advent of Code workspace using C++23 modules and explicit systems-style design. +Low-level Advent of Code workspace using C++26 modules and explicit systems-style design. This README is the documentation index and recommended reading path. @@ -48,10 +48,26 @@ The rendered Doxygen site now uses a dedicated Markdown landing page so authored ### Requirements -- clang++ with C++23 modules support +- clang++ with C++26 modules support - make - doxygen (for API docs) +### Clone and submodule setup + +The repository consumes core modules through a pinned git submodule. + +```sh +git clone https://github.com/efvincent/aoc-cpp.git +cd aoc-cpp +git submodule update --init --recursive +``` + +To verify the pin contract locally: + +```sh +make verify-submodule-pin +``` + ### Common commands ```sh @@ -62,6 +78,7 @@ make instrument make bear make docs make docs_pdf +make verify-submodule-pin make clean make clean_all @@ -149,7 +166,8 @@ Example row with omitted part: ## Repository Layout -- src/: executable entry point and C++ module units +- src/: executable entry point and AoC-specific module units +- third_party/cpp-core/: pinned core module source submodule - docs/: authored docs, Doxygen config, generated API docs - data/: Advent of Code inputs - build/: generated binaries, module caches, objects, and intermediate output diff --git a/ci/verify_cpp_core_submodule.sh b/ci/verify_cpp_core_submodule.sh new file mode 100755 index 0000000..a797f95 --- /dev/null +++ b/ci/verify_cpp_core_submodule.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +set -eu + +submodule_path="${CPP_CORE_SUBMODULE_PATH:-third_party/cpp-core}" +tag_pattern="${CPP_CORE_TAG_PATTERN:-v[0-9]*.[0-9]*.[0-9]*}" + +if ! git -C "$submodule_path" rev-parse --verify HEAD >/dev/null 2>&1; then + echo "cpp-core submodule is missing or not initialized: $submodule_path" + exit 1 +fi + +tag="$(git -C "$submodule_path" tag --points-at HEAD | head -n 1 || true)" + +if [ -z "$tag" ]; then + echo "cpp-core submodule is not pinned to a release tag" + exit 1 +fi + +case "$tag" in + $tag_pattern) + echo "cpp-core pinned to release tag: $tag" + ;; + *) + echo "cpp-core tag is not semver-shaped: $tag" + exit 1 + ;; +esac diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0f65fcb..54b639c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -9,6 +9,36 @@ This document serves as the absolute reference for the architectural foundations --- +## Submodule Dependency Contract + +- `cpp-core` is consumed via git submodule at `third_party/cpp-core`. +- Downstream pin policy: point only to released semver tags (`vMAJOR.MINOR.PATCH`). +- Reject untagged or floating submodule pointers in review/CI. + +### Initial setup + +```sh +git submodule update --init --recursive +``` + +### Standard upgrade workflow + +```sh +git -C third_party/cpp-core fetch --tags origin +git -C third_party/cpp-core checkout vX.Y.Z +git add third_party/cpp-core +git commit -m "chore(submodule): bump cpp-core to vX.Y.Z" +``` + +After any bump, run full local integration checks (build modes and puzzle smoke runs) before opening a PR. + +### Pin verification + +- Local check: `make verify-submodule-pin` +- CI/script entrypoint: `ci/verify_cpp_core_submodule.sh` + +--- + ## 1. Core Philosophy & Language Paradigm ### The "Orthodox C++" (C+) Paradigm diff --git a/docs/Doxyfile b/docs/Doxyfile index ceb469f..c682600 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -1010,6 +1010,7 @@ WARN_LOGFILE = docs/doxygen-warnings.log # Note: If this tag is empty the current directory is searched. INPUT = src \ + third_party/cpp-core/src \ docs # This tag can be used to specify the character encoding of the source files diff --git a/makefile b/makefile index 8eead26..d4ae80b 100644 --- a/makefile +++ b/makefile @@ -9,6 +9,7 @@ MODE ?= debug # Directory configuration SRC_DIR := src +CORE_SRC_DIR := third_party/cpp-core/src BUILD_DIR := build/$(MODE) MOD_DIR := $(BUILD_DIR)/modules OBJ_DIR := $(BUILD_DIR)/obj @@ -32,7 +33,7 @@ CXX := clang++ OPENSSL_CFLAGS := $(shell pkg-config --cflags openssl 2>/dev/null) OPENSSL_LIBS := $(shell pkg-config --libs openssl 2>/dev/null) -BASE_FLAGS := -std=c++23 -fno-exceptions -fno-rtti -Wall -Wextra -Wpedantic $(OPENSSL_CFLAGS) +BASE_FLAGS := -std=c++26 -fno-exceptions -fno-rtti -Wall -Wextra -Wpedantic -I./$(CORE_SRC_DIR) $(OPENSSL_CFLAGS) ifeq ($(MODE), release) # High optimization release @@ -58,32 +59,37 @@ MJFLAGS = $(if $(GENERATE_COMPDB),-MJ $(COMPDB_DIR)/$(subst /,_,$(patsubst $(O # Recursive source discovery for everything under src/ # ===================================================== -CPPM_SRCS := $(shell find $(SRC_DIR) -type f -name '*.cppm' | sort) +AOC_CPPM_SRCS := $(shell find $(SRC_DIR) -type f -name '*.cppm' | sort) +CORE_CPPM_SRCS := $(shell find $(CORE_SRC_DIR) -type f -name '*.cppm' | sort) CPP_SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp' | sort) -MODULE_SRCS := $(CPPM_SRCS) -MODULE_OBJS := $(patsubst $(SRC_DIR)/%.cppm,$(OBJ_DIR)/%.o,$(MODULE_SRCS)) -CPP_OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(CPP_SRCS)) +MODULE_SRCS := $(AOC_CPPM_SRCS) $(CORE_CPPM_SRCS) + +module_obj_from_src = $(OBJ_DIR)/$(patsubst %.cppm,%.o,$(1)) +module_pcm_from_src = $(MOD_DIR)/$(patsubst %.cppm,%.pcm,$(1)) + +MODULE_OBJS := $(foreach src,$(MODULE_SRCS),$(call module_obj_from_src,$(src))) +CPP_OBJS := $(patsubst %.cpp,$(OBJ_DIR)/%.o,$(CPP_SRCS)) ALL_OBJS := $(MODULE_OBJS) $(CPP_OBJS) # Point linker/consumers at every discovered prebuilt module. -LINK_MOD_FLAGS := $(foreach src,$(MODULE_SRCS),-fmodule-file=$(basename $(notdir $(src)))=$(MOD_DIR)/$(patsubst $(SRC_DIR)/%.cppm,%.pcm,$(src))) +LINK_MOD_FLAGS := $(foreach src,$(MODULE_SRCS),-fmodule-file=$(basename $(notdir $(src)))=$(call module_pcm_from_src,$(src))) # Automatic module dependency extraction from `import ;` statements. # Maps imported module names to local module object targets, so adding modules # normally requires no makefile edits. -module_obj_from_name = $(patsubst $(SRC_DIR)/%.cppm,$(OBJ_DIR)/%.o,$(firstword $(filter %/$(1).cppm,$(MODULE_SRCS)))) -imports_of_src = $(shell sed -n 's/^[[:space:]]*import[[:space:]]\([A-Za-z0-9_]*\)[[:space:]]*;.*/\1/p' $(1)) +module_obj_from_name = $(call module_obj_from_src,$(firstword $(filter %/$(1).cppm,$(MODULE_SRCS)))) +imports_of_src = $(shell sed -n 's/^[[:space:]]*\(export[[:space:]]\+\)\?import[[:space:]]\([A-Za-z0-9_.]*\)[[:space:]]*;.*/\2/p' $(1)) module_import_obj_deps = $(strip $(foreach m,$(call imports_of_src,$(1)),$(call module_obj_from_name,$(m)))) -$(foreach src,$(MODULE_SRCS),$(eval $(patsubst $(SRC_DIR)/%.cppm,$(OBJ_DIR)/%.o,$(src)): $(call module_import_obj_deps,$(src)))) +$(foreach src,$(MODULE_SRCS),$(eval $(call module_obj_from_src,$(src)): $(call module_import_obj_deps,$(src)))) #==================================== # Phase execution and meta rules #==================================== -.PHONY: all debug release lto instrument bench bench-debug bench-release bench-lto bench-instrument clean docs docs_pdf clean_all bear +.PHONY: all debug release lto instrument bench bench-debug bench-release bench-lto bench-instrument clean docs docs_pdf clean_all bear verify-submodule-pin N ?= 1000 ARGS ?= 2015 2 @@ -117,6 +123,9 @@ bench-lto: lto bench-instrument: instrument @./bench_nanos.sh $(N) instrument $(ARGS) +verify-submodule-pin: + @sh ci/verify_cpp_core_submodule.sh + # The compilation database target: rebuilds with Clang -MJ fragments so # module interface units are recorded correctly in compile_commands.json. bear: @@ -132,13 +141,13 @@ $(TARGET): $(ALL_OBJS) @$(MKDIR) build $(CXX) $(CXXFLAGS) $(ALL_OBJS) -o $(TARGET) $(OPENSSL_LIBS) -# rule A: Compile any module interface unit discovered under src/ -$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cppm +# rule A: Compile any module interface unit from AoC src or submodule core src. +$(OBJ_DIR)/%.o: %.cppm @$(MKDIR) $(dir $(MOD_DIR)/$*.pcm) $(@D) $(if $(GENERATE_COMPDB),$(COMPDB_DIR)) $(CXX) $(CXXFLAGS) $(MOD_FLAGS) $(LINK_MOD_FLAGS) $(MJFLAGS) -fmodule-output=$(MOD_DIR)/$*.pcm -c $< -o $@ # rule B: Compile any translation unit discovered under src/ -$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp $(MODULE_OBJS) +$(OBJ_DIR)/%.o: %.cpp $(MODULE_OBJS) @$(MKDIR) $(@D) $(if $(GENERATE_COMPDB),$(COMPDB_DIR)) $(CXX) $(CXXFLAGS) $(LINK_MOD_FLAGS) $(MJFLAGS) -c $< -o $@ diff --git a/src/base/core_console.cppm b/src/base/core_console.cppm deleted file mode 100644 index e4c1e08..0000000 --- a/src/base/core_console.cppm +++ /dev/null @@ -1,298 +0,0 @@ -module; -#include -#include - -/** - * @file core_console.cppm - * @brief Console input/output helpers backed by C stdio primitives. - */ - -/** - * @module core_console - * @brief Lightweight terminal read/write wrappers using Str8 and Arena. - */ - -export module core_console; - -import core_types; -import core_string; -import core_memory; -import core_result; - -using namespace base; -using namespace str; - -/** - * @namespace base::console - * @brief Low-level console IO utilities. - */ -export namespace base::console { - /** - * @brief Console input failure categories for line-oriented reads. - */ - enum struct ConsoleError : u8 { - None = 0, - OutOfMemory, - EndOfInput, - ReadFailed, - LineTooLong - }; - - /** - * @brief Console output failure categories. - */ - enum struct ConsoleWriteError : u8 { - None = 0, - FormatFailed, - OutOfMemory, - Truncated, - StdoutWriteFailed, - StderrWriteFailed, - FlushFailed - }; - - /** @brief Result alias for console write/format operations. */ - using ConsoleWriteResult = Result; - - /** @brief Result alias for console line reads. */ - using ConsoleReadResult = Result; - - // ========================================================================== - // Output - // ========================================================================== - - /** - * @brief Maps string formatting errors to console write errors. - */ - constexpr ConsoleWriteError map_string_error_to_console(StringFormatError e) { - using enum StringFormatError; - switch(e) { - case FormatFailed: return ConsoleWriteError::FormatFailed; - case OutOfMemory: return ConsoleWriteError::OutOfMemory; - case Truncated: return ConsoleWriteError::Truncated; - default: return ConsoleWriteError::FormatFailed; - } - } - - /** - * @brief Writes bytes to stdout with result checking. - * @param text Bytes to write. - * @return Ok(bytes_written) or Err(ConsoleWriteError::StdoutWriteFailed). - */ - inline ConsoleWriteResult write_stdout(Str8 text) { - if (text.len == 0) return ConsoleWriteResult::ok(0); - u64 written = static_cast(fwrite(text.str, 1, text.len, stdout)); - if (written != text.len) { - return ConsoleWriteResult::err(ConsoleWriteError::StdoutWriteFailed); - } - return ConsoleWriteResult::ok(written); - } - - /** - * @brief Writes bytes to stderr with result checking. - * @param text Bytes to write. - * @return Ok(bytes_written) or Err(ConsoleWriteError::StderrWriteFailed). - */ - inline ConsoleWriteResult write_stderr(Str8 text) { - if (text.len == 0) return ConsoleWriteResult::ok(0); - u64 written = static_cast(fwrite(text.str, 1, text.len, stderr)); - if (written != text.len) { - return ConsoleWriteResult::err(ConsoleWriteError::StderrWriteFailed); - } - return ConsoleWriteResult::ok(written); - } - - /** - * @brief Flushes stdout with result checking. - * @return Ok(0) on success or Err(ConsoleWriteError::FlushFailed). - */ - inline ConsoleWriteResult flush_stdout_checked() { - if (fflush(stdout) != 0) { - return ConsoleWriteResult::err(ConsoleWriteError::FlushFailed); - } - return ConsoleWriteResult::ok(0); - } - - /** - * @brief Formats and writes one line to stdout (exact two-pass formatting). - * @param arena Arena used by formatting. - * @param format Printf-style format string. - * @param ... Format arguments. - * @return Ok(total_bytes_written) or Err(ConsoleWriteError). - */ - inline ConsoleWriteResult printfl(mem::Arena& arena, const char* format, ...) { - va_list args; - va_start(args, format); - StringFormatResult fmt = vformat(arena, format, args); - va_end(args); - - if (!fmt.is_ok()) { - return ConsoleWriteResult::err(map_string_error_to_console(fmt.error)); - } - - auto w = write_stdout(fmt.value); - if (!w.is_ok()) return ConsoleWriteResult::err(w.error); - - auto n = write_stdout(Str8("\n")); - if (!n.is_ok()) return ConsoleWriteResult::err(n.error); - - auto f = flush_stdout_checked(); - if (!f.is_ok()) return ConsoleWriteResult::err(f.error); - - return ConsoleWriteResult::ok(fmt.value.len + 1); - } - - /** - * @brief Formats and writes one line to stdout (single-pass capped formatting). - * @param arena Arena used by formatting. - * @param cap Maximum formatted payload bytes. - * @param format Printf-style format string. - * @param ... Format arguments. - * @return Ok(total_bytes_written) or Err(ConsoleWriteError). - */ - inline ConsoleWriteResult printfl_cap(mem::Arena& arena, u64 cap, const char* format, ...) { - va_list args; - va_start(args, format); - StringFormatResult fmt = vformat_cap(arena, cap, format, args); - va_end(args); - - if (!fmt.is_ok()) { - return ConsoleWriteResult::err(map_string_error_to_console(fmt.error)); - } - - auto w = write_stdout(fmt.value); - if (!w.is_ok()) return ConsoleWriteResult::err(w.error); - - auto n = write_stdout(Str8("\n")); - if (!n.is_ok()) return ConsoleWriteResult::err(n.error); - - auto f = flush_stdout_checked(); - if (!f.is_ok()) return ConsoleWriteResult::err(f.error); - - return ConsoleWriteResult::ok(fmt.value.len + 1); - } - - /** - * @brief Formats and writes one line to stderr (single-pass capped formatting). - * @param arena Arena used by formatting. - * @param cap Maximum formatted payload bytes. - * @param format Printf-style format string. - * @param ... Format arguments. - * @return Ok(total_bytes_written) or Err(ConsoleWriteError). - */ - inline ConsoleWriteResult eprintfl_cap(mem::Arena& arena, u64 cap, const char* format, ...) { - va_list args; - va_start(args, format); - StringFormatResult fmt = vformat_cap(arena, cap, format, args); - va_end(args); - - if (!fmt.is_ok()) { - return ConsoleWriteResult::err(map_string_error_to_console(fmt.error)); - } - - auto w = write_stderr(fmt.value); - if (!w.is_ok()) return ConsoleWriteResult::err(w.error); - - auto n = write_stderr(Str8("\n")); - if (!n.is_ok()) return ConsoleWriteResult::err(n.error); - - return ConsoleWriteResult::ok(fmt.value.len + 1); - } - - /** - * @brief Writes raw text to stdout without appending a newline. - * @param text UTF-8 byte slice to write. - */ - inline void print(Str8 text) { - (void)write_stdout(text); - } - - /** - * @brief Writes text to stdout followed by a newline. - * @param text UTF-8 byte slice to write. - */ - inline void printl(Str8 text) { - auto _w = write_stdout(text); - if (!_w.is_ok()) return; - auto _n = write_stdout(Str8("\n")); - if (!_n.is_ok()) return; - (void)flush_stdout_checked(); - } - - /** - * @brief Flushes stdout explicitly. - * @note Useful after partial-line output written with print(). - */ - inline void flush_console() { - (void)flush_stdout_checked(); - } - - /** - * @brief Writes text to stderr and always appends a newline. - * @param text UTF-8 byte slice to write. - */ - inline void print_err(base::Str8 text) { - (void)write_stderr(text); - (void)write_stderr(base::Str8("\n")); - } - - // ========================================================================== - // Input - // ========================================================================== - - /** - * @brief Reads one line from stdin into arena-backed storage. - * @param arena Destination arena used for temporary line storage. - * @param max_bytes Maximum bytes to reserve/read including terminating null from fgets. - * @return Ok(Str8) with the line excluding trailing newline, or Err(ConsoleError) - * for EOF, allocation failure, input failure, or truncation. - * @note An empty line is returned as Ok(Str8{}). - */ - inline ConsoleReadResult read_line(base::mem::Arena& arena, u64 max_bytes = 1024) { - using enum ConsoleError; - u64 snapshot = arena.offset; - - u8* buffer = arena.alloc_array(max_bytes); - if (!buffer) { - return ConsoleReadResult::err(OutOfMemory); - } - - if (fgets(reinterpret_cast(buffer), static_cast(max_bytes), stdin) == nullptr) { - arena.offset = snapshot; - if (feof(stdin)) { - return ConsoleReadResult::err(EndOfInput); - } - return ConsoleReadResult::err(ReadFailed); - } - - u64 len = 0; - bool found_newline = false; - bool found_null = false; - - while (len < max_bytes) { - if (buffer[len] == '\n') { - found_newline = true; - break; - } - if (buffer[len] == 0) { - found_null = true; - break; - } - len++; - } - - if (len == 0) { - arena.offset = snapshot; - return ConsoleReadResult::ok(base::Str8{}); - } - - if (!found_newline && !found_null && len == max_bytes - 1 && !feof(stdin)) { - arena.offset = snapshot; - return ConsoleReadResult::err(LineTooLong); - } - - arena.offset -= (max_bytes - len); - return ConsoleReadResult::ok(base::Str8(buffer, len)); - } - -} // namespace base::console diff --git a/src/base/core_debug.h b/src/base/core_debug.h deleted file mode 100644 index caf0d31..0000000 --- a/src/base/core_debug.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -/** - * @file core_debug.h - * @brief Assertion macros used by base modules. - * @note Assertions are enabled in debug/instrument builds and compiled out in release. - */ - -#if defined(BUILD_DEBUG) || defined(BUILD_INSTRUMENT) - #if defined(_MSC_VER) - /** - * @def BASE_ASSERT(expression) - * @brief Terminates immediately when \p expression evaluates to false. - * @param expression Boolean condition that must hold. - * @note MSVC path uses __debugbreak(). - */ - #define BASE_ASSERT(expression) if(!(expression)) { __debugbreak(); } - #else - /** - * @def BASE_ASSERT(expression) - * @brief Terminates immediately when \p expression evaluates to false. - * @param expression Boolean condition that must hold. - * @note Clang/GCC path uses __builtin_trap(). - */ - #define BASE_ASSERT(expression) if(!(expression)) { __builtin_trap(); } - #endif -#else - /** - * @def BASE_ASSERT(expression) - * @brief No-op assertion macro for non-debug builds. - * @param expression Ignored. - */ - #define BASE_ASSERT(expression) -#endif \ No newline at end of file diff --git a/src/base/core_file.cppm b/src/base/core_file.cppm deleted file mode 100644 index b90715f..0000000 --- a/src/base/core_file.cppm +++ /dev/null @@ -1,129 +0,0 @@ -module; -#include - -/** - * @file core_file.cppm - * @brief Arena-friendly file read/write utilities built on C stdio. - */ - -/** - * @module core_file - * @brief File IO helpers returning explicit Result-based error states. - */ - -export module core_file; - -import core_types; -import core_string; -import core_memory; -import core_result; - -using namespace base; -using namespace base::str; - -/** - * @namespace fs - * @brief Arena-friendly file IO utilities. - */ -export namespace fs { - // ------------------------------------------------------------ - // File Diagnostics - // ------------------------------------------------------------ - /** - * @brief File operation failure categories returned by this module. - */ - enum class FileError : u8 { - None = 0, - NotFoundOrLocked, - OutOfMemory, - ReadFailed, - WriteFailed - }; - - /** @brief Result alias for file reads (error or byte slice). */ - using FileReadResult = Result; - /** @brief Result alias for file writes (error or bytes written). */ - using FileWriteResult = Result; - - // ------------------------------------------------------------ - // File Reading - // ------------------------------------------------------------ - - /** - * @brief Reads an entire file into arena-backed memory. - * @param arena Arena receiving file bytes. - * @param filepath Null-terminated path to open. - * @return Ok(Str8) on success, Err(FileError) on failure. - */ - FileReadResult read_all(mem::Arena& arena, const char* filepath) { - // Open in binary mode to avoid newline translation. - FILE* file = fopen(filepath, "rb"); - if (!file) { - return FileReadResult::err(FileError::NotFoundOrLocked); - } - - fseek(file, 0, SEEK_END); - i64 file_size = static_cast(ftell(file)); - if (file_size < 0) { - fclose(file); - return FileReadResult::err(FileError::ReadFailed); - } - fseek(file, 0, SEEK_SET); - - // Empty file is a valid state, not an error - if (file_size == 0) { - fclose(file); - return FileReadResult::ok(Str8{}); - } - - u64 file_size_u64 = static_cast(file_size); - - u8* buffer = arena.alloc_array(file_size_u64); - if (!buffer) { - fclose(file); - return FileReadResult::err(FileError::OutOfMemory); - } - - u64 bytes_read = fread(buffer, 1, file_size_u64, file); - fclose(file); - - // Detect hard failure when short read. - if (bytes_read != file_size_u64) { - arena.offset -= file_size_u64; // reclaim memory - return FileReadResult::err(FileError::ReadFailed); - } - - return FileReadResult::ok(Str8(buffer, bytes_read)); - } - - // ------------------------------------------------------------ - // File Writing - // ------------------------------------------------------------ - - /** - * @brief Writes all bytes from a Str8 into a file. - * @param filepath Null-terminated path to open for writing. - * @param data Byte slice to persist. - * @return Ok(bytes_written) on success, Err(FileError) on failure. - */ - FileWriteResult write_all(const char* filepath, Str8 data) { - FILE* file = fopen(filepath, "wb"); - if (!file) { - return FileWriteResult::err(FileError::NotFoundOrLocked); - } - - if (data.len == 0) { - fclose(file); - return FileWriteResult::ok(0); - } - - u64 bytes_written = fwrite(data.str, 1, data.len, file); - fclose(file); - - if (bytes_written != data.len) { - return FileWriteResult::err(FileError::WriteFailed); - } - - return FileWriteResult::ok(bytes_written); - } -} // namespace fs \ No newline at end of file diff --git a/src/base/core_hash.cppm b/src/base/core_hash.cppm deleted file mode 100644 index 8a51d53..0000000 --- a/src/base/core_hash.cppm +++ /dev/null @@ -1,102 +0,0 @@ -module; - -export module core_hash; - -import core_types; -import core_string; - -using namespace base; - -export namespace base::hash { - - namespace detail { - // 64-bit finalizer mix (splitmix style) for stable, high-quality integer diffusion - constexpr u64 mix64(u64 x) { - x ^= (x >> 30); - x *= 0xbf58476d1ce4e5b9ULL; - x ^= (x >> 27); - x *= 0x94d049bb133111ebULL; - x ^= (x >> 31); - return x; - } - } - - template - struct HashTraits; // unsupported keys fail at compile time - - // Unsigned integer key traits - - template<> - struct HashTraits { - static constexpr u64 hash(const u8 key) { return detail::mix64((u64)key); } - static constexpr bool eq(const u8 a, const u8 b) { return a == b; } - }; - - template<> - struct HashTraits { - static constexpr u64 hash(const u16 key) { return detail::mix64((u64)key); } - static constexpr bool eq(const u16 a, const u16 b) { return a == b; } - }; - - template <> - struct HashTraits { - static constexpr u64 hash(const u32 key) { return detail::mix64((u64)key); } - static constexpr bool eq(const u32 a, const u32 b) { return a == b; } - }; - - template <> - struct HashTraits { - static constexpr u64 hash(const u64 key) { return detail::mix64(key); } - static constexpr bool eq(const u64 a, const u64 b) { return a == b; } - }; - - // Signed integer key traits - - template <> - struct HashTraits { - static constexpr u64 hash(const i8 key) { return detail::mix64((u64)(i64)key); } - static constexpr bool eq(const i8 a, const i8 b) { return a == b; } - }; - - template <> - struct HashTraits { - static constexpr u64 hash(const i16 key) { return detail::mix64((u64)(i64)key); } - static constexpr bool eq(const i16 a, const i16 b) { return a == b; } - }; - - template <> - struct HashTraits { - static constexpr u64 hash(const i32 key) { return detail::mix64((u64)(i64)key); } - static constexpr bool eq(const i32 a, const i32 b) { return a == b; } - }; - - template <> - struct HashTraits { - static constexpr u64 hash(const i64 key) { return detail::mix64((u64)key); } - static constexpr bool eq(const i64 a, const i64 b) { return a == b; } - }; - - // Str8 key traits, using existing hash & equality behavior - template<> - struct HashTraits { - static constexpr u64 hash(const Str8& key) { - return key.hash_fnv1a(); - } - - static constexpr bool eq(const Str8& a, const Str8& b) { - return a.match(b); - } - }; - - // Generic trait-dispatch helpers used by HashMap core - template - constexpr u64 hash_of(const K& key) { - return HashTraits::hash(key); - } - - template - constexpr bool key_eq(const K& a, const K& b) { - return HashTraits::eq(a, b); - } - -} // namespace base::hash \ No newline at end of file diff --git a/src/base/core_hash_map.cppm b/src/base/core_hash_map.cppm deleted file mode 100644 index fb20d53..0000000 --- a/src/base/core_hash_map.cppm +++ /dev/null @@ -1,996 +0,0 @@ -module; - -#include -#include "core_debug.h" - -/** - * @file core_hash_map.cppm - * @brief Arena-backed Robin Hood hash map and hash set primitives. - */ - -/** - * @module core_hash_map - * @brief Open-addressed hash containers for map and set workloads. - */ -export module core_hash_map; - -import core_types; -import core_memory; -import core_hash; -import core_result; -import core_option; - -using namespace base; - -/** - * @namespace base::hashmap - * @brief Hash container primitives built on arena allocation. - */ -export namespace base::hashmap { - - /** - * @brief Error categories returned by HashMap operations. - */ - enum struct HashMapE : u8 { - /** @brief No error. */ - None = 0, - /** @brief Arena allocation failed while reserving or rehashing. */ - OutOfMemory, - /** @brief Requested capacity exceeds the largest normalizable value. */ - CapacityOverflow, - /** @brief Probe walk exhausted the full table unexpectedly. */ - ProbeSequenceExhausted - }; - - /** - * @brief Result alias used by HashMap and HashSet APIs. - * @tparam T Success payload type. - */ - template - using HashMapR = Result; - - namespace detail { - /** @brief Control-byte marker for an empty slot. */ - constexpr u8 CTRL_EMPTY = 0; - /** @brief Control-byte marker for an occupied slot. */ - constexpr u8 CTRL_FULL = 1; - - /** @brief Load-factor numerator (4/5 == 80%). */ - constexpr u64 LOAD_NUM = 4; // 80% - /** @brief Load-factor denominator (4/5 == 80%). */ - constexpr u64 LOAD_DEN = 5; - - /** @brief Smallest table capacity the implementation will allocate. */ - constexpr u64 MIN_CAPACITY = 8; - - /** @brief Largest capacity value that next_pow2 can normalize safely. */ - constexpr u64 MAX_NORMALIZABLE_CAPACITY = (1ULL << 63); - - /** @brief Tests whether x is a non-zero power of two. */ - constexpr bool is_pow2_nonzero(u64 x) { - return x != 0 && ((x & (x - 1)) == 0); - } - - /** @brief Rounds x up to the next power of two. */ - constexpr u64 next_pow2(u64 x) { - if (x <= 1) return 1; - x -= 1; - x |= (x >> 1); - x |= (x >> 2); - x |= (x >> 4); - x |= (x >> 8); - x |= (x >> 16); - x |= (x >> 32); - return x + 1; - } - - /** @brief Normalizes a caller request to a supported table capacity. */ - constexpr u64 normalize_capacity(u64 requested_capacity) { - u64 capped = (requested_capacity < MIN_CAPACITY) ? MIN_CAPACITY : requested_capacity; - return next_pow2(capped); - } - - /** @brief Returns the maximum live-entry count allowed before growth. */ - constexpr u64 max_count_for_capacity(u64 capacity) { - return (capacity * LOAD_NUM) / LOAD_DEN; - } - - /** @brief Computes Robin Hood probe distance from a key's home slot. */ - constexpr u64 probe_distance(u64 slot, u64 home, u64 capacity) { - return (slot + capacity - home) & (capacity - 1); - } - - template - /** @brief Lightweight swap helper for trivially movable values. */ - inline void swap_values(T& a, T& b) { - T tmp = a; - a = b; - b = tmp; - } - - template - /** - * @brief Inserts or overwrites one key-value entry in raw map storage. - * @return Ok(true) when a new key is inserted, Ok(false) when an existing - * key is overwritten, or Err(HashMapE) on failure. - */ - inline HashMapR robin_hood_upsert_raw_map( - K* keys, - V* values, - u64* hashes, - u8* ctrl, - u64 capacity, - u64& io_count, - const K& key, - const V& value - ) { - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(values != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - K cur_key = key; - V cur_value = value; - u64 cur_hash = hash::hash_of(cur_key); - u64 idx = cur_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - ctrl[idx] = CTRL_FULL; - keys[idx] = cur_key; - values[idx] = cur_value; - hashes[idx] = cur_hash; - io_count += 1; - return HashMapR::ok(true); - } - - if (hashes[idx] == cur_hash && hash::key_eq(keys[idx], cur_key)) { - values[idx] = cur_value; - return HashMapR::ok(false); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - swap_values(keys[idx], cur_key); - swap_values(values[idx], cur_value); - swap_values(hashes[idx], cur_hash); - dist = resident_dist; - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Inserts one key into raw set storage if it is not already present. - * @return Ok(true) when a new key is inserted, Ok(false) when the key - * already exists, or Err(HashMapE) on failure. - */ - inline HashMapR robin_hood_upsert_raw_set( - K* keys, - u64* hashes, - u8* ctrl, - u64 capacity, - u64& io_count, - const K& key - ) { - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - K cur_key = key; - u64 cur_hash = hash::hash_of(cur_key); - u64 idx = cur_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - ctrl[idx] = CTRL_FULL; - keys[idx] = cur_key; - hashes[idx] = cur_hash; - io_count += 1; - return HashMapR::ok(true); - } - - if (hashes[idx] == cur_hash && hash::key_eq(keys[idx], cur_key)) { - return HashMapR::ok(false); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - swap_values(keys[idx], cur_key); - swap_values(hashes[idx], cur_hash); - dist = resident_dist; - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Finds a mutable value pointer inside raw map storage. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on probe failure. - */ - inline HashMapR> robin_hood_find_ptr_raw_map( - K* keys, - V* values, - const u64* hashes, - const u8* ctrl, - u64 capacity, - const K& key - ) { - if (capacity == 0) { - return HashMapR>::ok(Option::none()); - } - - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(values != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - u64 key_hash = hash::hash_of(key); - u64 idx = key_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - return HashMapR>::ok(Option::none()); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - return HashMapR>::ok(Option::none()); - } - - if (hashes[idx] == key_hash && hash::key_eq(keys[idx], key)) { - return HashMapR>::ok(Option::some(&values[idx])); - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR>::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Finds a const value pointer inside raw map storage. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on probe failure. - */ - inline HashMapR> robin_hood_find_ptr_raw_map_const( - const K* keys, - const V* values, - const u64* hashes, - const u8* ctrl, - u64 capacity, - const K& key - ) { - if (capacity == 0) { - return HashMapR>::ok(Option::none()); - } - - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(values != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - u64 key_hash = hash::hash_of(key); - u64 idx = key_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - return HashMapR>::ok(Option::none()); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - return HashMapR>::ok(Option::none()); - } - - if (hashes[idx] == key_hash && hash::key_eq(keys[idx], key)) { - return HashMapR>::ok(Option::some(&values[idx])); - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR>::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Finds a mutable key pointer inside raw set storage. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on probe failure. - */ - inline HashMapR> robin_hood_find_key_ptr_raw_set( - K* keys, - const u64* hashes, - const u8* ctrl, - u64 capacity, - const K& key - ) { - if (capacity == 0) { - return HashMapR>::ok(Option::none()); - } - - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - u64 key_hash = hash::hash_of(key); - u64 idx = key_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - return HashMapR>::ok(Option::none()); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - return HashMapR>::ok(Option::none()); - } - - if (hashes[idx] == key_hash && hash::key_eq(keys[idx], key)) { - return HashMapR>::ok(Option::some(&keys[idx])); - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR>::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Finds a const key pointer inside raw set storage. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on probe failure. - */ - inline HashMapR> robin_hood_find_key_ptr_raw_set_const( - const K* keys, - const u64* hashes, - const u8* ctrl, - u64 capacity, - const K& key - ) { - if (capacity == 0) { - return HashMapR>::ok(Option::none()); - } - - BASE_ASSERT(keys != nullptr); - BASE_ASSERT(hashes != nullptr); - BASE_ASSERT(ctrl != nullptr); - BASE_ASSERT(is_pow2_nonzero(capacity)); - - const u64 mask = capacity - 1; - u64 key_hash = hash::hash_of(key); - u64 idx = key_hash & mask; - u64 dist = 0; - - for (u64 steps = 0; steps < capacity; ++steps) { - if (ctrl[idx] == CTRL_EMPTY) { - return HashMapR>::ok(Option::none()); - } - - u64 resident_home = hashes[idx] & mask; - u64 resident_dist = probe_distance(idx, resident_home, capacity); - - if (resident_dist < dist) { - return HashMapR>::ok(Option::none()); - } - - if (hashes[idx] == key_hash && hash::key_eq(keys[idx], key)) { - return HashMapR>::ok(Option::some(&keys[idx])); - } - - idx = (idx + 1) & mask; - dist += 1; - } - - return HashMapR>::err(HashMapE::ProbeSequenceExhausted); - } - - template - /** - * @brief Grows or initializes raw map storage and reinserts live entries. - * @return Ok(true) when new storage is allocated, Ok(false) when no growth - * is needed, or Err(HashMapE) on failure. - */ - inline HashMapR reserve_with_rehash_map( - Arena& arena, - u64 requested_capacity, - u64& io_capacity, - u64& io_count, - K*& io_keys, - V*& io_values, - u64*& io_hashes, - u8*& io_ctrl - ) { - if (requested_capacity > MAX_NORMALIZABLE_CAPACITY) { - return HashMapR::err(HashMapE::CapacityOverflow); - } - - u64 target_capacity = normalize_capacity(requested_capacity); - if (io_capacity >= target_capacity) { - return HashMapR::ok(false); - } - - u64 snapshot = arena.offset; - - K* new_keys = arena.alloc_array(target_capacity); - V* new_values = arena.alloc_array(target_capacity); - u64* new_hashes = arena.alloc_array(target_capacity); - u8* new_ctrl = arena.alloc_array(target_capacity); - - if (!new_keys || !new_ctrl || !new_values || !new_hashes) { - arena.offset = snapshot; - return HashMapR::err(HashMapE::OutOfMemory); - } - - memset(new_ctrl, CTRL_EMPTY, static_cast(target_capacity)); - - u64 new_count = 0; - for (u64 i = 0; i < io_capacity; ++i) { - if (io_ctrl && io_ctrl[i] == CTRL_FULL) { - auto ins = robin_hood_upsert_raw_map( - new_keys, new_values, new_hashes, new_ctrl, target_capacity, new_count, io_keys[i], io_values[i] - ); - if (!ins.is_ok()) { - arena.offset = snapshot; - return HashMapR::err(ins.error); - } - } - } - - BASE_ASSERT(new_count == io_count); - - io_keys = new_keys; - io_values = new_values; - io_hashes = new_hashes; - io_ctrl = new_ctrl; - io_capacity = target_capacity; - io_count = new_count; - - return HashMapR::ok(true); - } - - template - /** - * @brief Grows or initializes raw set storage and reinserts live entries. - * @return Ok(true) when new storage is allocated, Ok(false) when no growth - * is needed, or Err(HashMapE) on failure. - */ - inline HashMapR reserve_with_rehash_set( - Arena& arena, - u64 requested_capacity, - u64& io_capacity, - u64& io_count, - K*& io_keys, - u64*& io_hashes, - u8*& io_ctrl - ) { - if (requested_capacity > MAX_NORMALIZABLE_CAPACITY) { - return HashMapR::err(HashMapE::CapacityOverflow); - } - - u64 target_capacity = normalize_capacity(requested_capacity); - - if (io_capacity >= target_capacity) { - return HashMapR::ok(false); - } - - u64 snapshot = arena.offset; - - K* new_keys = arena.alloc_array(target_capacity); - u64* new_hashes = arena.alloc_array(target_capacity); - u8* new_ctrl = arena.alloc_array(target_capacity); - - if (!new_keys || !new_ctrl || !new_hashes) { - arena.offset = snapshot; - return HashMapR::err(HashMapE::OutOfMemory); - } - - memset(new_ctrl, CTRL_EMPTY, static_cast(target_capacity)); - - u64 new_count = 0; - for (u64 i = 0; i < io_capacity; ++i) { - if (io_ctrl && io_ctrl[i] == CTRL_FULL) { - auto ins = robin_hood_upsert_raw_set( - new_keys, new_hashes, new_ctrl, target_capacity, new_count, io_keys[i] - ); - if (!ins.is_ok()) { - arena.offset = snapshot; - return HashMapR::err(ins.error); - } - } - } - - BASE_ASSERT(new_count == io_count); - - io_keys = new_keys; - io_hashes = new_hashes; - io_ctrl = new_ctrl; - io_capacity = target_capacity; - io_count = new_count; - - return HashMapR::ok(true); - } - - } // namespace detail - - template - /** - * @brief Arena-backed hash map using Robin Hood open addressing. - * @tparam K Key type. - * @tparam V Value type. - * @note Intended primarily for trivially copyable key/value workloads. - */ - struct HashMap { - /** @brief Number of allocated slots; always a power of two when non-zero. */ - u64 capacity; - /** @brief Number of live entries currently stored in the table. */ - u64 count; - /** @brief Key array indexed in lockstep with values, hashes, and ctrl. */ - K* keys; - /** @brief Value array indexed in lockstep with keys, hashes, and ctrl. */ - V* values; - /** @brief Cached full hashes for resident keys. */ - u64* hashes; - /** @brief Occupancy control bytes for each slot. */ - u8* ctrl; - - /** @brief Constructs an empty, uninitialized map handle. */ - constexpr HashMap() - : capacity(0), count(0), keys(nullptr), values(nullptr), hashes(nullptr), ctrl(nullptr) {} - - /** @brief Returns the minimum supported capacity for a live table. */ - static constexpr u64 min_capacity() { - return detail::MIN_CAPACITY; - } - - /** @brief Returns the largest allowed live-entry count before growth. */ - static constexpr u64 max_count_for_capacity(u64 cap) { - return detail::max_count_for_capacity(cap); - } - - /** @brief Reports whether backing storage has been initialized. */ - constexpr bool is_initialized() const { - return this->capacity != 0; - } - - /** @brief Reports whether the table currently stores no live entries. */ - constexpr bool empty() const { - return this->count == 0; - } - - /** @brief Sentinel slot value used to represent end-of-iteration. */ - static constexpr u64 npos() { - return max_u64; - } - - /** @brief Returns true when slot equals the end sentinel. */ - constexpr bool is_end_slot(u64 slot) const { - return slot == npos(); - } - - /** - * @brief Returns the first live slot index, or npos() if none exist. - */ - inline u64 first_live_slot() const { - if (this->capacity == 0 || this->ctrl == nullptr) { - return npos(); - } - for(u64 i = 0; i < this->capacity; ++i) { - if (this->ctrl[i] == detail::CTRL_FULL) { - return i; - } - } - return npos(); - } - - /** - * @brief Returns the next live slot after \\p slot, or npos() at end. - */ - inline u64 next_live_slot(u64 slot) const { - if (this->capacity == 0 || this->ctrl == nullptr || slot >= this->capacity) { - return npos(); - } - for (u64 i = slot + 1; i < this->capacity; ++i) { - if (this->ctrl[i] == detail::CTRL_FULL) { - return i; - } - } - return npos(); - } - - /** @brief Returns mutable key pointer for a live slot. */ - inline K* key_at(u64 slot) { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->keys[slot]; - } - - /** @brief Returns const key pointer for a live slot. */ - inline const K* key_at(u64 slot) const { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->keys[slot]; - } - - /** @brief Returns mutable value pointer for a live slot. */ - inline V* value_at(u64 slot) { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->values[slot]; - } - - /** @brief Returns const value pointer for a live slot. */ - inline const V* value_at(u64 slot) const { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->values[slot]; - } - - /** - * @brief Initializes storage with at least the requested capacity. - * @param arena Arena supplying table storage. - * @param initial_capacity Requested minimum slot count. - */ - inline HashMapR init(Arena& arena, u64 initial_capacity = detail::MIN_CAPACITY) { - return this->reserve(arena, initial_capacity); - } - - /** - * @brief Ensures the table has capacity for at least requested_capacity slots. - * @param arena Arena supplying table storage. - * @param requested_capacity Requested minimum slot count before normalization. - */ - inline HashMapR reserve(Arena& arena, u64 requested_capacity) { - return detail::reserve_with_rehash_map( - arena, requested_capacity, - this->capacity, - this->count, - this->keys, - this->values, - this->hashes, - this->ctrl - ); - } - - /** - * @brief Grows the table if one more insertion would exceed the load target. - * @param arena Arena supplying new storage if growth is needed. - */ - inline HashMapR maybe_grow_for_insert(Arena& arena) { - if (this->capacity == 0) { - return this->reserve(arena, detail::MIN_CAPACITY); - } - - u64 next_count = this->count + 1; - if (next_count <= detail::max_count_for_capacity(this->capacity)) { - return HashMapR::ok(false); - } - - return this->reserve(arena, this->capacity * 2); - } - - /** - * @brief Finds a mutable pointer to the value for key. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on failure. - */ - inline HashMapR> find(const K& key) { - return detail::robin_hood_find_ptr_raw_map( - this->keys, this->values, this->hashes, this->ctrl, this->capacity, key - ); - } - - /** - * @brief Finds a const pointer to the value for key. - * @return Ok(Option::some(pointer)) when found, - * Ok(Option::none()) when absent, or Err(HashMapE) - * on failure. - */ - inline HashMapR> find(const K& key) const { - return detail::robin_hood_find_ptr_raw_map_const( - this->keys, this->values, this->hashes, this->ctrl, this->capacity, key - ); - } - - /** - * @brief Tests whether the table contains key. - * @return Ok(true) when present, Ok(false) when absent, or Err(HashMapE) - * on failure. - */ - inline HashMapR contains(const K& key) const { - auto f = this->find(key); - if (!f.is_ok()) { - return HashMapR::err(f.error); - } - return HashMapR::ok(f.value.is_some()); - } - - /** - * @brief Inserts or overwrites one key-value pair. - * @return Ok(true) when a new key is inserted, Ok(false) when an existing - * key is updated, or Err(HashMapE) on failure. - */ - inline HashMapR upsert(Arena& arena, const K& key, const V& value) { - auto grow = this->maybe_grow_for_insert(arena); - if (!grow.is_ok()) { - return HashMapR::err(grow.error); - } - return detail::robin_hood_upsert_raw_map( - this->keys, this->values, this->hashes, this->ctrl, this->capacity, this->count, key, value - ); - } - - /** @brief Marks every slot empty without releasing arena storage. */ - inline void clear() { - if (this->ctrl && this->capacity > 0) { - memset(this->ctrl, detail::CTRL_EMPTY, static_cast(this->capacity)); - } - this->count = 0; - } - - /** @brief Returns the slot mask used for bitmask indexing. */ - constexpr u64 bucket_mask() const { - BASE_ASSERT(detail::is_pow2_nonzero(this->capacity)); - return this->capacity - 1; - } - }; // struct HashMap - - template - /** - * @brief Hash-set specialization storing keys only. - * @tparam K Key type. - */ - struct HashMap { - /** @brief Number of allocated slots; always a power of two when non-zero. */ - u64 capacity; - /** @brief Number of live keys currently stored in the table. */ - u64 count; - /** @brief Key array indexed in lockstep with hashes and ctrl. */ - K* keys; - /** @brief Cached full hashes for resident keys. */ - u64* hashes; - /** @brief Occupancy control bytes for each slot. */ - u8* ctrl; - - /** @brief Constructs an empty, uninitialized set handle. */ - constexpr HashMap() - : capacity(0), count(0), keys(nullptr), hashes(nullptr), ctrl(nullptr) {} - - static constexpr u64 min_capacity() { - return detail::MIN_CAPACITY; - } - - static constexpr u64 max_count_for_capacity(u64 cap) { - return detail::max_count_for_capacity(cap); - } - - /** @brief Reports whether backing storage has been initialized. */ - constexpr bool is_initialized() const { - return this->capacity != 0; - } - - /** @brief Reports whether the table currently stores no live entries. */ - constexpr bool empty() const { - return this->count == 0; - } - - /** @brief Sentinel slot value used to represent end-of-iteration. */ - static constexpr u64 npos() { - return max_u64; - } - - /** @brief Returns true when slot equals the end sentinel. */ - constexpr bool is_end_slot(u64 slot) const { - return slot == npos(); - } - - /** - * @brief Returns the first live slot index, or npos() if none exist. - */ - inline u64 first_live_slot() const { - if (this->capacity == 0 || this->ctrl == nullptr) { - return npos(); - } - - for (u64 i = 0; i < this->capacity; ++i) { - if (this->ctrl[i] == detail::CTRL_FULL) { - return i; - } - } - - return npos(); - } - - /** - * @brief Returns the next live slot after \\p slot, or npos() at end. - */ - inline u64 next_live_slot(u64 slot) const { - if (this->capacity == 0 || this->ctrl == nullptr) { - return npos(); - } - - if (slot >= this->capacity) { - return npos(); - } - - for (u64 i = slot + 1; i < this->capacity; ++i) { - if (this->ctrl[i] == detail::CTRL_FULL) { - return i; - } - } - - return npos(); - } - - /** @brief Returns mutable key pointer for a live slot. */ - inline K* key_at(u64 slot) { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->keys[slot]; - } - - /** @brief Returns const key pointer for a live slot. */ - inline const K* key_at(u64 slot) const { - BASE_ASSERT(slot < this->capacity); - BASE_ASSERT(this->ctrl[slot] == detail::CTRL_FULL); - return &this->keys[slot]; - } - - /** - * @brief Initializes storage with at least the requested capacity. - * @param arena Arena supplying table storage. - * @param initial_capacity Requested minimum slot count. - */ - inline HashMapR init(Arena& arena, u64 initial_capacity = detail::MIN_CAPACITY) { - return this->reserve(arena, initial_capacity); - } - - /** - * @brief Ensures the set has capacity for at least requested_capacity slots. - * @param arena Arena supplying table storage. - * @param requested_capacity Requested minimum slot count before normalization. - */ - inline HashMapR reserve(Arena& arena, u64 requested_capacity) { - return detail::reserve_with_rehash_set( - arena, - requested_capacity, - this->capacity, - this->count, - this->keys, - this->hashes, - this->ctrl - ); - } - - /** - * @brief Grows the set if one more insertion would exceed the load target. - * @param arena Arena supplying new storage if growth is needed. - */ - inline HashMapR maybe_grow_for_insert(Arena& arena) { - if (this->capacity == 0) { - return this->reserve(arena, detail::MIN_CAPACITY); - } - - u64 next_count = this->count + 1; - if (next_count <= detail::max_count_for_capacity(this->capacity)) { - return HashMapR::ok(false); - } - - return this->reserve(arena, this->capacity * 2); - } - - inline HashMapR> find(const K& key) { - return detail::robin_hood_find_key_ptr_raw_set( - this->keys, this->hashes, this->ctrl, this->capacity, key - ); - } - - inline HashMapR> find(const K& key) const { - return detail::robin_hood_find_key_ptr_raw_set_const( - this->keys, this->hashes, this->ctrl, this->capacity, key - ); - } - - /** - * @brief Tests whether the set contains key. - * @return Ok(true) when present, Ok(false) when absent, or Err(HashMapE) - */ - inline HashMapR contains(const K& key) const { - auto f = this->find(key); - if (!f.is_ok()) { - return HashMapR::err(f.error); - } - return HashMapR::ok(f.value.is_some()); - } - - /** - * @brief Inserts key if absent. - * @return Ok(true) when a new key is inserted, Ok(false) when the key is - * already present, or Err(HashMapE) on failure. - */ - inline HashMapR upsert(Arena& arena, const K& key) { - auto grow = this->maybe_grow_for_insert(arena); - if (!grow.is_ok()) { - return HashMapR::err(grow.error); - } - - return detail::robin_hood_upsert_raw_set( - this->keys, this->hashes, this->ctrl, this->capacity, this->count, key - ); - } - - /** @brief Marks every slot empty without releasing arena storage. */ - inline void clear() { - if (this->ctrl && this->capacity > 0) { - memset(this->ctrl, detail::CTRL_EMPTY, static_cast(this->capacity)); - } - this->count = 0; - } - - /** @brief Returns the slot mask used for bitmask indexing. */ - constexpr u64 bucket_mask() const { - BASE_ASSERT(detail::is_pow2_nonzero(this->capacity)); - return this->capacity - 1; - } - - }; // struct HashMap - - /** @brief Convenience alias for key-only set usage. */ - template - using HashSet = HashMap; - -} // namespace base::hashmap - -export namespace base { - using hashmap::HashMap; - using hashmap::HashMapE; - using hashmap::HashMapR; - using hashmap::HashSet; -} \ No newline at end of file diff --git a/src/base/core_lexer.cppm b/src/base/core_lexer.cppm deleted file mode 100644 index f261c9a..0000000 --- a/src/base/core_lexer.cppm +++ /dev/null @@ -1,478 +0,0 @@ -export module core_lexer; - -import core_types; -import core_string; -import core_result; -import core_text_parse; -import core_portability; - -using namespace base; -using namespace str; -using namespace parse; - -export namespace base::lex { - template - using ParseResult = ParseResult; - - /** - * @defgroup LexerCore Zero-Allocation Lexer Primitives - * @brief Token and cursor utilities for byte-slice lexing with no heap usage. - * - * This module keeps all lexical state in small POD-style structs and raw - * `Str8` slices so higher layers can compose fast, data-oriented scanners. - * - * @par Error Accumulation Contract (Language Server Oriented) - * The lexer layer is designed for whole-buffer parsing with recovery and - * diagnostic accumulation: - * - Cursor progress rule: a top-level lex step must either emit a token or - * advance by at least one byte. - * - No silent consumed failure: if a routine consumes a candidate window and - * fails validation, that failure must be surfaced to the coordinator layer. - * - Strict parsing remains delegated to text-parse helpers; cursor methods own - * navigation and consumed-window boundaries. - * - Recovery behavior is intentional: some consume paths advance on failure to - * support forward progress in multi-error pipelines. - */ - - /** - * @ingroup LexerCore - * @brief Byte-sized token category used by higher-level parsers. - */ - enum struct TokenType : u8 { - // meta - Eof = 0, - Error, - Unknown, // the "escape valve" for unhandled symbols - - // Literals & data - Identifier, - String, - Number, - - // Context and whitespace - Whitespace, - Comment, - - // Exact Punctuation (Zero-cost branch evaluations) - Dot, - Comma, - Colon, - Semicolon, - OpenParen, - CloseParen, - OpenCurly, - CloseCurly, - OpenBracket, - CloseBracket, - AngleLeft, - AngleRight, - Plus, - Minus, - Asterisk, - Slash, - Backslash, - Percent, - Caret, - Ampersand, - Equals, - Pipe, - Bang - }; - - /** - * @ingroup LexerCore - * @brief Borrowed token view into the source input. - */ - struct Token { - /** @brief Raw token bytes in the original buffer. */ - Str8 text; - /** @brief Token classification for dispatch. */ - TokenType type; - }; - - /** - * @brief Build an error diagnostic over a consumed source window. - */ - constexpr ParseDiagnostic make_error_diag(ParseDiagCode code, u64 start_offset, u64 end_offset) { - return ParseDiagnostic{ - code, - DiagnosticSeverity::Error, - start_offset, - end_offset - }; - } - - /** - * @ingroup LexerCore - * @brief Stateful cursor over a `Str8` input slice. - * - * The cursor is intentionally minimal: offset arithmetic plus inlined - * byte traits and scanners that avoid dynamic allocation. - */ - struct Str8Cursor { - /** @brief Borrowed input buffer. */ - Str8 input; - /** @brief Current byte offset in `input`. */ - u64 offset; - - // -- Core primitives -- - - /** - * @brief Read current or lookahead byte without advancing. - * @param lookahead Byte distance ahead of `offset`. - * @return Byte value or `0` when lookahead is out of bounds. - */ - constexpr u8 peek(u64 lookahead = 0) { - if (this->offset + lookahead >= this->input.len) { - return 0; // null byte fallback prevents out-of-bounds reads - } - return this->input.str[this->offset + lookahead]; - } - - /** - * @brief Advance cursor by `count` bytes and clamp at end-of-input. - */ - constexpr void advance(u64 count = 1) { - this->offset += count; - if (this->offset > this->input.len) { - this->offset = this->input.len; - } - } - - /** @brief Check whether cursor reached end-of-input. */ - constexpr bool is_eof() { - return this->offset >= this->input.len; - } - - // -- Inline character traits -- - - /** @brief ASCII alphabetic predicate. */ - constexpr bool is_alpha(u8 c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - } - - /** @brief ASCII decimal digit predicate. */ - constexpr bool is_numeric(u8 c) { - return (c >= '0' && c <= '9'); - } - - /** @brief ASCII alphanumeric predicate. */ - constexpr bool is_alphanumeric(u8 c) { - return is_alpha(c) || is_numeric(c); - } - - /** @brief Whitespace predicate for ASCII space controls. */ - constexpr bool is_whitespace(u8 c) { - return c == ' ' || c == '\t' || c == '\n' || c == '\r'; - } - - // -- Zero-allocation scanners - - /** - * @brief Skip contiguous whitespace bytes from current cursor position. - */ - constexpr void skip_whitespace() { - while (!is_eof() && is_whitespace(peek())) { - this->offset++; - } - } - - /** - * @brief Parse an identifier slice `[A-Za-z_][A-Za-z0-9_]*`. - * @return Borrowed identifier slice, or empty slice if no identifier starts - * at the current position. - */ - constexpr Str8 consume_ident() { - // Guard against null-base pointer arithmetic for empty/null input slices - if (this->input.str == nullptr || this->input.len == 0) { - return Str8(nullptr, 0); - } - - u64 start = this->offset; - u8 first = peek(); - - if (is_alpha(first) || first == '_') { - advance(); - while (!is_eof()) { - u8 c = peek(); - if (is_alphanumeric(c) || c == '_') { - advance(); - } else { - break; - } - } - } - return Str8(this->input.str + start, this->offset - start); - } - - /** - * @brief Parse a double-quoted string literal in-place. - * - * Assumes the cursor currently points at the opening quote. Escapes are - * traversed without decoding (zero-allocation policy). - * - * @return `ok(length)` when a closing quote is found, where `length` - * includes both delimiters; otherwise - * `err(ParseDiagCode::UnterminatedString)` on EOF. - * @note Diagnostic accumulation is coordinator-layer policy; see - * core_lexer_coord wrappers. - */ - constexpr ParseResult consume_string_literal() { - u64 start = this->offset; - advance(); // advance past opening quote - while (!is_eof()) { - u8 c = peek(); - if (c == '"') { - advance(); // consume closing quote - return ParseResult::ok(this->offset - start); - } - if (c == '\\') { - // blind skip - bypass the backslash and the escaped character - advance(2); - } else { - advance(1); - } - } - return ParseResult::err(ParseDiagCode::UnterminatedString); - } - - /** - * @brief Consume an integer prefix and parse it through a strict parser backend. - * - * Unsigned targets consume `[0-9]+`. Signed targets consume `[+|-]?[0-9]+`. - * The consumed window is then delegated to a strict `base::parse::parse_*` - * function. - * - * Failure semantics: - * - If no valid integer prefix exists at the current offset, this returns error - * and does not advance the cursor. - * - If a candidate prefix is consumed and strict parsing fails (for example, - * overflow), the cursor remains advanced to the end of that consumed prefix. - * This is intentional for forward progress and error-accumulation workflows. - * - * @tparam T Integer output type. - * @param parser Strict parser callback (`ParseResult (*)(Str8)`). - */ - template - constexpr ParseResult consume_int_impl(ParseResult (*parser)(Str8)) { - u64 start = this->offset; - u64 scan = start; - - if constexpr (portability::is_signed_v) { - if (scan < this->input.len && (this->input.str[scan] == '+' || this->input.str[scan] == '-')) { - scan++; - } - } - - u64 digits_start = scan; - while (scan < this->input.len && is_numeric(this->input.str[scan])) { - scan++; - } - - if (scan == digits_start) { - return ParseResult::err(ParseDiagCode::NoDigits); - } - - this->offset = scan; - Str8 consumed{this->input.str + start, scan - start}; - return parser(consumed); - } - - /** - * @brief Consume `[0-9]+` and parse as `u8`. - * - * This method performs prefix window discovery and delegates numeric - * validation/range checks to strict `base::parse::parse_u8`. - */ - constexpr ParseResult consume_u8() { - return consume_int_impl(parse_u8); - } - - /** - * @brief Consume `[+|-]?[0-9]+` and parse as `i8`. - */ - constexpr ParseResult consume_i8() { - return consume_int_impl(parse_i8); - } - - /** @brief Consume `[0-9]+` and parse as `u16`. */ - constexpr ParseResult consume_u16() { - return consume_int_impl(parse_u16); - } - - /** @brief Consume `[+|-]?[0-9]+` and parse as `i16`. */ - constexpr ParseResult consume_i16() { - return consume_int_impl(parse_i16); - } - - /** @brief Consume `[0-9]+` and parse as `u32`. */ - constexpr ParseResult consume_u32() { - return consume_int_impl(parse_u32); - } - - /** @brief Consume `[+|-]?[0-9]+` and parse as `i32`. */ - constexpr ParseResult consume_i32() { - return consume_int_impl(parse_i32); - } - - /** @brief Consume `[0-9]+` and parse as `u64`. */ - constexpr ParseResult consume_u64() { - return consume_int_impl(parse_u64); - } - - /** @brief Consume `[+|-]?[0-9]+` and parse as `i64`. */ - constexpr ParseResult consume_i64() { - return consume_int_impl(parse_i64); - } - - /** - * @brief Consume `[+|-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+|-]?[0-9]+)?` and parse as `f64`. - * - * Failure semantics: - * - If no valid floating prefix exists at the current offset, this returns error - * and does not advance the cursor. - * - If a syntactically valid candidate prefix is consumed, strict parse - * validation is applied to that slice. If strict parsing fails, the cursor - * remains advanced to the end of the consumed prefix (no rewind). - * - * This behavior is intentional for recovery-friendly, whole-buffer parsing - * pipelines such as language-server style error accumulation. - */ - constexpr ParseResult consume_f64() { - u64 start = this->offset; - u64 scan = start; - - if (scan < this->input.len && (this->input.str[scan] == '+' || this->input.str[scan] == '-')) { - scan++; - } - - bool has_digits = false; - while (scan < this->input.len && is_numeric(this->input.str[scan])) { - has_digits = true; - scan++; - } - - if (scan < this->input.len && this->input.str[scan] == '.') { - scan++; - while (scan < this->input.len && is_numeric(this->input.str[scan])) { - has_digits = true; - scan++; - } - } - - if (!has_digits) { - return ParseResult::err(ParseDiagCode::NoDigits); - } - - if (scan < this->input.len && (this->input.str[scan] == 'e' || this->input.str[scan] == 'E')) { - u64 exp_scan = scan + 1; - if (exp_scan < this->input.len && (this->input.str[exp_scan] == '+' || this->input.str[exp_scan] == '-')) { - exp_scan++; - } - u64 exp_digits_start = exp_scan; - while (exp_scan < this->input.len && is_numeric(this->input.str[exp_scan])) { - exp_scan++; - } - - // If exponent marker has no digits, do not consume it here. - // Leave 'e'/'E' for subsequent tokenization and consume mantissa only. - if (exp_scan != exp_digits_start) { - scan = exp_scan; - } - } - - this->offset = scan; - Str8 consumed{this->input.str + start, scan - start}; - return parse_f64(consumed); - } - - /** @brief Consume and parse an `f32` prefix via `consume_f64`. */ - constexpr ParseResult consume_f32() { - auto res = consume_f64(); - if (!res.is_ok()) { - return ParseResult::err(res.error); - } - return ParseResult::ok(static_cast(res.value)); - } - - }; // struct Str8Cursor - - /** - * @brief Language-specific trivia markers for whitespace/comment skipping. - */ - struct TriviaConfig { - /** @brief Line comment introducer (for example `//` or `#`). */ - Str8 line_mark; - /** @brief Block comment opener (for example slash-star). */ - Str8 block_start; - /** @brief Block comment closer (for example star-slash). */ - Str8 block_end; - /** @brief Enable nested block comment depth handling. */ - bool nested; - }; - - /** - * @brief Check whether cursor input starts with `mark` at current offset. - * - * This helper is read-only and does not advance the cursor. Taking - * `const Str8Cursor&` makes that guarantee explicit for callers. - */ - constexpr bool cursor_starts_with(const Str8Cursor& cursor, Str8 mark) { - if (mark.len == 0 || cursor.offset + mark.len > cursor.input.len) { - return false; - } - for (u64 i = 0; i < mark.len; ++i) { - if (cursor.input.str[cursor.offset + i] != mark.str[i]) { - return false; - } - } - return true; - } - - /** - * @brief Skip whitespace and comments according to a trivia configuration. - * - * This primitive only performs cursor movement and does not emit diagnostics. - * Coordinator-layer wrappers own unterminated-comment reporting policy. - * - * This function advances `cursor.offset` in place and can handle nested block - * comments when `config.nested` is enabled. - */ - constexpr void skip_trivia(Str8Cursor& cursor, TriviaConfig config) { - while (!cursor.is_eof()) { - - // skip standard whitespace - if (cursor.is_whitespace(cursor.peek())) { - cursor.offset++; - continue; // outer while - } - - // skip line comments - if (config.line_mark.len > 0 && cursor_starts_with(cursor, config.line_mark)) { - while (!cursor.is_eof() && cursor.peek() != '\n') { - cursor.offset++; - } - continue; // outer while - } - - // skip block comments - if (config.block_start.len > 0 && cursor_starts_with(cursor, config.block_start)) { - cursor.offset += config.block_start.len; - u32 depth = 1; - while (!cursor.is_eof() && depth > 0) { - if (config.nested && cursor_starts_with(cursor, config.block_start)) { - depth++; - cursor.offset += config.block_start.len; - } else if (cursor_starts_with(cursor, config.block_end)) { - depth--; - cursor.offset += config.block_end.len; - } else { - cursor.offset++; - } - } - continue; // outer while - } - break; // outer while - } - } -} // namespace base::lex \ No newline at end of file diff --git a/src/base/core_lexer_coord.cppm b/src/base/core_lexer_coord.cppm deleted file mode 100644 index cbfdd04..0000000 --- a/src/base/core_lexer_coord.cppm +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @file core_lexer_coord.cppm - * @brief Coordinator-layer wrappers that bind lexer cursor operations to - * diagnostic accumulation. - */ - -/** - * @module core_lexer_coord - * @brief Explicit recovery/diagnostic coordination layer above pure lexer/parse primitives. - */ - -export module core_lexer_coord; - -import core_types; -import core_string; -import core_lexer; -import core_text_parse; -import core_result; - -using namespace base; -using namespace parse; - -export namespace base::lex { - /** - * @brief Failure categories for diagnostic buffer append operations. - */ - enum struct DiagBufferError : u8 { - None = 0, - NullStorage, - BufferFull - }; - - /** @brief Result alias for diagnostic-buffer append operations. */ - using DiagBufferPushResult = Result; - - /** - * @brief Caller-owned fixed-capacity diagnostic accumulation buffer. - * - * Coordinator wrappers append diagnostics to this buffer when recovery-aware - * cursor operations encounter consumed failures. - */ - struct DiagBuffer { - /** @brief Destination storage for diagnostics. */ - ParseDiagnostic* data; - /** @brief Number of populated entries in @ref data. */ - u32 count; - /** @brief Maximum writable entries in @ref data. */ - u32 capacity; - }; - - /** - * @brief Append one diagnostic to a caller-owned diagnostic buffer. - * @param buffer Diagnostic buffer. - * @param diag Diagnostic record to append. - * @return Ok(new_count) when the diagnostic is appended, or Err(DiagBufferError) - * when storage is null or capacity is exhausted. - */ - constexpr DiagBufferPushResult diag_buffer_push(DiagBuffer& buffer, ParseDiagnostic diag) { - if (buffer.data == nullptr) { - return DiagBufferPushResult::err(DiagBufferError::NullStorage); - } - if (buffer.count >= buffer.capacity) { - return DiagBufferPushResult::err(DiagBufferError::BufferFull); - } - - buffer.data[buffer.count] = diag; - buffer.count++; - return DiagBufferPushResult::ok(buffer.count); - } - - /** - * @brief Coordinator wrapper for strict integer consume + diagnostic emission. - * - * Emits a diagnostic only when bytes were consumed and strict parsing failed. - * Prefix-miss failures are returned without diagnostic emission. - * - * @tparam T Integer result type. - * @param cursor Input cursor. - * @param parser Strict parse callback. - * @param diags Destination diagnostic buffer. - * @return Parsed integer result or error code from strict parser. - */ - template - constexpr ParseResult consume_int(Str8Cursor& cursor, - ParseResult (*parser)(str::Str8), - DiagBuffer& diags) { - u64 start = cursor.offset; - ParseResult res = cursor.consume_int_impl(parser); - if (!res.is_ok() && cursor.offset > start) { - ParseDiagnostic diag = make_error_diag(res.error, start, cursor.offset); - (void)diag_buffer_push(diags, diag); - } - return res; - } - - /** @brief Coordinator wrapper for consuming `u8` with diagnostic emission. */ - constexpr ParseResult consume_u8(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_u8, diags); - } - - /** @brief Coordinator wrapper for consuming `i8` with diagnostic emission. */ - constexpr ParseResult consume_i8(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_i8, diags); - } - - /** @brief Coordinator wrapper for consuming `u16` with diagnostic emission. */ - constexpr ParseResult consume_u16(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_u16, diags); - } - - /** @brief Coordinator wrapper for consuming `i16` with diagnostic emission. */ - constexpr ParseResult consume_i16(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_i16, diags); - } - - /** @brief Coordinator wrapper for consuming `u32` with diagnostic emission. */ - constexpr ParseResult consume_u32(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_u32, diags); - } - - /** @brief Coordinator wrapper for consuming `i32` with diagnostic emission. */ - constexpr ParseResult consume_i32(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_i32, diags); - } - - /** @brief Coordinator wrapper for consuming `u64` with diagnostic emission. */ - constexpr ParseResult consume_u64(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_u64, diags); - } - - /** @brief Coordinator wrapper for consuming `i64` with diagnostic emission. */ - constexpr ParseResult consume_i64(Str8Cursor& cursor, DiagBuffer& diags) { - return consume_int(cursor, parse_i64, diags); - } - - /** - * @brief Coordinator wrapper for consuming `f64` with diagnostic emission. - * - * Emits diagnostics for consumed strict failures only. - */ - constexpr ParseResult consume_f64(Str8Cursor& cursor, DiagBuffer& diags) { - u64 start = cursor.offset; - ParseResult res = cursor.consume_f64(); - if (!res.is_ok() && cursor.offset > start) { - ParseDiagnostic diag = make_error_diag(res.error, start, cursor.offset); - (void)diag_buffer_push(diags, diag); - } - return res; - } - - /** - * @brief Coordinator wrapper for consuming `f32` with diagnostic emission. - * - * This delegates windowing and diagnostic emission to @ref coord_consume_f64 - * and narrows successful values to `f32`. - */ - constexpr ParseResult consume_f32(Str8Cursor& cursor, DiagBuffer& diags) { - auto res = consume_f64(cursor, diags); - if (!res.is_ok()) { - return ParseResult::err(res.error); - } - return ParseResult::ok(static_cast(res.value)); - } - - /** - * @brief Coordinator wrapper for string literal parsing with diagnostics. - * - * Strict parsing remains in @ref Str8Cursor::parse_string_literal. This - * wrapper binds that result to source-span diagnostics. - */ - constexpr ParseResult parse_string_literal(Str8Cursor& cursor, DiagBuffer& diags) { - u64 start = cursor.offset; - ParseResult res = cursor.consume_string_literal(); - if (!res.is_ok() && cursor.offset > start) { - ParseDiagnostic diag = make_error_diag(res.error, start, cursor.offset); - (void)diag_buffer_push(diags, diag); - } - return res; - } - - /** - * @brief Coordinator wrapper for trivia skipping with diagnostics. - * - * Reports unterminated block comments while preserving lexer cursor progress. - */ - constexpr ParseResult skip_trivia(Str8Cursor& cursor, TriviaConfig config, DiagBuffer& diags) { - while (!cursor.is_eof()) { - // skip standard whitespace - if (cursor.is_whitespace(cursor.peek())) { - cursor.offset++; - continue; // while(!cursor.is_eof) - } - - // skip line comments - if (config.line_mark.len > 0 && cursor_starts_with(cursor, config.line_mark)) { - while (!cursor.is_eof() && cursor.peek() != '\n') { - cursor.offset++; - } - continue; // while(!cursor.is_eof) - } - - // Skip block comments; report unterminated blocks - if (config.block_start.len > 0 && cursor_starts_with(cursor, config.block_start)) { - u64 block_start_offset = cursor.offset; - cursor.offset += config.block_start.len; - u32 depth = 1; - - while (!cursor.is_eof() && depth > 0) { - if (config.nested && cursor_starts_with(cursor, config.block_start)) { - depth++; - cursor.offset += config.block_start.len; - } else if (cursor_starts_with(cursor, config.block_end)) { - depth--; - cursor.offset += config.block_end.len; - } else { - cursor.offset++; - } - } - - if (depth > 0) { - auto code = ParseDiagCode::UnterminatedBlockComment; - ParseDiagnostic diag = make_error_diag(code, block_start_offset, cursor.offset); - (void)diag_buffer_push(diags, diag); - return ParseResult::err(code); - } - continue; // while(!cursor.is_eof) - } - break; // while(!cursor.is_eof) - } - return ParseResult::ok(0); - } -} // namespace base::lex \ No newline at end of file diff --git a/src/base/core_memory.cppm b/src/base/core_memory.cppm deleted file mode 100644 index 3c6aabd..0000000 --- a/src/base/core_memory.cppm +++ /dev/null @@ -1,197 +0,0 @@ -module; - -#include "core_debug.h" -#include "string.h" - -/** - * @file core_memory.cppm - * @brief Arena allocator primitives backed by virtual memory mappings. - */ - -/** - * @module core_memory - * @brief Linear arena allocation helpers for transient data. - */ -export module core_memory; - -import core_types; -import core_portability; -import core_vm; - -using namespace base; - -/** - * @namespace base::mem - * @brief Arena allocation primitives for transient data. - */ -export namespace base::mem { - - /** - * @brief Linear allocator using a single contiguous mapped memory region. - * @note Supports bump allocation only; individual frees are not available. - */ - struct Arena { - /** @brief Base pointer to mapped memory; null when uninitialized/freed. */ - u8* buffer; - /** @brief Total mapped capacity in bytes. */ - u64 capacity; - /** @brief Current allocation cursor measured in bytes from buffer. */ - u64 offset; - - /** @brief Alignment target for transparent huge page friendly mapping. */ - static constexpr u64 THP_ALIGNMENT = (2zu * 1024zu * 1024zu); - - /** @brief Default constructs a zero-initialized arena handle. */ - inline Arena() = default; - - /** - * @brief Initializes the arena by mapping a writable virtual memory region. - * @param requested_capacity Minimum capacity requested in bytes. - * @return True on success; false if the mapping fails. - * @post On success, buffer is non-null and capacity is >= requested_capacity. - * @note Capacity is rounded up to THP_ALIGNMENT. - */ - inline bool init(u64 requested_capacity) { - // round capacity up to nearest 2MB boundary for the THP daemon - u64 total_capacity = align_forward(requested_capacity, this->THP_ALIGNMENT); - - // request writable virtual memory through the platform VM backend - void* ptr = vm::alloc(total_capacity); - if (!ptr) { - return false; - } - - vm::hint_hugepages(ptr, total_capacity); - - this->buffer = reinterpret_cast(ptr); - this->capacity = total_capacity; - this->offset = 0; - return true; - } - - /** - * @brief Allocates storage for an array of \p count objects of type \p T. - * @tparam T Element type. - * @param count Number of elements to reserve. - * @param zero_mem Set to true to zero-initialize the returned storage. Defaults to false. - * @return Pointer to array storage, or nullptr on out-of-memory. - * @pre init() has succeeded. - */ - template - T* alloc_array(u64 count, bool zero_mem = false) { - BASE_ASSERT(this->buffer); - u64 total_bytes = sizeof(T) * count; - - // request aligned memory matching the type's natural alignment requirement - u8* raw_ptr = alloc_raw(total_bytes, alignof(T)); - BASE_ASSERT(raw_ptr); - if (!raw_ptr) return nullptr; - - if (zero_mem) { - memset(raw_ptr, 0, total_bytes); - } - - // Project portability shim formally begins typed array lifetime over the - // raw storage returned by the arena. - return portability::start_lifetime_as_array(raw_ptr, count); - } - - /** - * @brief Allocates storage for one object of type \p T. - * @tparam T Object type. - * @param zero_mem Forwards to alloc_array(1, zero_mem); zero-initializes storage when true. - * @return Pointer to allocated storage, or nullptr on failure. - * @pre init() has succeeded. - */ - template - T* alloc_struct(bool zero_mem = false) { - return this->alloc_array(1, zero_mem); - } - - /** - * @brief Resets the allocation cursor to the beginning of the arena. - * @post offset is zero. - * @warning All previously returned pointers become invalid for future writes. - */ - constexpr void reset() { - BASE_ASSERT(this->buffer); - if (this->buffer) { - this->offset = 0; - } - } - - /** - * @brief Executes a scoped allocation phase and rewinds cursor afterward. - * @tparam F Callable type accepting Arena&. - * @param lambda_pipeline Function invoked with this arena. - * @post offset is restored to its entry value. - */ - template - inline void scoped_scratch(F&& lambda_pipeline) { - u64 snapshot = this->offset; - lambda_pipeline(*this); - this->offset = snapshot; - } - - /** - * @brief Releases the mapped memory and returns arena to an empty state. - * @post buffer is null and capacity/offset are zero. - * @warning All previously returned pointers are invalid after this call. - */ - void free() { - BASE_ASSERT(this->buffer); - if (this->buffer) { - vm::free(this->buffer, this->capacity); - this->buffer = nullptr; - this->capacity = 0; - this->offset = 0; - } - } - - private: - /** - * @brief Aligns a byte offset up to the requested power-of-two boundary. - * @param ptr Unaligned offset. - * @param align Alignment value. - * @return Aligned offset. - * @pre \p align is a power of two. - */ - constexpr u64 align_forward(u64 ptr, u64 align) { - return (ptr + align - 1) & ~(align - 1); - } - - /** - * @brief Internal raw byte allocation primitive. - * @param size Byte count requested. - * @param alignment Requested power-of-two alignment. - * @param zero_mem Set to true to zero allocated memory. Defaults to false. - * @return Byte pointer to allocated span, or nullptr if capacity is exceeded. - * @pre init() has succeeded. - */ - u8* alloc_raw(u64 size, u64 alignment = 8, bool zero_mem = false) { - BASE_ASSERT(this->buffer); - - // use the single-evaluation constexpr helper from core_types - u64 aligned_offset = AlignUpPow2(this->offset, alignment); - - // OOM check - if (aligned_offset + size > this->capacity) { - return nullptr; - } - - u8* ptr = this->buffer + aligned_offset; - if (zero_mem) { - memset(ptr, 0, size); - } - - this->offset = aligned_offset + size; - return ptr; - } - - }; - -} // namespace base::mem - -export namespace base { - using Arena = mem::Arena; -} \ No newline at end of file diff --git a/src/base/core_option.cppm b/src/base/core_option.cppm deleted file mode 100644 index e9b67ac..0000000 --- a/src/base/core_option.cppm +++ /dev/null @@ -1,91 +0,0 @@ -module; - -/** - * @file core_option.cppm - * @brief Lightweight optional value carrier for explicit presence semantics. - */ - -export module core_option; - -import core_types; -import core_portability; - -/** - * @module core_option - * @brief Optional payload wrappers designed for low-level core APIs. - */ -export namespace base { - - /** - * @brief Optional value wrapper for small trivially-copyable payloads. - * @tparam T Payload type. - */ - template - struct Option { - T value; - bool has = false; - - static_assert(portability::is_trivially_copyable_v, - "FATAL: Option requires trivially copyable T."); - static_assert(sizeof(T) <= 8, - "FATAL: Option payload too large. Use pointer/slice handle types."); - - constexpr bool is_some() const { return this->has; } - constexpr bool is_none() const { return !this->has; } - - constexpr T& unwrap() { return this->value; } - constexpr const T& unwrap() const { return this->value; } - - constexpr T value_or(T fallback) const { - return this->has ? this->value : fallback; - } - - constexpr static Option some(T v) { - Option out = {}; - out.value = v; - out.has = true; - return out; - } - - constexpr static Option none() { - Option out = {}; - out.has = false; - return out; - } - }; - - /** - * @brief Pointer specialization that uses nullptr as the none-state. - * @tparam T Pointee type. - */ - template - struct Option { - T* value = nullptr; - - constexpr bool is_some() const { return this->value != nullptr; } - constexpr bool is_none() const { return this->value == nullptr; } - - constexpr T* unwrap() const { return this->value; } - - constexpr T* value_or(T* fallback) const { - return this->value ? this->value : fallback; - } - - constexpr static Option some(T* v) { - Option out = {}; - out.value = v; - return out; - } - - constexpr static Option none() { - Option out = {}; - out.value = nullptr; - return out; - } - }; - - static_assert(sizeof(Option) == sizeof(void*), - "FATAL: Option must remain pointer-sized."); - static_assert(sizeof(Option) <= 16, - "FATAL: Option exceeded 16-byte register-return envelope."); -} \ No newline at end of file diff --git a/src/base/core_portability.cppm b/src/base/core_portability.cppm deleted file mode 100644 index 97f1b42..0000000 --- a/src/base/core_portability.cppm +++ /dev/null @@ -1,231 +0,0 @@ -/** - * @file core_portability.cppm - * @brief Compiler and standard-library gap shims used by low-level modules. - */ - -/** - * @module core_portability - * @brief Project-owned portability layer for intrinsics, traits, and shims. - */ -export module core_portability; - -import core_types; - -/** - * @namespace base::portability - * @brief Compiler-facing portability helpers that replace selected std facilities. - */ -export namespace base::portability { - - #if defined(__clang__) - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wvla-cxx-extension" - #endif - - #if defined(_MSC_VER) || defined(__clang__) || defined(__GNUC__) - /** @brief Compiler-backed trivially-copyable trait wrapper. */ - template - inline constexpr bool is_trivially_copyable_v = __is_trivially_copyable(T); - #else - #error "core_portability requires compiler support for __is_trivially_copyable" - #endif - - /** - * @brief Starts array object lifetime over already-allocated raw storage. - * @tparam T Element type. - * @param p Base pointer to suitably aligned raw storage. - * @param n Number of elements whose lifetime should begin. - * @return Typed pointer to the first element in the array region. - * @note Restricted to trivially copyable types in this project. - */ - template - [[gnu::always_inline]] inline T* start_lifetime_as_array(void* p, u64 n) noexcept { - // Conservative gate: implicit-lifetime is broader, but this is a safe subset. - static_assert(is_trivially_copyable_v, - "Arena lifetime shim requires trivially copyable T"); - - T* q = reinterpret_cast(p); - if (n == 0) return q; - - #if defined(__GNUC__) || defined(__clang__) - // Same optimizer barrier idea used by libstdc++. - auto region = (__extension__ reinterpret_cast(p)); - __asm__ __volatile__("" : "=r"(q), "=m"(*region) : "0"(q), "m"(*region)); - #endif - return q; - } - - #if defined(__clang__) - #pragma clang diagnostic pop - #endif - /** - * @brief Marks a control-flow path as unreachable for supported compilers. - * @warning Executing this function is undefined behavior. - */ - [[noreturn]] inline void unreachable() noexcept { - #if defined(_MSC_VER) - __assume(0); - #elif defined(__clang__) || defined(__GNUC__) - __builtin_unreachable(); - #else - #error "base_unreachable requires compiler-specific unreachable intrinsic" - #endif - } - - /** @brief Trait mapping signedness for project scalar types. */ - template - struct is_signed { - static constexpr bool value = false; - }; - - template <> - struct is_signed { - static constexpr bool value = true; - }; - - template <> - struct is_signed { - static constexpr bool value = true; - }; - - template <> - struct is_signed { - static constexpr bool value = true; - }; - - template <> - struct is_signed { - static constexpr bool value = true; - }; - - template <> - struct is_signed { - static constexpr bool value = false; - }; - - template <> - struct is_signed { - static constexpr bool value = false; - }; - - template <> - struct is_signed { - static constexpr bool value = false; - }; - - template <> - struct is_signed { - static constexpr bool value = false; - }; - - template - inline constexpr bool is_signed_v = is_signed::value; - - #if defined(_MSC_VER) || defined(__clang__) || defined(__GNUC__) - /** @brief Compiler-backed standard-layout trait wrapper. */ - template - inline constexpr bool is_standard_layout_v = __is_standard_layout(T); - #else - #error "core_portability requires compiler support for __is_standard_layout" - #endif - - /** @brief Maps project scalar types to their unsigned counterparts. */ - template - struct make_unsigned; - - template <> - struct make_unsigned { - using type = u8; - }; - - template <> - struct make_unsigned { - using type = u16; - }; - - template <> - struct make_unsigned { - using type = u32; - }; - - template <> - struct make_unsigned { - using type = u64; - }; - - template <> - struct make_unsigned { - using type = u8; - }; - - template <> - struct make_unsigned { - using type = u16; - }; - - template <> - struct make_unsigned { - using type = u32; - }; - - template <> - struct make_unsigned { - using type = u64; - }; - - template - using make_unsigned_t = typename make_unsigned::type; - - /** @brief Project-owned numeric limits wrapper for core scalar aliases. */ - template - struct numeric_limits; - - template <> - struct numeric_limits { - static constexpr i8 min() noexcept { return min_i8; } - static constexpr i8 max() noexcept { return max_i8; } - }; - - template <> - struct numeric_limits { - static constexpr i16 min() noexcept { return min_i16; } - static constexpr i16 max() noexcept { return max_i16; } - }; - - template <> - struct numeric_limits { - static constexpr i32 min() noexcept { return min_i32; } - static constexpr i32 max() noexcept { return max_i32; } - }; - - template <> - struct numeric_limits { - static constexpr i64 min() noexcept { return min_i64; } - static constexpr i64 max() noexcept { return max_i64; } - }; - - template <> - struct numeric_limits { - static constexpr u8 min() noexcept { return 0; } - static constexpr u8 max() noexcept { return max_u8; } - }; - - template <> - struct numeric_limits { - static constexpr u16 min() noexcept { return 0; } - static constexpr u16 max() noexcept { return max_u16; } - }; - - template <> - struct numeric_limits { - static constexpr u32 min() noexcept { return 0; } - static constexpr u32 max() noexcept { return max_u32; } - }; - - template <> - struct numeric_limits { - static constexpr u64 min() noexcept { return 0; } - static constexpr u64 max() noexcept { return max_u64; } - }; - -} \ No newline at end of file diff --git a/src/base/core_result.cppm b/src/base/core_result.cppm deleted file mode 100644 index bf459f7..0000000 --- a/src/base/core_result.cppm +++ /dev/null @@ -1,47 +0,0 @@ -export module core_result; - -import core_types; - -export namespace base { - -template -struct Result { - // Structural Defense guards - static_assert(sizeof(E) <= 8, - "FATAL: Error E is too large. Use an enum or a lightweight primitive."); - - static_assert(sizeof(T) <= 16, - "FATAL: Payload T is too large. It will trigger stack spilling. Return a pointer or a Str8 slice instead."); - - union { - E error; // the "left" or error state - T value; // the "right" or success state - }; - - // Obfuscate the raw data so the pipeline uses the method - bool _is_ok; - - // --- Core Interaction Methods --- - constexpr bool is_ok() const { - return this->_is_ok; - } - - // --- Zero-cost static constructors --- - constexpr static Result ok(T v) { - Result res = {}; // FIX: Zero-initialize memory to appease constexpr evaluation - res.value = v; - res._is_ok = true; - return res; - } - - constexpr static Result err(E e) { - Result res = {}; // FIX: Zero-initialize memory - res.error = e; - res._is_ok = false; - return res; - } -}; - - - -} // namespace base \ No newline at end of file diff --git a/src/base/core_result_s.cppm b/src/base/core_result_s.cppm deleted file mode 100644 index f9b9715..0000000 --- a/src/base/core_result_s.cppm +++ /dev/null @@ -1,60 +0,0 @@ -export module core_result_s; - -import core_types; -import core_result; -import core_string; - -using namespace base; - -export namespace base { - -// ----------------------------------------------------------------------------- -// BIT-PACKED RESULTS (Strict 16-Byte Register Return) -// ----------------------------------------------------------------------------- - -// The MSB mask: 10000000 00000000 ... -constexpr u64 RESULT_OK_BIT = 0x8000000000000000ull; -// The Length mask: 01111111 11111111 ... -constexpr u64 RESULT_LEN_MASK = 0x7FFFFFFFFFFFFFFFull; - -template -struct ResultS { - // Structural Defense: T must fit in the first 8 bytes so it doesn't - // collide with our smuggled bit in the second 8 bytes. - static_assert(sizeof(T) <= 8, - "FATAL: Packed ResultS requires payload T to be 8 bytes or smaller (e.g., u64 or pointer)."); - - union { - Str8 error; - T value; - }; - - // --- Core Interaction Methods --- - - constexpr bool is_ok() const { - return (this->error.len & RESULT_OK_BIT) != 0; - } - - constexpr Str8 get_error() const { - return Str8(this->error.str, this->error.len & RESULT_LEN_MASK); - } - - // --- Static Constructors --- - - constexpr static ResultS ok(T val) { - ResultS res = {}; // Correctly zero-initialized! - res.value = val; - // Smuggle the '1' into the top bit of the unused padding space - res.error.len |= RESULT_OK_BIT; - return res; - } - - constexpr static ResultS err(Str8 err_str) { - ResultS res = {}; - res.error.str = err_str.str; - // Ensure the top bit is '0', leaving the raw string length perfectly intact - res.error.len = err_str.len & RESULT_LEN_MASK; - return res; - } -}; -} \ No newline at end of file diff --git a/src/base/core_string.cppm b/src/base/core_string.cppm deleted file mode 100644 index e58ab6b..0000000 --- a/src/base/core_string.cppm +++ /dev/null @@ -1,397 +0,0 @@ -module; -#include -#include -#include -#include "core_debug.h" - -/** - * @file core_string.cppm - * @brief UTF-8 byte-string views, list utilities, formatting, and hashing. - */ - -/** - * @module core_string - * @brief String primitives designed for arena-backed allocation workflows. - */ -export module core_string; - -import core_types; -import core_memory; -import core_result; - -using namespace base; -using namespace mem; - -/** - * @namespace base::str - * @brief String and formatting primitives for the base layer. - */ -export namespace base::str { - - /** - * @brief Failure categories for arena-backed string formatting. - */ - enum struct StringFormatError : u8 { - None = 0, - FormatFailed, - OutOfMemory, - Truncated - }; - - struct Str8; - using StringFormatResult = Result; - - /** - * @brief Non-owning UTF-8 byte-string view. - * @note The view is not null-terminated by contract. - * @note Lifetime is owned externally, commonly by Arena. - */ - struct Str8 { - /** @brief Pointer to first byte of the string view. */ - u8* str; - /** @brief Number of bytes in the view. */ - u64 len; - - - /** @brief Default empty string view constructor. */ - constexpr Str8() { - this->str = nullptr; - this->len = 0; - } - - /** - * @brief Constructs a string view from raw pointer and explicit length. - * @param target_str Pointer to first byte. - * @param target_len Length in bytes. - * @post The instance references the provided memory without owning it. - */ - constexpr Str8(u8* target_str, u64 target_len) { - this->str = target_str; - this->len = target_len; - } - - constexpr Str8(Arena& arena, u64 target_len) { - auto buffer = arena.alloc_array(target_len); - BASE_ASSERT(buffer != nullptr); - this->str = buffer; - this->len = target_len; - } - - /** - * @brief Creates a Str8 view from a null-terminated C-string. - * @warning This performs an O(N) strlen calculation. - */ - static constexpr Str8 from_cstr(const char* cstr) { - if (!cstr) return Str8{}; - return Str8(reinterpret_cast(const_cast(cstr)), static_cast(strlen(cstr))); - } - - /** - * @brief Constructs a view from a string literal, excluding trailing null. - * @tparam N Literal array length including null terminator. - * @post len is N - 1. - */ - template - constexpr Str8(const char (&literal)[N]) { - this->str = reinterpret_cast(const_cast(literal)); - this->len = N - 1; // Omit the null terminator - } - - /** - * @brief Constructs a null terminated c-string from a Str8 - * - * @param arena Arena from which to allocate new memory for the cstr - * @return const char* A null terminated C string - */ - const char* to_cstr(Arena& arena) const { - BASE_ASSERT(this->str != nullptr); - auto buffer = arena.alloc_array(this->len+1); - BASE_ASSERT(buffer != nullptr); - - memcpy(buffer, this->str, this->len); - buffer[this->len] = 0; - return buffer; - } - - /** - * @brief Returns a sub-view from [start, end). - * @param start Inclusive starting byte offset. - * @param end Exclusive ending byte offset. - * @return A Str8 view into the original storage. - * @pre start <= end. - * @pre end <= len. - */ - constexpr Str8 slice(u64 start, u64 end) const { - // Dev-time check using universal macro - BASE_ASSERT(start <= end); - BASE_ASSERT(end <= this->len); - return Str8(this->str + start, end - start); - } - - /** - * @brief Byte-wise equality comparison between two Str8 views. - * @param other Comparison target string view. - * @return True if lengths and bytes are equal. - */ - constexpr bool match(Str8 other) const { - if (this->len != other.len) return false; - for (u64 i = 0; i < this->len; i++) { - if (this->str[i] != other.str[i]) return false; - } - return true; - } - - /** - * @brief Computes the 64-bit FNV-1a hash of this string view. - * @return 64-bit hash value. - * @note Hash is stable for identical byte sequences. - */ - constexpr u64 hash_fnv1a() const { - u64 hash = 14695981039346656037ULL; - for (u64 i = 0; i < this->len; ++i) { - hash ^= this->str[i]; - hash *= 1099511628211ULL; - } - return hash; - } - }; - - /** - * @brief Extracts the next delimiter-separated segment from a string view. - * @param remaining_str In/out remaining source view; advances past consumed bytes. - * @param out_slice Output slice for the next segment. - * @param delim Delimiter byte used to split segments. - * @return True when a segment is produced; false when input is exhausted. - */ - bool iter_next(Str8& remaining_str, Str8& out_slice, char delim) { - - if (remaining_str.len == 0) { - return false; // Exhausted - } - - u8 *start = remaining_str.str; - u8 *next = reinterpret_cast(memchr(start, delim, remaining_str.len)); - - if (!next) { - // No more delimiters; return the rest of the string and exhaust the view - out_slice = remaining_str; - remaining_str.len = 0; - return true; - } - - out_slice.str = start; - out_slice.len = (u64)(next - start); - - // Advance the remaining view past the slice and the delimiter - u64 consumed = out_slice.len + 1; - remaining_str.str += consumed; - remaining_str.len -= consumed; - - return true; - } - - //------------------------------------------------------------- - // Str8 List - //------------------------------------------------------------- - - /** @brief Singly linked node containing one Str8 segment. */ - struct Str8Node { - /** @brief Next node in list, or null at tail. */ - Str8Node* next; - /** @brief Segment value stored at this node. */ - Str8 string; - }; - - /** - * @brief Builder structure for concatenating many Str8 segments efficiently. - * @note Nodes are expected to come from a scratch Arena. - */ - struct Str8List { - /** @brief First node in list. */ - Str8Node* first; - /** @brief Last node in list. */ - Str8Node* last; - /** @brief Number of nodes pushed into the list. */ - u64 node_count; - /** @brief Sum of all segment lengths (in bytes). */ - u64 total_len; - - /** - * @brief Appends a non-empty segment to the list. - * @param scratch_arena Arena used for node allocation. - * @param string Segment to append. - * @post node_count increments when string is non-empty. - */ - void push(Arena& scratch_arena, Str8 string) { - if (string.len == 0) return; - Str8Node* node = scratch_arena.alloc_struct(); - BASE_ASSERT(node != nullptr); - - node->string = string; - node->next = nullptr; - - if (this->last == nullptr) { - this->first = node; - this->last = node; - } else { - this->last->next = node; - this->last = node; - } - - this->node_count += 1; - this->total_len += string.len; - } - - /** - * @brief Concatenates all segments in a Str8List into one contiguous Str8. - * @param arena Arena used for destination allocation. - * @return Joined string view; empty view if input is empty. - * @post Returned memory is owned by \p arena. - */ - Str8 join(Arena& arena) { - if (this->total_len == 0) return Str8{}; - - // One single allocation for the entire compound string - u8* buffer = arena.alloc_array(this->total_len); - BASE_ASSERT(buffer != nullptr); - - u64 write_offset = 0; - for (Str8Node* node = this->first; node != nullptr; node = node->next) { - if (node->string.len > 0) { - memcpy(buffer + write_offset, node->string.str, node->string.len); - write_offset += node->string.len; - } - } - return Str8(buffer, this->total_len); - } - }; - - //------------------------------------------------------------- - // Formatting into Str8 - //------------------------------------------------------------- - - /** - * @brief Formats text into arena memory using exact two-pass sizing. - * @param arena Arena used to allocate destination bytes. - * @param format Printf-style format string. - * @param args Active vararg list. - * @return Ok(Str8) on success, or Err(StringFormatError) on failure. - */ - StringFormatResult vformat(Arena& arena, const char* format, va_list args) { - va_list count_args; - va_copy(count_args, args); - i32 formal_len = vsnprintf(nullptr, 0, format, count_args); - va_end(count_args); - - if (formal_len < 0) { - return StringFormatResult::err(StringFormatError::FormatFailed); - } - - u64 safe_len = static_cast(formal_len); - if (safe_len == 0) { - return StringFormatResult::ok(Str8{}); - } - - u64 snapshot = arena.offset; - u8* buffer = arena.alloc_array(safe_len + 1); // +1: null terminator safety - if (!buffer) { - return StringFormatResult::err(StringFormatError::OutOfMemory); - } - - va_list write_args; - va_copy(write_args, args); - i32 written_len = vsnprintf(reinterpret_cast(buffer), static_cast(safe_len + 1), format, write_args); - va_end(write_args); - - if (written_len < 0 || static_cast(written_len) != safe_len) { - arena.offset = snapshot; - return StringFormatResult::err(StringFormatError::FormatFailed); - } - - return StringFormatResult::ok(Str8{buffer, safe_len}); - } - - - /** - * @brief Formats text into arena memory with caller-provided capacity cap. - * @param arena Arena used to allocate destination bytes. - * @param cap Maximum payload bytes (excluding null terminator safety byte). - * @param format Printf-style format string. - * @param args Active vararg list. - * @return Ok(Str8) on success, or Err(StringFormatError) for format failure, - * out-of-memory, or truncation. - */ - StringFormatResult vformat_cap(Arena& arena, u64 cap, const char* format, va_list args) { - if (cap == 0) { - return StringFormatResult::ok(Str8{}); - } - - u64 snapshot = arena.offset; - u8* buffer = arena.alloc_array(cap + 1); // +1 for null terminator safety - if (!buffer) { - return StringFormatResult::err(StringFormatError::OutOfMemory); - } - - va_list write_args; - va_copy(write_args, args); - i32 written_len = vsnprintf(reinterpret_cast(buffer), static_cast(cap + 1), format, write_args); - va_end(write_args); - - if (written_len < 0) { - arena.offset = snapshot; - return StringFormatResult::err(StringFormatError::FormatFailed); - } - - u64 written_u64 = static_cast(written_len); - - // vsnprintf returns full required length on truncation. - if (written_u64 > cap) { - arena.offset = snapshot; - return StringFormatResult::err(StringFormatError::Truncated); - } - - if (written_u64 == 0) { - arena.offset = snapshot; - return StringFormatResult::ok(Str8{}); - } - - arena.offset = snapshot + written_u64; // trim to exact payload bytes - return StringFormatResult::ok(Str8{buffer, written_u64}); - } - - /** - * @brief Formats text into arena memory using exact two-pass sizing. - * @param arena Arena used to allocate destination bytes. - * @param format Printf-style format string. - * @param ... Format arguments. - * @return Ok(Str8) on success, or Err(StringFormatError) on failure. - */ - StringFormatResult format(Arena& arena, const char* format, ...) { - va_list args; - va_start(args, format); - StringFormatResult result = vformat(arena, format, args); - va_end(args); - return result; - } - - /** - * @brief Formats text into arena memory using caller-provided capacity cap. - * @param arena Arena used to allocate destination bytes. - * @param cap Maximum payload bytes (excluding null terminator safety byte). - * @param format Printf-style format string. - * @param ... Format arguments. - * @return Ok(Str8) on success, or Err(StringFormatError) on failure. - */ - StringFormatResult format_cap(Arena& arena, u64 cap, const char* format, ...) { - va_list args; - va_start(args, format); - StringFormatResult result = vformat_cap(arena, cap, format, args); - va_end(args); - return result; - } - -} // namespace base::str - -export namespace base { - using Str8 = str::Str8; -} \ No newline at end of file diff --git a/src/base/core_text_parse.cppm b/src/base/core_text_parse.cppm deleted file mode 100644 index 448942b..0000000 --- a/src/base/core_text_parse.cppm +++ /dev/null @@ -1,522 +0,0 @@ -export module core_text_parse; - -import core_types; -import core_result; -import core_string; -import core_portability; - -using namespace base; -using namespace str; - -export namespace base::parse { - /** - * @defgroup StringResolution Zero-Allocation String Parsing - * @brief How to handle escaped string literals in downstream modules. - * - * In this Data-Oriented architecture, the core lexer and parser utilities - * are strictly zero-allocation. When the lexer extracts a string literal - * (for example, "Hello\\nWorld"), it returns a raw Str8 slice directly - * pointing to the source buffer. This means escape sequences (for example, - * "\\n", "\\t", and "\\\"") remain physical, multi-byte characters in - * the slice. - * - * Because resolving escapes requires a string to shrink in size and physically - * decouple from the source buffer, it requires memory allocation. Therefore, - * resolving strings is the responsibility of the Coordinator Layer - * (for example, your AST Builder or String Interner), not the core utility - * layer. - * - * This separation preserves deterministic behavior in low-level parsing code - * and keeps ownership boundaries explicit: parser utilities only classify and - * slice, while downstream systems decide allocation policy and lifetime. - * - * @par Data-oriented separation - * This routine is intentionally pure "math" logic and does not touch cursor - * state. Cursor movement belongs in lexer `consume_*` style functions. - * - * @par Coordinator contract - * These strict parsers validate only the provided slice and return success or - * failure for that slice. They do not perform recovery, cursor rewinds, or - * diagnostic list mutation. In language-server style workflows, a coordinator - * layer is responsible for aggregating diagnostics and selecting recovery steps. - * - * @code - * // Inside my_language_ast.cppm (or similar) - * import core_memory; - * import core_text_parse; - * - * namespace my_language { - * - * base::str::Str8 resolve_string_literal(base::mem::Arena& arena, - * base::str::Str8 raw_slice) { - * // Pre-allocate worst-case size (the original string length) - * u8* dest = arena.alloc_array(raw_slice.len); - * u64 write_idx = 0; - * - * for (u64 i = 0; i < raw_slice.len; ++i) { - * if (raw_slice.str[i] == '\\' && i + 1 < raw_slice.len) { - * i++; // Skip the backslash - * switch (raw_slice.str[i]) { - * case 'n': dest[write_idx++] = '\n'; break; - * case 'r': dest[write_idx++] = '\r'; break; - * case 't': dest[write_idx++] = '\t'; break; - * case '\\': dest[write_idx++] = '\\'; break; - * case '"': dest[write_idx++] = '"'; break; - * // Keep unknown escapes as raw bytes - * default: dest[write_idx++] = raw_slice.str[i]; break; - * } - * } else { - * dest[write_idx++] = raw_slice.str[i]; - * } - * } - * - * // Return the bounded slice representing the exact resolved length - * return base::str::Str8{dest, write_idx}; - * } - * - * } // namespace my_language - * @endcode - */ - - //------------------------------------------------------------- - // Location and meta utilities - //------------------------------------------------------------ - - /** - * @brief 1-based source location measured in bytes. - */ - struct SourceLocation { - /** @brief 1-based line index. */ - u32 line; - /** @brief 1-based column index. */ - u32 column; - }; - - /** - * @brief Precomputed line-start offsets for O(log N) location lookup. - * - * `line_starts[i]` stores the byte offset for 1-based line `i+1`. - */ - struct LineIndex { - /** @brief Pointer to caller-owned line-start offset storage. */ - const u64* line_starts; - /** @brief Number of entries in @ref line_starts. */ - u32 line_count; - }; - - /** - * @brief Failure categories for line-index construction. - */ - enum struct LineIndexError : u8 { - None = 0, - InvalidArguments, - InsufficientCapacity - }; - - /** @brief Result alias for building a line index */ - using LineIndexBuildResult = Result; - - /** - * @brief Compute required entry count for a line-start index. - * - * Always returns at least 1 (line 1 at offset 0). - */ - constexpr u32 line_index_required_capacity(Str8 buffer) { - u32 lines = 1; - for(u64 i = 0; i < buffer.len; ++i) { - if (buffer.str[i] == '\n') { - lines++; - } - } - return lines; - } - - /** - * @brief Build a line-start index into caller-provided storage. - * - * @param buffer Source buffer. - * @param out_line_starts Destination storage for line starts. - * @param capacity Number of writable entries in @p out_line_starts. - * @return Ok(LineIndex) on success, or Err(LineIndexError) for invalid arguments - * or insufficient capacity. - */ - constexpr LineIndexBuildResult line_index_build(Str8 buffer, u64* out_line_starts, u32 capacity) { - if (out_line_starts == nullptr || capacity == 0) { - return LineIndexBuildResult::err(LineIndexError::InvalidArguments); - } - - u32 needed = line_index_required_capacity(buffer); - if (capacity < needed) { - return LineIndexBuildResult::err(LineIndexError::InsufficientCapacity); - } - - u32 write = 0; - out_line_starts[write++] = 0; // line 1 always starts at offset 0 - - for (u64 i = 0; i < buffer.len; ++i) { - if (buffer.str[i] == '\n') { - out_line_starts[write++] = i + 1; - } - } - - return LineIndexBuildResult::ok(LineIndex{out_line_starts, write}); - } - - /** - * @brief Diagnostic severity for parser/lexer recovery workflows. - * - */ - enum struct DiagnosticSeverity : u8 { - Note = 0, - Warning, - Error - }; - - /** - * @brief Stable diagnostic code identifiers for text parse failures. - * - * Keep this list compact and allocator-free for code-first diagnostics. - * Human-readable text is a coordinator/reporting concern. - */ - enum struct ParseDiagCode : u16 { - None = 0, - EmptyInput, - NoDigits, - InvalidNumericChar, - IntOverflow, - InvalidExponent, - ExponentOverflow, - TrailingChars, - UnterminatedString, - UnterminatedBlockComment - }; - - struct ParseDiagnostic { - ParseDiagCode code; - DiagnosticSeverity severity; - u64 start_offset; - u64 end_offset; - }; - - template - using ParseResult = Result; - - static_assert(portability::is_standard_layout_v, - "ParseDiagnostic must remain standard layout."); - static_assert(portability::is_trivially_copyable_v, - "ParseDiagnostic must remain trivially copyable."); - - constexpr SourceLocation compute_location(Str8 buffer, u64 offset) { - u32 line = 1; - u32 col = 1; - u64 limit = Min(offset, buffer.len); - - for (u64 i = 0; i < limit; ++i) { - if (buffer.str[i] == '\n') { - line++; - col = 1; - } else { - col++; - } - } - return {line,col}; - } - - /** - * @brief Compute line/column location from a precomputed line index. - * @param index Precomputed line-start index. - * @param buffer_len Source buffer length used for clamping. - * @param offset Byte offset into source data. - * @return 1-based line/column location clamped at end-of-buffer. - */ - constexpr SourceLocation compute_location(LineIndex index, u64 buffer_len, u64 offset) { - if (index.line_starts == nullptr || index.line_count == 0) { - return {1, 1}; - } - - u64 clamped = (offset < buffer_len) ? offset : buffer_len; - - // upper_bound(line_starts, clamped) - u32 lo = 0; - u32 hi = index.line_count; - while (lo < hi) { - u32 mid = lo + ((hi - lo) / 2); - if (index.line_starts[mid] <= clamped) { - lo = mid + 1; - } else { - hi = mid; - } - } - - u32 line_idx = (lo == 0) ? 0 : (lo - 1); - u64 line_start = index.line_starts[line_idx]; - u64 col64 = (clamped - line_start) + 1; - u32 column = (col64 > static_cast(portability::numeric_limits::max())) - ? portability::numeric_limits::max() - : static_cast(col64); - return {line_idx + 1, column}; - } - - //------------------------------------------------------------- - // Integer Parsers (template engine) - //------------------------------------------------------------- - - /** - * @brief Generic integer parser for ASCII decimal slices. - * - * Parses an optional sign for signed integer targets and then validates each - * remaining byte as `[0-9]`. - * - * @note Strict parser contract: the full input slice must be a valid integer. - * Any non-digit payload after an optional sign fails immediately. - * - * @par Data-oriented separation - * This routine is intentionally pure "math" logic and does not touch cursor - * state. Cursor movement belongs in lexer `consume_*` style functions. - * - * @par Signed safety strategy - * Digits are accumulated in the unsigned counterpart of `T` so intermediate - * multiply/add steps cannot trigger signed overflow UB. A final range gate - * then applies sign and validates representability for the target type. - * - * @tparam T Integer destination type. - * @param text Input byte slice. - * @return Parsed integer or error when invalid/empty/overflowing. - */ - template - constexpr ParseResult parse_int_impl(Str8 text) { - if (text.len == 0) return ParseResult::err(ParseDiagCode::EmptyInput); - - u64 i = 0; - bool is_negative = false; - - // compile time branch: check sign handling only if type is signed - if (portability::is_signed_v) { - if (text.str[0] == '-') { - is_negative = true; - i++; - } else if (text.str[0] == '+') { - i++; - } - } - - if (i == text.len) return ParseResult::err(ParseDiagCode::NoDigits); - - // Accumulate in unsigned space first; signed range checks happen once. - using UnsignedT = portability::make_unsigned_t; - UnsignedT result = 0; - - for (; i < text.len; ++i) { - u8 c = text.str[i]; - if (c < '0' || c > '9') { - return ParseResult::err(ParseDiagCode::InvalidNumericChar); - } - UnsignedT digit = c - '0'; - - // Pre-multiply overflow check: result * 10 + digit must stay representable. - if (result > (portability::numeric_limits::max() - digit) / 10) { - return ParseResult::err(ParseDiagCode::IntOverflow); - } - result = result * 10 + digit; - } - - // Final sign application and target-range validation - // Portability safe path: never cast an out-of-range unsigned magnitude to signed. - if constexpr (portability::is_signed_v) { - const UnsignedT max_pos = static_cast(portability::numeric_limits::max()); - const UnsignedT max_neg_mag = max_pos + 1; // |T::min()| - - if (is_negative) { - if (result > max_neg_mag) { - return ParseResult::err(ParseDiagCode::IntOverflow); - } - if (result == max_neg_mag) { - return ParseResult::ok(portability::numeric_limits::min()); - } - - // result <= max_pos, so cast to T is representable - T mag = static_cast(result); - return ParseResult::ok(static_cast(-mag)); - } - - if (result > max_pos) { - return ParseResult::err(ParseDiagCode::IntOverflow); - } - return ParseResult::ok(static_cast(result)); - } - return ParseResult::ok(static_cast(result)); - } - - /** @brief Parse `Str8` into `u8`. */ - constexpr ParseResult parse_u8(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `i8`. */ - constexpr ParseResult parse_i8(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `u16`. */ - constexpr ParseResult parse_u16(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `i16`. */ - constexpr ParseResult parse_i16(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `u32`. */ - constexpr ParseResult parse_u32(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `i32`. */ - constexpr ParseResult parse_i32(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `u64`. */ - constexpr ParseResult parse_u64(Str8 t) { return parse_int_impl(t); } - /** @brief Parse `Str8` into `i64`. */ - constexpr ParseResult parse_i64(Str8 t) { return parse_int_impl(t); } - - template - constexpr ParseResult parse_int_impl(const char* cstr) { - auto str = Str8::from_cstr(cstr); - return parse_int_impl(str); - } - - /** @brief Parse `const char*` into `u8`. */ - constexpr ParseResult parse_u8(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `i8`. */ - constexpr ParseResult parse_i8(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `u16`. */ - constexpr ParseResult parse_u16(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `i16`. */ - constexpr ParseResult parse_i16(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `u32`. */ - constexpr ParseResult parse_u32(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `i32`. */ - constexpr ParseResult parse_i32(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `u64`. */ - constexpr ParseResult parse_u64(const char* t) { return parse_int_impl(t); } - /** @brief Parse `const char*` into `i64`. */ - constexpr ParseResult parse_i64(const char* t) { return parse_int_impl(t); } - - - //------------------------------------------------------------- - // Floating point parsers - //------------------------------------------------------------- - - /** - * @brief Parse decimal/scientific ASCII into `f64`. - * - * Supports optional sign, fractional part, and exponent marker (`e`/`E`). - * - * @param text Input byte slice. - * @return Parsed `f64` on full-slice success; otherwise an error for empty - * input, invalid exponent, exponent overflow, trailing characters, - * or missing digits. - * @note This function is strict over the passed slice only. Error accumulation - * and continued parsing across a full buffer are coordinator-layer tasks. - */ - constexpr ParseResult parse_f64(Str8 text) { - if (text.len == 0) { - return ParseResult::err(ParseDiagCode::EmptyInput); - } - - u64 i = 0; - bool is_negative = false; - - if (text.str[0] == '-') { - is_negative = true; - i++; - } else if (text.str[0] == '+') { - i++; - } - - f64 result = 0.0; - bool found_digits = false; - - // Mantissa: integer part - while (i < text.len && text.str[i] >= '0' && text.str[i] <= '9') { - result = result * 10.0 + (text.str[i] - '0'); - i++; - found_digits = true; - } - - // Mantissa: fractional part - if (i < text.len && text.str[i] == '.') { - i++; - f64 fraction = 1.0; - while (i < text.len && text.str[i] >= '0' && text.str[i] <= '9') { - fraction *= 0.1; - result += (text.str[i] - '0') * fraction; - i++; - found_digits = true; - } - } - - if (!found_digits) { - return ParseResult::err(ParseDiagCode::NoDigits); - } - - // Exponent part (for example, e-4) - // Strict rule: at least one digit is required after e, e+, or e-. - // Performance rule: 10^exp is computed with exponentiation by squaring - // to keep multiplication cost logarithmic in the exponent size. - if (i < text.len && (text.str[i] == 'e' || text.str[i] == 'E')) { - i++; - bool exp_negative = false; - if (i < text.len && text.str[i] == '-') { - exp_negative = true; - i++; - } else if (i < text.len && text.str[i] == '+') { - i++; - } - - u64 exp_digits_start = i; - u32 exp_val = 0; - while (i < text.len && text.str[i] >= '0' && text.str[i] <= '9') { - u32 digit = static_cast(text.str[i] - '0'); - if (exp_val > (portability::numeric_limits::max() - digit) / 10) { - return ParseResult::err(ParseDiagCode::ExponentOverflow); - } - exp_val = exp_val * 10 + (text.str[i] - '0'); - i++; - } - - if (i == exp_digits_start) { - return ParseResult::err(ParseDiagCode::InvalidExponent); - } - - // Compute 10^e using binary exponentiation. - // Invariant during loop: out * base^(remaining e) == 10^(original e). - // Complexity: O(log e) multiplications instead of O(e). - auto pow10 = [](u32 e) constexpr -> f64 { - f64 base = 10.0; - f64 out = 1.0; - while (e > 0) { - if (e & 1u) { - out *= base; - } - base *= base; - e >>= 1u; - } - return out; - }; - - f64 p = pow10(exp_val); - if (exp_negative) { - result /= p; - } else { - result *= p; - } - } - - if (i != text.len) { - return ParseResult::err(ParseDiagCode::TrailingChars); - } - - return ParseResult::ok(is_negative ? -result : result); - } - - /** - * @brief Parse decimal/scientific ASCII into `f32` via `parse_f64`. - * @param text Input byte slice. - * @return Parsed `f32` or forwarded parse error. - */ - constexpr ParseResult parse_f32(Str8 text) { - auto res = parse_f64(text); - if (!res.is_ok()) { - return ParseResult::err(res.error); - } - return ParseResult::ok(static_cast(res.value)); - } - -} // namespace base::parse - diff --git a/src/base/core_types.cppm b/src/base/core_types.cppm deleted file mode 100644 index f81e340..0000000 --- a/src/base/core_types.cppm +++ /dev/null @@ -1,229 +0,0 @@ -module; - -#include -#include "core_debug.h" - -/** - * @file core_types.cppm - * @brief Foundational scalar aliases, limits, and constexpr numeric helpers. - */ - -/** - * @module core_types - * @brief Base type and arithmetic utilities shared by core modules. - */ -export module core_types; - -/** - * @namespace base - * @brief Shared low-level primitives for runtime and puzzle code. - */ -export namespace base { - - // ========================================================================== - // Explicit Fixed-Width Scalar Aliases - // ========================================================================== - /** @brief Signed 8-bit integer alias. */ - using i8 = int8_t; - /** @brief Signed 16-bit integer alias. */ - using i16 = int16_t; - /** @brief Signed 32-bit integer alias. */ - using i32 = int32_t; - /** @brief Signed 64-bit integer alias. */ - using i64 = int64_t; - - /** @brief Unsigned 8-bit integer alias. */ - using u8 = uint8_t; - /** @brief Unsigned 16-bit integer alias. */ - using u16 = uint16_t; - /** @brief Unsigned 32-bit integer alias. */ - using u32 = uint32_t; - /** @brief Unsigned 64-bit integer alias. */ - using u64 = uint64_t; - - /** @brief 32-bit floating-point alias. */ - using f32 = float; - /** @brief 64-bit floating-point alias. */ - using f64 = double; - - /** @brief Function pointer alias for a no-argument void callback. */ - using VoidFunc = void(*)(void); - - // ========================================================================== - // Type-Safe Limits - // ========================================================================== - /** @brief Minimum representable i8 value. */ - constexpr i8 min_i8 = -128; - /** @brief Minimum representable i16 value. */ - constexpr i16 min_i16 = -32768; - /** @brief Minimum representable i32 value. */ - constexpr i32 min_i32 = (-2147483647 - 1); - /** @brief Minimum representable i64 value. */ - constexpr i64 min_i64 = (-9223372036854775807LL - 1LL); - - /** @brief Maximum representable i8 value. */ - constexpr i8 max_i8 = 127; - /** @brief Maximum representable i16 value. */ - constexpr i16 max_i16 = 32767; - /** @brief Maximum representable i32 value. */ - constexpr i32 max_i32 = 2147483647; - /** @brief Maximum representable i64 value. */ - constexpr i64 max_i64 = 9223372036854775807LL; - - /** @brief Maximum representable u8 value. */ - constexpr u8 max_u8 = 255U; - /** @brief Maximum representable u16 value. */ - constexpr u16 max_u16 = 65535U; - /** @brief Maximum representable u32 value. */ - constexpr u32 max_u32 = 4294967295U; - /** @brief Maximum representable u64 value. */ - constexpr u64 max_u64 = 18446744073709551615ULL; - - // ========================================================================== - // Generic Slice/List - // ========================================================================== - template - struct Slice { - T* data; - u64 len; - - // Zero cost inline helpers allow standard C++ range-based loops - // like (for auto& item : slice) without violating HMH standards, - // they compile away to raw pointer math - inline T* begin() { return data; } - inline T* end() { return data + len; } - - // Quick indexing - inline T& operator[](u64 index) { - BASE_ASSERT(index < len); - return data[index]; - } - }; - - // ========================================================================== - // Constexpr Mathematical Helpers (Standardized on u64) - // ========================================================================== - /** - * @brief Returns the lesser of two values. - * @tparam T Value type supporting operator<. - * @param a First operand. - * @param b Second operand. - * @return Lesser value. - */ - template - constexpr T Min(T a, T b) { return (a < b) ? a : b; } - - /** - * @brief Returns the greater of two values. - * @tparam T Value type supporting operator>. - * @param a First operand. - * @param b Second operand. - * @return Greater value. - */ - template - constexpr T Max(T a, T b) { return (a > b) ? a : b; } - - /** - * @brief Clamps to an upper bound. - * @tparam T Value type supporting ordering. - * @param a Candidate value. - * @param b Upper bound. - * @return min(a, b). - */ - template - constexpr T ClampTop(T a, T b) { return Min(a, b); } - - /** - * @brief Clamps to a lower bound. - * @tparam T Value type supporting ordering. - * @param a Candidate value. - * @param b Lower bound. - * @return max(a, b). - */ - template - constexpr T ClampBottom(T a, T b) { return Max(a, b); } - - /** - * @brief Aligns value up to the next multiple of a power-of-two alignment. - * @param x Input value. - * @param p Alignment, expected power of two. - * @return Smallest value >= x aligned to p. - * @pre \p p is a power of two. - */ - constexpr u64 AlignUpPow2(u64 x, u64 p) { - return (x + (p - 1)) & ~(p - 1); - } - - /** - * @brief Aligns value down to a power-of-two boundary. - * @param x Input value. - * @param p Alignment, expected power of two. - * @return Greatest value <= x aligned to p. - * @pre \p p is a power of two. - */ - constexpr u64 AlignDownPow2(u64 x, u64 p) { - return x & ~(p - 1); - } - - /** - * @brief Tests whether a value is zero or a power of two. - * @param x Input value. - * @return True if x is 0 or power-of-two; otherwise false. - */ - constexpr bool IsPow2OrZero(u64 x) { - return (x & (x - 1)) == 0; - } - - // ========================================================================== - // Literal Sizing Operators (Using u64) - // ========================================================================== - /** - * @brief Converts kibibytes to bytes. - * @param x Input count in KiB. - * @return Byte count (x * 1024). - */ - constexpr u64 KB(u64 x) { return x << 10; } - /** - * @brief Converts mebibytes to bytes. - * @param x Input count in MiB. - * @return Byte count (x * 1024^2). - */ - constexpr u64 MB(u64 x) { return x << 20; } - /** - * @brief Converts gibibytes to bytes. - * @param x Input count in GiB. - * @return Byte count (x * 1024^3). - */ - constexpr u64 GB(u64 x) { return x << 30; } - /** - * @brief Converts tebibytes to bytes. - * @param x Input count in TiB. - * @return Byte count (x * 1024^4). - */ - constexpr u64 TB(u64 x) { return x << 40; } - - /** - * @brief Scales a value by one thousand. - * @param x Input value. - * @return x * 1000. - */ - constexpr u64 Thousand(u64 x) { return x * 1'000; } - /** - * @brief Scales a value by one million. - * @param x Input value. - * @return x * 1000000. - */ - constexpr u64 Million(u64 x) { return x * 1'000'000; } - /** - * @brief Scales a value by one billion. - * @param x Input value. - * @return x * 1000000000. - */ - constexpr u64 Billion(u64 x) { return x * 1'000'000'000; } - /** - * @brief Scales a value by one trillion. - * @param x Input value. - * @return x * 1000000000000. - */ - constexpr u64 Trillion(u64 x) { return x * 1'000'000'000'000ULL; } -} \ No newline at end of file diff --git a/src/base/core_vm.cppm b/src/base/core_vm.cppm deleted file mode 100644 index d987d19..0000000 --- a/src/base/core_vm.cppm +++ /dev/null @@ -1,78 +0,0 @@ -module; - -#if defined(__linux__) - #include -#elif defined(_WIN32) - #error "core_vm: Windows VM backend not yet implemented" -#else - #error "core_vm: Unsupported platform; add an explicit core_vm backend first" -#endif - -/** - * @file core_vm.cppm - * @brief Operating-system virtual memory wrappers used by arena allocation. - */ - -/** - * @module core_vm - * @brief VM backend operations that isolate OS-specific allocation calls. - */ -export module core_vm; - -import core_types; - -/** - * @namespace base::vm - * @brief Operating-system virtual memory boundaries for base allocators. - */ -export namespace base::vm { - - /** - * @brief Allocates a contiguous writable virtual memory region. - * @param bytes Number of bytes requested. - * @return Base pointer to writable storage, or nullptr on failure. - * @note Current implementation delegates to the Linux mmap backend. - */ - inline void* alloc(u64 bytes) noexcept { - #if defined(__linux__) - - int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; - - #if defined(MAP_POPULATE) - - map_flags |= MAP_POPULATE; - - #endif - - void* ptr = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, map_flags, -1, 0); - return (ptr == MAP_FAILED) ? nullptr : ptr; - - #endif - } - - /** - * @brief Releases a region previously returned by alloc. - * @param ptr Base pointer returned by alloc. - * @param bytes Size of the mapped region in bytes. - */ - inline void free(void* ptr, u64 bytes) noexcept { - #if defined(__linux__) - if (ptr) munmap(ptr, bytes); - #endif - } - - /** - * @brief Best-effort huge-page hint for a mapped region. - * @note This hint is optional and never treated as an allocation failure. - * @param ptr Base pointer of the mapped region. - * @param bytes Length of the mapped region. - */ - inline void hint_hugepages(void* ptr, u64 bytes) noexcept { - #if defined(MADV_HUGEPAGE) - madvise(ptr, bytes, MADV_HUGEPAGE); - #else - (void)ptr; - (void)bytes; - #endif - } -} // namespace base::vm \ No newline at end of file diff --git a/src/puzzles/2015/y2015d00.cppm b/src/puzzles/2015/y2015d00.cppm index bceb298..7b32f84 100644 --- a/src/puzzles/2015/y2015d00.cppm +++ b/src/puzzles/2015/y2015d00.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d00; diff --git a/src/puzzles/2015/y2015d02.cppm b/src/puzzles/2015/y2015d02.cppm index c76c397..a1f2b59 100644 --- a/src/puzzles/2015/y2015d02.cppm +++ b/src/puzzles/2015/y2015d02.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d02; diff --git a/src/puzzles/2015/y2015d03.cppm b/src/puzzles/2015/y2015d03.cppm index 0fb3004..e7ce2c0 100644 --- a/src/puzzles/2015/y2015d03.cppm +++ b/src/puzzles/2015/y2015d03.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d03; diff --git a/src/puzzles/2015/y2015d04.cppm b/src/puzzles/2015/y2015d04.cppm index e8e9e4f..d2f2532 100644 --- a/src/puzzles/2015/y2015d04.cppm +++ b/src/puzzles/2015/y2015d04.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" #include "openssl/md5.h" #include diff --git a/src/puzzles/2015/y2015d05.cppm b/src/puzzles/2015/y2015d05.cppm index 72beb1d..e813d34 100644 --- a/src/puzzles/2015/y2015d05.cppm +++ b/src/puzzles/2015/y2015d05.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d05; diff --git a/src/puzzles/2015/y2015d06.cppm b/src/puzzles/2015/y2015d06.cppm index 110e604..f77a1ae 100644 --- a/src/puzzles/2015/y2015d06.cppm +++ b/src/puzzles/2015/y2015d06.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d06; diff --git a/src/puzzles/2015/y2015d07.cppm b/src/puzzles/2015/y2015d07.cppm index 9fa2e87..08d1c4b 100644 --- a/src/puzzles/2015/y2015d07.cppm +++ b/src/puzzles/2015/y2015d07.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d07; diff --git a/src/puzzles/2015/y2015d08.cppm b/src/puzzles/2015/y2015d08.cppm index 1718c67..7289488 100644 --- a/src/puzzles/2015/y2015d08.cppm +++ b/src/puzzles/2015/y2015d08.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d08; diff --git a/src/puzzles/2015/y2015d09.cppm b/src/puzzles/2015/y2015d09.cppm index 3ba77a2..9dd9535 100644 --- a/src/puzzles/2015/y2015d09.cppm +++ b/src/puzzles/2015/y2015d09.cppm @@ -1,6 +1,6 @@ module; -#include "../../base/core_debug.h" +#include "core_debug.h" export module y2015d09; diff --git a/third_party/cpp-core b/third_party/cpp-core new file mode 160000 index 0000000..eaa7a4c --- /dev/null +++ b/third_party/cpp-core @@ -0,0 +1 @@ +Subproject commit eaa7a4cf5d32180714c53e6ca04d47176f41da38 From 1377f27cd4afaeb30ff3a62816472813e592af63 Mon Sep 17 00:00:00 2001 From: "Eric Vincent (arch)" Date: Wed, 22 Jul 2026 13:06:49 -0400 Subject: [PATCH 2/2] ci: verify cpp-core submodule pin in github actions --- .github/workflows/verify-cpp-core-pin.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/verify-cpp-core-pin.yml diff --git a/.github/workflows/verify-cpp-core-pin.yml b/.github/workflows/verify-cpp-core-pin.yml new file mode 100644 index 0000000..2b7725a --- /dev/null +++ b/.github/workflows/verify-cpp-core-pin.yml @@ -0,0 +1,19 @@ +name: Verify cpp-core submodule pin + +on: + push: + branches: + - main + pull_request: + +jobs: + verify-cpp-core-pin: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Verify cpp-core tag pin + run: make verify-submodule-pin