aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libbase/CMakeLists.txt68
-rw-r--r--libbase/include/core/adt/PointerIntPair.h301
-rw-r--r--libbase/include/core/adt/PointerLikeTypeTraits.h165
-rw-r--r--libbase/include/core/adt/STLFunctionalExtras.h (renamed from rcc/include/rcc/ADT/STLFunctionalExtras.h)20
-rw-r--r--libbase/include/core/adt/SmallVector.h1926
-rw-r--r--libbase/include/core/adt/Twine.h679
-rw-r--r--libbase/include/core/adt/ilist.h546
-rw-r--r--libbase/include/core/adt/ilist_base.h139
-rw-r--r--libbase/include/core/adt/ilist_iterator.h591
-rw-r--r--libbase/include/core/adt/ilist_node.h470
-rw-r--r--libbase/include/core/adt/ilist_node_base.h167
-rw-r--r--libbase/include/core/adt/ilist_node_options.h244
-rw-r--r--libbase/include/core/adt/simple_ilist.h482
-rw-r--r--libbase/include/core/compiler/Compiler.h860
-rw-r--r--libbase/include/core/compiler/DataTypes.h (renamed from rcc/include/rcc/DataTypes.h)4
-rw-r--r--libbase/include/core/config/config.h.cmake219
-rw-r--r--libbase/include/core/config/libbase-config.h.cmake89
-rw-r--r--libbase/include/core/error/Errno.h (renamed from rcc/include/rcc/Support/Errno.h)16
-rw-r--r--libbase/include/core/error/ErrorHandling.h217
-rw-r--r--libbase/include/core/math/MathExtras.h (renamed from rcc/include/rcc/Support/MathExtras.h)16
-rw-r--r--libbase/include/core/memory/Alignment.h497
-rw-r--r--libbase/include/core/memory/AllocatorBase.h200
-rw-r--r--libbase/include/core/memory/MemAlloc.h125
-rw-r--r--libbase/src/core/adt/SmallVector.cpp234
-rw-r--r--libbase/src/core/error/Errno.cc (renamed from rcc/lib/Support/Errno.cc)8
-rw-r--r--libbase/src/core/math/MathExtras.cc (renamed from rcc/lib/Support/MathExtras.cc)6
-rw-r--r--libbase/src/core/memory/MemAlloc.cpp63
-rw-r--r--proofsearch/.clang-format62
-rw-r--r--proofsearch/.clang-tidy76
-rw-r--r--proofsearch/.gitignore15
-rw-r--r--proofsearch/CMakeLists.txt111
-rw-r--r--proofsearch/LICENSE674
-rw-r--r--proofsearch/README.md49
-rw-r--r--proofsearch/data/monkey_banana.p27
-rw-r--r--proofsearch/data/simple_propositional.p5
-rw-r--r--proofsearch/docs/architecture.md630
-rw-r--r--proofsearch/grammar/TPTP.g40
-rw-r--r--proofsearch/include/atp/core/clause.h52
-rw-r--r--proofsearch/include/atp/core/clause_store.h64
-rw-r--r--proofsearch/include/atp/core/literal.h39
-rw-r--r--proofsearch/include/atp/core/symbol_table.h71
-rw-r--r--proofsearch/include/atp/core/term.h43
-rw-r--r--proofsearch/include/atp/core/term_bank.h86
-rw-r--r--proofsearch/include/atp/core/types.h79
-rw-r--r--proofsearch/include/atp/frontend/tptp_parser.h99
-rw-r--r--proofsearch/include/atp/index/discrim_tree.h76
-rw-r--r--proofsearch/include/atp/infer/resolution.h50
-rw-r--r--proofsearch/include/atp/infer/substitution.h76
-rw-r--r--proofsearch/include/atp/infer/unification.h43
-rw-r--r--proofsearch/include/atp/normalize/clausifier.h69
-rw-r--r--proofsearch/include/atp/normalize/formula.h84
-rw-r--r--proofsearch/include/atp/proof/proof_trace.h55
-rw-r--r--proofsearch/include/atp/rewrite/ordering.h52
-rw-r--r--proofsearch/include/atp/rewrite/paramodulation.h47
-rw-r--r--proofsearch/include/atp/search/clause_queue.h43
-rw-r--r--proofsearch/include/atp/search/prover.h92
-rw-r--r--proofsearch/include/atp/simplify/subsumption.h56
-rw-r--r--proofsearch/include/atp/simplify/tautology.h31
-rw-r--r--proofsearch/include/atp/strategy/calculus.h67
-rw-r--r--proofsearch/include/atp/strategy/inference_rule.h80
-rw-r--r--proofsearch/include/atp/strategy/problem_analyzer.h61
-rw-r--r--proofsearch/include/atp/utils/hash_utils.h46
-rw-r--r--proofsearch/include/atp/utils/logger.h41
-rw-r--r--proofsearch/src/core/clause_store.cc59
-rw-r--r--proofsearch/src/core/symbol_table.cc65
-rw-r--r--proofsearch/src/core/term_bank.cc63
-rw-r--r--proofsearch/src/frontend/tptp_parser.cc325
-rw-r--r--proofsearch/src/index/discrim_tree.cc17
-rw-r--r--proofsearch/src/infer/resolution.cc176
-rw-r--r--proofsearch/src/infer/substitution.cc119
-rw-r--r--proofsearch/src/infer/unification.cc92
-rw-r--r--proofsearch/src/main.cc99
-rw-r--r--proofsearch/src/normalize/clausifier.cc17
-rw-r--r--proofsearch/src/proof/proof_trace.cc249
-rw-r--r--proofsearch/src/rewrite/ordering.cc17
-rw-r--r--proofsearch/src/rewrite/paramodulation.cc17
-rw-r--r--proofsearch/src/search/prover.cc167
-rw-r--r--proofsearch/src/simplify/subsumption.cc43
-rw-r--r--proofsearch/src/simplify/tautology.cc37
-rw-r--r--proofsearch/src/strategy/problem_analyzer.cc17
-rw-r--r--proofsearch/tests/CMakeLists.txt31
-rw-r--r--proofsearch/tests/test_clausifier.cc17
-rw-r--r--proofsearch/tests/test_discrim_tree.cc17
-rw-r--r--proofsearch/tests/test_end_to_end.cc184
-rw-r--r--proofsearch/tests/test_proof_trace.cc207
-rw-r--r--proofsearch/tests/test_prover.cc183
-rw-r--r--proofsearch/tests/test_resolution.cc257
-rw-r--r--proofsearch/tests/test_simplify.cc191
-rw-r--r--proofsearch/tests/test_term_bank.cc339
-rw-r--r--proofsearch/tests/test_unification.cc355
-rw-r--r--raytracing/.clang-format23
-rw-r--r--raytracing/.clang-tidy9
-rw-r--r--raytracing/.gitignore269
-rw-r--r--raytracing/CMakeLists.txt39
-rw-r--r--raytracing/LICENSE674
-rw-r--r--raytracing/README.md44
-rw-r--r--raytracing/apps/CMakeLists.txt3
-rw-r--r--raytracing/apps/main.cc95
-rw-r--r--raytracing/etc/build_type.cmake11
-rw-r--r--raytracing/etc/ccache.cmake5
-rw-r--r--raytracing/etc/cflags.cmake16
-rw-r--r--raytracing/etc/lto.cmake8
-rw-r--r--raytracing/etc/scanners.cmake52
-rwxr-xr-xraytracing/scripts/make-parallel.sh2
-rw-r--r--raytracing/src/CMakeLists.txt9
-rw-r--r--raytracing/src/color.cc32
-rw-r--r--raytracing/src/color.h28
-rw-r--r--raytracing/src/ray.cc16
-rw-r--r--raytracing/src/ray.h22
-rw-r--r--raytracing/src/vec3.cc76
-rw-r--r--raytracing/src/vec3.h102
-rw-r--r--rcc/CMakeLists.txt15
-rw-r--r--rcc/cmake/modules/AddRCC.cmake14
-rw-r--r--rcc/include/rcc/Config/config.h.cmake215
-rw-r--r--rcc/include/rcc/Config/rcc-config.h.cmake60
-rw-r--r--rcc/include/rcc/Support/Compiler.h791
-rw-r--r--rcc/include/rcc/Support/ErrorOr.h378
-rw-r--r--rcc/lib/CMakeLists.txt7
-rw-r--r--ricochet/.clang-format23
-rw-r--r--ricochet/.clang-tidy9
-rw-r--r--ricochet/.gitignore53
-rw-r--r--ricochet/CMakeLists.txt47
-rw-r--r--ricochet/COPYING674
-rw-r--r--ricochet/README.md163
-rw-r--r--ricochet/etc/build_type.cmake11
-rw-r--r--ricochet/etc/cflags.cmake13
-rw-r--r--ricochet/etc/cmake_uninstall.cmake.in21
-rw-r--r--ricochet/etc/scanners.cmake27
-rw-r--r--ricochet/etc/tests.cmake19
-rw-r--r--ricochet/include/.gitkeep0
-rw-r--r--ricochet/include/ricochet/core/browser.hh40
-rw-r--r--ricochet/include/ricochet/core/form_handler.hh27
-rw-r--r--ricochet/include/ricochet/core/license.hh29
-rw-r--r--ricochet/include/ricochet/core/local_data.hh32
-rw-r--r--ricochet/include/ricochet/core/page_loader.hh32
-rw-r--r--ricochet/include/ricochet/core/page_processor.hh32
-rw-r--r--ricochet/include/ricochet/core/types.hh50
-rw-r--r--ricochet/include/ricochet/core/utils.hh30
-rw-r--r--ricochet/include/ricochet/net/config.hh35
-rw-r--r--ricochet/include/ricochet/net/cookie_jar.hh35
-rw-r--r--ricochet/include/ricochet/net/http_client.hh63
-rw-r--r--ricochet/include/ricochet/net/http_utils.hh30
-rw-r--r--ricochet/include/ricochet/net/ssl_socket.hh93
-rw-r--r--ricochet/include/ricochet/net/sys/address.hh88
-rw-r--r--ricochet/include/ricochet/net/sys/exception.hh80
-rw-r--r--ricochet/include/ricochet/net/sys/file_descriptor.hh141
-rw-r--r--ricochet/include/ricochet/net/sys/socket.hh160
-rw-r--r--ricochet/include/ricochet/net/sys/string_view_range.hh18
-rw-r--r--ricochet/include/ricochet/net/url_parser.hh37
-rw-r--r--ricochet/include/ricochet/parser/dom_tree.hh36
-rw-r--r--ricochet/include/ricochet/parser/html_lexer.hh64
-rw-r--r--ricochet/include/ricochet/parser/tree_builder.hh45
-rw-r--r--ricochet/include/ricochet/render/input_renderer.hh34
-rw-r--r--ricochet/include/ricochet/render/internal.hh35
-rw-r--r--ricochet/include/ricochet/render/renderer.hh60
-rw-r--r--ricochet/include/ricochet/render/table_renderer.hh30
-rw-r--r--ricochet/include/ricochet/render/utils.hh33
-rw-r--r--ricochet/include/ricochet/tui/document.hh255
-rw-r--r--ricochet/include/ricochet/tui/identity.hh63
-rw-r--r--ricochet/include/ricochet/tui/input.hh119
-rw-r--r--ricochet/include/ricochet/tui/messages.hh268
-rw-r--r--ricochet/include/ricochet/tui/position.hh116
-rw-r--r--ricochet/include/ricochet/tui/session.hh110
-rw-r--r--ricochet/include/ricochet/tui/view.hh74
-rw-r--r--ricochet/scripts/add_license.py57
-rw-r--r--ricochet/scripts/make-parallel.sh2
-rw-r--r--ricochet/src/CMakeLists.txt25
-rw-r--r--ricochet/src/core/browser.cc66
-rw-r--r--ricochet/src/core/form_handler.cc64
-rw-r--r--ricochet/src/core/license.cc69
-rw-r--r--ricochet/src/core/local_data.cc171
-rw-r--r--ricochet/src/core/page_loader.cc78
-rw-r--r--ricochet/src/core/page_processor.cc97
-rw-r--r--ricochet/src/core/utils.cc85
-rw-r--r--ricochet/src/main.cc73
-rw-r--r--ricochet/src/net/config.cc64
-rw-r--r--ricochet/src/net/cookie_jar.cc124
-rw-r--r--ricochet/src/net/http_client.cc212
-rw-r--r--ricochet/src/net/http_utils.cc93
-rw-r--r--ricochet/src/net/ssl_socket.cc118
-rw-r--r--ricochet/src/net/sys/address.cc207
-rw-r--r--ricochet/src/net/sys/file_descriptor.cc213
-rw-r--r--ricochet/src/net/sys/socket.cc286
-rw-r--r--ricochet/src/net/url_parser.cc87
-rw-r--r--ricochet/src/parser/html_lexer.cc119
-rw-r--r--ricochet/src/parser/tree_builder.cc85
-rw-r--r--ricochet/src/render/input_renderer.cc82
-rw-r--r--ricochet/src/render/renderer.cc123
-rw-r--r--ricochet/src/render/table_renderer.cc147
-rw-r--r--ricochet/src/render/utils.cc135
-rw-r--r--ricochet/src/tui/CMakeLists.txt10
-rw-r--r--ricochet/src/tui/document.cc30
-rw-r--r--ricochet/src/tui/document_text.cc231
-rw-r--r--ricochet/src/tui/document_text.hh66
-rw-r--r--ricochet/src/tui/document_view.cc399
-rw-r--r--ricochet/src/tui/document_view.hh117
-rw-r--r--ricochet/src/tui/editor.cc629
-rw-r--r--ricochet/src/tui/editor.hh169
-rw-r--r--ricochet/src/tui/layout.cc939
-rw-r--r--ricochet/src/tui/layout.hh164
-rw-r--r--ricochet/src/tui/mailbox.cc93
-rw-r--r--ricochet/src/tui/mailbox.hh50
-rw-r--r--ricochet/src/tui/paint.hh48
-rw-r--r--ricochet/src/tui/screen.cc601
-rw-r--r--ricochet/src/tui/screen.hh60
-rw-r--r--ricochet/src/tui/session.cc1064
-rw-r--r--ricochet/src/tui/text.cc163
-rw-r--r--ricochet/src/tui/text.hh80
-rw-r--r--ricochet/tests/CMakeLists.txt7
209 files changed, 27761 insertions, 1473 deletions
diff --git a/libbase/CMakeLists.txt b/libbase/CMakeLists.txt
new file mode 100644
index 0000000..461c02f
--- /dev/null
+++ b/libbase/CMakeLists.txt
@@ -0,0 +1,68 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+include(CheckSymbolExists)
+include(GNUInstallDirs)
+
+if(NOT DEFINED RCC_ENABLE_THREADS)
+ set(RCC_ENABLE_THREADS ON)
+endif()
+
+if(NOT DEFINED LIBBASE_ENABLE_THREADS)
+ set(LIBBASE_ENABLE_THREADS "${RCC_ENABLE_THREADS}")
+endif()
+if(NOT DEFINED LIBBASE_BUILD_SHARED_LIBS)
+ set(LIBBASE_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}")
+endif()
+if(NOT DEFINED LIBBASE_BUILD_STATIC)
+ if(BUILD_SHARED_LIBS)
+ set(LIBBASE_BUILD_STATIC OFF)
+ else()
+ set(LIBBASE_BUILD_STATIC ON)
+ endif()
+endif()
+if(NOT DEFINED LIBBASE_BUILD_LIBBASE_DYLIB)
+ set(LIBBASE_BUILD_LIBBASE_DYLIB OFF)
+endif()
+
+set(LIBBASE_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include")
+set(LIBBASE_BINARY_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/include")
+file(MAKE_DIRECTORY "${LIBBASE_BINARY_INCLUDE_DIR}/core/config")
+
+check_symbol_exists(strerror_r "string.h" HAVE_STRERROR_R)
+check_symbol_exists(strerror_s "string.h" HAVE_DECL_STRERROR_S)
+
+configure_file(
+ "${LIBBASE_INCLUDE_DIR}/core/config/config.h.cmake"
+ "${LIBBASE_BINARY_INCLUDE_DIR}/core/config/config.h")
+configure_file(
+ "${LIBBASE_INCLUDE_DIR}/core/config/libbase-config.h.cmake"
+ "${LIBBASE_BINARY_INCLUDE_DIR}/core/config/libbase-config.h")
+
+add_library(RCCCore
+ src/core/adt/SmallVector.cpp
+ src/core/math/MathExtras.cc
+ src/core/memory/MemAlloc.cpp
+ src/core/error/Errno.cc
+)
+
+target_compile_features(RCCCore PUBLIC cxx_std_23)
+target_compile_definitions(RCCCore PRIVATE LIBBASE_EXPORTS)
+
+target_include_directories(RCCCore
+ PUBLIC
+ "$<BUILD_INTERFACE:${LIBBASE_BINARY_INCLUDE_DIR}>"
+ "$<BUILD_INTERFACE:${LIBBASE_INCLUDE_DIR}>"
+ "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
+
+install(TARGETS RCCCore
+ EXPORT RCCTargets
+ ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+
+install(DIRECTORY include/core DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+ FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" PATTERN "*.def")
+install(DIRECTORY "${LIBBASE_BINARY_INCLUDE_DIR}/core"
+ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+ FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" PATTERN "*.def"
+ PATTERN "config.h" EXCLUDE)
diff --git a/libbase/include/core/adt/PointerIntPair.h b/libbase/include/core/adt/PointerIntPair.h
new file mode 100644
index 0000000..6a7f5db
--- /dev/null
+++ b/libbase/include/core/adt/PointerIntPair.h
@@ -0,0 +1,301 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/PointerIntPair.h - Pair for pointer and int -----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines the PointerIntPair class.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_POINTERINTPAIR_H
+#define LIBBASE_ADT_POINTERINTPAIR_H
+
+#include "core/compiler/Compiler.h"
+#include "core/adt/PointerLikeTypeTraits.h"
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include <type_traits>
+
+namespace libbase {
+
+namespace detail {
+template <typename Ptr> struct PunnedPointer {
+ static_assert(sizeof(Ptr) == sizeof(intptr_t), "");
+
+ // Asserts that allow us to let the compiler implement the destructor and
+ // copy/move constructors
+ static_assert(std::is_trivially_destructible<Ptr>::value, "");
+ static_assert(std::is_trivially_copy_constructible<Ptr>::value, "");
+ static_assert(std::is_trivially_move_constructible<Ptr>::value, "");
+
+ explicit constexpr PunnedPointer(intptr_t i = 0) { *this = i; }
+
+ constexpr intptr_t asInt() const {
+ intptr_t R = 0;
+ std::memcpy(&R, Data, sizeof(R));
+ return R;
+ }
+
+ constexpr operator intptr_t() const { return asInt(); }
+
+ constexpr PunnedPointer &operator=(intptr_t V) {
+ std::memcpy(Data, &V, sizeof(Data));
+ return *this;
+ }
+
+ Ptr *getPointerAddress() { return reinterpret_cast<Ptr *>(Data); }
+ const Ptr *getPointerAddress() const { return reinterpret_cast<Ptr *>(Data); }
+
+private:
+ alignas(Ptr) unsigned char Data[sizeof(Ptr)];
+};
+} // namespace detail
+
+template <typename T, typename Enable> struct DenseMapInfo;
+template <typename PointerT, unsigned IntBits, typename PtrTraits>
+struct PointerIntPairInfo;
+
+/// PointerIntPair - This class implements a pair of a pointer and small
+/// integer. It is designed to represent this in the space required by one
+/// pointer by bitmangling the integer into the low part of the pointer. This
+/// can only be done for small integers: typically up to 3 bits, but it depends
+/// on the number of bits available according to PointerLikeTypeTraits for the
+/// type.
+///
+/// Note that PointerIntPair always puts the IntVal part in the highest bits
+/// possible. For example, PointerIntPair<void*, 1, bool> will put the bit for
+/// the bool into bit #2, not bit #0, which allows the low two bits to be used
+/// for something else. For example, this allows:
+/// PointerIntPair<PointerIntPair<void*, 1, bool>, 1, bool>
+/// ... and the two bools will land in different bits.
+template <typename PointerTy, unsigned IntBits, typename IntType = unsigned,
+ typename PtrTraits = PointerLikeTypeTraits<PointerTy>,
+ typename Info = PointerIntPairInfo<PointerTy, IntBits, PtrTraits>>
+class PointerIntPair {
+ // Used by MSVC visualizer and generally helpful for debugging/visualizing.
+ using InfoTy = Info;
+ detail::PunnedPointer<PointerTy> Value;
+
+public:
+ constexpr PointerIntPair() = default;
+
+ PointerIntPair(PointerTy PtrVal, IntType IntVal) {
+ setPointerAndInt(PtrVal, IntVal);
+ }
+
+ explicit PointerIntPair(PointerTy PtrVal) { initWithPointer(PtrVal); }
+
+ PointerTy getPointer() const { return Info::getPointer(Value); }
+
+ IntType getInt() const { return (IntType)Info::getInt(Value); }
+
+ void setPointer(PointerTy PtrVal) & {
+ Value = Info::updatePointer(Value, PtrVal);
+ }
+
+ void setInt(IntType IntVal) & {
+ Value = Info::updateInt(Value, static_cast<intptr_t>(IntVal));
+ }
+
+ void initWithPointer(PointerTy PtrVal) & {
+ Value = Info::updatePointer(0, PtrVal);
+ }
+
+ void setPointerAndInt(PointerTy PtrVal, IntType IntVal) & {
+ Value = Info::updateInt(Info::updatePointer(0, PtrVal),
+ static_cast<intptr_t>(IntVal));
+ }
+
+ PointerTy const *getAddrOfPointer() const {
+ return const_cast<PointerIntPair *>(this)->getAddrOfPointer();
+ }
+
+ PointerTy *getAddrOfPointer() {
+ assert(Value == reinterpret_cast<intptr_t>(getPointer()) &&
+ "Can only return the address if IntBits is cleared and "
+ "PtrTraits doesn't change the pointer");
+ return Value.getPointerAddress();
+ }
+
+ void *getOpaqueValue() const {
+ return reinterpret_cast<void *>(Value.asInt());
+ }
+
+ void setFromOpaqueValue(void *Val) & {
+ Value = reinterpret_cast<intptr_t>(Val);
+ }
+
+ static PointerIntPair getFromOpaqueValue(void *V) {
+ PointerIntPair P;
+ P.setFromOpaqueValue(V);
+ return P;
+ }
+
+ // Allow PointerIntPairs to be created from const void * if and only if the
+ // pointer type could be created from a const void *.
+ static PointerIntPair getFromOpaqueValue(const void *V) {
+ (void)PtrTraits::getFromVoidPointer(V);
+ return getFromOpaqueValue(const_cast<void *>(V));
+ }
+
+ bool operator==(const PointerIntPair &RHS) const {
+ return Value == RHS.Value;
+ }
+
+ bool operator!=(const PointerIntPair &RHS) const {
+ return Value != RHS.Value;
+ }
+
+ bool operator<(const PointerIntPair &RHS) const { return Value < RHS.Value; }
+ bool operator>(const PointerIntPair &RHS) const { return Value > RHS.Value; }
+
+ bool operator<=(const PointerIntPair &RHS) const {
+ return Value <= RHS.Value;
+ }
+
+ bool operator>=(const PointerIntPair &RHS) const {
+ return Value >= RHS.Value;
+ }
+};
+
+template <typename PointerT, unsigned IntBits, typename PtrTraits>
+struct PointerIntPairInfo {
+ static_assert(PtrTraits::NumLowBitsAvailable <
+ std::numeric_limits<uintptr_t>::digits,
+ "cannot use a pointer type that has all bits free");
+ static_assert(IntBits <= PtrTraits::NumLowBitsAvailable,
+ "PointerIntPair with integer size too large for pointer");
+ enum MaskAndShiftConstants : uintptr_t {
+ /// PointerBitMask - The bits that come from the pointer.
+ PointerBitMask = (~(uintptr_t)0) << PtrTraits::NumLowBitsAvailable,
+
+ /// IntShift - The number of low bits that we reserve for other uses, and
+ /// keep zero.
+ IntShift = (uintptr_t)PtrTraits::NumLowBitsAvailable - IntBits,
+
+ /// IntMask - This is the unshifted mask for valid bits of the int type.
+ IntMask = ((uintptr_t)1 << IntBits) - 1,
+
+ // ShiftedIntMask - This is the bits for the integer shifted in place.
+ ShiftedIntMask = (uintptr_t)(IntMask << IntShift)
+ };
+
+ static PointerT getPointer(intptr_t Value) {
+ return PtrTraits::getFromVoidPointer(
+ reinterpret_cast<void *>(Value & PointerBitMask));
+ }
+
+ static intptr_t getInt(intptr_t Value) {
+ return (Value >> IntShift) & IntMask;
+ }
+
+ static intptr_t updatePointer(intptr_t OrigValue, PointerT Ptr) {
+ intptr_t PtrWord =
+ reinterpret_cast<intptr_t>(PtrTraits::getAsVoidPointer(Ptr));
+ assert((PtrWord & ~PointerBitMask) == 0 &&
+ "Pointer is not sufficiently aligned");
+ // Preserve all low bits, just update the pointer.
+ return PtrWord | (OrigValue & ~PointerBitMask);
+ }
+
+ static intptr_t updateInt(intptr_t OrigValue, intptr_t Int) {
+ assert((Int & ~IntMask) == 0 && "Integer too large for field");
+
+ // Preserve all bits other than the ones we are updating.
+ return (OrigValue & ~ShiftedIntMask) | Int << IntShift;
+ }
+};
+
+// Provide specialization of DenseMapInfo for PointerIntPair.
+template <typename PointerTy, unsigned IntBits, typename IntType>
+struct DenseMapInfo<PointerIntPair<PointerTy, IntBits, IntType>, void> {
+ using Ty = PointerIntPair<PointerTy, IntBits, IntType>;
+
+ static unsigned getHashValue(Ty V) {
+ uintptr_t IV = reinterpret_cast<uintptr_t>(V.getOpaqueValue());
+ return unsigned(IV) ^ unsigned(IV >> 9);
+ }
+
+ static bool isEqual(const Ty &LHS, const Ty &RHS) { return LHS == RHS; }
+};
+
+// Teach SmallPtrSet that PointerIntPair is "basically a pointer".
+template <typename PointerTy, unsigned IntBits, typename IntType,
+ typename PtrTraits>
+struct PointerLikeTypeTraits<
+ PointerIntPair<PointerTy, IntBits, IntType, PtrTraits>> {
+ static inline void *
+ getAsVoidPointer(const PointerIntPair<PointerTy, IntBits, IntType> &P) {
+ return P.getOpaqueValue();
+ }
+
+ static inline PointerIntPair<PointerTy, IntBits, IntType>
+ getFromVoidPointer(void *P) {
+ return PointerIntPair<PointerTy, IntBits, IntType>::getFromOpaqueValue(P);
+ }
+
+ static inline PointerIntPair<PointerTy, IntBits, IntType>
+ getFromVoidPointer(const void *P) {
+ return PointerIntPair<PointerTy, IntBits, IntType>::getFromOpaqueValue(P);
+ }
+
+ static constexpr int NumLowBitsAvailable =
+ PtrTraits::NumLowBitsAvailable - IntBits;
+};
+
+// Allow structured bindings on PointerIntPair.
+template <std::size_t I, typename PointerTy, unsigned IntBits, typename IntType,
+ typename PtrTraits, typename Info>
+decltype(auto)
+get(const PointerIntPair<PointerTy, IntBits, IntType, PtrTraits, Info> &Pair) {
+ static_assert(I < 2);
+ if constexpr (I == 0)
+ return Pair.getPointer();
+ else
+ return Pair.getInt();
+}
+
+} // end namespace libbase
+
+namespace std {
+template <typename PointerTy, unsigned IntBits, typename IntType,
+ typename PtrTraits, typename Info>
+struct tuple_size<
+ libbase::PointerIntPair<PointerTy, IntBits, IntType, PtrTraits, Info>>
+ : std::integral_constant<std::size_t, 2> {};
+
+template <std::size_t I, typename PointerTy, unsigned IntBits, typename IntType,
+ typename PtrTraits, typename Info>
+struct tuple_element<
+ I, libbase::PointerIntPair<PointerTy, IntBits, IntType, PtrTraits, Info>>
+ : std::conditional<I == 0, PointerTy, IntType> {};
+} // namespace std
+
+#endif // LIBBASE_ADT_POINTERINTPAIR_H
diff --git a/libbase/include/core/adt/PointerLikeTypeTraits.h b/libbase/include/core/adt/PointerLikeTypeTraits.h
new file mode 100644
index 0000000..9c1a36f
--- /dev/null
+++ b/libbase/include/core/adt/PointerLikeTypeTraits.h
@@ -0,0 +1,165 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/PointerLikeTypeTraits.h - Pointer Traits ----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines the PointerLikeTypeTraits class. This allows data
+// structures to reason about pointers and other things that are pointer sized.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_POINTERLIKETYPETRAITS_H
+#define LIBBASE_ADT_POINTERLIKETYPETRAITS_H
+
+#include "core/compiler/DataTypes.h"
+#include "core/math/MathExtras.h"
+#include <cassert>
+
+namespace libbase {
+
+/// A traits type that is used to handle pointer types and things that are just
+/// wrappers for pointers as a uniform entity.
+template <typename T> struct PointerLikeTypeTraits;
+
+namespace detail {
+// Provide a trait to check if T is pointer-like.
+template <typename T, typename U = void> struct HasPointerLikeTypeTraits {
+ static const bool value = false;
+};
+
+// sizeof(T) is valid only for a complete T.
+template <typename T>
+struct HasPointerLikeTypeTraits<
+ T, decltype((sizeof(PointerLikeTypeTraits<T>) + sizeof(T)), void())> {
+ static const bool value = true;
+};
+
+template <typename T> struct IsPointerLike {
+ static const bool value = HasPointerLikeTypeTraits<T>::value;
+};
+
+template <typename T> struct IsPointerLike<T *> {
+ static const bool value = true;
+};
+} // namespace detail
+
+// Provide PointerLikeTypeTraits for non-cvr pointers.
+template <typename T> struct PointerLikeTypeTraits<T *> {
+ static inline void *getAsVoidPointer(T *P) { return P; }
+ static inline T *getFromVoidPointer(void *P) { return static_cast<T *>(P); }
+
+ static constexpr int NumLowBitsAvailable = ConstantLog2<alignof(T)>();
+};
+
+template <> struct PointerLikeTypeTraits<void *> {
+ static inline void *getAsVoidPointer(void *P) { return P; }
+ static inline void *getFromVoidPointer(void *P) { return P; }
+
+ /// Note, we assume here that void* is related to raw malloc'ed memory and
+ /// that malloc returns objects at least 4-byte aligned. However, this may be
+ /// wrong, or pointers may be from something other than malloc. In this case,
+ /// you should specify a real typed pointer or avoid this template.
+ ///
+ /// All clients should use assertions to do a run-time check to ensure that
+ /// this is actually true.
+ static constexpr int NumLowBitsAvailable = 2;
+};
+
+// Provide PointerLikeTypeTraits for const things.
+template <typename T> struct PointerLikeTypeTraits<const T> {
+ using NonConst = PointerLikeTypeTraits<T>;
+
+ static inline const void *getAsVoidPointer(const T P) {
+ return NonConst::getAsVoidPointer(P);
+ }
+ static inline const T getFromVoidPointer(const void *P) {
+ return NonConst::getFromVoidPointer(const_cast<void *>(P));
+ }
+ static constexpr int NumLowBitsAvailable = NonConst::NumLowBitsAvailable;
+};
+
+// Provide PointerLikeTypeTraits for const pointers.
+template <typename T> struct PointerLikeTypeTraits<const T *> {
+ using NonConst = PointerLikeTypeTraits<T *>;
+
+ static inline const void *getAsVoidPointer(const T *P) {
+ return NonConst::getAsVoidPointer(const_cast<T *>(P));
+ }
+ static inline const T *getFromVoidPointer(const void *P) {
+ return NonConst::getFromVoidPointer(const_cast<void *>(P));
+ }
+ static constexpr int NumLowBitsAvailable = NonConst::NumLowBitsAvailable;
+};
+
+// Provide PointerLikeTypeTraits for uintptr_t.
+template <> struct PointerLikeTypeTraits<uintptr_t> {
+ static inline void *getAsVoidPointer(uintptr_t P) {
+ return reinterpret_cast<void *>(P);
+ }
+ static inline uintptr_t getFromVoidPointer(void *P) {
+ return reinterpret_cast<uintptr_t>(P);
+ }
+ // No bits are available!
+ static constexpr int NumLowBitsAvailable = 0;
+};
+
+/// Provide suitable custom traits struct for function pointers.
+///
+/// Function pointers can't be directly given these traits as functions can't
+/// have their alignment computed with `alignof` and we need different casting.
+///
+/// To rely on higher alignment for a specialized use, you can provide a
+/// customized form of this template explicitly with higher alignment, and
+/// potentially use alignment attributes on functions to satisfy that.
+template <int Alignment, typename FunctionPointerT>
+struct FunctionPointerLikeTypeTraits {
+ static constexpr int NumLowBitsAvailable = ConstantLog2<Alignment>();
+ static inline void *getAsVoidPointer(FunctionPointerT P) {
+ assert((reinterpret_cast<uintptr_t>(P) &
+ ~((uintptr_t)-1 << NumLowBitsAvailable)) == 0 &&
+ "Alignment not satisfied for an actual function pointer!");
+ return reinterpret_cast<void *>(P);
+ }
+ static inline FunctionPointerT getFromVoidPointer(void *P) {
+ return reinterpret_cast<FunctionPointerT>(P);
+ }
+};
+
+/// Provide a default specialization for function pointers that assumes 4-byte
+/// alignment.
+///
+/// We assume here that functions used with this are always at least 4-byte
+/// aligned. This means that, for example, thumb functions won't work or systems
+/// with weird unaligned function pointers won't work. But all practical systems
+/// we support satisfy this requirement.
+template <typename ReturnT, typename... ParamTs>
+struct PointerLikeTypeTraits<ReturnT (*)(ParamTs...)>
+ : FunctionPointerLikeTypeTraits<4, ReturnT (*)(ParamTs...)> {};
+
+} // end namespace libbase
+
+#endif
diff --git a/rcc/include/rcc/ADT/STLFunctionalExtras.h b/libbase/include/core/adt/STLFunctionalExtras.h
index 71a5b85..8c88905 100644
--- a/rcc/include/rcc/ADT/STLFunctionalExtras.h
+++ b/libbase/include/core/adt/STLFunctionalExtras.h
@@ -1,5 +1,5 @@
/*
- R *CC-Project
+ RCC-Project
Copyright (C) 2026 Richard Qin
@@ -33,17 +33,17 @@
//
//===----------------------------------------------------------------------===//
-#ifndef RCC_ADT_STLFUNCTIONALEXTRAS_H
-#define RCC_ADT_STLFUNCTIONALEXTRAS_H
+#ifndef LIBBASE_ADT_STLFUNCTIONALEXTRAS_H
+#define LIBBASE_ADT_STLFUNCTIONALEXTRAS_H
-#include "rcc/Support/Compiler.h"
+#include "core/compiler/Compiler.h"
-#include <cstddef
+#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <utility>
-namespace rcc
+namespace libbase
{
//===----------------------------------------------------------------------===//
@@ -59,7 +59,7 @@ namespace rcc
template <typename Fn> class function_ref;
template <typename Ret, typename... Params>
-class RCC_GSL_POINTER function_ref<Ret (Params...)>
+class LIBBASE_GSL_POINTER function_ref<Ret (Params...)>
{
Ret (*callback) (intptr_t callable, Params... params) = nullptr;
intptr_t callable = 0;
@@ -80,7 +80,7 @@ public:
template <typename Callable>
function_ref (
- Callable &&callable RCC_LIFETIME_BOUND,
+ Callable &&callable LIBBASE_LIFETIME_BOUND,
// This is not the copy-constructor.
std::enable_if_t<!std::is_same<std::remove_cvref_t<Callable>,
function_ref>::value> * = nullptr,
@@ -107,6 +107,6 @@ public:
}
};
-} // end namespace rcc
+} // end namespace libbase
-#endif // RCC_ADT_STLFUNCTIONALEXTRAS_H
+#endif // LIBBASE_ADT_STLFUNCTIONALEXTRAS_H
diff --git a/libbase/include/core/adt/SmallVector.h b/libbase/include/core/adt/SmallVector.h
new file mode 100644
index 0000000..37d974a
--- /dev/null
+++ b/libbase/include/core/adt/SmallVector.h
@@ -0,0 +1,1926 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- rcc/ADT/SmallVector.h - 'Normally small' vectors --------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines the SmallVector class.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_SMALLVECTOR_H
+#define LIBBASE_ADT_SMALLVECTOR_H
+
+#include "core/compiler/Compiler.h"
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <initializer_list>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <new>
+#include <ranges>
+#include <type_traits>
+#include <utility>
+
+namespace libbase
+{
+
+template <typename T> class ArrayRef;
+
+template <typename IteratorT> class iterator_range;
+
+template <class Iterator, class Tag>
+using HasIteratorTag = std::is_convertible<
+ typename std::iterator_traits<Iterator>::iterator_category, Tag>;
+
+template <class Iterator>
+using EnableIfConvertibleToInputIterator =
+ std::enable_if_t<HasIteratorTag<Iterator, std::input_iterator_tag>::value>;
+
+/// This is all the stuff common to all SmallVectors.
+///
+/// The template parameter specifies the type which should be used to hold the
+/// Size and Capacity of the SmallVector, so it can be adjusted.
+/// Using 32 bit size is desirable to shrink the size of the SmallVector.
+/// Using 64 bit size is desirable for cases like SmallVector<char>, where a
+/// 32 bit size would limit the vector to ~4GB. SmallVectors are used for
+/// buffering bitcode output - which can exceed 4GB.
+template <class Size_T> class SmallVectorBase
+{
+protected:
+ void *BeginX;
+ Size_T Size = 0, Capacity;
+
+ /// The maximum value of the Size_T used.
+ static constexpr size_t
+ SizeTypeMax ()
+ {
+ return std::numeric_limits<Size_T>::max ();
+ }
+
+ SmallVectorBase () = delete;
+ SmallVectorBase (void *FirstEl, size_t TotalCapacity)
+ : BeginX (FirstEl), Capacity (static_cast<Size_T> (TotalCapacity))
+ {
+ }
+
+ /// This is a helper for \a grow() that's out of line to reduce code
+ /// duplication. This function will report a fatal error if it can't grow at
+ /// least to \p MinSize.
+ LIBBASE_ABI void *
+ mallocForGrow (void *FirstEl, size_t MinSize, size_t TSize,
+ size_t &NewCapacity);
+
+ /// This is an implementation of the grow() method which only works
+ /// on POD-like data types and is out of line to reduce code duplication.
+ /// This function will report a fatal error if it cannot increase capacity.
+ LIBBASE_ABI void
+ grow_pod (void *FirstEl, size_t MinSize, size_t TSize);
+
+public:
+ size_t
+ size () const
+ {
+ return Size;
+ }
+ size_t
+ capacity () const
+ {
+ return Capacity;
+ }
+
+ [[nodiscard]] bool
+ empty () const
+ {
+ return !Size;
+ }
+
+protected:
+ /// Set the array size to \p N, which the current array must have enough
+ /// capacity for.
+ ///
+ /// This does not construct or destroy any elements in the vector.
+ void
+ set_size (size_t N)
+ {
+ assert (N <= capacity ()); // implies no overflow in assignment
+ Size = static_cast<Size_T> (N);
+ }
+
+ /// Set the array data pointer to \p Begin and capacity to \p N.
+ ///
+ /// This does not construct or destroy any elements in the vector.
+ // This does not clean up any existing allocation.
+ void
+ set_allocation_range (void *Begin, size_t N)
+ {
+ assert (N <= SizeTypeMax ());
+ BeginX = Begin;
+ Capacity = static_cast<Size_T> (N);
+ }
+};
+
+template <class T>
+using SmallVectorSizeType =
+ std::conditional_t<sizeof (T) < 4 && sizeof (void *) >= 8, uint64_t,
+ uint32_t>;
+
+/// Figure out the offset of the first element.
+template <class T, typename = void> struct SmallVectorAlignmentAndSize
+{
+ alignas (SmallVectorBase<SmallVectorSizeType<T>>) char Base[sizeof (
+ SmallVectorBase<SmallVectorSizeType<T>>)];
+ alignas (T) char FirstEl[sizeof (T)];
+};
+
+/// This is the part of SmallVectorTemplateBase which does not depend on whether
+/// the type T is a POD. The extra dummy template argument is used by ArrayRef
+/// to avoid unnecessarily requiring T to be complete.
+template <typename T, typename = void>
+class SmallVectorTemplateCommon : public SmallVectorBase<SmallVectorSizeType<T>>
+{
+ using Base = SmallVectorBase<SmallVectorSizeType<T>>;
+
+protected:
+ /// Find the address of the first element. For this pointer math to be valid
+ /// with small-size of 0 for T with lots of alignment, it's important that
+ /// SmallVectorStorage is properly-aligned even for small-size of 0.
+ void *
+ getFirstEl () const
+ {
+ return const_cast<void *> (reinterpret_cast<const void *> (
+ reinterpret_cast<const char *> (this) +
+ offsetof (SmallVectorAlignmentAndSize<T>, FirstEl)));
+ }
+ // Space after 'FirstEl' is clobbered, do not add any instance vars after it.
+
+ SmallVectorTemplateCommon (size_t SizeArg) : Base (getFirstEl (), SizeArg)
+ {
+ }
+
+ void
+ grow_pod (size_t MinSize, size_t TSize)
+ {
+ Base::grow_pod (getFirstEl (), MinSize, TSize);
+ }
+
+ /// Return true if this is a smallvector which has not had dynamic
+ /// memory allocated for it.
+ bool
+ isSmall () const
+ {
+ return this->BeginX == getFirstEl ();
+ }
+
+ /// Put this vector in a state of being small.
+ void
+ resetToSmall ()
+ {
+ this->BeginX = getFirstEl ();
+ // The inline capacity is unknown here. SmallVector restores it when moving
+ // from the same concrete type; moves through SmallVectorImpl leave it at 0.
+ this->Size = this->Capacity = 0;
+ }
+
+ /// Return true if V is an internal reference to the given range.
+ bool
+ isReferenceToRange (const void *V, const void *First, const void *Last) const
+ {
+ // Use std::less to avoid UB.
+ std::less<> LessThan;
+ return !LessThan (V, First) && LessThan (V, Last);
+ }
+
+ /// Return true if V is an internal reference to this vector.
+ bool
+ isReferenceToStorage (const void *V) const
+ {
+ return isReferenceToRange (V, this->begin (), this->end ());
+ }
+
+ /// Return true if First and Last form a valid (possibly empty) range in this
+ /// vector's storage.
+ bool
+ isRangeInStorage (const void *First, const void *Last) const
+ {
+ // Use std::less to avoid UB.
+ std::less<> LessThan;
+ return !LessThan (First, this->begin ()) && !LessThan (Last, First) &&
+ !LessThan (this->end (), Last);
+ }
+
+ /// Return true unless Elt will be invalidated by resizing the vector to
+ /// NewSize.
+ bool
+ isSafeToReferenceAfterResize (const void *Elt, size_t NewSize)
+ {
+ // Past the end.
+ if (LIBBASE_LIKELY (!isReferenceToStorage (Elt)))
+ {
+ return true;
+ }
+
+ // Return false if Elt will be destroyed by shrinking.
+ if (NewSize <= this->size ())
+ {
+ return Elt < this->begin () + NewSize;
+ }
+
+ // Return false if we need to grow.
+ return NewSize <= this->capacity ();
+ }
+
+ /// Check whether Elt will be invalidated by resizing the vector to NewSize.
+ void
+ assertSafeToReferenceAfterResize (const void *Elt, size_t NewSize)
+ {
+ assert (isSafeToReferenceAfterResize (Elt, NewSize) &&
+ "Attempting to reference an element of the vector in an operation "
+ "that invalidates it");
+ }
+
+ /// Check whether Elt will be invalidated by increasing the size of the
+ /// vector by N.
+ void
+ assertSafeToAdd (const void *Elt, size_t N = 1)
+ {
+ this->assertSafeToReferenceAfterResize (Elt, this->size () + N);
+ }
+
+ /// Check whether any part of the range will be invalidated by clearing.
+ template <class ItTy>
+ void
+ assertSafeToReferenceAfterClear (ItTy From, ItTy To)
+ {
+ if constexpr (std::is_pointer_v<ItTy> &&
+ std::is_same_v<
+ std::remove_const_t<std::remove_pointer_t<ItTy>>,
+ std::remove_const_t<T>>)
+ {
+ if (From == To)
+ {
+ return;
+ }
+ this->assertSafeToReferenceAfterResize (From, 0);
+ this->assertSafeToReferenceAfterResize (To - 1, 0);
+ }
+ (void)From;
+ (void)To;
+ }
+
+ /// Check whether any part of the range will be invalidated by growing.
+ template <class ItTy>
+ void
+ assertSafeToAddRange (ItTy From, ItTy To)
+ {
+ if constexpr (std::is_pointer_v<ItTy> &&
+ std::is_same_v<std::remove_cv_t<std::remove_pointer_t<ItTy>>,
+ T>)
+ {
+ if (From == To)
+ {
+ return;
+ }
+ this->assertSafeToAdd (From, To - From);
+ this->assertSafeToAdd (To - 1, To - From);
+ }
+ (void)From;
+ (void)To;
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ template <class U>
+ static const T *
+ reserveForParamAndGetAddressImpl (U *This, const T &Elt, size_t N)
+ {
+ size_t NewSize = This->size () + N;
+ if (LIBBASE_LIKELY (NewSize <= This->capacity ()))
+ {
+ return &Elt;
+ }
+
+ bool ReferencesStorage = false;
+ int64_t Index = -1;
+ if (!U::TakesParamByValue)
+ {
+ if (LIBBASE_UNLIKELY (This->isReferenceToStorage (&Elt)))
+ {
+ ReferencesStorage = true;
+ Index = &Elt - This->begin ();
+ }
+ }
+ This->grow (NewSize);
+ return ReferencesStorage ? This->begin () + Index : &Elt;
+ }
+
+public:
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+ using value_type = T;
+ using iterator = T *;
+ using const_iterator = const T *;
+
+ using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+
+ using reference = T &;
+ using const_reference = const T &;
+ using pointer = T *;
+ using const_pointer = const T *;
+
+ using Base::capacity;
+ using Base::empty;
+ using Base::size;
+
+ // forward iterator creation methods.
+ iterator
+ begin ()
+ {
+ return (iterator)this->BeginX;
+ }
+ const_iterator
+ begin () const
+ {
+ return (const_iterator)this->BeginX;
+ }
+ iterator
+ end ()
+ {
+ return begin () + size ();
+ }
+ const_iterator
+ end () const
+ {
+ return begin () + size ();
+ }
+
+ // reverse iterator creation methods.
+ reverse_iterator
+ rbegin ()
+ {
+ return reverse_iterator (end ());
+ }
+ const_reverse_iterator
+ rbegin () const
+ {
+ return const_reverse_iterator (end ());
+ }
+ reverse_iterator
+ rend ()
+ {
+ return reverse_iterator (begin ());
+ }
+ const_reverse_iterator
+ rend () const
+ {
+ return const_reverse_iterator (begin ());
+ }
+
+ size_type
+ size_in_bytes () const
+ {
+ return size () * sizeof (T);
+ }
+ size_type
+ max_size () const
+ {
+ return std::min (this->SizeTypeMax (), size_type (-1) / sizeof (T));
+ }
+
+ size_t
+ capacity_in_bytes () const
+ {
+ return capacity () * sizeof (T);
+ }
+
+ /// Return a pointer to the vector's buffer, even if empty().
+ pointer
+ data ()
+ {
+ return pointer (begin ());
+ }
+ /// Return a pointer to the vector's buffer, even if empty().
+ const_pointer
+ data () const
+ {
+ return const_pointer (begin ());
+ }
+
+ reference
+ operator[] (size_type idx)
+ {
+ assert (idx < size ());
+ return begin ()[idx];
+ }
+ const_reference
+ operator[] (size_type idx) const
+ {
+ assert (idx < size ());
+ return begin ()[idx];
+ }
+
+ reference
+ front ()
+ {
+ assert (!empty ());
+ return begin ()[0];
+ }
+ const_reference
+ front () const
+ {
+ assert (!empty ());
+ return begin ()[0];
+ }
+
+ reference
+ back ()
+ {
+ assert (!empty ());
+ return end ()[-1];
+ }
+ const_reference
+ back () const
+ {
+ assert (!empty ());
+ return end ()[-1];
+ }
+};
+
+/// SmallVectorTemplateBase<TriviallyCopyable = false> - This is where we put
+/// method implementations that are designed to work with non-trivial T's.
+///
+/// We approximate is_trivially_copyable with trivial move/copy construction and
+/// trivial destruction. While the standard doesn't specify that you're allowed
+/// copy these types with memcpy, there is no way for the type to observe this.
+/// This catches the important case of std::pair<POD, POD>, which is not
+/// trivially assignable.
+template <typename T, bool = (std::is_trivially_copy_constructible<T>::value) &&
+ (std::is_trivially_move_constructible<T>::value) &&
+ std::is_trivially_destructible<T>::value>
+class SmallVectorTemplateBase : public SmallVectorTemplateCommon<T>
+{
+ friend class SmallVectorTemplateCommon<T>;
+
+protected:
+ static constexpr bool TakesParamByValue = false;
+ using ValueParamT = const T &;
+
+ SmallVectorTemplateBase (size_t SizeArg)
+ : SmallVectorTemplateCommon<T> (SizeArg)
+ {
+ }
+
+ static void
+ destroy_range (T *S, T *E)
+ {
+ while (S != E)
+ {
+ --E;
+ E->~T ();
+ }
+ }
+
+ /// Move the range [I, E) into the uninitialized memory starting with "Dest",
+ /// constructing elements as needed.
+ template <typename It1, typename It2>
+ static void
+ uninitialized_move (It1 I, It1 E, It2 Dest)
+ {
+ std::uninitialized_move (I, E, Dest);
+ }
+
+ /// Copy the range [I, E) onto the uninitialized memory starting with "Dest",
+ /// constructing elements as needed.
+ template <typename It1, typename It2>
+ static void
+ uninitialized_copy (It1 I, It1 E, It2 Dest)
+ {
+ std::uninitialized_copy (I, E, Dest);
+ }
+
+ /// Grow the allocated memory (without initializing new elements), doubling
+ /// the size of the allocated memory. Guarantees space for at least one more
+ /// element, or MinSize more elements if specified.
+ void
+ grow (size_t MinSize = 0);
+
+ /// Create a new allocation big enough for \p MinSize and pass back its size
+ /// in \p NewCapacity. This is the first section of \a grow().
+ T *
+ mallocForGrow (size_t MinSize, size_t &NewCapacity);
+
+ /// Move existing elements over to the new allocation \p NewElts, the middle
+ /// section of \a grow().
+ void
+ moveElementsForGrow (T *NewElts);
+
+ /// Transfer ownership of the allocation, finishing up \a grow().
+ void
+ takeAllocationForGrow (T *NewElts, size_t NewCapacity);
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ const T *
+ reserveForParamAndGetAddress (const T &Elt, size_t N = 1)
+ {
+ return this->reserveForParamAndGetAddressImpl (this, Elt, N);
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ T *
+ reserveForParamAndGetAddress (T &Elt, size_t N = 1)
+ {
+ return const_cast<T *> (
+ this->reserveForParamAndGetAddressImpl (this, Elt, N));
+ }
+
+ static T &&
+ forward_value_param (T &&V)
+ {
+ return std::move (V);
+ }
+ static const T &
+ forward_value_param (const T &V)
+ {
+ return V;
+ }
+
+ void
+ growAndAssign (size_t NumElts, const T &Elt)
+ {
+ // Grow manually in case Elt is an internal reference.
+ size_t NewCapacity;
+ T *NewElts = mallocForGrow (NumElts, NewCapacity);
+ std::uninitialized_fill_n (NewElts, NumElts, Elt);
+ this->destroy_range (this->begin (), this->end ());
+ takeAllocationForGrow (NewElts, NewCapacity);
+ this->set_size (NumElts);
+ }
+
+ template <typename... ArgTypes>
+ T &
+ growAndEmplaceBack (ArgTypes &&...Args)
+ {
+ // Grow manually in case one of Args is an internal reference.
+ size_t NewCapacity;
+ T *NewElts = mallocForGrow (0, NewCapacity);
+ ::new ((void *)(NewElts + this->size ()))
+ T (std::forward<ArgTypes> (Args)...);
+ moveElementsForGrow (NewElts);
+ takeAllocationForGrow (NewElts, NewCapacity);
+ this->set_size (this->size () + 1);
+ return this->back ();
+ }
+
+public:
+ void
+ push_back (const T &Elt)
+ {
+ const T *EltPtr = reserveForParamAndGetAddress (Elt);
+ ::new ((void *)this->end ()) T (*EltPtr);
+ this->set_size (this->size () + 1);
+ }
+
+ void
+ push_back (T &&Elt)
+ {
+ T *EltPtr = reserveForParamAndGetAddress (Elt);
+ ::new ((void *)this->end ()) T (::std::move (*EltPtr));
+ this->set_size (this->size () + 1);
+ }
+
+ void
+ pop_back ()
+ {
+ this->set_size (this->size () - 1);
+ this->end ()->~T ();
+ }
+};
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void
+SmallVectorTemplateBase<T, TriviallyCopyable>::grow (size_t MinSize)
+{
+ size_t NewCapacity;
+ T *NewElts = mallocForGrow (MinSize, NewCapacity);
+ moveElementsForGrow (NewElts);
+ takeAllocationForGrow (NewElts, NewCapacity);
+}
+
+template <typename T, bool TriviallyCopyable>
+T *
+SmallVectorTemplateBase<T, TriviallyCopyable>::mallocForGrow (
+ size_t MinSize, size_t &NewCapacity)
+{
+ return static_cast<T *> (
+ SmallVectorBase<SmallVectorSizeType<T>>::mallocForGrow (
+ this->getFirstEl (), MinSize, sizeof (T), NewCapacity));
+}
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void
+SmallVectorTemplateBase<T, TriviallyCopyable>::moveElementsForGrow (T *NewElts)
+{
+ // Move the elements over.
+ this->uninitialized_move (this->begin (), this->end (), NewElts);
+
+ // Destroy the original elements.
+ destroy_range (this->begin (), this->end ());
+}
+
+// Define this out-of-line to dissuade the C++ compiler from inlining it.
+template <typename T, bool TriviallyCopyable>
+void
+SmallVectorTemplateBase<T, TriviallyCopyable>::takeAllocationForGrow (
+ T *NewElts, size_t NewCapacity)
+{
+ // If this wasn't grown from the inline copy, deallocate the old space.
+ if (!this->isSmall ())
+ {
+ free (this->begin ());
+ }
+
+ this->set_allocation_range (NewElts, NewCapacity);
+}
+
+/// SmallVectorTemplateBase<TriviallyCopyable = true> - This is where we put
+/// method implementations that are designed to work with trivially copyable
+/// T's. This allows using memcpy in place of copy/move construction and
+/// skipping destruction.
+template <typename T>
+class SmallVectorTemplateBase<T, true> : public SmallVectorTemplateCommon<T>
+{
+ friend class SmallVectorTemplateCommon<T>;
+
+protected:
+ /// True if it's cheap enough to take parameters by value. Doing so avoids
+ /// overhead related to mitigations for reference invalidation.
+ static constexpr bool TakesParamByValue = sizeof (T) <= 2 * sizeof (void *);
+
+ /// Either const T& or T, depending on whether it's cheap enough to take
+ /// parameters by value.
+ using ValueParamT = std::conditional_t<TakesParamByValue, T, const T &>;
+
+ SmallVectorTemplateBase (size_t SizeArg)
+ : SmallVectorTemplateCommon<T> (SizeArg)
+ {
+ }
+
+ // No need to do a destroy loop for POD's.
+ static void
+ destroy_range (T *, T *)
+ {
+ }
+
+ /// Move the range [I, E) onto the uninitialized memory
+ /// starting with "Dest", constructing elements into it as needed.
+ template <typename It1, typename It2>
+ static void
+ uninitialized_move (It1 I, It1 E, It2 Dest)
+ {
+ // Just do a copy.
+ uninitialized_copy (I, E, Dest);
+ }
+
+ /// Copy the range [I, E) onto the uninitialized memory
+ /// starting with "Dest", constructing elements into it as needed.
+ template <typename It1, typename It2>
+ static void
+ uninitialized_copy (It1 I, It1 E, It2 Dest)
+ {
+ if constexpr (std::is_pointer_v<It1> && std::is_pointer_v<It2> &&
+ std::is_same_v<
+ std::remove_const_t<std::remove_pointer_t<It1>>,
+ std::remove_pointer_t<It2>>)
+ {
+ // Use memcpy for PODs iterated by pointers (which includes SmallVector
+ // iterators): std::uninitialized_copy optimizes to memmove, but we can
+ // use memcpy here. Note that I and E are iterators and thus might be
+ // invalid for memcpy if they are equal.
+ if (I != E)
+ {
+ std::memcpy (reinterpret_cast<void *> (Dest), I, (E - I) * sizeof (T));
+ }
+ }
+ else
+ {
+ // Arbitrary iterator types; just use the basic implementation.
+ std::uninitialized_copy (I, E, Dest);
+ }
+ }
+
+ /// Double the size of the allocated memory, guaranteeing space for at
+ /// least one more element or MinSize if specified.
+ void
+ grow (size_t MinSize = 0)
+ {
+ this->grow_pod (MinSize, sizeof (T));
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ const T *
+ reserveForParamAndGetAddress (const T &Elt, size_t N = 1)
+ {
+ return this->reserveForParamAndGetAddressImpl (this, Elt, N);
+ }
+
+ /// Reserve enough space to add one element, and return the updated element
+ /// pointer in case it was a reference to the storage.
+ T *
+ reserveForParamAndGetAddress (T &Elt, size_t N = 1)
+ {
+ return const_cast<T *> (
+ this->reserveForParamAndGetAddressImpl (this, Elt, N));
+ }
+
+ /// Copy \p V or return a reference, depending on \a ValueParamT.
+ static ValueParamT
+ forward_value_param (ValueParamT V)
+ {
+ return V;
+ }
+
+ void
+ growAndAssign (size_t NumElts, T Elt)
+ {
+ // Elt has been copied in case it's an internal reference, side-stepping
+ // reference invalidation problems without losing the realloc optimization.
+ this->set_size (0);
+ this->grow (NumElts);
+ std::uninitialized_fill_n (this->begin (), NumElts, Elt);
+ this->set_size (NumElts);
+ }
+
+ template <typename... ArgTypes>
+ T &
+ growAndEmplaceBack (ArgTypes &&...Args)
+ {
+ // Use push_back with a copy in case Args has an internal reference,
+ // side-stepping reference invalidation problems without losing the realloc
+ // optimization.
+ push_back (T (std::forward<ArgTypes> (Args)...));
+ return this->back ();
+ }
+
+ // Out-of-line slow path so the inline push_back needs no callee-saved
+ // registers or stack frame on its hot path.
+ LIBBASE_ATTRIBUTE_NOINLINE void
+ growAndPushBack (ValueParamT Elt)
+ {
+ // Copy in case Elt is an internal reference invalidated by grow.
+ T Tmp = Elt;
+ this->grow (this->size () + 1);
+ std::memcpy (reinterpret_cast<void *> (this->end ()), &Tmp, sizeof (T));
+ this->set_size (this->size () + 1);
+ }
+
+public:
+ void
+ push_back (ValueParamT Elt)
+ {
+ if (LIBBASE_UNLIKELY (this->size () >= this->capacity ()))
+ {
+ return growAndPushBack (Elt);
+ }
+ std::memcpy (reinterpret_cast<void *> (this->end ()), &Elt, sizeof (T));
+ this->set_size (this->size () + 1);
+ }
+
+ void
+ pop_back ()
+ {
+ this->set_size (this->size () - 1);
+ }
+};
+
+/// This class consists of common code factored out of the SmallVector class to
+/// reduce code duplication based on the SmallVector 'N' template parameter.
+template <typename T> class SmallVectorImpl : public SmallVectorTemplateBase<T>
+{
+ using SuperClass = SmallVectorTemplateBase<T>;
+
+public:
+ using iterator = typename SuperClass::iterator;
+ using const_iterator = typename SuperClass::const_iterator;
+ using reference = typename SuperClass::reference;
+ using size_type = typename SuperClass::size_type;
+
+protected:
+ using SmallVectorTemplateBase<T>::TakesParamByValue;
+ using ValueParamT = typename SuperClass::ValueParamT;
+
+ // Default ctor - Initialize to empty.
+ explicit SmallVectorImpl (unsigned N) : SmallVectorTemplateBase<T> (N)
+ {
+ }
+
+ void
+ assignRemote (SmallVectorImpl &&RHS)
+ {
+ this->destroy_range (this->begin (), this->end ());
+ if (!this->isSmall ())
+ {
+ free (this->begin ());
+ }
+ this->BeginX = RHS.BeginX;
+ this->Size = RHS.Size;
+ this->Capacity = RHS.Capacity;
+ RHS.resetToSmall ();
+ }
+
+ void
+ moveConstructFrom (SmallVectorImpl &&RHS)
+ {
+ assert (this->empty () && "move construction requires an empty vector");
+ if (!RHS.isSmall ())
+ {
+ assignRemote (std::move (RHS));
+ return;
+ }
+
+ // Construct inline elements directly instead of requiring T to be move
+ // assignable through the general move-assignment implementation.
+ size_type RHSSize = RHS.size ();
+ reserve (RHSSize);
+ this->uninitialized_move (RHS.begin (), RHS.end (), this->begin ());
+ this->set_size (RHSSize);
+ RHS.clear ();
+ }
+
+ ~SmallVectorImpl ()
+ {
+ // Subclass has already destructed this vector's elements.
+ // If this wasn't grown from the inline copy, deallocate the old space.
+ if (!this->isSmall ())
+ {
+ free (this->begin ());
+ }
+ }
+
+public:
+ SmallVectorImpl (const SmallVectorImpl &) = delete;
+
+ void
+ clear ()
+ {
+ this->destroy_range (this->begin (), this->end ());
+ this->Size = 0;
+ }
+
+private:
+ // Make set_size() private to avoid misuse in subclasses.
+ using SuperClass::set_size;
+
+ template <bool ForOverwrite>
+ void
+ resizeImpl (size_type N)
+ {
+ if (N == this->size ())
+ {
+ return;
+ }
+
+ if (N < this->size ())
+ {
+ this->truncate (N);
+ return;
+ }
+
+ this->reserve (N);
+ for (auto I = this->end (), E = this->begin () + N; I != E; ++I)
+ {
+ if (ForOverwrite)
+ {
+ new (&*I) T;
+ }
+ else
+ {
+ new (&*I) T ();
+ }
+ }
+ this->set_size (N);
+ }
+
+public:
+ void
+ resize (size_type N)
+ {
+ resizeImpl<false> (N);
+ }
+
+ /// Like resize, but \ref T is POD, the new values won't be initialized.
+ void
+ resize_for_overwrite (size_type N)
+ {
+ resizeImpl<true> (N);
+ }
+
+ /// Like resize, but requires that \p N is less than \a size().
+ void
+ truncate (size_type N)
+ {
+ assert (this->size () >= N && "Cannot increase size with truncate");
+ this->destroy_range (this->begin () + N, this->end ());
+ this->set_size (N);
+ }
+
+ void
+ resize (size_type N, ValueParamT NV)
+ {
+ if (N == this->size ())
+ {
+ return;
+ }
+
+ if (N < this->size ())
+ {
+ this->truncate (N);
+ return;
+ }
+
+ // N > this->size(). Defer to append.
+ this->append (N - this->size (), NV);
+ }
+
+ void
+ reserve (size_type N)
+ {
+ if (this->capacity () < N)
+ {
+ this->grow (N);
+ }
+ }
+
+ void
+ pop_back_n (size_type NumItems)
+ {
+ assert (this->size () >= NumItems);
+ truncate (this->size () - NumItems);
+ }
+
+ [[nodiscard]] T
+ pop_back_val ()
+ {
+ T Result = ::std::move (this->back ());
+ this->pop_back ();
+ return Result;
+ }
+
+ void
+ swap (SmallVectorImpl &RHS);
+
+ /// Add the specified range to the end of the SmallVector.
+ template <typename ItTy, typename = EnableIfConvertibleToInputIterator<ItTy>>
+ void
+ append (ItTy in_start, ItTy in_end)
+ {
+ if constexpr (HasIteratorTag<ItTy, std::forward_iterator_tag>::value)
+ {
+ this->assertSafeToAddRange (in_start, in_end);
+ size_type NumInputs = std::distance (in_start, in_end);
+ this->reserve (this->size () + NumInputs);
+ this->uninitialized_copy (in_start, in_end, this->end ());
+ this->set_size (this->size () + NumInputs);
+ }
+ else
+ {
+ // Input iterator, we can't know ahead how many elements we'll add.
+ for (; in_start != in_end; ++in_start)
+ {
+ this->emplace_back (*in_start);
+ }
+ }
+ }
+
+ /// Append \p NumInputs copies of \p Elt to the end.
+ void
+ append (size_type NumInputs, ValueParamT Elt)
+ {
+ const T *EltPtr = this->reserveForParamAndGetAddress (Elt, NumInputs);
+ std::uninitialized_fill_n (this->end (), NumInputs, *EltPtr);
+ this->set_size (this->size () + NumInputs);
+ }
+
+ void
+ append (std::initializer_list<T> IL)
+ {
+ append (IL.begin (), IL.end ());
+ }
+
+ void
+ append (const SmallVectorImpl &RHS)
+ {
+ append (RHS.begin (), RHS.end ());
+ }
+
+ void
+ assign (size_type NumElts, ValueParamT Elt)
+ {
+ // Note that Elt could be an internal reference.
+ if (NumElts > this->capacity ())
+ {
+ this->growAndAssign (NumElts, Elt);
+ return;
+ }
+
+ // Assign over existing elements.
+ std::fill_n (this->begin (), std::min (NumElts, this->size ()), Elt);
+ if (NumElts > this->size ())
+ {
+ std::uninitialized_fill_n (this->end (), NumElts - this->size (), Elt);
+ }
+ else if (NumElts < this->size ())
+ {
+ this->destroy_range (this->begin () + NumElts, this->end ());
+ }
+ this->set_size (NumElts);
+ }
+
+ // FIXME: Consider assigning over existing elements, rather than clearing &
+ // re-initializing them - for all assign(...) variants.
+
+ template <typename ItTy, typename = EnableIfConvertibleToInputIterator<ItTy>>
+ void
+ assign (ItTy in_start, ItTy in_end)
+ {
+ this->assertSafeToReferenceAfterClear (in_start, in_end);
+ clear ();
+ append (in_start, in_end);
+ }
+
+ void
+ assign (std::initializer_list<T> IL)
+ {
+ clear ();
+ append (IL);
+ }
+
+ void
+ assign (const SmallVectorImpl &RHS)
+ {
+ assign (RHS.begin (), RHS.end ());
+ }
+
+ template <typename U,
+ typename = std::enable_if_t<std::is_convertible_v<U, T>>>
+ void
+ assign (ArrayRef<U> AR)
+ {
+ assign (AR.begin (), AR.end ());
+ }
+
+ iterator
+ erase (const_iterator CI)
+ {
+ // Just cast away constness because this is a non-const member function.
+ iterator I = const_cast<iterator> (CI);
+
+ assert (this->isReferenceToStorage (CI) &&
+ "Iterator to erase is out of bounds.");
+
+ iterator N = I;
+ // Shift all elts down one.
+ std::move (I + 1, this->end (), I);
+ // Drop the last elt.
+ this->pop_back ();
+ return (N);
+ }
+
+ iterator
+ erase (const_iterator CS, const_iterator CE)
+ {
+ // Just cast away constness because this is a non-const member function.
+ iterator S = const_cast<iterator> (CS);
+ iterator E = const_cast<iterator> (CE);
+
+ assert (this->isRangeInStorage (S, E) &&
+ "Range to erase is out of bounds.");
+
+ iterator N = S;
+ // Shift all elts down.
+ iterator I = std::move (E, this->end (), S);
+ // Drop the last elts.
+ this->destroy_range (I, this->end ());
+ this->set_size (I - this->begin ());
+ return (N);
+ }
+
+private:
+ template <class ArgType>
+ iterator
+ insert_one_impl (iterator I, ArgType &&Elt)
+ {
+ // Callers ensure that ArgType is derived from T.
+ static_assert (
+ std::is_same<std::remove_const_t<std::remove_reference_t<ArgType>>,
+ T>::value,
+ "ArgType must be derived from T!");
+
+ if (I == this->end ())
+ { // Important special case for empty vector.
+ this->push_back (::std::forward<ArgType> (Elt));
+ return this->end () - 1;
+ }
+
+ assert (this->isReferenceToStorage (I) &&
+ "Insertion iterator is out of bounds.");
+
+ // Grow if necessary.
+ size_t Index = I - this->begin ();
+ std::remove_reference_t<ArgType> *EltPtr =
+ this->reserveForParamAndGetAddress (Elt);
+ I = this->begin () + Index;
+
+ ::new ((void *)this->end ()) T (::std::move (this->back ()));
+ // Push everything else over.
+ std::move_backward (I, this->end () - 1, this->end ());
+ this->set_size (this->size () + 1);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ static_assert (!TakesParamByValue || std::is_same<ArgType, T>::value,
+ "ArgType must be 'T' when taking by value!");
+ if (!TakesParamByValue &&
+ this->isReferenceToRange (EltPtr, I, this->end ()))
+ {
+ ++EltPtr;
+ }
+
+ *I = ::std::forward<ArgType> (*EltPtr);
+ return I;
+ }
+
+public:
+ iterator
+ insert (iterator I, T &&Elt)
+ {
+ return insert_one_impl (I, this->forward_value_param (std::move (Elt)));
+ }
+
+ iterator
+ insert (iterator I, const T &Elt)
+ {
+ return insert_one_impl (I, this->forward_value_param (Elt));
+ }
+
+ iterator
+ insert (iterator I, size_type NumToInsert, ValueParamT Elt)
+ {
+ // Convert iterator to elt# to avoid invalidating iterator when we reserve()
+ size_t InsertElt = I - this->begin ();
+
+ if (I == this->end ())
+ { // Important special case for empty vector.
+ append (NumToInsert, Elt);
+ return this->begin () + InsertElt;
+ }
+
+ assert (this->isReferenceToStorage (I) &&
+ "Insertion iterator is out of bounds.");
+
+ // Ensure there is enough space, and get the (maybe updated) address of
+ // Elt.
+ const T *EltPtr = this->reserveForParamAndGetAddress (Elt, NumToInsert);
+
+ // Uninvalidate the iterator.
+ I = this->begin () + InsertElt;
+
+ // If there are more elements between the insertion point and the end of the
+ // range than there are being inserted, we can use a simple approach to
+ // insertion. Since we already reserved space, we know that this won't
+ // reallocate the vector.
+ if (size_t (this->end () - I) >= NumToInsert)
+ {
+ T *OldEnd = this->end ();
+ append (std::move_iterator<iterator> (this->end () - NumToInsert),
+ std::move_iterator<iterator> (this->end ()));
+
+ // Copy the existing elements that get replaced.
+ std::move_backward (I, OldEnd - NumToInsert, OldEnd);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ if (!TakesParamByValue && I <= EltPtr && EltPtr < this->end ())
+ {
+ EltPtr += NumToInsert;
+ }
+
+ std::fill_n (I, NumToInsert, *EltPtr);
+ return I;
+ }
+
+ // Otherwise, we're inserting more elements than exist already, and we're
+ // not inserting at the end.
+
+ // Move over the elements that we're about to overwrite.
+ T *OldEnd = this->end ();
+ this->set_size (this->size () + NumToInsert);
+ size_t NumOverwritten = OldEnd - I;
+ this->uninitialized_move (I, OldEnd, this->end () - NumOverwritten);
+
+ // If we just moved the element we're inserting, be sure to update
+ // the reference (never happens if TakesParamByValue).
+ if (!TakesParamByValue && I <= EltPtr && EltPtr < this->end ())
+ {
+ EltPtr += NumToInsert;
+ }
+
+ // Replace the overwritten part.
+ std::fill_n (I, NumOverwritten, *EltPtr);
+
+ // Insert the non-overwritten middle part.
+ std::uninitialized_fill_n (OldEnd, NumToInsert - NumOverwritten, *EltPtr);
+ return I;
+ }
+
+ template <typename ItTy, typename = EnableIfConvertibleToInputIterator<ItTy>>
+ iterator
+ insert (iterator I, ItTy From, ItTy To)
+ {
+ // Convert iterator to elt# to avoid invalidating iterator when we reserve()
+ size_t InsertElt = I - this->begin ();
+
+ if (I == this->end ())
+ { // Important special case for empty vector.
+ append (From, To);
+ return this->begin () + InsertElt;
+ }
+
+ if constexpr (!HasIteratorTag<ItTy, std::forward_iterator_tag>::value)
+ {
+ // For input iterators, we don't know the number of elements to insert.
+ size_t OldSize = this->size ();
+ append (From, To);
+ I = this->begin () + InsertElt; // Uninvalidate the iterator.
+ std::rotate (I, this->begin () + OldSize, this->end ());
+ return I;
+ }
+
+ assert (this->isReferenceToStorage (I) &&
+ "Insertion iterator is out of bounds.");
+
+ // Check that the reserve that follows doesn't invalidate the iterators.
+ this->assertSafeToAddRange (From, To);
+
+ size_t NumToInsert = std::distance (From, To);
+
+ // Ensure there is enough space.
+ reserve (this->size () + NumToInsert);
+
+ // Uninvalidate the iterator.
+ I = this->begin () + InsertElt;
+
+ // If there are more elements between the insertion point and the end of the
+ // range than there are being inserted, we can use a simple approach to
+ // insertion. Since we already reserved space, we know that this won't
+ // reallocate the vector.
+ if (size_t (this->end () - I) >= NumToInsert)
+ {
+ T *OldEnd = this->end ();
+ append (std::move_iterator<iterator> (this->end () - NumToInsert),
+ std::move_iterator<iterator> (this->end ()));
+
+ // Copy the existing elements that get replaced.
+ std::move_backward (I, OldEnd - NumToInsert, OldEnd);
+
+ std::copy (From, To, I);
+ return I;
+ }
+
+ // Otherwise, we're inserting more elements than exist already, and we're
+ // not inserting at the end.
+
+ // Move over the elements that we're about to overwrite.
+ T *OldEnd = this->end ();
+ this->set_size (this->size () + NumToInsert);
+ size_t NumOverwritten = OldEnd - I;
+ this->uninitialized_move (I, OldEnd, this->end () - NumOverwritten);
+
+ // Replace the overwritten part.
+ for (T *J = I; NumOverwritten > 0; --NumOverwritten)
+ {
+ *J = *From;
+ ++J;
+ ++From;
+ }
+
+ // Insert the non-overwritten middle part.
+ this->uninitialized_copy (From, To, OldEnd);
+ return I;
+ }
+
+ void
+ insert (iterator I, std::initializer_list<T> IL)
+ {
+ insert (I, IL.begin (), IL.end ());
+ }
+
+ template <typename... ArgTypes>
+ reference
+ emplace_back (ArgTypes &&...Args)
+ {
+ if (LIBBASE_UNLIKELY (this->size () >= this->capacity ()))
+ {
+ return this->growAndEmplaceBack (std::forward<ArgTypes> (Args)...);
+ }
+
+ ::new ((void *)this->end ()) T (std::forward<ArgTypes> (Args)...);
+ this->set_size (this->size () + 1);
+ return this->back ();
+ }
+
+ SmallVectorImpl &
+ operator= (const SmallVectorImpl &RHS);
+
+ SmallVectorImpl &
+ operator= (SmallVectorImpl &&RHS);
+
+ bool
+ operator== (const SmallVectorImpl &RHS) const
+ {
+ if (this->size () != RHS.size ())
+ {
+ return false;
+ }
+ return std::equal (this->begin (), this->end (), RHS.begin ());
+ }
+ bool
+ operator!= (const SmallVectorImpl &RHS) const
+ {
+ return !(*this == RHS);
+ }
+
+ bool
+ operator< (const SmallVectorImpl &RHS) const
+ {
+ return std::lexicographical_compare (this->begin (), this->end (),
+ RHS.begin (), RHS.end ());
+ }
+ bool
+ operator> (const SmallVectorImpl &RHS) const
+ {
+ return RHS < *this;
+ }
+ bool
+ operator<= (const SmallVectorImpl &RHS) const
+ {
+ return !(*this > RHS);
+ }
+ bool
+ operator>= (const SmallVectorImpl &RHS) const
+ {
+ return !(*this < RHS);
+ }
+};
+
+template <typename T>
+void
+SmallVectorImpl<T>::swap (SmallVectorImpl<T> &RHS)
+{
+ if (this == &RHS)
+ {
+ return;
+ }
+
+ // We can only avoid copying elements if neither vector is small.
+ if (!this->isSmall () && !RHS.isSmall ())
+ {
+ std::swap (this->BeginX, RHS.BeginX);
+ std::swap (this->Size, RHS.Size);
+ std::swap (this->Capacity, RHS.Capacity);
+ return;
+ }
+ this->reserve (RHS.size ());
+ RHS.reserve (this->size ());
+
+ // Swap the shared elements.
+ size_t NumShared = this->size ();
+ if (NumShared > RHS.size ())
+ {
+ NumShared = RHS.size ();
+ }
+ for (size_type i = 0; i != NumShared; ++i)
+ {
+ std::swap ((*this)[i], RHS[i]);
+ }
+
+ // Copy over the extra elts.
+ if (this->size () > RHS.size ())
+ {
+ size_t EltDiff = this->size () - RHS.size ();
+ this->uninitialized_copy (this->begin () + NumShared, this->end (),
+ RHS.end ());
+ RHS.set_size (RHS.size () + EltDiff);
+ this->destroy_range (this->begin () + NumShared, this->end ());
+ this->set_size (NumShared);
+ }
+ else if (RHS.size () > this->size ())
+ {
+ size_t EltDiff = RHS.size () - this->size ();
+ this->uninitialized_copy (RHS.begin () + NumShared, RHS.end (),
+ this->end ());
+ this->set_size (this->size () + EltDiff);
+ this->destroy_range (RHS.begin () + NumShared, RHS.end ());
+ RHS.set_size (NumShared);
+ }
+}
+
+template <typename T>
+SmallVectorImpl<T> &
+SmallVectorImpl<T>::operator= (const SmallVectorImpl<T> &RHS)
+{
+ // Avoid self-assignment.
+ if (this == &RHS)
+ {
+ return *this;
+ }
+
+ // If we already have sufficient space, assign the common elements, then
+ // destroy any excess.
+ size_t RHSSize = RHS.size ();
+ size_t CurSize = this->size ();
+ if (CurSize >= RHSSize)
+ {
+ // Assign common elements.
+ iterator NewEnd;
+ if (RHSSize)
+ {
+ NewEnd = std::copy (RHS.begin (), RHS.begin () + RHSSize, this->begin ());
+ }
+ else
+ {
+ NewEnd = this->begin ();
+ }
+
+ // Destroy excess elements.
+ this->destroy_range (NewEnd, this->end ());
+
+ // Trim.
+ this->set_size (RHSSize);
+ return *this;
+ }
+
+ // If we have to grow to have enough elements, destroy the current elements.
+ // This allows us to avoid copying them during the grow.
+ // FIXME: don't do this if they're efficiently moveable.
+ if (this->capacity () < RHSSize)
+ {
+ // Destroy current elements.
+ this->clear ();
+ CurSize = 0;
+ this->grow (RHSSize);
+ }
+ else if (CurSize)
+ {
+ // Otherwise, use assignment for the already-constructed elements.
+ std::copy (RHS.begin (), RHS.begin () + CurSize, this->begin ());
+ }
+
+ // Copy construct the new elements in place.
+ this->uninitialized_copy (RHS.begin () + CurSize, RHS.end (),
+ this->begin () + CurSize);
+
+ // Set end.
+ this->set_size (RHSSize);
+ return *this;
+}
+
+template <typename T>
+SmallVectorImpl<T> &
+SmallVectorImpl<T>::operator= (SmallVectorImpl<T> &&RHS)
+{
+ // Avoid self-assignment.
+ if (this == &RHS)
+ {
+ return *this;
+ }
+
+ // If the RHS isn't small, clear this vector and then steal its buffer.
+ if (!RHS.isSmall ())
+ {
+ this->assignRemote (std::move (RHS));
+ return *this;
+ }
+
+ // If we already have sufficient space, assign the common elements, then
+ // destroy any excess.
+ size_t RHSSize = RHS.size ();
+ size_t CurSize = this->size ();
+ if (CurSize >= RHSSize)
+ {
+ // Assign common elements.
+ iterator NewEnd = this->begin ();
+ if (RHSSize)
+ {
+ NewEnd = std::move (RHS.begin (), RHS.end (), NewEnd);
+ }
+
+ // Destroy excess elements and trim the bounds.
+ this->destroy_range (NewEnd, this->end ());
+ this->set_size (RHSSize);
+
+ // Clear the RHS.
+ RHS.clear ();
+
+ return *this;
+ }
+
+ // If we have to grow to have enough elements, destroy the current elements.
+ // This allows us to avoid copying them during the grow.
+ // FIXME: this may not actually make any sense if we can efficiently move
+ // elements.
+ if (this->capacity () < RHSSize)
+ {
+ // Destroy current elements.
+ this->clear ();
+ CurSize = 0;
+ this->grow (RHSSize);
+ }
+ else if (CurSize)
+ {
+ // Otherwise, use assignment for the already-constructed elements.
+ std::move (RHS.begin (), RHS.begin () + CurSize, this->begin ());
+ }
+
+ // Move-construct the new elements in place.
+ this->uninitialized_move (RHS.begin () + CurSize, RHS.end (),
+ this->begin () + CurSize);
+
+ // Set end.
+ this->set_size (RHSSize);
+
+ RHS.clear ();
+ return *this;
+}
+
+/// Storage for the SmallVector elements. This is specialized for the N=0 case
+/// to avoid allocating unnecessary storage.
+template <typename T, unsigned N> struct SmallVectorStorage
+{
+ alignas (T) char InlineElts[N * sizeof (T)];
+};
+
+/// We need the storage to be properly aligned even for small-size of 0 so that
+/// the pointer math in \a SmallVectorTemplateCommon::getFirstEl() is
+/// well-defined.
+template <typename T> struct alignas (T) SmallVectorStorage<T, 0>
+{
+};
+
+/// Forward declaration of SmallVector so that
+/// calculateSmallVectorDefaultInlinedElements can reference
+/// `sizeof(SmallVector<T, 0>)`.
+template <typename T, unsigned N> class LIBBASE_GSL_OWNER SmallVector;
+
+/// Helper class for calculating the default number of inline elements for
+/// `SmallVector<T>`.
+///
+/// This should be migrated to a constexpr function when our minimum
+/// compiler support is enough for multi-statement constexpr functions.
+template <typename T> struct CalculateSmallVectorDefaultInlinedElements
+{
+ // Parameter controlling the default number of inlined elements
+ // for `SmallVector<T>`.
+ //
+ // The default number of inlined elements ensures that
+ // 1. There is at least one inlined element.
+ // 2. `sizeof(SmallVector<T>) <= kPreferredSmallVectorSizeof` unless
+ // it contradicts 1.
+ static constexpr size_t kPreferredSmallVectorSizeof = 64;
+
+ // static_assert that sizeof(T) is not "too big".
+ //
+ // Because our policy guarantees at least one inlined element, it is possible
+ // for an arbitrarily large inlined element to allocate an arbitrarily large
+ // amount of inline storage. We generally consider it an antipattern for a
+ // SmallVector to allocate an excessive amount of inline storage, so we want
+ // to call attention to these cases and make sure that users are making an
+ // intentional decision if they request a lot of inline storage.
+ //
+ // We want this assertion to trigger in pathological cases, but otherwise
+ // not be too easy to hit. To accomplish that, the cutoff is actually somewhat
+ // larger than kPreferredSmallVectorSizeof (otherwise,
+ // `SmallVector<SmallVector<T>>` would be one easy way to trip it, and that
+ // pattern seems useful in practice).
+ //
+ // One wrinkle is that this assertion is in theory non-portable, since
+ // sizeof(T) is in general platform-dependent. However, we don't expect this
+ // to be much of an issue, because most LLVM development happens on 64-bit
+ // hosts, and therefore sizeof(T) is expected to *decrease* when compiled for
+ // 32-bit hosts, dodging the issue. The reverse situation, where development
+ // happens on a 32-bit host and then fails due to sizeof(T) *increasing* on a
+ // 64-bit host, is expected to be very rare.
+ static_assert (
+ sizeof (T) <= 256,
+ "You are trying to use a default number of inlined elements for "
+ "`SmallVector<T>` but `sizeof(T)` is really big! Please use an "
+ "explicit number of inlined elements with `SmallVector<T, N>` to make "
+ "sure you really want that much inline storage.");
+
+ // Discount the size of the header itself when calculating the maximum inline
+ // bytes.
+ static constexpr size_t PreferredInlineBytes =
+ kPreferredSmallVectorSizeof - sizeof (SmallVector<T, 0>);
+ static constexpr size_t NumElementsThatFit =
+ PreferredInlineBytes / sizeof (T);
+ static constexpr size_t value =
+ NumElementsThatFit == 0 ? 1 : NumElementsThatFit;
+};
+
+/// This is a 'vector' (really, a variable-sized array), optimized
+/// for the case when the array is small. It contains some number of elements
+/// in-place, which allows it to avoid heap allocation when the actual number of
+/// elements is below that threshold. This allows normal "small" cases to be
+/// fast without losing generality for large inputs.
+///
+/// \note
+/// In the absence of a well-motivated choice for the number of inlined
+/// elements \p N, it is recommended to use \c SmallVector<T> (that is,
+/// omitting the \p N). This will choose a default number of inlined elements
+/// reasonable for allocation on the stack (for example, trying to keep \c
+/// sizeof(SmallVector<T>) around 64 bytes).
+///
+/// \warning This does not attempt to be exception safe.
+///
+/// \see https://llvm.org/docs/ProgrammersManual.html#llvm-adt-smallvector-h
+template <typename T,
+ unsigned N = CalculateSmallVectorDefaultInlinedElements<T>::value>
+class LIBBASE_GSL_OWNER SmallVector : public SmallVectorImpl<T>,
+ SmallVectorStorage<T, N>
+{
+public:
+ SmallVector () : SmallVectorImpl<T> (N)
+ {
+ }
+
+ ~SmallVector ()
+ {
+ // Destroy the constructed elements in the vector.
+ this->destroy_range (this->begin (), this->end ());
+ }
+
+ explicit SmallVector (size_t SizeArg) : SmallVectorImpl<T> (N)
+ {
+ this->resize (SizeArg);
+ }
+
+ SmallVector (size_t SizeArg, const T &Value) : SmallVectorImpl<T> (N)
+ {
+ this->assign (SizeArg, Value);
+ }
+
+ template <typename ItTy, typename = EnableIfConvertibleToInputIterator<ItTy>>
+ SmallVector (ItTy S, ItTy E) : SmallVectorImpl<T> (N)
+ {
+ this->append (S, E);
+ }
+
+ template <typename RangeTy>
+ explicit SmallVector (const iterator_range<RangeTy> &R)
+ : SmallVectorImpl<T> (N)
+ {
+ this->append (R.begin (), R.end ());
+ }
+
+ SmallVector (std::initializer_list<T> IL) : SmallVectorImpl<T> (N)
+ {
+ this->append (IL);
+ }
+
+ template <typename U,
+ typename = std::enable_if_t<std::is_convertible_v<U, T>>>
+ explicit SmallVector (ArrayRef<U> A) : SmallVectorImpl<T> (N)
+ {
+ this->append (A.begin (), A.end ());
+ }
+
+ SmallVector (const SmallVector &RHS) : SmallVectorImpl<T> (N)
+ {
+ if (!RHS.empty ())
+ {
+ SmallVectorImpl<T>::operator= (RHS);
+ }
+ }
+
+ SmallVector &
+ operator= (const SmallVector &RHS)
+ {
+ SmallVectorImpl<T>::operator= (RHS);
+ return *this;
+ }
+
+ // Reuse the move-assignment instantiation for trivially move-assignable
+ // elements. Calling moveConstructFrom unconditionally would emit an
+ // additional per-T wrapper at -O0, increasing object size.
+ // std::is_move_assignable_v<T> is too broad: it can be a false positive for
+ // types with an unconstrained assignment operator whose body is ill-formed.
+ SmallVector (SmallVector &&RHS) : SmallVectorImpl<T> (N)
+ {
+ if (!RHS.empty ())
+ {
+ bool StoleAllocation = N > 0 && !RHS.isSmall ();
+ if constexpr (std::is_trivially_move_assignable_v<T>)
+ {
+ SmallVectorImpl<T>::operator= (::std::move (RHS));
+ }
+ else
+ {
+ this->moveConstructFrom (::std::move (RHS));
+ }
+ if constexpr (N > 0)
+ {
+ if (StoleAllocation)
+ {
+ RHS.set_allocation_range (RHS.begin (), N);
+ }
+ }
+ }
+ }
+
+ SmallVector (SmallVectorImpl<T> &&RHS) : SmallVectorImpl<T> (N)
+ {
+ if (!RHS.empty ())
+ {
+ if constexpr (std::is_trivially_move_assignable_v<T>)
+ {
+ SmallVectorImpl<T>::operator= (::std::move (RHS));
+ }
+ else
+ {
+ this->moveConstructFrom (::std::move (RHS));
+ }
+ }
+ }
+
+ SmallVector &
+ operator= (SmallVector &&RHS)
+ {
+ if (N)
+ {
+ bool StoleAllocation = this != &RHS && !RHS.isSmall ();
+ SmallVectorImpl<T>::operator= (::std::move (RHS));
+ // Restore the inline capacity after stealing RHS's allocation.
+ if (StoleAllocation)
+ {
+ RHS.set_allocation_range (RHS.begin (), N);
+ }
+ return *this;
+ }
+ // SmallVectorImpl<T>::operator= does not leverage N==0. Optimize the
+ // case.
+ if (this == &RHS)
+ {
+ return *this;
+ }
+ if (RHS.empty ())
+ {
+ this->destroy_range (this->begin (), this->end ());
+ this->Size = 0;
+ }
+ else
+ {
+ this->assignRemote (std::move (RHS));
+ }
+ return *this;
+ }
+
+ SmallVector &
+ operator= (SmallVectorImpl<T> &&RHS)
+ {
+ SmallVectorImpl<T>::operator= (::std::move (RHS));
+ return *this;
+ }
+
+ SmallVector &
+ operator= (std::initializer_list<T> IL)
+ {
+ this->assign (IL);
+ return *this;
+ }
+};
+
+template <typename T, unsigned N>
+inline size_t
+capacity_in_bytes (const SmallVector<T, N> &X)
+{
+ return X.capacity_in_bytes ();
+}
+
+template <typename RangeType>
+using ValueTypeFromRangeType = std::ranges::range_value_t<RangeType>;
+
+/// Given a range of type R, iterate the entire range and return a
+/// SmallVector with elements of the vector. This is useful, for example,
+/// when you want to iterate a range and then sort the results.
+template <unsigned Size, typename R>
+SmallVector<ValueTypeFromRangeType<R>, Size>
+to_vector (R &&Range)
+{
+ return SmallVector<ValueTypeFromRangeType<R>, Size> (
+ std::ranges::begin (Range), std::ranges::end (Range));
+}
+template <typename R>
+SmallVector<ValueTypeFromRangeType<R>>
+to_vector (R &&Range)
+{
+ return SmallVector<ValueTypeFromRangeType<R>> (std::ranges::begin (Range),
+ std::ranges::end (Range));
+}
+
+template <typename Out, unsigned Size, typename R>
+SmallVector<Out, Size>
+to_vector_of (R &&Range)
+{
+ return SmallVector<Out, Size> (std::ranges::begin (Range),
+ std::ranges::end (Range));
+}
+
+template <typename Out, typename R>
+SmallVector<Out>
+to_vector_of (R &&Range)
+{
+ return SmallVector<Out> (std::ranges::begin (Range),
+ std::ranges::end (Range));
+}
+
+// Explicit instantiations
+extern template class libbase::SmallVectorBase<uint32_t>;
+#if SIZE_MAX > UINT32_MAX
+extern template class libbase::SmallVectorBase<uint64_t>;
+#endif
+
+} // end namespace libbase
+
+namespace std
+{
+
+template <typename T, unsigned N> struct hash<libbase::SmallVector<T, N>>
+{
+ size_t
+ operator() (const libbase::SmallVector<T, N> &Value) const
+ {
+ size_t Seed = 0;
+ for (const T &Element : Value)
+ {
+ size_t ElementHash = std::hash<T>{}(Element);
+ Seed ^= ElementHash + static_cast<size_t> (0x9e3779b9U) + (Seed << 6) +
+ (Seed >> 2);
+ }
+ return Seed;
+ }
+};
+
+/// Implement std::swap in terms of SmallVector swap.
+template <typename T>
+inline void
+swap (libbase::SmallVectorImpl<T> &LHS, libbase::SmallVectorImpl<T> &RHS)
+{
+ LHS.swap (RHS);
+}
+
+/// Implement std::swap in terms of SmallVector swap.
+template <typename T, unsigned N>
+inline void
+swap (libbase::SmallVector<T, N> &LHS, libbase::SmallVector<T, N> &RHS)
+{
+ LHS.swap (RHS);
+}
+
+} // end namespace std
+
+#endif // LIBBASE_ADT_SMALLVECTOR_H
diff --git a/libbase/include/core/adt/Twine.h b/libbase/include/core/adt/Twine.h
new file mode 100644
index 0000000..66d8264
--- /dev/null
+++ b/libbase/include/core/adt/Twine.h
@@ -0,0 +1,679 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- core/adt/Twine.h - Fast Temporary String Concatenation -*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_TWINE_H
+#define LIBBASE_ADT_TWINE_H
+
+#include "core/adt/SmallVector.h"
+#include "core/compiler/Compiler.h"
+#include "core/error/ErrorHandling.h"
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <string_view>
+
+namespace libbase
+{
+
+class formatv_object_base;
+class raw_ostream;
+
+/// Twine - A lightweight data structure for efficiently representing the
+/// concatenation of temporary values as strings.
+///
+/// A Twine is a kind of rope, it represents a concatenated string using a
+/// binary-tree, where the string is the preorder of the nodes. Since the
+/// Twine can be efficiently rendered into a buffer when its result is used,
+/// it avoids the cost of generating temporary values for intermediate string
+/// results -- particularly in cases when the Twine result is never
+/// required. By explicitly tracking the type of leaf nodes, we can also avoid
+/// the creation of temporary strings for conversions operations (such as
+/// appending an integer to a string).
+///
+/// A Twine is not intended for use directly and should not be stored, its
+/// implementation relies on the ability to store pointers to temporary stack
+/// objects which may be deallocated at the end of a statement. Twines should
+/// only be used as const references in arguments, when an API wishes
+/// to accept possibly-concatenated strings.
+///
+/// Twines support a special 'null' value, which always concatenates to form
+/// itself, and renders as an empty string. This can be returned from APIs to
+/// effectively nullify any concatenations performed on the result.
+///
+/// \b Implementation
+///
+/// Given the nature of a Twine, it is not possible for the Twine's
+/// concatenation method to construct interior nodes; the result must be
+/// represented inside the returned value. For this reason a Twine object
+/// actually holds two values, the left- and right-hand sides of a
+/// concatenation. We also have nullary Twine objects, which are effectively
+/// sentinel values that represent empty strings.
+///
+/// Thus, a Twine can effectively have zero, one, or two children. The \see
+/// isNullary(), \see isUnary(), and \see isBinary() predicates exist for
+/// testing the number of children.
+///
+/// We maintain a number of invariants on Twine objects (FIXME: Why):
+/// - Nullary twines are always represented with their Kind on the left-hand
+/// side, and the Empty kind on the right-hand side.
+/// - Unary twines are always represented with the value on the left-hand
+/// side, and the Empty kind on the right-hand side.
+/// - If a Twine has another Twine as a child, that child should always be
+/// binary (otherwise it could have been folded into the parent).
+///
+/// These invariants are check by \see isValid().
+///
+/// \b Efficiency Considerations
+///
+/// The Twine is designed to yield efficient and small code for common
+/// situations. For this reason, the concat() method is inlined so that
+/// concatenations of leaf nodes can be optimized into stores directly into a
+/// single stack allocated object.
+///
+/// In practice, not all compilers can be trusted to optimize concat() fully,
+/// so we provide two additional methods (and accompanying operator+
+/// overloads) to guarantee that particularly important cases (cstring plus
+/// string view) codegen as desired.
+class Twine
+{
+ /// NodeKind - Represent the type of an argument.
+ enum NodeKind : unsigned char
+ {
+ /// An empty string; the result of concatenating anything with it is also
+ /// empty.
+ NullKind,
+
+ /// The empty string.
+ EmptyKind,
+
+ /// A pointer to a Twine instance.
+ TwineKind,
+
+ /// A pointer to a C string instance.
+ CStringKind,
+
+ /// A pointer to an std::string instance.
+ StdStringKind,
+
+ /// A pointer and length representation. Used for std::string_view and
+ /// SmallVector. It stores the pointer and length directly.
+ PtrAndLengthKind,
+
+ /// A pointer to a formatv_object_base instance.
+ FormatvObjectKind,
+
+ /// A char value, to render as a character.
+ CharKind,
+
+ /// An unsigned int value, to render as an unsigned decimal integer.
+ DecUIKind,
+
+ /// An int value, to render as a signed decimal integer.
+ DecIKind,
+
+ /// An unsigned long value, to render as an unsigned decimal integer.
+ DecULKind,
+
+ /// A long value, to render as a signed decimal integer.
+ DecLKind,
+
+ /// An unsigned long long value, to render as an unsigned decimal integer.
+ DecULLKind,
+
+ /// A long long value, to render as a signed decimal integer.
+ DecLLKind,
+
+ /// A uint64_t value, to render as an unsigned hexadecimal integer.
+ UHexKind
+ };
+
+ union Child
+ {
+ const Twine *twine;
+ const char *cString;
+ const std::string *stdString;
+ struct
+ {
+ const char *ptr;
+ size_t length;
+ } ptrAndLength;
+ const formatv_object_base *formatvObject;
+ char character;
+ unsigned int decUI;
+ int decI;
+ unsigned long decUL;
+ long decL;
+ unsigned long long decULL;
+ long long decLL;
+ uint64_t uHex;
+ };
+
+ /// LHS - The prefix in the concatenation, which may be uninitialized for
+ /// Null or Empty kinds.
+ Child LHS;
+
+ /// RHS - The suffix in the concatenation, which may be uninitialized for
+ /// Null or Empty kinds.
+ Child RHS;
+
+ /// LHSKind - The NodeKind of the left hand side, \see getLHSKind().
+ NodeKind LHSKind = EmptyKind;
+
+ /// RHSKind - The NodeKind of the right hand side, \see getRHSKind().
+ NodeKind RHSKind = EmptyKind;
+
+ /// Construct a nullary twine; the kind must be NullKind or EmptyKind.
+ explicit Twine (NodeKind Kind) : LHSKind (Kind)
+ {
+ assert (isNullary () && "Invalid kind!");
+ }
+
+ /// Construct a binary twine.
+ explicit Twine (const Twine &LHS, const Twine &RHS)
+ : LHSKind (TwineKind), RHSKind (TwineKind)
+ {
+ this->LHS.twine = &LHS;
+ this->RHS.twine = &RHS;
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct a twine from explicit values.
+ explicit Twine (Child LHS, NodeKind LHSKind, Child RHS, NodeKind RHSKind)
+ : LHS (LHS), RHS (RHS), LHSKind (LHSKind), RHSKind (RHSKind)
+ {
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Check for the null twine.
+ bool
+ isNull () const
+ {
+ return getLHSKind () == NullKind;
+ }
+
+ /// Check for the empty twine.
+ bool
+ isEmpty () const
+ {
+ return getLHSKind () == EmptyKind;
+ }
+
+ /// Check if this is a nullary twine (null or empty).
+ bool
+ isNullary () const
+ {
+ return isNull () || isEmpty ();
+ }
+
+ /// Check if this is a unary twine.
+ bool
+ isUnary () const
+ {
+ return getRHSKind () == EmptyKind && !isNullary ();
+ }
+
+ /// Check if this is a binary twine.
+ bool
+ isBinary () const
+ {
+ return getLHSKind () != NullKind && getRHSKind () != EmptyKind;
+ }
+
+ /// Check if this is a valid twine (satisfying the invariants on
+ /// order and number of arguments).
+ bool
+ isValid () const
+ {
+ // Nullary twines always have Empty on the RHS.
+ if (isNullary () && getRHSKind () != EmptyKind)
+ {
+ return false;
+ }
+
+ // Null should never appear on the RHS.
+ if (getRHSKind () == NullKind)
+ {
+ return false;
+ }
+
+ // The RHS cannot be non-empty if the LHS is empty.
+ if (getRHSKind () != EmptyKind && getLHSKind () == EmptyKind)
+ {
+ return false;
+ }
+
+ // A twine child should always be binary.
+ if (getLHSKind () == TwineKind && !LHS.twine->isBinary ())
+ {
+ return false;
+ }
+ if (getRHSKind () == TwineKind && !RHS.twine->isBinary ())
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// Get the NodeKind of the left-hand side.
+ NodeKind
+ getLHSKind () const
+ {
+ return LHSKind;
+ }
+
+ /// Get the NodeKind of the right-hand side.
+ NodeKind
+ getRHSKind () const
+ {
+ return RHSKind;
+ }
+
+ /// Print one child from a twine.
+ void
+ printOneChild (raw_ostream &OS, Child Ptr, NodeKind Kind) const;
+
+ /// Print the representation of one child from a twine.
+ void
+ printOneChildRepr (raw_ostream &OS, Child Ptr, NodeKind Kind) const;
+
+public:
+ /// @name Constructors
+ /// @{
+
+ /// Construct from an empty string.
+ /*implicit*/ Twine ()
+ {
+ assert (isValid () && "Invalid twine!");
+ }
+
+ Twine (const Twine &) = default;
+
+ /// Construct from a C string.
+ ///
+ /// We take care here to optimize "" into the empty twine -- this will be
+ /// optimized out for string constants. This allows Twine arguments have
+ /// default "" values, without introducing unnecessary string constants.
+ /*implicit*/ Twine (const char *Str)
+ {
+ if (Str[0] != '\0')
+ {
+ LHS.cString = Str;
+ LHSKind = CStringKind;
+ }
+ else
+ {
+ LHSKind = EmptyKind;
+ }
+
+ assert (isValid () && "Invalid twine!");
+ }
+ /// Delete the implicit conversion from nullptr as Twine(const char *)
+ /// cannot take nullptr.
+ /*implicit*/ Twine (std::nullptr_t) = delete;
+
+ /// Construct from an std::string.
+ /*implicit*/ Twine (const std::string &Str) : LHSKind (StdStringKind)
+ {
+ LHS.stdString = &Str;
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct from an std::string_view by converting it to a pointer and
+ /// length. This handles string_views on a pure API basis, and avoids
+ /// storing one (or a pointer to one) inside a Twine, which avoids problems
+ /// when mixing code compiled under various C++ standards.
+ /*implicit*/ Twine (std::string_view Str) : LHSKind (PtrAndLengthKind)
+ {
+ LHS.ptrAndLength.ptr = Str.data ();
+ LHS.ptrAndLength.length = Str.length ();
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct from a SmallString.
+ /*implicit*/ Twine (const SmallVectorImpl<char> &Str)
+ : LHSKind (PtrAndLengthKind)
+ {
+ LHS.ptrAndLength.ptr = Str.data ();
+ LHS.ptrAndLength.length = Str.size ();
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct from a formatv_object_base.
+ /*implicit*/ Twine (const formatv_object_base &Fmt)
+ : LHSKind (FormatvObjectKind)
+ {
+ LHS.formatvObject = &Fmt;
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct from a char.
+ explicit Twine (char Val) : LHSKind (CharKind)
+ {
+ LHS.character = Val;
+ }
+
+ /// Construct from a signed char.
+ explicit Twine (signed char Val) : LHSKind (CharKind)
+ {
+ LHS.character = static_cast<char> (Val);
+ }
+
+ /// Construct from an unsigned char.
+ explicit Twine (unsigned char Val) : LHSKind (CharKind)
+ {
+ LHS.character = static_cast<char> (Val);
+ }
+
+ /// Construct a twine to print \p Val as an unsigned decimal integer.
+ explicit Twine (unsigned Val) : LHSKind (DecUIKind)
+ {
+ LHS.decUI = Val;
+ }
+
+ /// Construct a twine to print \p Val as a signed decimal integer.
+ explicit Twine (int Val) : LHSKind (DecIKind)
+ {
+ LHS.decI = Val;
+ }
+
+ /// Construct a twine to print \p Val as an unsigned decimal integer.
+ explicit Twine (unsigned long Val) : LHSKind (DecULKind)
+ {
+ LHS.decUL = Val;
+ }
+
+ /// Construct a twine to print \p Val as a signed decimal integer.
+ explicit Twine (long Val) : LHSKind (DecLKind)
+ {
+ LHS.decL = Val;
+ }
+
+ /// Construct a twine to print \p Val as an unsigned decimal integer.
+ explicit Twine (unsigned long long Val) : LHSKind (DecULLKind)
+ {
+ LHS.decULL = Val;
+ }
+
+ /// Construct a twine to print \p Val as a signed decimal integer.
+ explicit Twine (long long Val) : LHSKind (DecLLKind)
+ {
+ LHS.decLL = Val;
+ }
+
+ // FIXME: Unfortunately, to make sure this is as efficient as possible we
+ // need extra binary constructors from particular types. We can't rely on
+ // the compiler to be smart enough to fold operator+()/concat() down to the
+ // right thing. Yet.
+
+ /// Construct as the concatenation of a C string and a string view.
+ /*implicit*/ Twine (const char *LHS, std::string_view RHS)
+ : LHSKind (CStringKind), RHSKind (PtrAndLengthKind)
+ {
+ this->LHS.cString = LHS;
+ this->RHS.ptrAndLength.ptr = RHS.data ();
+ this->RHS.ptrAndLength.length = RHS.size ();
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Construct as the concatenation of a string view and a C string.
+ /*implicit*/ Twine (std::string_view LHS, const char *RHS)
+ : LHSKind (PtrAndLengthKind), RHSKind (CStringKind)
+ {
+ this->LHS.ptrAndLength.ptr = LHS.data ();
+ this->LHS.ptrAndLength.length = LHS.size ();
+ this->RHS.cString = RHS;
+ assert (isValid () && "Invalid twine!");
+ }
+
+ /// Since the intended use of twines is as temporary objects, assignments
+ /// when concatenating might cause undefined behavior or stack corruptions
+ Twine &
+ operator= (const Twine &) = delete;
+
+ /// Create a 'null' string, which is an empty string that always
+ /// concatenates to form another empty string.
+ static Twine
+ createNull ()
+ {
+ return Twine (NullKind);
+ }
+
+ /// @}
+ /// @name Numeric Conversions
+ /// @{
+
+ // Construct a twine to print \p Val as an unsigned hexadecimal integer.
+ static Twine
+ utohexstr (uint64_t Val)
+ {
+ Child LHS, RHS;
+ LHS.uHex = Val;
+ RHS.twine = nullptr;
+ return Twine (LHS, UHexKind, RHS, EmptyKind);
+ }
+
+ /// @}
+ /// @name Predicate Operations
+ /// @{
+
+ /// Check if this twine is trivially empty; a false return value does not
+ /// necessarily mean the twine is empty.
+ bool
+ isTriviallyEmpty () const
+ {
+ return isNullary ();
+ }
+
+ /// Return true if this twine can be dynamically accessed as a single
+ /// string view with getSingleStringView().
+ bool
+ isSingleStringView () const
+ {
+ if (getRHSKind () != EmptyKind)
+ {
+ return false;
+ }
+
+ switch (getLHSKind ())
+ {
+ case EmptyKind:
+ case CStringKind:
+ case StdStringKind:
+ case PtrAndLengthKind:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /// @}
+ /// @name String Operations
+ /// @{
+
+ Twine
+ concat (const Twine &Suffix) const;
+
+ /// @}
+ /// @name Output & Conversion.
+ /// @{
+
+ /// Return the twine contents as a std::string.
+ LIBBASE_ABI std::string
+ str () const;
+
+ /// Append the concatenated string into the given SmallString or SmallVector.
+ LIBBASE_ABI void
+ toVector (SmallVectorImpl<char> &Out) const;
+
+ /// This returns the twine as a single string view. This method is only valid
+ /// if isSingleStringView() is true.
+ std::string_view
+ getSingleStringView () const
+ {
+ assert (isSingleStringView () &&
+ "This cannot be had as a single string view!");
+ switch (getLHSKind ())
+ {
+ default:
+ libbase_unreachable ("Out of sync with isSingleStringView");
+ case EmptyKind:
+ return std::string_view ();
+ case CStringKind:
+ return std::string_view (LHS.cString);
+ case StdStringKind:
+ return std::string_view (*LHS.stdString);
+ case PtrAndLengthKind:
+ return std::string_view (LHS.ptrAndLength.ptr, LHS.ptrAndLength.length);
+ }
+ }
+
+ /// This returns the twine as a single string view if it can be
+ /// represented as such. Otherwise the twine is written into the given
+ /// SmallVector and a string view to the SmallVector's data is returned.
+ std::string_view
+ toStringView (SmallVectorImpl<char> &Out) const
+ {
+ if (isSingleStringView ())
+ {
+ return getSingleStringView ();
+ }
+ toVector (Out);
+ return std::string_view (Out.data (), Out.size ());
+ }
+
+ /// This returns the twine as a single null terminated string view if it
+ /// can be represented as such. Otherwise the twine is written into the
+ /// given SmallVector and a string view to the SmallVector's data is returned.
+ ///
+ /// The returned string view's size does not include the null terminator.
+ LIBBASE_ABI std::string_view
+ toNullTerminatedStringView (SmallVectorImpl<char> &Out) const;
+
+ /// Write the concatenated string represented by this twine to the
+ /// stream \p OS.
+ LIBBASE_ABI void
+ print (raw_ostream &OS) const;
+
+ /// Write the representation of this twine to the stream \p OS.
+ LIBBASE_ABI void
+ printRepr (raw_ostream &OS) const;
+
+#if !defined(NDEBUG) || defined(LIBBASE_ENABLE_DUMP)
+ /// Dump the concatenated string represented by this twine to stderr.
+ LIBBASE_DUMP_METHOD void
+ dump () const;
+
+ /// Dump the representation of this twine to stderr.
+ LIBBASE_DUMP_METHOD void
+ dumpRepr () const;
+#endif
+
+ /// @}
+};
+
+/// @name Twine Inline Implementations
+/// @{
+
+inline Twine
+Twine::concat (const Twine &Suffix) const
+{
+ // Concatenation with null is null.
+ if (isNull () || Suffix.isNull ())
+ {
+ return Twine (NullKind);
+ }
+
+ // Concatenation with empty yields the other side.
+ if (isEmpty ())
+ {
+ return Suffix;
+ }
+ if (Suffix.isEmpty ())
+ {
+ return *this;
+ }
+
+ // Otherwise we need to create a new node, taking care to fold in unary
+ // twines.
+ Child NewLHS, NewRHS;
+ NewLHS.twine = this;
+ NewRHS.twine = &Suffix;
+ NodeKind NewLHSKind = TwineKind, NewRHSKind = TwineKind;
+ if (isUnary ())
+ {
+ NewLHS = LHS;
+ NewLHSKind = getLHSKind ();
+ }
+ if (Suffix.isUnary ())
+ {
+ NewRHS = Suffix.LHS;
+ NewRHSKind = Suffix.getLHSKind ();
+ }
+
+ return Twine (NewLHS, NewLHSKind, NewRHS, NewRHSKind);
+}
+
+inline Twine
+operator+ (const Twine &LHS, const Twine &RHS)
+{
+ return LHS.concat (RHS);
+}
+
+/// Additional overload to guarantee simplified codegen; this is equivalent to
+/// concat().
+
+inline Twine
+operator+ (const char *LHS, std::string_view RHS)
+{
+ return Twine (LHS, RHS);
+}
+
+/// Additional overload to guarantee simplified codegen; this is equivalent to
+/// concat().
+
+inline Twine
+operator+ (std::string_view LHS, const char *RHS)
+{
+ return Twine (LHS, RHS);
+}
+
+inline raw_ostream &
+operator<< (raw_ostream &OS, const Twine &RHS)
+{
+ RHS.print (OS);
+ return OS;
+}
+
+/// @}
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_TWINE_H
diff --git a/libbase/include/core/adt/ilist.h b/libbase/include/core/adt/ilist.h
new file mode 100644
index 0000000..c3c769a
--- /dev/null
+++ b/libbase/include/core/adt/ilist.h
@@ -0,0 +1,546 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//==-- core/adt/ilist.h - Intrusive Linked List Template ---------*- C++ -*-==//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines classes to implement an intrusive doubly linked list class
+/// (i.e. each node of the list must contain a next and previous field for the
+/// list.
+///
+/// The ilist class itself should be a plug in replacement for list. This list
+/// replacement does not provide a constant time size() method, so be careful to
+/// use empty() when you really want to know if it's empty.
+///
+/// The ilist class is implemented as a circular list. The list itself contains
+/// a sentinel node, whose Next points at begin() and whose Prev points at
+/// rbegin(). The sentinel node itself serves as end() and rend().
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_H
+#define LIBBASE_ADT_ILIST_H
+
+#include "core/adt/simple_ilist.h"
+#include <cassert>
+#include <cstddef>
+#include <iterator>
+
+namespace libbase
+{
+
+/// Use delete by default for iplist and ilist.
+///
+/// Specialize this to get different behaviour for ownership-related API. (If
+/// you really want ownership semantics, consider using std::list or building
+/// something like \a BumpPtrList.)
+///
+/// \see ilist_noalloc_traits
+template <typename NodeTy> struct ilist_alloc_traits
+{
+ static void
+ deleteNode (NodeTy *V)
+ {
+ delete V;
+ }
+};
+
+/// Custom traits to do nothing on deletion.
+///
+/// Specialize ilist_alloc_traits to inherit from this to disable the
+/// non-intrusive deletion in iplist (which implies ownership).
+///
+/// If you want purely intrusive semantics with no callbacks, consider using \a
+/// simple_ilist instead.
+///
+/// \code
+/// template <>
+/// struct ilist_alloc_traits<MyType> : ilist_noalloc_traits<MyType> {};
+/// \endcode
+template <typename NodeTy> struct ilist_noalloc_traits
+{
+ static void
+ deleteNode (NodeTy *V)
+ {
+ }
+};
+
+/// Callbacks do nothing by default in iplist and ilist.
+///
+/// Specialize this for to use callbacks for when nodes change their list
+/// membership.
+template <typename NodeTy> struct ilist_callback_traits
+{
+ void
+ addNodeToList (NodeTy *)
+ {
+ }
+ void
+ removeNodeFromList (NodeTy *)
+ {
+ }
+
+ /// Callback before transferring nodes to this list. The nodes may already be
+ /// in this same list.
+ template <class Iterator>
+ void
+ transferNodesFromList (ilist_callback_traits &OldList, Iterator /*first*/,
+ Iterator /*last*/)
+ {
+ (void)OldList;
+ }
+};
+
+/// A fragment for template traits for intrusive list that provides default
+/// node related operations.
+///
+/// TODO: Remove this layer of indirection. It's not necessary.
+template <typename NodeTy>
+struct ilist_node_traits : ilist_alloc_traits<NodeTy>,
+ ilist_callback_traits<NodeTy>
+{
+};
+
+/// Template traits for intrusive list.
+///
+/// Customize callbacks and allocation semantics.
+template <typename NodeTy>
+struct ilist_traits : public ilist_node_traits<NodeTy>
+{
+};
+
+/// Const traits should never be instantiated.
+template <typename Ty> struct ilist_traits<const Ty>
+{
+};
+
+//===----------------------------------------------------------------------===//
+//
+/// A wrapper around an intrusive list with callbacks and non-intrusive
+/// ownership.
+///
+/// This wraps a purely intrusive list (like simple_ilist) with a configurable
+/// traits class. The traits can implement callbacks and customize the
+/// ownership semantics.
+///
+/// This is a subset of ilist functionality that can safely be used on nodes of
+/// polymorphic types, i.e. a heterogeneous list with a common base class that
+/// holds the next/prev pointers. The only state of the list itself is an
+/// ilist_sentinel, which holds pointers to the first and last nodes in the
+/// list.
+template <class IntrusiveListT, class TraitsT>
+class iplist_impl : public TraitsT, IntrusiveListT
+{
+ using base_list_type = IntrusiveListT;
+
+public:
+ using pointer = typename base_list_type::pointer;
+ using const_pointer = typename base_list_type::const_pointer;
+ using reference = typename base_list_type::reference;
+ using const_reference = typename base_list_type::const_reference;
+ using value_type = typename base_list_type::value_type;
+ using size_type = typename base_list_type::size_type;
+ using difference_type = typename base_list_type::difference_type;
+ using iterator = typename base_list_type::iterator;
+ using const_iterator = typename base_list_type::const_iterator;
+ using reverse_iterator = typename base_list_type::reverse_iterator;
+ using const_reverse_iterator =
+ typename base_list_type::const_reverse_iterator;
+
+private:
+ static bool
+ op_less (const_reference L, const_reference R)
+ {
+ return L < R;
+ }
+ static bool
+ op_equal (const_reference L, const_reference R)
+ {
+ return L == R;
+ }
+
+public:
+ iplist_impl () = default;
+
+ iplist_impl (const iplist_impl &) = delete;
+ iplist_impl &
+ operator= (const iplist_impl &) = delete;
+
+ iplist_impl (iplist_impl &&X)
+ : TraitsT (std::move (static_cast<TraitsT &> (X))),
+ IntrusiveListT (std::move (static_cast<IntrusiveListT &> (X)))
+ {
+ }
+ iplist_impl &
+ operator= (iplist_impl &&X)
+ {
+ *static_cast<TraitsT *> (this) = std::move (static_cast<TraitsT &> (X));
+ *static_cast<IntrusiveListT *> (this) =
+ std::move (static_cast<IntrusiveListT &> (X));
+ return *this;
+ }
+
+ ~iplist_impl ()
+ {
+ clear ();
+ }
+
+ // Miscellaneous inspection routines.
+ size_type
+ max_size () const
+ {
+ return size_type (-1);
+ }
+
+ using base_list_type::back;
+ using base_list_type::begin;
+ using base_list_type::empty;
+ using base_list_type::end;
+ using base_list_type::front;
+ using base_list_type::rbegin;
+ using base_list_type::rend;
+
+ void
+ swap (iplist_impl &RHS)
+ {
+ assert (0 && "Swap does not use list traits callback correctly yet!");
+ base_list_type::swap (RHS);
+ }
+
+ iterator
+ insert (iterator where, pointer New)
+ {
+ this->addNodeToList (New); // Notify traits that we added a node...
+ return base_list_type::insert (where, *New);
+ }
+
+ iterator
+ insert (iterator where, const_reference New)
+ {
+ return this->insert (where, new value_type (New));
+ }
+
+ iterator
+ insertAfter (iterator where, pointer New)
+ {
+ if (empty ())
+ {
+ return insert (begin (), New);
+ }
+ else
+ {
+ return insert (++where, New);
+ }
+ }
+
+ /// Clone another list.
+ template <class Cloner>
+ void
+ cloneFrom (const iplist_impl &L2, Cloner clone)
+ {
+ clear ();
+ for (const_reference V : L2)
+ {
+ push_back (clone (V));
+ }
+ }
+
+ pointer
+ remove (iterator &IT)
+ {
+ pointer Node = &*IT++;
+ this->removeNodeFromList (Node); // Notify traits that we removed a node...
+ base_list_type::remove (*Node);
+ return Node;
+ }
+
+ pointer
+ remove (const iterator &IT)
+ {
+ iterator MutIt = IT;
+ return remove (MutIt);
+ }
+
+ pointer
+ remove (pointer IT)
+ {
+ return remove (iterator (IT));
+ }
+ pointer
+ remove (reference IT)
+ {
+ return remove (iterator (IT));
+ }
+
+ // erase - remove a node from the controlled sequence... and delete it.
+ iterator
+ erase (iterator where)
+ {
+ this->deleteNode (remove (where));
+ return where;
+ }
+
+ iterator
+ erase (pointer IT)
+ {
+ return erase (iterator (IT));
+ }
+ iterator
+ erase (reference IT)
+ {
+ return erase (iterator (IT));
+ }
+
+ /// Remove all nodes from the list like clear(), but do not call
+ /// removeNodeFromList() or deleteNode().
+ ///
+ /// This should only be used immediately before freeing nodes in bulk to
+ /// avoid traversing the list and bringing all the nodes into cache.
+ void
+ clearAndLeakNodesUnsafely ()
+ {
+ base_list_type::clear ();
+ }
+
+private:
+ // transfer - The heart of the splice function. Move linked list nodes from
+ // [first, last) into position.
+ //
+ void
+ transfer (iterator position, iplist_impl &L2, iterator first, iterator last)
+ {
+ if (position == last)
+ {
+ return;
+ }
+
+ // Notify traits we moved the nodes...
+ this->transferNodesFromList (L2, first, last);
+
+ base_list_type::splice (position, L2, first, last);
+ }
+
+public:
+ //===----------------------------------------------------------------------===
+ // Functionality derived from other functions defined above...
+ //
+
+ using base_list_type::size;
+
+ iterator
+ erase (iterator first, iterator last)
+ {
+ while (first != last)
+ {
+ first = erase (first);
+ }
+ return last;
+ }
+
+ void
+ clear ()
+ {
+ erase (begin (), end ());
+ }
+
+ // Front and back inserters...
+ void
+ push_front (pointer val)
+ {
+ insert (begin (), val);
+ }
+ void
+ push_back (pointer val)
+ {
+ insert (end (), val);
+ }
+ void
+ pop_front ()
+ {
+ assert (!empty () && "pop_front() on empty list!");
+ erase (begin ());
+ }
+ void
+ pop_back ()
+ {
+ assert (!empty () && "pop_back() on empty list!");
+ iterator t = end ();
+ erase (--t);
+ }
+
+ // Special forms of insert...
+ template <class InIt>
+ void
+ insert (iterator where, InIt first, InIt last)
+ {
+ for (; first != last; ++first)
+ {
+ insert (where, *first);
+ }
+ }
+
+ // Splice members - defined in terms of transfer...
+ void
+ splice (iterator where, iplist_impl &L2)
+ {
+ if (!L2.empty ())
+ {
+ transfer (where, L2, L2.begin (), L2.end ());
+ }
+ }
+ void
+ splice (iterator where, iplist_impl &L2, iterator first)
+ {
+ iterator last = first;
+ ++last;
+ if (where == first || where == last)
+ {
+ return; // No change
+ }
+ transfer (where, L2, first, last);
+ }
+ void
+ splice (iterator where, iplist_impl &L2, iterator first, iterator last)
+ {
+ if (first != last)
+ {
+ transfer (where, L2, first, last);
+ }
+ }
+ void
+ splice (iterator where, iplist_impl &L2, reference N)
+ {
+ splice (where, L2, iterator (N));
+ }
+ void
+ splice (iterator where, iplist_impl &L2, pointer N)
+ {
+ splice (where, L2, iterator (N));
+ }
+
+ template <class Compare>
+ void
+ merge (iplist_impl &Right, Compare comp)
+ {
+ if (this == &Right)
+ {
+ return;
+ }
+ this->transferNodesFromList (Right, Right.begin (), Right.end ());
+ base_list_type::merge (Right, comp);
+ }
+ void
+ merge (iplist_impl &Right)
+ {
+ return merge (Right, op_less);
+ }
+
+ using base_list_type::sort;
+
+ /// Get the previous node, or \c nullptr for the list head.
+ pointer
+ getPrevNode (reference N) const
+ {
+ auto I = N.getIterator ();
+ if (I == begin ())
+ {
+ return nullptr;
+ }
+ return &*std::prev (I);
+ }
+ /// Get the previous node, or \c nullptr for the list head.
+ const_pointer
+ getPrevNode (const_reference N) const
+ {
+ return getPrevNode (const_cast<reference> (N));
+ }
+
+ /// Get the next node, or \c nullptr for the list tail.
+ pointer
+ getNextNode (reference N) const
+ {
+ auto Next = std::next (N.getIterator ());
+ if (Next == end ())
+ {
+ return nullptr;
+ }
+ return &*Next;
+ }
+ /// Get the next node, or \c nullptr for the list tail.
+ const_pointer
+ getNextNode (const_reference N) const
+ {
+ return getNextNode (const_cast<reference> (N));
+ }
+};
+
+/// An intrusive list with ownership and callbacks specified/controlled by
+/// ilist_traits, only with API safe for polymorphic types.
+///
+/// The \p Options parameters are the same as those for \a simple_ilist. See
+/// there for a description of what's available.
+template <class T, class... Options>
+class iplist : public iplist_impl<simple_ilist<T, Options...>, ilist_traits<T>>
+{
+ using iplist_impl_type = typename iplist::iplist_impl;
+
+public:
+ iplist () = default;
+
+ iplist (const iplist &X) = delete;
+ iplist &
+ operator= (const iplist &X) = delete;
+
+ iplist (iplist &&X) : iplist_impl_type (std::move (X))
+ {
+ }
+ iplist &
+ operator= (iplist &&X)
+ {
+ *static_cast<iplist_impl_type *> (this) = std::move (X);
+ return *this;
+ }
+};
+
+template <class T, class... Options> using ilist = iplist<T, Options...>;
+
+} // end namespace libbase
+
+namespace std
+{
+
+// Ensure that swap uses the fast list swap...
+template <class Ty>
+void
+swap (libbase::iplist<Ty> &Left, libbase::iplist<Ty> &Right)
+{
+ Left.swap (Right);
+}
+
+} // end namespace std
+
+#endif // LIBBASE_ADT_ILIST_H
diff --git a/libbase/include/core/adt/ilist_base.h b/libbase/include/core/adt/ilist_base.h
new file mode 100644
index 0000000..3aa8d11
--- /dev/null
+++ b/libbase/include/core/adt/ilist_base.h
@@ -0,0 +1,139 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/ilist_base.h - Intrusive List Base --------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_BASE_H
+#define LIBBASE_ADT_ILIST_BASE_H
+
+#include "core/adt/ilist_node_base.h"
+#include <cassert>
+
+namespace libbase
+{
+
+/// Implementations of list algorithms using ilist_node_base.
+template <bool EnableSentinelTracking, class ParentTy> class ilist_base
+{
+public:
+ using node_base_type = ilist_node_base<EnableSentinelTracking, ParentTy>;
+
+ static void
+ insertBeforeImpl (node_base_type &Next, node_base_type &N)
+ {
+ node_base_type &Prev = *Next.getPrev ();
+ N.setNext (&Next);
+ N.setPrev (&Prev);
+ Prev.setNext (&N);
+ Next.setPrev (&N);
+ }
+
+ static void
+ removeImpl (node_base_type &N)
+ {
+ node_base_type *Prev = N.getPrev ();
+ node_base_type *Next = N.getNext ();
+ Next->setPrev (Prev);
+ Prev->setNext (Next);
+
+ // Not strictly necessary, but helps catch a class of bugs.
+ N.setPrev (nullptr);
+ N.setNext (nullptr);
+ }
+
+ static void
+ removeRangeImpl (node_base_type &First, node_base_type &Last)
+ {
+ node_base_type *Prev = First.getPrev ();
+ node_base_type *Final = Last.getPrev ();
+ Last.setPrev (Prev);
+ Prev->setNext (&Last);
+
+ // Not strictly necessary, but helps catch a class of bugs.
+ First.setPrev (nullptr);
+ Final->setNext (nullptr);
+ }
+
+ static void
+ transferBeforeImpl (node_base_type &Next, node_base_type &First,
+ node_base_type &Last)
+ {
+ if (&Next == &Last || &First == &Last)
+ {
+ return;
+ }
+
+ // Position cannot be contained in the range to be transferred.
+ assert (&Next != &First &&
+ // Check for the most common mistake.
+ "Insertion point can't be one of the transferred nodes");
+
+ node_base_type &Final = *Last.getPrev ();
+
+ // Detach from old list/position.
+ First.getPrev ()->setNext (&Last);
+ Last.setPrev (First.getPrev ());
+
+ // Splice [First, Final] into its new list/position.
+ node_base_type &Prev = *Next.getPrev ();
+ Final.setNext (&Next);
+ First.setPrev (&Prev);
+ Prev.setNext (&First);
+ Next.setPrev (&Final);
+ }
+
+ template <class T>
+ static void
+ insertBefore (T &Next, T &N)
+ {
+ insertBeforeImpl (Next, N);
+ }
+
+ template <class T>
+ static void
+ remove (T &N)
+ {
+ removeImpl (N);
+ }
+ template <class T>
+ static void
+ removeRange (T &First, T &Last)
+ {
+ removeRangeImpl (First, Last);
+ }
+
+ template <class T>
+ static void
+ transferBefore (T &Next, T &First, T &Last)
+ {
+ transferBeforeImpl (Next, First, Last);
+ }
+};
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_ILIST_BASE_H
diff --git a/libbase/include/core/adt/ilist_iterator.h b/libbase/include/core/adt/ilist_iterator.h
new file mode 100644
index 0000000..90985d8
--- /dev/null
+++ b/libbase/include/core/adt/ilist_iterator.h
@@ -0,0 +1,591 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/ilist_iterator.h - Intrusive List Iterator ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_ITERATOR_H
+#define LIBBASE_ADT_ILIST_ITERATOR_H
+
+#include "core/adt/ilist_node.h"
+#include <cassert>
+#include <cstddef>
+#include <iterator>
+#include <type_traits>
+
+namespace libbase
+{
+
+namespace ilist_detail
+{
+
+/// Find const-correct node types.
+template <class OptionsT, bool IsConst> struct IteratorTraits;
+template <class OptionsT> struct IteratorTraits<OptionsT, false>
+{
+ using value_type = typename OptionsT::value_type;
+ using pointer = typename OptionsT::pointer;
+ using reference = typename OptionsT::reference;
+ using node_pointer = ilist_node_impl<OptionsT> *;
+ using node_reference = ilist_node_impl<OptionsT> &;
+};
+template <class OptionsT> struct IteratorTraits<OptionsT, true>
+{
+ using value_type = const typename OptionsT::value_type;
+ using pointer = typename OptionsT::const_pointer;
+ using reference = typename OptionsT::const_reference;
+ using node_pointer = const ilist_node_impl<OptionsT> *;
+ using node_reference = const ilist_node_impl<OptionsT> &;
+};
+
+template <bool IsReverse> struct IteratorHelper;
+template <> struct IteratorHelper<false> : ilist_detail::NodeAccess
+{
+ using Access = ilist_detail::NodeAccess;
+
+ template <class T>
+ static void
+ increment (T *&I)
+ {
+ I = Access::getNext (*I);
+ }
+ template <class T>
+ static void
+ decrement (T *&I)
+ {
+ I = Access::getPrev (*I);
+ }
+};
+template <> struct IteratorHelper<true> : ilist_detail::NodeAccess
+{
+ using Access = ilist_detail::NodeAccess;
+
+ template <class T>
+ static void
+ increment (T *&I)
+ {
+ I = Access::getPrev (*I);
+ }
+ template <class T>
+ static void
+ decrement (T *&I)
+ {
+ I = Access::getNext (*I);
+ }
+};
+
+/// Mixin class used to add a \a getNodeParent() function to iterators iff the
+/// list uses \a ilist_parent, calling through to the node's \a getParent(). For
+/// more details see \a ilist_node.
+template <class IteratorTy, class ParentTy, bool IsConst>
+class iterator_parent_access;
+template <class IteratorTy, class ParentTy>
+class iterator_parent_access<IteratorTy, ParentTy, true>
+{
+public:
+ inline const ParentTy *
+ getNodeParent () const
+ {
+ return static_cast<const IteratorTy *> (this)->NodePtr->getParent ();
+ }
+};
+template <class IteratorTy, class ParentTy>
+class iterator_parent_access<IteratorTy, ParentTy, false>
+{
+public:
+ inline ParentTy *
+ getNodeParent () const
+ {
+ return static_cast<const IteratorTy *> (this)->NodePtr->getParent ();
+ }
+};
+template <class IteratorTy> class iterator_parent_access<IteratorTy, void, true>
+{
+};
+template <class IteratorTy>
+class iterator_parent_access<IteratorTy, void, false>
+{
+};
+
+} // end namespace ilist_detail
+
+/// Iterator for intrusive lists based on ilist_node.
+template <class OptionsT, bool IsReverse, bool IsConst>
+class ilist_iterator : ilist_detail::SpecificNodeAccess<OptionsT>,
+ public ilist_detail::iterator_parent_access<
+ ilist_iterator<OptionsT, IsReverse, IsConst>,
+ typename OptionsT::parent_ty, IsConst>
+{
+ friend ilist_iterator<OptionsT, IsReverse, !IsConst>;
+ friend ilist_iterator<OptionsT, !IsReverse, IsConst>;
+ friend ilist_iterator<OptionsT, !IsReverse, !IsConst>;
+ friend ilist_detail::iterator_parent_access<
+ ilist_iterator<OptionsT, IsReverse, IsConst>,
+ typename OptionsT::parent_ty, IsConst>;
+
+ using Traits = ilist_detail::IteratorTraits<OptionsT, IsConst>;
+ using Access = ilist_detail::SpecificNodeAccess<OptionsT>;
+
+public:
+ using value_type = typename Traits::value_type;
+ using pointer = typename Traits::pointer;
+ using reference = typename Traits::reference;
+ using difference_type = ptrdiff_t;
+ using iterator_category = std::bidirectional_iterator_tag;
+ using const_pointer = typename OptionsT::const_pointer;
+ using const_reference = typename OptionsT::const_reference;
+
+private:
+ using node_pointer = typename Traits::node_pointer;
+ using node_reference = typename Traits::node_reference;
+
+ node_pointer NodePtr = nullptr;
+
+public:
+ /// Create from an ilist_node.
+ explicit ilist_iterator (node_reference N) : NodePtr (&N)
+ {
+ }
+
+ explicit ilist_iterator (pointer NP) : NodePtr (Access::getNodePtr (NP))
+ {
+ }
+ explicit ilist_iterator (reference NR) : NodePtr (Access::getNodePtr (&NR))
+ {
+ }
+ ilist_iterator () = default;
+
+ // This is templated so that we can allow constructing a const iterator from
+ // a nonconst iterator...
+ template <bool RHSIsConst>
+ ilist_iterator (const ilist_iterator<OptionsT, IsReverse, RHSIsConst> &RHS,
+ std::enable_if_t<IsConst || !RHSIsConst, void *> = nullptr)
+ : NodePtr (RHS.NodePtr)
+ {
+ }
+
+ // This is templated so that we can allow assigning to a const iterator from
+ // a nonconst iterator...
+ template <bool RHSIsConst>
+ std::enable_if_t<IsConst || !RHSIsConst, ilist_iterator &>
+ operator= (const ilist_iterator<OptionsT, IsReverse, RHSIsConst> &RHS)
+ {
+ NodePtr = RHS.NodePtr;
+ return *this;
+ }
+
+ /// Explicit conversion between forward/reverse iterators.
+ ///
+ /// Translate between forward and reverse iterators without changing range
+ /// boundaries. The resulting iterator will dereference (and have a handle)
+ /// to the previous node, which is somewhat unexpected; but converting the
+ /// two endpoints in a range will give the same range in reverse.
+ ///
+ /// This matches std::reverse_iterator conversions.
+ explicit ilist_iterator (
+ const ilist_iterator<OptionsT, !IsReverse, IsConst> &RHS)
+ : ilist_iterator (++RHS.getReverse ())
+ {
+ }
+
+ /// Get a reverse iterator to the same node.
+ ///
+ /// Gives a reverse iterator that will dereference (and have a handle) to the
+ /// same node. Converting the endpoint iterators in a range will give a
+ /// different range; for range operations, use the explicit conversions.
+ ilist_iterator<OptionsT, !IsReverse, IsConst>
+ getReverse () const
+ {
+ if (NodePtr)
+ {
+ return ilist_iterator<OptionsT, !IsReverse, IsConst> (*NodePtr);
+ }
+ return ilist_iterator<OptionsT, !IsReverse, IsConst> ();
+ }
+
+ /// Const-cast.
+ ilist_iterator<OptionsT, IsReverse, false>
+ getNonConst () const
+ {
+ if (NodePtr)
+ {
+ return ilist_iterator<OptionsT, IsReverse, false> (
+ const_cast<typename ilist_iterator<OptionsT, IsReverse,
+ false>::node_reference> (
+ *NodePtr));
+ }
+ return ilist_iterator<OptionsT, IsReverse, false> ();
+ }
+
+ // Accessors...
+ reference
+ operator* () const
+ {
+ assert (!NodePtr->isKnownSentinel ());
+ return *Access::getValuePtr (NodePtr);
+ }
+ pointer
+ operator->() const
+ {
+ return &operator* ();
+ }
+
+ // Comparison operators
+ friend bool
+ operator== (const ilist_iterator &LHS, const ilist_iterator &RHS)
+ {
+ return LHS.NodePtr == RHS.NodePtr;
+ }
+ friend bool
+ operator!= (const ilist_iterator &LHS, const ilist_iterator &RHS)
+ {
+ return LHS.NodePtr != RHS.NodePtr;
+ }
+
+ // Increment and decrement operators...
+ ilist_iterator &
+ operator-- ()
+ {
+ NodePtr = IsReverse ? NodePtr->getNext () : NodePtr->getPrev ();
+ return *this;
+ }
+ ilist_iterator &
+ operator++ ()
+ {
+ NodePtr = IsReverse ? NodePtr->getPrev () : NodePtr->getNext ();
+ return *this;
+ }
+ ilist_iterator
+ operator-- (int)
+ {
+ ilist_iterator tmp = *this;
+ --*this;
+ return tmp;
+ }
+ ilist_iterator
+ operator++ (int)
+ {
+ ilist_iterator tmp = *this;
+ ++*this;
+ return tmp;
+ }
+
+ bool
+ isValid () const
+ {
+ return NodePtr;
+ }
+
+ /// Get the underlying ilist_node.
+ node_pointer
+ getNodePtr () const
+ {
+ return static_cast<node_pointer> (NodePtr);
+ }
+
+ /// Check for end. Only valid if ilist_sentinel_tracking<true>.
+ bool
+ isEnd () const
+ {
+ return NodePtr ? NodePtr->isSentinel () : false;
+ }
+};
+
+/// Iterator for intrusive lists based on ilist_node. Much like ilist_iterator,
+/// but with the addition of two bits recording whether this position (when in
+/// a range) is half or fully open.
+template <class OptionsT, bool IsReverse, bool IsConst>
+class ilist_iterator_w_bits
+ : ilist_detail::SpecificNodeAccess<OptionsT>,
+ public ilist_detail::iterator_parent_access<
+ ilist_iterator_w_bits<OptionsT, IsReverse, IsConst>,
+ typename OptionsT::parent_ty, IsConst>
+{
+ friend ilist_iterator_w_bits<OptionsT, IsReverse, !IsConst>;
+ friend ilist_iterator_w_bits<OptionsT, !IsReverse, IsConst>;
+ friend ilist_iterator<OptionsT, !IsReverse, !IsConst>;
+ friend ilist_detail::iterator_parent_access<
+ ilist_iterator_w_bits<OptionsT, IsReverse, IsConst>,
+ typename OptionsT::parent_ty, IsConst>;
+
+ using Traits = ilist_detail::IteratorTraits<OptionsT, IsConst>;
+ using Access = ilist_detail::SpecificNodeAccess<OptionsT>;
+
+public:
+ using value_type = typename Traits::value_type;
+ using pointer = typename Traits::pointer;
+ using reference = typename Traits::reference;
+ using difference_type = ptrdiff_t;
+ using iterator_category = std::bidirectional_iterator_tag;
+ using const_pointer = typename OptionsT::const_pointer;
+ using const_reference = typename OptionsT::const_reference;
+
+private:
+ using node_pointer = typename Traits::node_pointer;
+ using node_reference = typename Traits::node_reference;
+
+ node_pointer NodePtr = nullptr;
+
+ /// Is this position intended to contain any debug-info immediately before
+ /// the position?
+ mutable bool HeadInclusiveBit = false;
+ /// Is this position intended to contain any debug-info immediately after
+ /// the position?
+ mutable bool TailInclusiveBit = false;
+
+public:
+ /// Create from an ilist_node.
+ explicit ilist_iterator_w_bits (node_reference N) : NodePtr (&N)
+ {
+ }
+
+ explicit ilist_iterator_w_bits (pointer NP)
+ : NodePtr (Access::getNodePtr (NP))
+ {
+ }
+ explicit ilist_iterator_w_bits (reference NR)
+ : NodePtr (Access::getNodePtr (&NR))
+ {
+ }
+ ilist_iterator_w_bits () = default;
+
+ // This is templated so that we can allow constructing a const iterator from
+ // a nonconst iterator...
+ template <bool RHSIsConst>
+ ilist_iterator_w_bits (
+ const ilist_iterator_w_bits<OptionsT, IsReverse, RHSIsConst> &RHS,
+ std::enable_if_t<IsConst || !RHSIsConst, void *> = nullptr)
+ : NodePtr (RHS.NodePtr)
+ {
+ HeadInclusiveBit = RHS.HeadInclusiveBit;
+ TailInclusiveBit = RHS.TailInclusiveBit;
+ }
+
+ // This is templated so that we can allow assigning to a const iterator from
+ // a nonconst iterator...
+ template <bool RHSIsConst>
+ std::enable_if_t<IsConst || !RHSIsConst, ilist_iterator_w_bits &>
+ operator= (const ilist_iterator_w_bits<OptionsT, IsReverse, RHSIsConst> &RHS)
+ {
+ NodePtr = RHS.NodePtr;
+ HeadInclusiveBit = RHS.HeadInclusiveBit;
+ TailInclusiveBit = RHS.TailInclusiveBit;
+ return *this;
+ }
+
+ /// Explicit conversion between forward/reverse iterators.
+ ///
+ /// Translate between forward and reverse iterators without changing range
+ /// boundaries. The resulting iterator will dereference (and have a handle)
+ /// to the previous node, which is somewhat unexpected; but converting the
+ /// two endpoints in a range will give the same range in reverse.
+ ///
+ /// This matches std::reverse_iterator conversions.
+ explicit ilist_iterator_w_bits (
+ const ilist_iterator_w_bits<OptionsT, !IsReverse, IsConst> &RHS)
+ : ilist_iterator_w_bits (++RHS.getReverse ())
+ {
+ }
+
+ /// Get a reverse iterator to the same node.
+ ///
+ /// Gives a reverse iterator that will dereference (and have a handle) to the
+ /// same node. Converting the endpoint iterators in a range will give a
+ /// different range; for range operations, use the explicit conversions.
+ ilist_iterator_w_bits<OptionsT, !IsReverse, IsConst>
+ getReverse () const
+ {
+ if (NodePtr)
+ {
+ return ilist_iterator_w_bits<OptionsT, !IsReverse, IsConst> (*NodePtr);
+ }
+ return ilist_iterator_w_bits<OptionsT, !IsReverse, IsConst> ();
+ }
+
+ /// Const-cast.
+ ilist_iterator_w_bits<OptionsT, IsReverse, false>
+ getNonConst () const
+ {
+ if (NodePtr)
+ {
+ auto New = ilist_iterator_w_bits<OptionsT, IsReverse, false> (
+ const_cast<typename ilist_iterator_w_bits<OptionsT, IsReverse,
+ false>::node_reference> (
+ *NodePtr));
+ New.HeadInclusiveBit = HeadInclusiveBit;
+ New.TailInclusiveBit = TailInclusiveBit;
+ return New;
+ }
+ return ilist_iterator_w_bits<OptionsT, IsReverse, false> ();
+ }
+
+ // Accessors...
+ reference
+ operator* () const
+ {
+ assert (!NodePtr->isKnownSentinel ());
+ return *Access::getValuePtr (NodePtr);
+ }
+ pointer
+ operator->() const
+ {
+ return &operator* ();
+ }
+
+ // Comparison operators
+ friend bool
+ operator== (const ilist_iterator_w_bits &LHS,
+ const ilist_iterator_w_bits &RHS)
+ {
+ return LHS.NodePtr == RHS.NodePtr;
+ }
+ friend bool
+ operator!= (const ilist_iterator_w_bits &LHS,
+ const ilist_iterator_w_bits &RHS)
+ {
+ return LHS.NodePtr != RHS.NodePtr;
+ }
+
+ // Increment and decrement operators...
+ ilist_iterator_w_bits &
+ operator-- ()
+ {
+ NodePtr = IsReverse ? NodePtr->getNext () : NodePtr->getPrev ();
+ HeadInclusiveBit = false;
+ TailInclusiveBit = false;
+ return *this;
+ }
+ ilist_iterator_w_bits &
+ operator++ ()
+ {
+ NodePtr = IsReverse ? NodePtr->getPrev () : NodePtr->getNext ();
+ HeadInclusiveBit = false;
+ TailInclusiveBit = false;
+ return *this;
+ }
+ ilist_iterator_w_bits
+ operator-- (int)
+ {
+ ilist_iterator_w_bits tmp = *this;
+ --*this;
+ return tmp;
+ }
+ ilist_iterator_w_bits
+ operator++ (int)
+ {
+ ilist_iterator_w_bits tmp = *this;
+ ++*this;
+ return tmp;
+ }
+
+ bool
+ isValid () const
+ {
+ return NodePtr;
+ }
+
+ /// Get the underlying ilist_node.
+ node_pointer
+ getNodePtr () const
+ {
+ return static_cast<node_pointer> (NodePtr);
+ }
+
+ /// Check for end. Only valid if ilist_sentinel_tracking<true>.
+ bool
+ isEnd () const
+ {
+ return NodePtr ? NodePtr->isSentinel () : false;
+ }
+
+ bool
+ getHeadBit () const
+ {
+ return HeadInclusiveBit;
+ }
+ bool
+ getTailBit () const
+ {
+ return TailInclusiveBit;
+ }
+ void
+ setHeadBit (bool SetBit) const
+ {
+ HeadInclusiveBit = SetBit;
+ }
+ void
+ setTailBit (bool SetBit) const
+ {
+ TailInclusiveBit = SetBit;
+ }
+};
+
+template <typename From> struct simplify_type;
+
+/// Allow ilist_iterators to convert into pointers to a node automatically when
+/// used by the dyn_cast, cast, isa mechanisms...
+///
+/// FIXME: remove this, since there is no implicit conversion to NodeTy.
+template <class OptionsT, bool IsConst>
+struct simplify_type<ilist_iterator<OptionsT, false, IsConst>>
+{
+ using iterator = ilist_iterator<OptionsT, false, IsConst>;
+ using SimpleType = typename iterator::pointer;
+
+ static SimpleType
+ getSimplifiedValue (const iterator &Node)
+ {
+ return &*Node;
+ }
+};
+template <class OptionsT, bool IsConst>
+struct simplify_type<const ilist_iterator<OptionsT, false, IsConst>>
+ : simplify_type<ilist_iterator<OptionsT, false, IsConst>>
+{
+};
+
+// ilist_iterator_w_bits should also be accessible via isa/dyn_cast.
+template <class OptionsT, bool IsConst>
+struct simplify_type<ilist_iterator_w_bits<OptionsT, false, IsConst>>
+{
+ using iterator = ilist_iterator_w_bits<OptionsT, false, IsConst>;
+ using SimpleType = typename iterator::pointer;
+
+ static SimpleType
+ getSimplifiedValue (const iterator &Node)
+ {
+ return &*Node;
+ }
+};
+template <class OptionsT, bool IsConst>
+struct simplify_type<const ilist_iterator_w_bits<OptionsT, false, IsConst>>
+ : simplify_type<ilist_iterator_w_bits<OptionsT, false, IsConst>>
+{
+};
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_ILIST_ITERATOR_H
diff --git a/libbase/include/core/adt/ilist_node.h b/libbase/include/core/adt/ilist_node.h
new file mode 100644
index 0000000..9ca9ed3
--- /dev/null
+++ b/libbase/include/core/adt/ilist_node.h
@@ -0,0 +1,470 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/ilist_node.h - Intrusive Linked List Helper -----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines the ilist_node class template, which is a convenient
+/// base class for creating classes that can be used with ilists.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_NODE_H
+#define LIBBASE_ADT_ILIST_NODE_H
+
+#include "core/adt/ilist_node_base.h"
+#include "core/adt/ilist_node_options.h"
+
+#include <type_traits>
+
+namespace libbase
+{
+
+namespace ilist_detail
+{
+
+struct NodeAccess;
+
+/// Mixin base class that is used to add \a getParent() and
+/// \a setParent(ParentTy*) methods to \a ilist_node_impl iff \a ilist_parent
+/// has been set in the list options.
+template <class NodeTy, class ParentTy> class node_parent_access
+{
+public:
+ inline const ParentTy *
+ getParent () const
+ {
+ return static_cast<const NodeTy *> (this)->getNodeBaseParent ();
+ }
+ inline ParentTy *
+ getParent ()
+ {
+ return static_cast<NodeTy *> (this)->getNodeBaseParent ();
+ }
+ void
+ setParent (ParentTy *Parent)
+ {
+ return static_cast<NodeTy *> (this)->setNodeBaseParent (Parent);
+ }
+};
+template <class NodeTy> class node_parent_access<NodeTy, void>
+{
+};
+
+} // end namespace ilist_detail
+
+template <class OptionsT, bool IsReverse, bool IsConst> class ilist_iterator;
+template <class OptionsT, bool IsReverse, bool IsConst>
+class ilist_iterator_w_bits;
+template <class OptionsT> class ilist_sentinel;
+
+// Selector for which iterator type to pick given the iterator-bits node option.
+template <class OptionsT, bool IsReverse, bool IsConst>
+using ilist_select_iterator_type =
+ std::conditional_t<OptionsT::has_iterator_bits,
+ ilist_iterator_w_bits<OptionsT, IsReverse, IsConst>,
+ ilist_iterator<OptionsT, IsReverse, IsConst>>;
+
+/// Implementation for an ilist node.
+///
+/// Templated on an appropriate \a ilist_detail::node_options, usually computed
+/// by \a ilist_detail::compute_node_options.
+///
+/// This is a wrapper around \a ilist_node_base whose main purpose is to
+/// provide type safety: you can't insert nodes of \a ilist_node_impl into the
+/// wrong \a simple_ilist or \a iplist.
+template <class OptionsT>
+class ilist_node_impl
+ : OptionsT::node_base_type,
+ public ilist_detail::node_parent_access<ilist_node_impl<OptionsT>,
+ typename OptionsT::parent_ty>
+{
+ using value_type = typename OptionsT::value_type;
+ using node_base_type = typename OptionsT::node_base_type;
+ using list_base_type = typename OptionsT::list_base_type;
+
+ friend typename OptionsT::list_base_type;
+ friend struct ilist_detail::NodeAccess;
+ friend class ilist_sentinel<OptionsT>;
+
+ friend class ilist_detail::node_parent_access<ilist_node_impl<OptionsT>,
+ typename OptionsT::parent_ty>;
+ friend class ilist_iterator<OptionsT, false, false>;
+ friend class ilist_iterator<OptionsT, false, true>;
+ friend class ilist_iterator<OptionsT, true, false>;
+ friend class ilist_iterator<OptionsT, true, true>;
+ friend class ilist_iterator_w_bits<OptionsT, false, false>;
+ friend class ilist_iterator_w_bits<OptionsT, false, true>;
+ friend class ilist_iterator_w_bits<OptionsT, true, false>;
+ friend class ilist_iterator_w_bits<OptionsT, true, true>;
+
+protected:
+ using self_iterator = ilist_select_iterator_type<OptionsT, false, false>;
+ using const_self_iterator = ilist_select_iterator_type<OptionsT, false, true>;
+ using reverse_self_iterator =
+ ilist_select_iterator_type<OptionsT, true, false>;
+ using const_reverse_self_iterator =
+ ilist_select_iterator_type<OptionsT, true, true>;
+
+ ilist_node_impl () = default;
+
+private:
+ ilist_node_impl *
+ getPrev ()
+ {
+ return static_cast<ilist_node_impl *> (node_base_type::getPrev ());
+ }
+
+ ilist_node_impl *
+ getNext ()
+ {
+ return static_cast<ilist_node_impl *> (node_base_type::getNext ());
+ }
+
+ const ilist_node_impl *
+ getPrev () const
+ {
+ return static_cast<ilist_node_impl *> (node_base_type::getPrev ());
+ }
+
+ const ilist_node_impl *
+ getNext () const
+ {
+ return static_cast<ilist_node_impl *> (node_base_type::getNext ());
+ }
+
+ void
+ setPrev (ilist_node_impl *N)
+ {
+ node_base_type::setPrev (N);
+ }
+ void
+ setNext (ilist_node_impl *N)
+ {
+ node_base_type::setNext (N);
+ }
+
+public:
+ self_iterator
+ getIterator ()
+ {
+ return self_iterator (*this);
+ }
+ const_self_iterator
+ getIterator () const
+ {
+ return const_self_iterator (*this);
+ }
+
+ reverse_self_iterator
+ getReverseIterator ()
+ {
+ return reverse_self_iterator (*this);
+ }
+
+ const_reverse_self_iterator
+ getReverseIterator () const
+ {
+ return const_reverse_self_iterator (*this);
+ }
+
+ // Under-approximation, but always available for assertions.
+ using node_base_type::isKnownSentinel;
+
+ /// Check whether this is the sentinel node.
+ ///
+ /// This requires sentinel tracking to be explicitly enabled. Use the
+ /// ilist_sentinel_tracking<true> option to get this API.
+ ///
+ /// Rather than using static_assert to enforce the API is not called when
+ /// configured with is_sentinel_tracking_explicit=false, the method is
+ /// conditionally provided using std::enable_if. This way, clients of
+ /// ilist_node_impl can be fully instantiated for DLLExport on Windows.
+ template <typename T = OptionsT>
+ std::enable_if_t<T::is_sentinel_tracking_explicit, bool>
+ isSentinel () const
+ {
+ return node_base_type::isSentinel ();
+ }
+};
+
+/// An intrusive list node.
+///
+/// A base class to enable membership in intrusive lists, including \a
+/// simple_ilist, \a iplist, and \a ilist. The first template parameter is the
+/// \a value_type for the list.
+///
+/// An ilist node can be configured with compile-time options to change
+/// behaviour and/or add API.
+///
+/// By default, an \a ilist_node does not track whether it is the list sentinel
+/// (an instance of \a ilist_sentinel). The function \a isKnownSentinel() always
+/// returns \c false tracking is off. Sentinel tracking steals a bit from the
+/// "prev" link, which adds a mask operation when decrementing an iterator, but
+/// enables bug-finding assertions in \a ilist_iterator.
+///
+/// To turn sentinel tracking on all the time, pass in the
+/// ilist_sentinel_tracking<true> template parameter. This also enables the \a
+/// isSentinel() function. The same option must be passed to the intrusive
+/// list. (ilist_sentinel_tracking<false> turns sentinel tracking off all the
+/// time.)
+///
+/// A type can inherit from ilist_node multiple times by passing in different
+/// \a ilist_tag options. This allows a single instance to be inserted into
+/// multiple lists simultaneously, where each list is given the same tag.
+///
+/// \example
+/// struct A {};
+/// struct B {};
+/// struct N : ilist_node<N, ilist_tag<A>>, ilist_node<N, ilist_tag<B>> {};
+///
+/// void foo() {
+/// simple_ilist<N, ilist_tag<A>> ListA;
+/// simple_ilist<N, ilist_tag<B>> ListB;
+/// N N1;
+/// ListA.push_back(N1);
+/// ListB.push_back(N1);
+/// }
+/// \endexample
+///
+/// When the \a ilist_parent<ParentTy> option is passed to an ilist_node and the
+/// owning ilist, each node contains a pointer to the ilist's owner. This adds
+/// \a getParent() and \a setParent(ParentTy*) methods to the ilist_node, which
+/// will be used for node access by the ilist if the node class publicly
+/// inherits from \a ilist_node_with_parent. By default, setParent() is not
+/// automatically called by the ilist; a SymbolTableList will call setParent()
+/// on inserted nodes, but the sentinel must still be manually set after the
+/// list is created (e.g. SymTabList.end()->setParent(Parent)).
+///
+/// The primary benefit of using ilist_parent is that a parent
+/// pointer will be stored in the sentinel, meaning that you can safely use \a
+/// ilist_iterator::getNodeParent() to get the node parent from any valid (i.e.
+/// non-null) iterator, even one that points to a sentinel value.
+///
+/// See \a is_valid_option for steps on adding a new option.
+template <class T, class... Options>
+class ilist_node
+ : public ilist_node_impl<
+ typename ilist_detail::compute_node_options<T, Options...>::type>
+{
+ static_assert (ilist_detail::check_options<Options...>::value,
+ "Unrecognized node option!");
+};
+
+namespace ilist_detail
+{
+
+/// An access class for ilist_node private API.
+///
+/// This gives access to the private parts of ilist nodes. Nodes for an ilist
+/// should friend this class if they inherit privately from ilist_node.
+///
+/// Using this class outside of the ilist implementation is unsupported.
+struct NodeAccess
+{
+protected:
+ template <class OptionsT>
+ static ilist_node_impl<OptionsT> *
+ getNodePtr (typename OptionsT::pointer N)
+ {
+ return N;
+ }
+
+ template <class OptionsT>
+ static const ilist_node_impl<OptionsT> *
+ getNodePtr (typename OptionsT::const_pointer N)
+ {
+ return N;
+ }
+
+ template <class OptionsT>
+ static typename OptionsT::pointer
+ getValuePtr (ilist_node_impl<OptionsT> *N)
+ {
+ return static_cast<typename OptionsT::pointer> (N);
+ }
+
+ template <class OptionsT>
+ static typename OptionsT::const_pointer
+ getValuePtr (const ilist_node_impl<OptionsT> *N)
+ {
+ return static_cast<typename OptionsT::const_pointer> (N);
+ }
+
+ template <class OptionsT>
+ static ilist_node_impl<OptionsT> *
+ getPrev (ilist_node_impl<OptionsT> &N)
+ {
+ return N.getPrev ();
+ }
+
+ template <class OptionsT>
+ static ilist_node_impl<OptionsT> *
+ getNext (ilist_node_impl<OptionsT> &N)
+ {
+ return N.getNext ();
+ }
+
+ template <class OptionsT>
+ static const ilist_node_impl<OptionsT> *
+ getPrev (const ilist_node_impl<OptionsT> &N)
+ {
+ return N.getPrev ();
+ }
+
+ template <class OptionsT>
+ static const ilist_node_impl<OptionsT> *
+ getNext (const ilist_node_impl<OptionsT> &N)
+ {
+ return N.getNext ();
+ }
+};
+
+template <class OptionsT> struct SpecificNodeAccess : NodeAccess
+{
+protected:
+ using pointer = typename OptionsT::pointer;
+ using const_pointer = typename OptionsT::const_pointer;
+ using node_type = ilist_node_impl<OptionsT>;
+
+ static node_type *
+ getNodePtr (pointer N)
+ {
+ return NodeAccess::getNodePtr<OptionsT> (N);
+ }
+
+ static const node_type *
+ getNodePtr (const_pointer N)
+ {
+ return NodeAccess::getNodePtr<OptionsT> (N);
+ }
+
+ static pointer
+ getValuePtr (node_type *N)
+ {
+ return NodeAccess::getValuePtr<OptionsT> (N);
+ }
+
+ static const_pointer
+ getValuePtr (const node_type *N)
+ {
+ return NodeAccess::getValuePtr<OptionsT> (N);
+ }
+};
+
+} // end namespace ilist_detail
+
+template <class OptionsT>
+class ilist_sentinel : public ilist_node_impl<OptionsT>
+{
+public:
+ ilist_sentinel ()
+ {
+ this->initializeSentinel ();
+ reset ();
+ }
+
+ void
+ reset ()
+ {
+ this->setPrev (this);
+ this->setNext (this);
+ }
+
+ bool
+ empty () const
+ {
+ return this == this->getPrev ();
+ }
+};
+
+/// An ilist node that can access its parent list.
+///
+/// Requires \c NodeTy to have \a getParent() to find the parent node, and the
+/// \c ParentTy to have \a getSublistAccess() to get a reference to the list.
+template <typename NodeTy, typename ParentTy, class... Options>
+class ilist_node_with_parent : public ilist_node<NodeTy, Options...>
+{
+protected:
+ ilist_node_with_parent () = default;
+
+private:
+ /// Forward to NodeTy::getParent().
+ ///
+ /// Note: do not use the name "getParent()". We want a compile error
+ /// (instead of recursion) when the subclass fails to implement \a
+ /// getParent().
+ const ParentTy *
+ getNodeParent () const
+ {
+ return static_cast<const NodeTy *> (this)->getParent ();
+ }
+
+public:
+ /// @name Adjacent Node Accessors
+ /// @{
+ /// Get the previous node, or \c nullptr for the list head.
+ NodeTy *
+ getPrevNode ()
+ {
+ // Should be separated to a reused function, but then we couldn't use auto
+ // (and would need the type of the list).
+ const auto &List =
+ getNodeParent ()->*(ParentTy::getSublistAccess ((NodeTy *)nullptr));
+ return List.getPrevNode (*static_cast<NodeTy *> (this));
+ }
+
+ /// Get the previous node, or \c nullptr for the list head.
+ const NodeTy *
+ getPrevNode () const
+ {
+ return const_cast<ilist_node_with_parent *> (this)->getPrevNode ();
+ }
+
+ /// Get the next node, or \c nullptr for the list tail.
+ NodeTy *
+ getNextNode ()
+ {
+ // Should be separated to a reused function, but then we couldn't use auto
+ // (and would need the type of the list).
+ const auto &List =
+ getNodeParent ()->*(ParentTy::getSublistAccess ((NodeTy *)nullptr));
+ return List.getNextNode (*static_cast<NodeTy *> (this));
+ }
+
+ /// Get the next node, or \c nullptr for the list tail.
+ const NodeTy *
+ getNextNode () const
+ {
+ return const_cast<ilist_node_with_parent *> (this)->getNextNode ();
+ }
+ /// @}
+};
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_ILIST_NODE_H
diff --git a/libbase/include/core/adt/ilist_node_base.h b/libbase/include/core/adt/ilist_node_base.h
new file mode 100644
index 0000000..2395efe
--- /dev/null
+++ b/libbase/include/core/adt/ilist_node_base.h
@@ -0,0 +1,167 @@
+//===- core/adt/ilist_node_base.h - Intrusive List Node Base -----*- C++ -*-==//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_NODE_BASE_H
+#define LIBBASE_ADT_ILIST_NODE_BASE_H
+
+#include "core/adt/PointerIntPair.h"
+
+namespace libbase
+{
+
+namespace ilist_detail
+{
+
+template <class NodeBase, bool EnableSentinelTracking> class node_base_prevnext;
+
+template <class NodeBase> class node_base_prevnext<NodeBase, false>
+{
+ NodeBase *Prev = nullptr;
+ NodeBase *Next = nullptr;
+
+public:
+ void
+ setPrev (NodeBase *Prev)
+ {
+ this->Prev = Prev;
+ }
+ void
+ setNext (NodeBase *Next)
+ {
+ this->Next = Next;
+ }
+ NodeBase *
+ getPrev () const
+ {
+ return Prev;
+ }
+ NodeBase *
+ getNext () const
+ {
+ return Next;
+ }
+
+ bool
+ isKnownSentinel () const
+ {
+ return false;
+ }
+ void
+ initializeSentinel ()
+ {
+ }
+};
+
+template <class NodeBase> class node_base_prevnext<NodeBase, true>
+{
+ PointerIntPair<NodeBase *, 1> PrevAndSentinel;
+ NodeBase *Next = nullptr;
+
+public:
+ void
+ setPrev (NodeBase *Prev)
+ {
+ PrevAndSentinel.setPointer (Prev);
+ }
+ void
+ setNext (NodeBase *Next)
+ {
+ this->Next = Next;
+ }
+ NodeBase *
+ getPrev () const
+ {
+ return PrevAndSentinel.getPointer ();
+ }
+ NodeBase *
+ getNext () const
+ {
+ return Next;
+ }
+
+ bool
+ isSentinel () const
+ {
+ return PrevAndSentinel.getInt ();
+ }
+ bool
+ isKnownSentinel () const
+ {
+ return isSentinel ();
+ }
+ void
+ initializeSentinel ()
+ {
+ PrevAndSentinel.setInt (true);
+ }
+};
+
+template <class ParentTy> class node_base_parent
+{
+ ParentTy *Parent = nullptr;
+
+public:
+ void
+ setNodeBaseParent (ParentTy *Parent)
+ {
+ this->Parent = Parent;
+ }
+ inline const ParentTy *
+ getNodeBaseParent () const
+ {
+ return Parent;
+ }
+ inline ParentTy *
+ getNodeBaseParent ()
+ {
+ return Parent;
+ }
+};
+template <> class node_base_parent<void>
+{
+};
+
+} // end namespace ilist_detail
+
+/// Base class for ilist nodes.
+///
+/// Optionally tracks whether this node is the sentinel.
+template <bool EnableSentinelTracking, class ParentTy>
+class ilist_node_base : public ilist_detail::node_base_prevnext<
+ ilist_node_base<EnableSentinelTracking, ParentTy>,
+ EnableSentinelTracking>,
+ public ilist_detail::node_base_parent<ParentTy>
+{
+};
+
+// Specialization implemented in the core LLVM library.
+template class LIBBASE_ABI ilist_node_base<true, void>;
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_ILIST_NODE_BASE_H
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
diff --git a/libbase/include/core/adt/ilist_node_options.h b/libbase/include/core/adt/ilist_node_options.h
new file mode 100644
index 0000000..d26a6b8
--- /dev/null
+++ b/libbase/include/core/adt/ilist_node_options.h
@@ -0,0 +1,244 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/ilist_node_options.h - ilist_node Options -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_ILIST_NODE_OPTIONS_H
+#define LIBBASE_ADT_ILIST_NODE_OPTIONS_H
+
+#include <type_traits>
+
+namespace libbase
+{
+
+template <bool EnableSentinelTracking, class ParentTy> class ilist_node_base;
+template <bool EnableSentinelTracking, class ParentTy> class ilist_base;
+
+/// Option to choose whether to track sentinels.
+///
+/// This option affects the ABI for the nodes. When not specified explicitly,
+/// sentinel tracking is disabled by default. Specify explicitly to
+/// enable \a ilist_node::isSentinel().
+template <bool EnableSentinelTracking> struct ilist_sentinel_tracking
+{
+};
+
+/// Option to specify a tag for the node type.
+///
+/// This option allows a single value type to be inserted in multiple lists
+/// simultaneously. See \a ilist_node for usage examples.
+template <class Tag> struct ilist_tag
+{
+};
+
+/// Option to add extra bits to the ilist_iterator.
+///
+/// Some use-cases (debug-info) need to know whether a position is intended
+/// to be half-open or fully open, i.e. whether to include any immediately
+/// adjacent debug-info in an operation. This option adds two bits to the
+/// iterator class to store that information.
+template <bool ExtraIteratorBits> struct ilist_iterator_bits
+{
+};
+
+/// Option to add a pointer to this list's owner in every node.
+///
+/// This option causes the \a ilist_base_node for this list to contain a pointer
+/// ParentTy *Parent, returned by \a ilist_base_node::getNodeBaseParent() and
+/// set by \a ilist_base_node::setNodeBaseParent(ParentTy *Parent). The parent
+/// value is not set automatically; the ilist owner should set itself as the
+/// parent of the list sentinel, and the parent should be set on each node
+/// inserted into the list. This value is also not used by
+/// \a ilist_node_with_parent::getNodeParent(), but is used by \a
+/// ilist_iterator::getNodeParent(), which allows the parent to be fetched from
+/// any valid (non-null) iterator to this list, including the sentinel.
+template <class ParentTy> struct ilist_parent
+{
+};
+
+namespace ilist_detail
+{
+
+/// Helper trait for recording whether an option is specified explicitly.
+template <bool IsExplicit> struct explicitness
+{
+ static const bool is_explicit = IsExplicit;
+};
+using is_explicit = explicitness<true>;
+using is_implicit = explicitness<false>;
+
+/// Check whether an option is valid.
+///
+/// The steps for adding and enabling a new ilist option include:
+/// \li define the option, ilist_foo<Bar>, above;
+/// \li add new parameters for Bar to \a ilist_detail::node_options;
+/// \li add an extraction meta-function, ilist_detail::extract_foo;
+/// \li call extract_foo from \a ilist_detail::compute_node_options and pass it
+/// into \a ilist_detail::node_options; and
+/// \li specialize \c is_valid_option<ilist_foo<Bar>> to inherit from \c
+/// std::true_type to get static assertions passing in \a simple_ilist and \a
+/// ilist_node.
+template <class Option> struct is_valid_option : std::false_type
+{
+};
+
+/// Extract sentinel tracking option.
+///
+/// Look through \p Options for the \a ilist_sentinel_tracking option, with the
+/// default disabled.
+template <class... Options> struct extract_sentinel_tracking;
+template <bool EnableSentinelTracking, class... Options>
+struct extract_sentinel_tracking<
+ ilist_sentinel_tracking<EnableSentinelTracking>, Options...>
+ : std::bool_constant<EnableSentinelTracking>, is_explicit
+{
+};
+template <class Option1, class... Options>
+struct extract_sentinel_tracking<Option1, Options...>
+ : extract_sentinel_tracking<Options...>
+{
+};
+template <> struct extract_sentinel_tracking<> : std::false_type, is_implicit
+{
+};
+template <bool EnableSentinelTracking>
+struct is_valid_option<ilist_sentinel_tracking<EnableSentinelTracking>>
+ : std::true_type
+{
+};
+
+/// Extract custom tag option.
+///
+/// Look through \p Options for the \a ilist_tag option, pulling out the
+/// custom tag type, using void as a default.
+template <class... Options> struct extract_tag;
+template <class Tag, class... Options>
+struct extract_tag<ilist_tag<Tag>, Options...>
+{
+ using type = Tag;
+};
+template <class Option1, class... Options>
+struct extract_tag<Option1, Options...> : extract_tag<Options...>
+{
+};
+template <> struct extract_tag<>
+{
+ using type = void;
+};
+template <class Tag> struct is_valid_option<ilist_tag<Tag>> : std::true_type
+{
+};
+
+/// Extract iterator bits option.
+///
+/// Look through \p Options for the \a ilist_iterator_bits option. Defaults
+/// to false.
+template <class... Options> struct extract_iterator_bits;
+template <bool IteratorBits, class... Options>
+struct extract_iterator_bits<ilist_iterator_bits<IteratorBits>, Options...>
+ : std::bool_constant<IteratorBits>
+{
+};
+template <class Option1, class... Options>
+struct extract_iterator_bits<Option1, Options...>
+ : extract_iterator_bits<Options...>
+{
+};
+template <> struct extract_iterator_bits<> : std::false_type, is_implicit
+{
+};
+template <bool IteratorBits>
+struct is_valid_option<ilist_iterator_bits<IteratorBits>> : std::true_type
+{
+};
+
+/// Extract node parent option.
+///
+/// Look through \p Options for the \a ilist_parent option, pulling out the
+/// custom parent type, using void as a default.
+template <class... Options> struct extract_parent;
+template <class ParentTy, class... Options>
+struct extract_parent<ilist_parent<ParentTy>, Options...>
+{
+ using type = ParentTy;
+};
+template <class Option1, class... Options>
+struct extract_parent<Option1, Options...> : extract_parent<Options...>
+{
+};
+template <> struct extract_parent<>
+{
+ using type = void;
+};
+template <class ParentTy>
+struct is_valid_option<ilist_parent<ParentTy>> : std::true_type
+{
+};
+
+/// Check whether options are valid.
+///
+/// The conjunction of \a is_valid_option on each individual option.
+template <class... Options>
+struct check_options : std::conjunction<is_valid_option<Options>...>
+{
+};
+
+/// Traits for options for \a ilist_node.
+///
+/// This is usually computed via \a compute_node_options.
+template <class T, bool EnableSentinelTracking, bool IsSentinelTrackingExplicit,
+ class TagT, bool HasIteratorBits, class ParentTy>
+struct node_options
+{
+ using value_type = T;
+ using pointer = T *;
+ using reference = T &;
+ using const_pointer = const T *;
+ using const_reference = const T &;
+
+ static const bool enable_sentinel_tracking = EnableSentinelTracking;
+ static const bool is_sentinel_tracking_explicit = IsSentinelTrackingExplicit;
+ static const bool has_iterator_bits = HasIteratorBits;
+ using tag = TagT;
+ using parent_ty = ParentTy;
+ using node_base_type = ilist_node_base<enable_sentinel_tracking, parent_ty>;
+ using list_base_type = ilist_base<enable_sentinel_tracking, parent_ty>;
+};
+
+template <class T, class... Options> struct compute_node_options
+{
+ using type = node_options<T, extract_sentinel_tracking<Options...>::value,
+ extract_sentinel_tracking<Options...>::is_explicit,
+ typename extract_tag<Options...>::type,
+ extract_iterator_bits<Options...>::value,
+ typename extract_parent<Options...>::type>;
+};
+
+} // end namespace ilist_detail
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_ILIST_NODE_OPTIONS_H
diff --git a/libbase/include/core/adt/simple_ilist.h b/libbase/include/core/adt/simple_ilist.h
new file mode 100644
index 0000000..e87f15b
--- /dev/null
+++ b/libbase/include/core/adt/simple_ilist.h
@@ -0,0 +1,482 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/simple_ilist.h - Simple Intrusive List ----------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_ADT_SIMPLE_ILIST_H
+#define LIBBASE_ADT_SIMPLE_ILIST_H
+
+#include "core/adt/ilist_base.h"
+#include "core/adt/ilist_iterator.h"
+#include "core/adt/ilist_node.h"
+#include "core/adt/ilist_node_options.h"
+#include "core/compiler/Compiler.h"
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <iterator>
+#include <utility>
+
+namespace libbase
+{
+
+/// A simple intrusive list implementation.
+///
+/// This is a simple intrusive list for a \c T that inherits from \c
+/// ilist_node<T>. The list never takes ownership of anything inserted in it.
+///
+/// Unlike \a iplist<T> and \a ilist<T>, \a simple_ilist<T> never deletes
+/// values, and has no callback traits.
+///
+/// The API for adding nodes include \a push_front(), \a push_back(), and \a
+/// insert(). These all take values by reference (not by pointer), except for
+/// the range version of \a insert().
+///
+/// There are three sets of API for discarding nodes from the list: \a
+/// remove(), which takes a reference to the node to remove, \a erase(), which
+/// takes an iterator or iterator range and returns the next one, and \a
+/// clear(), which empties out the container. All three are constant time
+/// operations. None of these deletes any nodes; in particular, if there is a
+/// single node in the list, then these have identical semantics:
+/// \li \c L.remove(L.front());
+/// \li \c L.erase(L.begin());
+/// \li \c L.clear();
+///
+/// As a convenience for callers, there are parallel APIs that take a \c
+/// Disposer (such as \c std::default_delete<T>): \a removeAndDispose(), \a
+/// eraseAndDispose(), and \a clearAndDispose(). These have different names
+/// because the extra semantic is otherwise non-obvious. They are equivalent
+/// to calling \a std::for_each() on the range to be discarded.
+///
+/// The currently available \p Options customize the nodes in the list. The
+/// same options must be specified in the \a ilist_node instantiation for
+/// compatibility (although the order is irrelevant).
+/// \li Use \a ilist_tag to designate which ilist_node for a given \p T this
+/// list should use. This is useful if a type \p T is part of multiple,
+/// independent lists simultaneously.
+/// \li Use \a ilist_sentinel_tracking to always (or never) track whether a
+/// node is a sentinel. Specifying \c true enables the \a
+/// ilist_node::isSentinel() API. Unlike \a ilist_node::isKnownSentinel(),
+/// which is only appropriate for assertions, \a ilist_node::isSentinel() is
+/// appropriate for real logic.
+///
+/// Here are examples of \p Options usage:
+/// \li \c simple_ilist<T> gives the defaults. \li \c
+/// simple_ilist<T,ilist_sentinel_tracking<true>> enables the \a
+/// ilist_node::isSentinel() API.
+/// \li \c simple_ilist<T,ilist_tag<A>,ilist_sentinel_tracking<false>>
+/// specifies a tag of A and that tracking should be off (regardless of the
+/// default setting).
+/// \li \c simple_ilist<T,ilist_sentinel_tracking<false>,ilist_tag<A>> is
+/// equivalent to the last.
+///
+/// See \a is_valid_option for steps on adding a new option.
+template <typename T, class... Options>
+class simple_ilist
+ : ilist_detail::compute_node_options<T, Options...>::type::list_base_type,
+ ilist_detail::SpecificNodeAccess<
+ typename ilist_detail::compute_node_options<T, Options...>::type>
+{
+ static_assert (ilist_detail::check_options<Options...>::value,
+ "Unrecognized node option!");
+ using OptionsT =
+ typename ilist_detail::compute_node_options<T, Options...>::type;
+ using list_base_type = typename OptionsT::list_base_type;
+ ilist_sentinel<OptionsT> Sentinel;
+
+public:
+ using value_type = typename OptionsT::value_type;
+ using pointer = typename OptionsT::pointer;
+ using reference = typename OptionsT::reference;
+ using const_pointer = typename OptionsT::const_pointer;
+ using const_reference = typename OptionsT::const_reference;
+ using iterator = ilist_select_iterator_type<OptionsT, false, false>;
+ using const_iterator = ilist_select_iterator_type<OptionsT, false, true>;
+ using reverse_iterator = ilist_select_iterator_type<OptionsT, true, false>;
+ using const_reverse_iterator =
+ ilist_select_iterator_type<OptionsT, true, true>;
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+
+ simple_ilist () = default;
+ ~simple_ilist () = default;
+
+ // No copy constructors.
+ simple_ilist (const simple_ilist &) = delete;
+ simple_ilist &
+ operator= (const simple_ilist &) = delete;
+
+ // Move constructors.
+ simple_ilist (simple_ilist &&X)
+ {
+ splice (end (), X);
+ }
+ simple_ilist &
+ operator= (simple_ilist &&X)
+ {
+ clear ();
+ splice (end (), X);
+ return *this;
+ }
+
+ iterator
+ begin ()
+ {
+ return ++iterator (Sentinel);
+ }
+ const_iterator
+ begin () const
+ {
+ return ++const_iterator (Sentinel);
+ }
+ iterator
+ end ()
+ {
+ return iterator (Sentinel);
+ }
+ const_iterator
+ end () const
+ {
+ return const_iterator (Sentinel);
+ }
+ reverse_iterator
+ rbegin ()
+ {
+ return ++reverse_iterator (Sentinel);
+ }
+ const_reverse_iterator
+ rbegin () const
+ {
+ return ++const_reverse_iterator (Sentinel);
+ }
+ reverse_iterator
+ rend ()
+ {
+ return reverse_iterator (Sentinel);
+ }
+ const_reverse_iterator
+ rend () const
+ {
+ return const_reverse_iterator (Sentinel);
+ }
+
+ /// Check if the list is empty in constant time.
+ [[nodiscard]] bool
+ empty () const
+ {
+ return Sentinel.empty ();
+ }
+
+ /// Calculate the size of the list in linear time.
+ [[nodiscard]] size_type
+ size () const
+ {
+ return std::distance (begin (), end ());
+ }
+
+ reference
+ front ()
+ {
+ return *begin ();
+ }
+ const_reference
+ front () const
+ {
+ return *begin ();
+ }
+ reference
+ back ()
+ {
+ return *rbegin ();
+ }
+ const_reference
+ back () const
+ {
+ return *rbegin ();
+ }
+
+ /// Insert a node at the front; never copies.
+ void
+ push_front (reference Node)
+ {
+ insert (begin (), Node);
+ }
+
+ /// Insert a node at the back; never copies.
+ void
+ push_back (reference Node)
+ {
+ insert (end (), Node);
+ }
+
+ /// Remove the node at the front; never deletes.
+ void
+ pop_front ()
+ {
+ erase (begin ());
+ }
+
+ /// Remove the node at the back; never deletes.
+ void
+ pop_back ()
+ {
+ erase (--end ());
+ }
+
+ /// Swap with another list in place using std::swap.
+ void
+ swap (simple_ilist &X)
+ {
+ std::swap (*this, X);
+ }
+
+ /// Insert a node by reference; never copies.
+ iterator
+ insert (iterator I, reference Node)
+ {
+ list_base_type::insertBefore (*I.getNodePtr (), *this->getNodePtr (&Node));
+ return iterator (&Node);
+ }
+
+ /// Insert a range of nodes; never copies.
+ template <class Iterator>
+ void
+ insert (iterator I, Iterator First, Iterator Last)
+ {
+ for (; First != Last; ++First)
+ {
+ insert (I, *First);
+ }
+ }
+
+ /// Clone another list.
+ template <class Cloner, class Disposer>
+ void
+ cloneFrom (const simple_ilist &L2, Cloner clone, Disposer dispose)
+ {
+ clearAndDispose (dispose);
+ for (const_reference V : L2)
+ {
+ push_back (*clone (V));
+ }
+ }
+
+ /// Remove a node by reference; never deletes.
+ ///
+ /// \see \a erase() for removing by iterator.
+ /// \see \a removeAndDispose() if the node should be deleted.
+ void
+ remove (reference N)
+ {
+ list_base_type::remove (*this->getNodePtr (&N));
+ }
+
+ /// Remove a node by reference and dispose of it.
+ template <class Disposer>
+ void
+ removeAndDispose (reference N, Disposer dispose)
+ {
+ remove (N);
+ dispose (&N);
+ }
+
+ /// Remove a node by iterator; never deletes.
+ ///
+ /// \see \a remove() for removing by reference.
+ /// \see \a eraseAndDispose() it the node should be deleted.
+ iterator
+ erase (iterator I)
+ {
+ assert (I != end () && "Cannot remove end of list!");
+ remove (*I++);
+ return I;
+ }
+
+ /// Remove a range of nodes; never deletes.
+ ///
+ /// \see \a eraseAndDispose() if the nodes should be deleted.
+ iterator
+ erase (iterator First, iterator Last)
+ {
+ list_base_type::removeRange (*First.getNodePtr (), *Last.getNodePtr ());
+ return Last;
+ }
+
+ /// Remove a node by iterator and dispose of it.
+ template <class Disposer>
+ iterator
+ eraseAndDispose (iterator I, Disposer dispose)
+ {
+ auto Next = std::next (I);
+ erase (I);
+ dispose (&*I);
+ return Next;
+ }
+
+ /// Remove a range of nodes and dispose of them.
+ template <class Disposer>
+ iterator
+ eraseAndDispose (iterator First, iterator Last, Disposer dispose)
+ {
+ while (First != Last)
+ {
+ First = eraseAndDispose (First, dispose);
+ }
+ return Last;
+ }
+
+ /// Clear the list; never deletes.
+ ///
+ /// \see \a clearAndDispose() if the nodes should be deleted.
+ void
+ clear ()
+ {
+ Sentinel.reset ();
+ }
+
+ /// Clear the list and dispose of the nodes.
+ template <class Disposer>
+ void
+ clearAndDispose (Disposer dispose)
+ {
+ eraseAndDispose (begin (), end (), dispose);
+ }
+
+ /// Splice in another list.
+ void
+ splice (iterator I, simple_ilist &L2)
+ {
+ splice (I, L2, L2.begin (), L2.end ());
+ }
+
+ /// Splice in a node from another list.
+ void
+ splice (iterator I, simple_ilist &L2, iterator Node)
+ {
+ splice (I, L2, Node, std::next (Node));
+ }
+
+ /// Splice in a range of nodes from another list.
+ void
+ splice (iterator I, simple_ilist &, iterator First, iterator Last)
+ {
+ list_base_type::transferBefore (*I.getNodePtr (), *First.getNodePtr (),
+ *Last.getNodePtr ());
+ }
+
+ /// Merge in another list.
+ ///
+ /// \pre \c this and \p RHS are sorted.
+ ///@{
+ void
+ merge (simple_ilist &RHS)
+ {
+ merge (RHS, std::less<T> ());
+ }
+ template <class Compare>
+ void
+ merge (simple_ilist &RHS, Compare comp);
+ ///@}
+
+ /// Sort the list.
+ ///@{
+ void
+ sort ()
+ {
+ sort (std::less<T> ());
+ }
+ template <class Compare>
+ void
+ sort (Compare comp);
+ ///@}
+};
+
+template <class T, class... Options>
+template <class Compare>
+void
+simple_ilist<T, Options...>::merge (simple_ilist &RHS, Compare comp)
+{
+ if (this == &RHS || RHS.empty ())
+ {
+ return;
+ }
+ iterator LI = begin (), LE = end ();
+ iterator RI = RHS.begin (), RE = RHS.end ();
+ while (LI != LE)
+ {
+ if (comp (*RI, *LI))
+ {
+ // Transfer a run of at least size 1 from RHS to LHS.
+ iterator RunStart = RI++;
+ RI = std::find_if (RI, RE,
+ [&] (reference RV)
+ {
+ return !comp (RV, *LI);
+ });
+ splice (LI, RHS, RunStart, RI);
+ if (RI == RE)
+ {
+ return;
+ }
+ }
+ ++LI;
+ }
+ // Transfer the remaining RHS nodes once LHS is finished.
+ splice (LE, RHS, RI, RE);
+}
+
+template <class T, class... Options>
+template <class Compare>
+void
+simple_ilist<T, Options...>::sort (Compare comp)
+{
+ // Vacuously sorted.
+ if (empty () || std::next (begin ()) == end ())
+ {
+ return;
+ }
+
+ // Split the list in the middle.
+ iterator Center = begin (), End = begin ();
+ while (End != end () && ++End != end ())
+ {
+ ++Center;
+ ++End;
+ }
+ simple_ilist RHS;
+ RHS.splice (RHS.end (), *this, Center, end ());
+
+ // Sort the sublists and merge back together.
+ sort (comp);
+ RHS.sort (comp);
+ merge (RHS, comp);
+}
+
+} // end namespace libbase
+
+#endif // LIBBASE_ADT_SIMPLE_ILIST_H
diff --git a/libbase/include/core/compiler/Compiler.h b/libbase/include/core/compiler/Compiler.h
new file mode 100644
index 0000000..e0bc822
--- /dev/null
+++ b/libbase/include/core/compiler/Compiler.h
@@ -0,0 +1,860 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===-- core/compiler/Compiler.h - Compiler abstraction support --*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines several macros, based on the current compiler. This allows
+// use of compiler-specific features in a way that remains portable. This header
+// can be included from either C or C++.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_CORE_COMPILER_H
+#define LIBBASE_CORE_COMPILER_H
+
+#include "core/config/libbase-config.h"
+
+#include <stddef.h>
+
+#if defined(_MSC_VER)
+#include <sal.h>
+#endif
+
+#ifndef __has_feature
+#define __has_feature(x) 0
+#endif
+
+#ifndef __has_extension
+#define __has_extension(x) 0
+#endif
+
+#ifndef __has_attribute
+#define __has_attribute(x) 0
+#endif
+
+#ifndef __has_builtin
+#define __has_builtin(x) 0
+#endif
+
+#ifndef __has_warning
+#define __has_warning(x) 0
+#endif
+
+// Only use __has_cpp_attribute in C++ mode. GCC defines __has_cpp_attribute in
+// C mode, but the :: in __has_cpp_attribute(scoped::attribute) is invalid.
+#ifndef LIBBASE_HAS_CPP_ATTRIBUTE
+#if defined(__cplusplus) && defined(__has_cpp_attribute)
+#define LIBBASE_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute (x)
+#else
+#define LIBBASE_HAS_CPP_ATTRIBUTE(x) 0
+#endif
+#endif
+
+/// \macro LIBBASE_GNUC_PREREQ
+/// Extend the default __GNUC_PREREQ even if glibc's features.h isn't
+/// available.
+#ifndef LIBBASE_GNUC_PREREQ
+#if defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__)
+#define LIBBASE_GNUC_PREREQ(maj, min, patch) \
+ ((__GNUC__ << 20) + (__GNUC_MINOR__ << 10) + __GNUC_PATCHLEVEL__ >= \
+ ((maj) << 20) + ((min) << 10) + (patch))
+#elif defined(__GNUC__) && defined(__GNUC_MINOR__)
+#define LIBBASE_GNUC_PREREQ(maj, min, patch) \
+ ((__GNUC__ << 20) + (__GNUC_MINOR__ << 10) >= ((maj) << 20) + ((min) << 10))
+#else
+#define LIBBASE_GNUC_PREREQ(maj, min, patch) 0
+#endif
+#endif
+
+/// \macro LIBBASE_MSC_PREREQ
+/// Is the compiler MSVC of at least the specified version?
+/// The common \param version values to check for are:
+/// * 1910: VS2017, version 15.1 & 15.2
+/// * 1911: VS2017, version 15.3 & 15.4
+/// * 1912: VS2017, version 15.5
+/// * 1913: VS2017, version 15.6
+/// * 1914: VS2017, version 15.7
+/// * 1915: VS2017, version 15.8
+/// * 1916: VS2017, version 15.9
+/// * 1920: VS2019, version 16.0
+/// * 1921: VS2019, version 16.1
+/// * 1922: VS2019, version 16.2
+/// * 1923: VS2019, version 16.3
+/// * 1924: VS2019, version 16.4
+/// * 1925: VS2019, version 16.5
+/// * 1926: VS2019, version 16.6
+/// * 1927: VS2019, version 16.7
+/// * 1928: VS2019, version 16.8 + 16.9
+/// * 1929: VS2019, version 16.10 + 16.11
+/// * 1930: VS2022, version 17.0
+#ifdef _MSC_VER
+#define LIBBASE_MSC_PREREQ(version) (_MSC_VER >= (version))
+
+// We require at least VS 2019.
+#if !defined(LIBBASE_FORCE_USE_OLD_TOOLCHAIN)
+#if !LIBBASE_MSC_PREREQ(1920)
+#error LLVM requires at least VS 2019.
+#endif
+#endif
+
+#else
+#define LIBBASE_MSC_PREREQ(version) 0
+#endif
+
+/// LIBBASE_LIBRARY_VISIBILITY - If a class marked with this attribute is linked
+/// into a shared library, then the class should be private to the library and
+/// not accessible from outside it. Can also be used to mark variables and
+/// functions, making them private to any shared library they are linked into.
+/// On PE/COFF targets, library visibility is the default, so this isn't needed.
+///
+/// LIBBASE_EXTERNAL_VISIBILITY - classes, functions, and variables marked with
+/// this attribute will be made public and visible outside of any shared library
+/// they are linked in to.
+
+#if LIBBASE_HAS_CPP_ATTRIBUTE(gnu::visibility) && defined(__GNUC__) && \
+ !defined(__clang__)
+#define LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN [[gnu::visibility ("hidden")]]
+#define LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT [[gnu::visibility ("default")]]
+#elif __has_attribute(visibility)
+#define LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN \
+ __attribute__ ((visibility ("hidden")))
+#define LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT \
+ __attribute__ ((visibility ("default")))
+#else
+#define LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN
+#define LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
+#endif
+
+#if defined(LIBBASE_BUILD_SHARED_LIBS)
+#define LIBBASE_EXTERNAL_VISIBILITY LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
+#else
+#define LIBBASE_EXTERNAL_VISIBILITY
+#endif
+
+#if (!(defined(_WIN32) || defined(__CYGWIN__)) || \
+ ((defined(__MINGW32__) || defined(__CYGWIN__)) && defined(__clang__)))
+#define LIBBASE_LIBRARY_VISIBILITY LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN
+// Clang compilers older then 15 do not support gnu style attributes on
+// namespaces.
+#if defined(__clang__) && __clang_major__ < 15
+#define LIBBASE_LIBRARY_VISIBILITY_NAMESPACE [[gnu::visibility ("hidden")]]
+#else
+#define LIBBASE_LIBRARY_VISIBILITY_NAMESPACE LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN
+#endif
+#define LIBBASE_ALWAYS_EXPORT LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
+#elif defined(_WIN32)
+#define LIBBASE_ALWAYS_EXPORT __declspec (dllexport)
+#define LIBBASE_LIBRARY_VISIBILITY
+#define LIBBASE_LIBRARY_VISIBILITY_NAMESPACE
+#else
+#define LIBBASE_LIBRARY_VISIBILITY
+#define LIBBASE_ALWAYS_EXPORT
+#define LIBBASE_LIBRARY_VISIBILITY_NAMESPACE
+#endif
+
+/// LIBBASE_ABI is the main export/visibility macro to mark something as
+/// explicitly exported when llvm is built as a shared library with everything
+/// else that is unannotated will have internal visibility.
+///
+/// LIBBASE_ABI_EXPORT is for the special case for things like plugin symbol
+/// declarations or definitions where we don't want the macro to be switching
+/// between dllexport and dllimport on windows based on what codebase is being
+/// built, it will only be dllexport. For non windows platforms this macro
+/// behaves the same as LIBBASE_ABI.
+///
+/// LIBBASE_EXPORT_TEMPLATE is used on explicit template instantiations in
+/// source files that were declared extern in a header. This macro is only set
+/// as a compiler export attribute on windows, on other platforms it does
+/// nothing.
+///
+/// LIBBASE_TEMPLATE_ABI is for annotating extern template declarations in
+/// headers for both functions and classes. On windows its turned in to
+/// dllimport for library consumers, for other platforms its a default
+/// visibility attribute.
+///
+/// LIBBASE_ABI_FOR_TEST is for annotating symbols that are exported from a
+/// library-internal header solely so that unit tests can link against them.
+/// Symbols in LLVM's public headers are part of the LLVM public interface and
+/// should use LIBBASE_ABI. LIBBASE_ABI_FOR_TEST is reserved for internal
+/// headers, whose symbols could be conditionally excluded when not building
+/// tests in the future.
+///
+#ifndef LIBBASE_ABI_GENERATING_ANNOTATIONS
+// Marker to add to classes or functions in public headers that should not have
+// export macros added to them by the clang tool
+#define LIBBASE_ABI_NOT_EXPORTED
+// TODO(https://github.com/llvm/llvm-project/issues/145406): eliminate need for
+// two preprocessor definitions to gate LIBBASE_ABI macro definitions.
+#if defined(LIBBASE_ENABLE_EXPORT_ANNOTATIONS) && !defined(LIBBASE_BUILD_STATIC)
+#if defined(_WIN32) && !defined(__MINGW32__)
+#if defined(LIBBASE_EXPORTS)
+#define LIBBASE_ABI __declspec (dllexport)
+#define LIBBASE_TEMPLATE_ABI
+#define LIBBASE_EXPORT_TEMPLATE __declspec (dllexport)
+#else
+#define LIBBASE_ABI __declspec (dllimport)
+#define LIBBASE_TEMPLATE_ABI __declspec (dllimport)
+#define LIBBASE_EXPORT_TEMPLATE
+#endif
+#define LIBBASE_ABI_EXPORT __declspec (dllexport)
+#elif __has_attribute(visibility)
+#if defined(__ELF__) || defined(__MINGW32__) || defined(_AIX) || \
+ defined(__MVS__) || defined(__CYGWIN__)
+#define LIBBASE_ABI __attribute__ ((visibility ("default")))
+#define LIBBASE_TEMPLATE_ABI LIBBASE_ABI
+#define LIBBASE_EXPORT_TEMPLATE
+#define LIBBASE_ABI_EXPORT LIBBASE_ABI
+#elif defined(__MACH__) || defined(__wasm__) || defined(__EMSCRIPTEN__)
+#define LIBBASE_ABI __attribute__ ((visibility ("default")))
+#define LIBBASE_TEMPLATE_ABI
+#define LIBBASE_EXPORT_TEMPLATE
+#define LIBBASE_ABI_EXPORT LIBBASE_ABI
+#endif
+#endif
+#endif
+#if !defined(LIBBASE_ABI)
+#define LIBBASE_ABI
+#define LIBBASE_TEMPLATE_ABI
+#define LIBBASE_EXPORT_TEMPLATE
+#define LIBBASE_ABI_EXPORT
+#endif
+#define LIBBASE_ABI_FOR_TEST LIBBASE_ABI
+#endif
+
+#if defined(__GNUC__)
+#define LIBBASE_PREFETCH(addr, rw, locality) \
+ __builtin_prefetch (addr, rw, locality)
+#else
+#define LIBBASE_PREFETCH(addr, rw, locality)
+#endif
+
+#if __has_attribute(uninitialized)
+#define LIBBASE_ATTRIBUTE_UNINITIALIZED __attribute__ ((uninitialized))
+#else
+#define LIBBASE_ATTRIBUTE_UNINITIALIZED
+#endif
+
+#if __has_attribute(used)
+#define LIBBASE_ATTRIBUTE_USED __attribute__ ((__used__))
+#else
+#define LIBBASE_ATTRIBUTE_USED
+#endif
+
+#if __has_attribute(warn_unused)
+#define LIBBASE_ATTRIBUTE_WARN_UNUSED __attribute__ ((warn_unused))
+#else
+#define LIBBASE_ATTRIBUTE_WARN_UNUSED
+#endif
+
+// Only enabled for clang:
+// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99587
+// GCC may produce "warning: 'retain' attribute ignored" (despite
+// __has_attribute(retain) being 1).
+#if defined(__clang__) && __has_attribute(retain)
+#define LIBBASE_ATTRIBUTE_RETAIN __attribute__ ((__retain__))
+#else
+#define LIBBASE_ATTRIBUTE_RETAIN
+#endif
+
+// For deprecations that are not a simple rename, use [[deprecated(MSG)]]
+// instead.
+#if defined(__clang__)
+#define LIBBASE_DEPRECATED_WITH_FIXIT(MSG, FIX) \
+ __attribute__ ((deprecated (MSG, FIX)))
+#else
+#define LIBBASE_DEPRECATED_WITH_FIXIT(MSG, FIX) [[deprecated (MSG)]]
+#endif
+
+// clang-format off
+#if defined(__clang__) || defined(__GNUC__)
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_POP \
+ _Pragma("GCC diagnostic pop")
+#elif defined(_MSC_VER)
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH \
+ __pragma(warning(push)) \
+ __pragma(warning(disable : 4996))
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_POP \
+ __pragma(warning(pop))
+#else
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH
+#define LIBBASE_SUPPRESS_DEPRECATED_DECLARATIONS_POP
+#endif
+// clang-format on
+
+// Indicate that a non-static, non-const C++ member function reinitializes
+// the entire object to a known state, independent of the previous state of
+// the object.
+//
+// The clang-tidy check bugprone-use-after-move recognizes this attribute as a
+// marker that a moved-from object has left the indeterminate state and can be
+// reused.
+#if LIBBASE_HAS_CPP_ATTRIBUTE(clang::reinitializes)
+#define LIBBASE_ATTRIBUTE_REINITIALIZES [[clang::reinitializes]]
+#else
+#define LIBBASE_ATTRIBUTE_REINITIALIZES
+#endif
+
+// Some compilers warn about unused functions. When a function is sometimes
+// used or not depending on build settings (e.g. a function only called from
+// within "assert"), this attribute can be used to suppress such warnings.
+//
+// However, it shouldn't be used for unused *variables*, as those have a much
+// more portable solution:
+// (void)unused_var_name;
+// Prefer cast-to-void wherever it is sufficient.
+#if __has_attribute(unused)
+#define LIBBASE_ATTRIBUTE_UNUSED __attribute__ ((__unused__))
+#else
+#define LIBBASE_ATTRIBUTE_UNUSED
+#endif
+
+// FIXME: Provide this for PE/COFF targets.
+#if __has_attribute(weak) && !defined(__MINGW32__) && !defined(__CYGWIN__) && \
+ !defined(_WIN32)
+#define LIBBASE_HAS_ATTRIBUTE_WEAK 1
+#else
+#define LIBBASE_HAS_ATTRIBUTE_WEAK 0
+#endif
+#if LIBBASE_HAS_ATTRIBUTE_WEAK
+#define LIBBASE_ATTRIBUTE_WEAK __attribute__ ((__weak__))
+#else
+#define LIBBASE_ATTRIBUTE_WEAK
+#endif
+
+// Prior to clang 3.2, clang did not accept any spelling of
+// __has_attribute(const), so assume it is supported.
+#if defined(__clang__) || defined(__GNUC__)
+// aka 'CONST' but following LLVM Conventions.
+#define LIBBASE_READNONE __attribute__ ((__const__))
+#else
+#define LIBBASE_READNONE
+#endif
+
+#if __has_attribute(pure) || defined(__GNUC__)
+// aka 'PURE' but following LLVM Conventions.
+#define LIBBASE_READONLY __attribute__ ((__pure__))
+#else
+#define LIBBASE_READONLY
+#endif
+
+#if __has_attribute(minsize)
+#define LIBBASE_ATTRIBUTE_MINSIZE __attribute__ ((minsize))
+#else
+#define LIBBASE_ATTRIBUTE_MINSIZE
+#endif
+
+#if __has_builtin(__builtin_expect) || defined(__GNUC__)
+#define LIBBASE_LIKELY(EXPR) __builtin_expect ((bool)(EXPR), true)
+#define LIBBASE_UNLIKELY(EXPR) __builtin_expect ((bool)(EXPR), false)
+#else
+#define LIBBASE_LIKELY(EXPR) (EXPR)
+#define LIBBASE_UNLIKELY(EXPR) (EXPR)
+#endif
+
+/// LIBBASE_ATTRIBUTE_NOINLINE - On compilers where we have a directive to do
+/// so, mark a method "not for inlining".
+#if __has_attribute(noinline)
+#define LIBBASE_ATTRIBUTE_NOINLINE __attribute__ ((noinline))
+#elif defined(_MSC_VER)
+#define LIBBASE_ATTRIBUTE_NOINLINE __declspec (noinline)
+#else
+#define LIBBASE_ATTRIBUTE_NOINLINE
+#endif
+
+/// LIBBASE_ATTRIBUTE_ALWAYS_INLINE - On compilers where we have a directive to
+/// do so, mark a method "always inline" because it is performance sensitive.
+#if __has_attribute(always_inline)
+#define LIBBASE_ATTRIBUTE_ALWAYS_INLINE inline __attribute__ ((always_inline))
+#elif defined(_MSC_VER)
+#define LIBBASE_ATTRIBUTE_ALWAYS_INLINE __forceinline
+#else
+#define LIBBASE_ATTRIBUTE_ALWAYS_INLINE inline
+#endif
+
+/// LIBBASE_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG - Like
+/// LIBBASE_ATTRIBUTE_ALWAYS_INLINE but disabled in debug builds to avoid stack
+/// overflow with deep recursion.
+#if defined(NDEBUG)
+#define LIBBASE_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG \
+ LIBBASE_ATTRIBUTE_ALWAYS_INLINE
+#else
+#define LIBBASE_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG inline
+#endif
+
+/// LIBBASE_ATTRIBUTE_NO_DEBUG - On compilers where we have a directive to do
+/// so, mark a method "no debug" because debug info makes the debugger
+/// experience worse.
+#if __has_attribute(nodebug)
+#define LIBBASE_ATTRIBUTE_NODEBUG __attribute__ ((nodebug))
+#else
+#define LIBBASE_ATTRIBUTE_NODEBUG
+#endif
+
+#if __has_attribute(returns_nonnull)
+#define LIBBASE_ATTRIBUTE_RETURNS_NONNULL __attribute__ ((returns_nonnull))
+#elif defined(_MSC_VER)
+#define LIBBASE_ATTRIBUTE_RETURNS_NONNULL _Ret_notnull_
+#else
+#define LIBBASE_ATTRIBUTE_RETURNS_NONNULL
+#endif
+
+/// LIBBASE_ATTRIBUTE_RESTRICT - Annotates a pointer to tell the compiler that
+/// it is not aliased in the current scope.
+#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER)
+#define LIBBASE_ATTRIBUTE_RESTRICT __restrict
+#else
+#define LIBBASE_ATTRIBUTE_RESTRICT
+#endif
+
+/// \macro LIBBASE_ATTRIBUTE_RETURNS_NOALIAS Used to mark a function as
+/// returning a pointer that does not alias any other valid pointer.
+#ifdef __GNUC__
+#define LIBBASE_ATTRIBUTE_RETURNS_NOALIAS __attribute__ ((__malloc__))
+#elif defined(_MSC_VER)
+#define LIBBASE_ATTRIBUTE_RETURNS_NOALIAS __declspec (restrict)
+#else
+#define LIBBASE_ATTRIBUTE_RETURNS_NOALIAS
+#endif
+
+/// LIBBASE_FALLTHROUGH - Mark fallthrough cases in switch statements.
+#if defined(__cplusplus) && __cplusplus > 201402L && \
+ LIBBASE_HAS_CPP_ATTRIBUTE(fallthrough)
+#define LIBBASE_FALLTHROUGH [[fallthrough]]
+#elif LIBBASE_HAS_CPP_ATTRIBUTE(gnu::fallthrough)
+#define LIBBASE_FALLTHROUGH [[gnu::fallthrough]]
+#elif __has_attribute(fallthrough)
+#define LIBBASE_FALLTHROUGH __attribute__ ((fallthrough))
+#elif LIBBASE_HAS_CPP_ATTRIBUTE(clang::fallthrough)
+#define LIBBASE_FALLTHROUGH [[clang::fallthrough]]
+#else
+#define LIBBASE_FALLTHROUGH
+#endif
+
+/// LIBBASE_REQUIRE_CONSTANT_INITIALIZATION - Apply this to globals to ensure
+/// that they are constant initialized.
+#if LIBBASE_HAS_CPP_ATTRIBUTE(clang::require_constant_initialization)
+#define LIBBASE_REQUIRE_CONSTANT_INITIALIZATION \
+ [[clang::require_constant_initialization]]
+#else
+#define LIBBASE_REQUIRE_CONSTANT_INITIALIZATION
+#endif
+
+/// LIBBASE_GSL_OWNER - Apply this to owning classes like SmallVector to enable
+/// lifetime warnings.
+#if LIBBASE_HAS_CPP_ATTRIBUTE(gsl::Owner)
+#define LIBBASE_GSL_OWNER [[gsl::Owner]]
+#else
+#define LIBBASE_GSL_OWNER
+#endif
+
+/// LIBBASE_GSL_POINTER - Apply this to non-owning classes like
+/// StringRef to enable lifetime warnings.
+#if LIBBASE_HAS_CPP_ATTRIBUTE(gsl::Pointer)
+#define LIBBASE_GSL_POINTER [[gsl::Pointer]]
+#else
+#define LIBBASE_GSL_POINTER
+#endif
+
+#if LIBBASE_HAS_CPP_ATTRIBUTE(clang::lifetimebound)
+#define LIBBASE_LIFETIME_BOUND [[clang::lifetimebound]]
+#else
+#define LIBBASE_LIFETIME_BOUND
+#endif
+
+#if LIBBASE_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L
+#define LIBBASE_CTOR_NODISCARD [[nodiscard]]
+#else
+#define LIBBASE_CTOR_NODISCARD
+#endif
+
+// Macro to suppress the MSVC warning C4848:
+// "support for attribute [[msvc::no_unique_address]] in C++17 and earlier
+// is a vendor extension".
+// This warning is removed in versions >= 19.43.
+#if !defined(_MSC_VER) || _MSC_VER >= 1943 || defined(__clang__)
+#define LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH
+#define LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP
+#else // MSVC < 19.43
+#define LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH \
+ __pragma (warning (push)) __pragma (warning (disable : 4848))
+#define LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP __pragma (warning (pop))
+#endif
+
+#if LIBBASE_HAS_CPP_ATTRIBUTE(no_unique_address)
+#define LIBBASE_NO_UNIQUE_ADDRESS [[no_unique_address]]
+#elif LIBBASE_HAS_CPP_ATTRIBUTE(msvc::no_unique_address)
+#define LIBBASE_NO_UNIQUE_ADDRESS \
+ LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH \
+ [[msvc::no_unique_address]] LIBBASE_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP
+#else
+#define LIBBASE_NO_UNIQUE_ADDRESS
+#endif
+
+/// LIBBASE_EXTENSION - Support compilers where we have a keyword to suppress
+/// pedantic diagnostics.
+#ifdef __GNUC__
+#define LIBBASE_EXTENSION __extension__
+#else
+#define LIBBASE_EXTENSION
+#endif
+
+/// LIBBASE_BUILTIN_UNREACHABLE - On compilers which support it, expands
+/// to an expression which states that it is undefined behavior for the
+/// compiler to reach this point. Otherwise is not defined.
+///
+/// '#else' is intentionally left out so that other macro logic (e.g.,
+/// LIBBASE_ASSUME_ALIGNED and llvm_unreachable()) can detect whether
+/// LIBBASE_BUILTIN_UNREACHABLE has a definition.
+#if __has_builtin(__builtin_unreachable) || defined(__GNUC__)
+#define LIBBASE_BUILTIN_UNREACHABLE __builtin_unreachable ()
+#elif defined(_MSC_VER)
+#define LIBBASE_BUILTIN_UNREACHABLE __assume (false)
+#endif
+
+/// LIBBASE_BUILTIN_TRAP - On compilers which support it, expands to an
+/// expression which causes the program to exit abnormally.
+#if __has_builtin(__builtin_trap) || defined(__GNUC__)
+#define LIBBASE_BUILTIN_TRAP __builtin_trap ()
+#elif defined(_MSC_VER)
+// The __debugbreak intrinsic is supported by MSVC, does not require forward
+// declarations involving platform-specific typedefs (unlike RaiseException),
+// results in a call to vectored exception handlers, and encodes to a short
+// instruction that still causes the trapping behavior we want.
+#define LIBBASE_BUILTIN_TRAP __debugbreak ()
+#else
+#define LIBBASE_BUILTIN_TRAP *(volatile int *)0x11 = 0
+#endif
+
+/// LIBBASE_BUILTIN_DEBUGTRAP - On compilers which support it, expands to
+/// an expression which causes the program to break while running
+/// under a debugger.
+#if __has_builtin(__builtin_debugtrap)
+#define LIBBASE_BUILTIN_DEBUGTRAP __builtin_debugtrap ()
+#elif defined(_MSC_VER)
+// The __debugbreak intrinsic is supported by MSVC and breaks while
+// running under the debugger, and also supports invoking a debugger
+// when the OS is configured appropriately.
+#define LIBBASE_BUILTIN_DEBUGTRAP __debugbreak ()
+#else
+// Just continue execution when built with compilers that have no
+// support. This is a debugging aid and not intended to force the
+// program to abort if encountered.
+#define LIBBASE_BUILTIN_DEBUGTRAP
+#endif
+
+/// \macro LIBBASE_ASSUME_ALIGNED
+/// Returns a pointer with an assumed alignment.
+#if __has_builtin(__builtin_assume_aligned) || defined(__GNUC__)
+#define LIBBASE_ASSUME_ALIGNED(p, a) __builtin_assume_aligned (p, a)
+#elif defined(LIBBASE_BUILTIN_UNREACHABLE)
+#define LIBBASE_ASSUME_ALIGNED(p, a) \
+ (((uintptr_t (p) % (a)) == 0) ? (p) : (LIBBASE_BUILTIN_UNREACHABLE, (p)))
+#else
+#define LIBBASE_ASSUME_ALIGNED(p, a) (p)
+#endif
+
+/// \macro LIBBASE_PACKED
+/// Used to specify a packed structure.
+/// LIBBASE_PACKED(
+/// struct A {
+/// int i;
+/// int j;
+/// int k;
+/// long long l;
+/// });
+///
+/// LIBBASE_PACKED_START
+/// struct B {
+/// int i;
+/// int j;
+/// int k;
+/// long long l;
+/// };
+/// LIBBASE_PACKED_END
+#ifdef _MSC_VER
+#define LIBBASE_PACKED(d) __pragma (pack (push, 1)) d __pragma (pack (pop))
+#define LIBBASE_PACKED_START __pragma (pack (push, 1))
+#define LIBBASE_PACKED_END __pragma (pack (pop))
+#else
+#define LIBBASE_PACKED(d) d __attribute__ ((packed))
+#define LIBBASE_PACKED_START _Pragma ("pack(push, 1)")
+#define LIBBASE_PACKED_END _Pragma ("pack(pop)")
+#endif
+
+/// \macro LIBBASE_MEMORY_SANITIZER_BUILD
+/// Whether LLVM itself is built with MemorySanitizer instrumentation.
+#if __has_feature(memory_sanitizer)
+#define LIBBASE_MEMORY_SANITIZER_BUILD 1
+#include <sanitizer/msan_interface.h>
+#define LIBBASE_NO_SANITIZE_MEMORY_ATTRIBUTE \
+ __attribute__ ((no_sanitize_memory))
+#else
+#define LIBBASE_MEMORY_SANITIZER_BUILD 0
+#define __msan_allocated_memory(p, size)
+#define __msan_unpoison(p, size)
+#define LIBBASE_NO_SANITIZE_MEMORY_ATTRIBUTE
+#endif
+
+/// \macro LIBBASE_ADDRESS_SANITIZER_BUILD
+/// Whether LLVM itself is built with AddressSanitizer instrumentation.
+#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
+#define LIBBASE_ADDRESS_SANITIZER_BUILD 1
+#if __has_include(<sanitizer/asan_interface.h>)
+#include <sanitizer/asan_interface.h>
+#else
+// These declarations exist to support ASan with MSVC. If MSVC eventually ships
+// asan_interface.h in their headers, then we can remove this.
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+ void
+ __asan_poison_memory_region (void const volatile *addr, size_t size);
+ void
+ __asan_unpoison_memory_region (void const volatile *addr, size_t size);
+#ifdef __cplusplus
+} // extern "C"
+#endif
+#endif
+#else
+#define LIBBASE_ADDRESS_SANITIZER_BUILD 0
+#define __asan_poison_memory_region(p, size)
+#define __asan_unpoison_memory_region(p, size)
+#endif
+
+/// \macro LIBBASE_HWADDRESS_SANITIZER_BUILD
+/// Whether LLVM itself is built with HWAddressSanitizer instrumentation.
+#if __has_feature(hwaddress_sanitizer)
+#define LIBBASE_HWADDRESS_SANITIZER_BUILD 1
+#else
+#define LIBBASE_HWADDRESS_SANITIZER_BUILD 0
+#endif
+
+/// \macro LIBBASE_THREAD_SANITIZER_BUILD
+/// Whether LLVM itself is built with ThreadSanitizer instrumentation.
+#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__)
+#define LIBBASE_THREAD_SANITIZER_BUILD 1
+#else
+#define LIBBASE_THREAD_SANITIZER_BUILD 0
+#endif
+
+#if LIBBASE_THREAD_SANITIZER_BUILD
+// Thread Sanitizer is a tool that finds races in code.
+// See http://code.google.com/p/data-race-test/wiki/DynamicAnnotations .
+// tsan detects these exact functions by name.
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+ void
+ AnnotateHappensAfter (const char *file, int line, const volatile void *cv);
+ void
+ AnnotateHappensBefore (const char *file, int line, const volatile void *cv);
+ void
+ AnnotateIgnoreWritesBegin (const char *file, int line);
+ void
+ AnnotateIgnoreWritesEnd (const char *file, int line);
+#ifdef __cplusplus
+}
+#endif
+
+// This marker is used to define a happens-before arc. The race detector will
+// infer an arc from the begin to the end when they share the same pointer
+// argument.
+#define TsanHappensBefore(cv) AnnotateHappensBefore (__FILE__, __LINE__, cv)
+
+// This marker defines the destination of a happens-before arc.
+#define TsanHappensAfter(cv) AnnotateHappensAfter (__FILE__, __LINE__, cv)
+
+// Ignore any races on writes between here and the next TsanIgnoreWritesEnd.
+#define TsanIgnoreWritesBegin() AnnotateIgnoreWritesBegin (__FILE__, __LINE__)
+
+// Resume checking for racy writes.
+#define TsanIgnoreWritesEnd() AnnotateIgnoreWritesEnd (__FILE__, __LINE__)
+#else
+#define TsanHappensBefore(cv)
+#define TsanHappensAfter(cv)
+#define TsanIgnoreWritesBegin()
+#define TsanIgnoreWritesEnd()
+#endif
+
+/// \macro LIBBASE_NO_SANITIZE
+/// Disable a particular sanitizer for a function.
+#if __has_attribute(no_sanitize)
+#define LIBBASE_NO_SANITIZE(KIND) __attribute__ ((no_sanitize (KIND)))
+#else
+#define LIBBASE_NO_SANITIZE(KIND)
+#endif
+
+/// Mark debug helper function definitions like dump() that should not be
+/// stripped from debug builds.
+/// Note that you should also surround dump() functions with
+/// `#if !defined(NDEBUG) || defined(LIBBASE_ENABLE_DUMP)` so they do always
+/// get stripped in release builds.
+// FIXME: Move this to a private config.h as it's not usable in public headers.
+#if !defined(NDEBUG) || defined(LIBBASE_ENABLE_DUMP)
+#define LIBBASE_DUMP_METHOD \
+ LIBBASE_ATTRIBUTE_NOINLINE LIBBASE_ATTRIBUTE_USED LIBBASE_ATTRIBUTE_RETAIN
+#else
+#define LIBBASE_DUMP_METHOD LIBBASE_ATTRIBUTE_NOINLINE
+#endif
+
+/// \macro LIBBASE_PRETTY_FUNCTION
+/// Gets a user-friendly looking function signature for the current scope
+/// using the best available method on each platform. The exact format of the
+/// resulting string is implementation specific and non-portable, so this should
+/// only be used, for example, for logging or diagnostics.
+#if defined(_MSC_VER)
+#define LIBBASE_PRETTY_FUNCTION __FUNCSIG__
+#elif defined(__GNUC__) || defined(__clang__)
+#define LIBBASE_PRETTY_FUNCTION __PRETTY_FUNCTION__
+#else
+#define LIBBASE_PRETTY_FUNCTION __func__
+#endif
+
+/// \macro LIBBASE_THREAD_LOCAL
+/// A thread-local storage specifier which can be used with globals,
+/// extern globals, and static globals.
+///
+/// This is essentially an extremely restricted analog to C++11's thread_local
+/// support. It uses thread_local if available, falling back on gcc __thread
+/// if not. __thread doesn't support many of the C++11 thread_local's
+/// features. You should only use this for PODs that you can statically
+/// initialize to some constant value. In almost all circumstances this is most
+/// appropriate for use with a pointer, integer, or small aggregation of
+/// pointers and integers.
+#if LIBBASE_ENABLE_THREADS
+#if __has_feature(cxx_thread_local) || defined(_MSC_VER)
+#define LIBBASE_THREAD_LOCAL thread_local
+#else
+// Clang, GCC, and other compatible compilers used __thread prior to C++11 and
+// we only need the restricted functionality that provides.
+#define LIBBASE_THREAD_LOCAL __thread
+#endif
+#else // !LIBBASE_ENABLE_THREADS
+// If threading is disabled entirely, this compiles to nothing and you get
+// a normal global variable.
+#define LIBBASE_THREAD_LOCAL
+#endif
+
+/// \macro LIBBASE_ENABLE_EXCEPTIONS
+/// Whether LLVM is built with exception support.
+#if __has_feature(cxx_exceptions)
+#define LIBBASE_ENABLE_EXCEPTIONS 1
+#elif defined(__GNUC__) && defined(__EXCEPTIONS)
+#define LIBBASE_ENABLE_EXCEPTIONS 1
+#elif defined(_MSC_VER) && defined(_CPPUNWIND)
+#define LIBBASE_ENABLE_EXCEPTIONS 1
+#endif
+
+/// \macro LIBBASE_NO_PROFILE_INSTRUMENT_FUNCTION
+/// Disable the profile instrument for a function.
+#if __has_attribute(no_profile_instrument_function)
+#define LIBBASE_NO_PROFILE_INSTRUMENT_FUNCTION \
+ __attribute__ ((no_profile_instrument_function))
+#else
+#define LIBBASE_NO_PROFILE_INSTRUMENT_FUNCTION
+#endif
+
+/// \macro LIBBASE_PREFERRED_TYPE
+/// Adjust type of bit-field in debug info.
+#if __has_attribute(preferred_type)
+#define LIBBASE_PREFERRED_TYPE(T) __attribute__ ((preferred_type (T)))
+#else
+#define LIBBASE_PREFERRED_TYPE(T)
+#endif
+
+#if LIBBASE_HAS_CPP_ATTRIBUTE(clang::ptrauth_vtable_pointer) && \
+ (defined(__PTRAUTH__) || __has_feature(ptrauth_calls))
+#define LIBBASE_MOVABLE_POLYMORPHIC_TYPE \
+ [[clang::ptrauth_vtable_pointer (default_key, no_address_discrimination, \
+ default_extra_discrimination)]]
+#else
+#define LIBBASE_MOVABLE_POLYMORPHIC_TYPE
+#endif
+
+/// \macro LIBBASE_VIRTUAL_ANCHOR_FUNCTION
+/// This macro is used to adhere to LLVM's policy that each class with a vtable
+/// must have at least one out-of-line virtual function. This macro allows us
+/// to declare such a function in `final` classes without triggering a warning.
+// clang-format off
+// Autoformatting makes this look awful.
+#if defined(__clang__)
+ // Make sure this is only parsed if __clang__ is defined
+ #if __has_warning("-Wunnecessary-virtual-specifier")
+ #define LIBBASE_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wunnecessary-virtual-specifier\"") \
+ virtual void anchor() \
+ _Pragma("clang diagnostic pop")
+ #else // __has_warning
+ #define LIBBASE_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
+ virtual void anchor()
+ #endif
+#else // defined(__clang__)
+ #define LIBBASE_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
+ virtual void anchor()
+#endif
+// clang-format on
+
+/// \macro LIBBASE_IS_X86
+/// Whether the target architecture is x86 / x86-64.
+#if defined(__x86_64__) || defined(__i386__)
+#define LIBBASE_IS_X86 1
+#else
+#define LIBBASE_IS_X86 0
+#endif
+
+/// \macro LIBBASE_TARGET_SSE42
+/// Function attribute to compile a function with SSE4.2 enabled.
+#if defined(__has_attribute) && __has_attribute(target)
+#define LIBBASE_TARGET_SSE42 __attribute__ ((target ("sse4.2")))
+#else
+#define LIBBASE_TARGET_SSE42
+#endif
+
+#if __has_builtin(__builtin_cpu_supports) && \
+ (defined(__linux__) || defined(__APPLE__))
+#define LIBBASE_CPU_SUPPORTS(feature) __builtin_cpu_supports (feature)
+#else
+#define LIBBASE_CPU_SUPPORTS(feature) 0
+#endif
+
+/// \macro LIBBASE_CPU_SUPPORTS_SSE42
+/// Expands to true if the runtime cpu supports SSE4.2, or if compiled with
+/// SSE4.2 enabled.
+#if defined(__SSE4_2__)
+#define LIBBASE_CPU_SUPPORTS_SSE42 1
+#else
+#define LIBBASE_CPU_SUPPORTS_SSE42 LIBBASE_CPU_SUPPORTS ("sse4.2")
+#endif
+
+#endif
diff --git a/rcc/include/rcc/DataTypes.h b/libbase/include/core/compiler/DataTypes.h
index 59e9b75..bfcf577 100644
--- a/rcc/include/rcc/DataTypes.h
+++ b/libbase/include/core/compiler/DataTypes.h
@@ -46,8 +46,8 @@ along with RCC-Project; see the file COPYING. If not see
|*===----------------------------------------------------------------------===*/
/* Please leave this file C-compatible. */
-#ifndef RCC_DATATYPES_H
-#define RCC_DATATYPES_H
+#ifndef LIBBASE_DATATYPES_H
+#define LIBBASE_DATATYPES_H
#include <inttypes.h>
#include <stdint.h>
diff --git a/libbase/include/core/config/config.h.cmake b/libbase/include/core/config/config.h.cmake
new file mode 100644
index 0000000..cf9b602
--- /dev/null
+++ b/libbase/include/core/config/config.h.cmake
@@ -0,0 +1,219 @@
+#ifndef LIBBASE_PRIVATE_CONFIG_H
+#define LIBBASE_PRIVATE_CONFIG_H
+
+// Include this header only under the libbase source tree.
+// This is a private header.
+
+/* Exported configuration */
+#include "core/config/libbase-config.h"
+
+/* Define to 1 to enable backtraces, and to 0 otherwise. */
+#cmakedefine01 ENABLE_BACKTRACES
+
+/* Define to 1 to enable crash overrides, and to 0 otherwise. */
+#cmakedefine01 ENABLE_CRASH_OVERRIDES
+
+/* Define to 1 to enable crash memory dumps, and to 0 otherwise. */
+#cmakedefine01 RCC_ENABLE_CRASH_DUMPS
+
+/* Define to 1 to prefer forward slashes on Windows, and to 0 otherwise. */
+#cmakedefine01 RCC_WINDOWS_PREFER_FORWARD_SLASH
+
+/* Define to 1 if you have the `backtrace' function. */
+#cmakedefine HAVE_BACKTRACE ${HAVE_BACKTRACE}
+
+#define BACKTRACE_HEADER <${BACKTRACE_HEADER}>
+
+/* can use __crashreporter_info__ */
+#cmakedefine01 HAVE_CRASHREPORTER_INFO
+
+/* Define to 1 if you have the declaration of `arc4random', and to 0 if you
+ don't. */
+#cmakedefine01 HAVE_DECL_ARC4RANDOM
+
+/* Define to 1 if you have the declaration of `strerror_s', and to 0 if you
+ don't. */
+#cmakedefine01 HAVE_DECL_STRERROR_S
+
+/* Define if dlopen() is available on this platform. */
+#cmakedefine HAVE_DLOPEN ${HAVE_DLOPEN}
+
+/* Define to 1 if you have the `futimens' function. */
+#cmakedefine HAVE_FUTIMENS ${HAVE_FUTIMENS}
+
+/* Define to 1 if you have the `futimes' function. */
+#cmakedefine HAVE_FUTIMES ${HAVE_FUTIMES}
+
+/* Define to 1 if you have the `getpagesize' function. */
+#cmakedefine HAVE_GETPAGESIZE ${HAVE_GETPAGESIZE}
+
+/* Define to 1 if you have the `getrusage' function. */
+#cmakedefine HAVE_GETRUSAGE ${HAVE_GETRUSAGE}
+
+/* Define to 1 if you have the `isatty' function. */
+#cmakedefine HAVE_ISATTY 1
+
+/* Define to 1 if you have the `edit' library (-ledit). */
+#cmakedefine HAVE_LIBEDIT ${HAVE_LIBEDIT}
+
+/* Define to 1 if you have the `psapi' library (-lpsapi). */
+#cmakedefine HAVE_LIBPSAPI ${HAVE_LIBPSAPI}
+
+/* Define to 1 if you have the `pthread' library (-lpthread). */
+#cmakedefine HAVE_LIBPTHREAD ${HAVE_LIBPTHREAD}
+
+/* Define to 1 if you have the `pthread_getname_np' function. */
+#cmakedefine HAVE_PTHREAD_GETNAME_NP ${HAVE_PTHREAD_GETNAME_NP}
+
+/* Define to 1 if you have the `pthread_setname_np' function. */
+#cmakedefine HAVE_PTHREAD_SETNAME_NP ${HAVE_PTHREAD_SETNAME_NP}
+
+/* Define to 1 if you have the `pthread_get_name_np' function. */
+#cmakedefine HAVE_PTHREAD_GET_NAME_NP ${HAVE_PTHREAD_GET_NAME_NP}
+
+/* Define to 1 if you have the `pthread_set_name_np' function. */
+#cmakedefine HAVE_PTHREAD_SET_NAME_NP ${HAVE_PTHREAD_SET_NAME_NP}
+
+/* Define to 1 if you have the <mach/mach.h> header file. */
+#cmakedefine HAVE_MACH_MACH_H ${HAVE_MACH_MACH_H}
+
+/* Define to 1 if you have the `mallctl' function. */
+#cmakedefine HAVE_MALLCTL ${HAVE_MALLCTL}
+
+/* Define to 1 if you have the `mallinfo' function. */
+#cmakedefine HAVE_MALLINFO ${HAVE_MALLINFO}
+
+/* Define to 1 if you have the `mallinfo2' function. */
+#cmakedefine HAVE_MALLINFO2 ${HAVE_MALLINFO2}
+
+/* Define to 1 if you have the <malloc/malloc.h> header file. */
+#cmakedefine HAVE_MALLOC_MALLOC_H ${HAVE_MALLOC_MALLOC_H}
+
+/* Define to 1 if you have the `malloc_zone_statistics' function. */
+#cmakedefine HAVE_MALLOC_ZONE_STATISTICS ${HAVE_MALLOC_ZONE_STATISTICS}
+
+/* Define to 1 if you have the `posix_spawn' function. */
+#cmakedefine HAVE_POSIX_SPAWN ${HAVE_POSIX_SPAWN}
+
+/* Define to 1 if you have the `pread' function. */
+#cmakedefine HAVE_PREAD ${HAVE_PREAD}
+
+/* Define to 1 if you have the <pthread.h> header file. */
+#cmakedefine HAVE_PTHREAD_H ${HAVE_PTHREAD_H}
+
+/* Have pthread_rwlock_init */
+#cmakedefine HAVE_PTHREAD_RWLOCK_INIT ${HAVE_PTHREAD_RWLOCK_INIT}
+
+/* Define to 1 if you have the `sbrk' function. */
+#cmakedefine HAVE_SBRK ${HAVE_SBRK}
+
+/* Define to 1 if you have the `setenv' function. */
+#cmakedefine HAVE_SETENV ${HAVE_SETENV}
+
+/* Define to 1 if you have the `sigaltstack' function. */
+#cmakedefine HAVE_SIGALTSTACK ${HAVE_SIGALTSTACK}
+
+/* Define to 1 if you have the `strerror_r' function. */
+#cmakedefine HAVE_STRERROR_R ${HAVE_STRERROR_R}
+
+/* Define to 1 if you have the `sysconf' function. */
+#cmakedefine HAVE_SYSCONF ${HAVE_SYSCONF}
+
+/* Define to 1 if you have the <sys/mman.h> header file. */
+#cmakedefine HAVE_SYS_MMAN_H ${HAVE_SYS_MMAN_H}
+
+/* Define to 1 if you have the <sys/ioctl.h> header file. */
+#cmakedefine HAVE_SYS_IOCTL_H ${HAVE_SYS_IOCTL_H}
+
+/* Define to 1 if stat struct has st_mtimespec member .*/
+#cmakedefine HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC ${HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC}
+
+/* Define to 1 if stat struct has st_mtim member. */
+#cmakedefine HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC ${HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC}
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#cmakedefine HAVE_UNISTD_H ${HAVE_UNISTD_H}
+
+/* Have host's _alloca */
+#cmakedefine HAVE__ALLOCA ${HAVE__ALLOCA}
+
+/* Define to 1 if you have the `_chsize_s' function. */
+#cmakedefine HAVE__CHSIZE_S ${HAVE__CHSIZE_S}
+
+/* Define to 1 if you have the `_Unwind_Backtrace' function. */
+#cmakedefine HAVE__UNWIND_BACKTRACE ${HAVE__UNWIND_BACKTRACE}
+
+/* Have host's __alloca */
+#cmakedefine HAVE___ALLOCA ${HAVE___ALLOCA}
+
+/* Have host's __ashldi3 */
+#cmakedefine HAVE___ASHLDI3 ${HAVE___ASHLDI3}
+
+/* Have host's __ashrdi3 */
+#cmakedefine HAVE___ASHRDI3 ${HAVE___ASHRDI3}
+
+/* Have host's __chkstk */
+#cmakedefine HAVE___CHKSTK ${HAVE___CHKSTK}
+
+/* Have host's __chkstk_ms */
+#cmakedefine HAVE___CHKSTK_MS ${HAVE___CHKSTK_MS}
+
+/* Have host's __cmpdi2 */
+#cmakedefine HAVE___CMPDI2 ${HAVE___CMPDI2}
+
+/* Have host's __divdi3 */
+#cmakedefine HAVE___DIVDI3 ${HAVE___DIVDI3}
+
+/* Have host's __fixdfdi */
+#cmakedefine HAVE___FIXDFDI ${HAVE___FIXDFDI}
+
+/* Have host's __fixsfdi */
+#cmakedefine HAVE___FIXSFDI ${HAVE___FIXSFDI}
+
+/* Have host's __floatdidf */
+#cmakedefine HAVE___FLOATDIDF ${HAVE___FLOATDIDF}
+
+/* Have host's __lshrdi3 */
+#cmakedefine HAVE___LSHRDI3 ${HAVE___LSHRDI3}
+
+/* Have host's __main */
+#cmakedefine HAVE___MAIN ${HAVE___MAIN}
+
+/* Have host's __moddi3 */
+#cmakedefine HAVE___MODDI3 ${HAVE___MODDI3}
+
+/* Have host's __udivdi3 */
+#cmakedefine HAVE___UDIVDI3 ${HAVE___UDIVDI3}
+
+/* Have host's __umoddi3 */
+#cmakedefine HAVE___UMODDI3 ${HAVE___UMODDI3}
+
+/* Have host's ___chkstk */
+#cmakedefine HAVE____CHKSTK ${HAVE____CHKSTK}
+
+/* Have host's ___chkstk_ms */
+#cmakedefine HAVE____CHKSTK_MS ${HAVE____CHKSTK_MS}
+
+/* Define if ICU library is available */
+#cmakedefine01 HAVE_ICU
+
+/* Define if Windows vendored ICU is available */
+#cmakedefine01 HAVE_WINDOWS_ICU
+
+/* Define if iconv library is available */
+#cmakedefine01 HAVE_ICONV
+
+/* Define to a function implementing stricmp */
+#cmakedefine stricmp ${stricmp}
+
+/* Define to a function implementing strdup */
+#cmakedefine strdup ${strdup}
+
+/* Whether Timers signpost passes in Xcode Instruments */
+#cmakedefine01 RCC_SUPPORT_XCODE_SIGNPOSTS
+
+#cmakedefine HAVE_PROC_PID_RUSAGE 1
+
+#cmakedefine HAVE_GETAUXVAL ${HAVE_GETAUXVAL}
+
+#endif
diff --git a/libbase/include/core/config/libbase-config.h.cmake b/libbase/include/core/config/libbase-config.h.cmake
new file mode 100644
index 0000000..2ea111b
--- /dev/null
+++ b/libbase/include/core/config/libbase-config.h.cmake
@@ -0,0 +1,89 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+/* This file enumerates variables from the libbase configuration so that they
+ can be used by exported headers without depending on RCC configuration. */
+
+#ifndef LIBBASE_CONFIG_H
+#define LIBBASE_CONFIG_H
+
+/* Define if libbase dump support is enabled. */
+#cmakedefine LIBBASE_ENABLE_DUMP
+
+/* Define if threads are enabled in libbase. */
+#cmakedefine01 LIBBASE_ENABLE_THREADS
+
+/* Has gcc/MSVC atomic intrinsics. */
+#cmakedefine01 LIBBASE_HAS_ATOMICS
+
+/* Define if this is a Unixish platform. */
+#cmakedefine LIBBASE_ON_UNIX ${LIBBASE_ON_UNIX}
+
+/* Whether libbase records statistics. */
+#cmakedefine01 LIBBASE_FORCE_ENABLE_STATS
+
+/* Define if libbase has z3 support. */
+#cmakedefine LIBBASE_WITH_Z3 ${LIBBASE_WITH_Z3}
+
+/* Define if libbase has curl support. */
+#cmakedefine LIBBASE_ENABLE_CURL ${LIBBASE_ENABLE_CURL}
+
+/* Define if libbase has cpp-httplib support. */
+#cmakedefine LIBBASE_ENABLE_HTTPLIB ${LIBBASE_ENABLE_HTTPLIB}
+
+/* Define if zlib compression is available to libbase. */
+#cmakedefine01 LIBBASE_ENABLE_ZLIB
+
+/* Define if zstd compression is available to libbase. */
+#cmakedefine01 LIBBASE_ENABLE_ZSTD
+
+/* Define if xz (lzma) decompression is available to libbase. */
+#cmakedefine01 LIBBASE_ENABLE_LZMA
+
+/* Define to 1 if libbase has the <sysexits.h> header file. */
+#cmakedefine LIBBASE_HAVE_SYSEXITS_H ${LIBBASE_HAVE_SYSEXITS_H}
+
+/* Define if libbase is built with BUILD_SHARED_LIBS. */
+#cmakedefine LIBBASE_BUILD_SHARED_LIBS
+
+/* Define if libbase is built as a static library. */
+#cmakedefine LIBBASE_BUILD_STATIC
+
+/* Define if exporting the libbase public interface for a shared library. */
+#cmakedefine LIBBASE_ENABLE_EXPORT_ANNOTATIONS
+
+/* Define if libbase uses the old toolchain behavior. */
+#cmakedefine LIBBASE_FORCE_USE_OLD_TOOLCHAIN ${LIBBASE_FORCE_USE_OLD_TOOLCHAIN}
+
+/* Define if libbase_unreachable should be optimized with undefined behavior
+ * in non-assert builds. */
+#cmakedefine01 LIBBASE_UNREACHABLE_OPTIMIZE
+
+/* Define if libbase provides I/O sandbox support. */
+#cmakedefine01 LIBBASE_ENABLE_IO_SANDBOX
+
+/* Define if logf128 is available to libbase. */
+#cmakedefine LIBBASE_HAS_LOGF128
+
+/* Define if libbase provides on-disk content-addressable storage. */
+#cmakedefine01 LIBBASE_ENABLE_ONDISK_CAS
+
+#endif
diff --git a/rcc/include/rcc/Support/Errno.h b/libbase/include/core/error/Errno.h
index 21d8d78..9f0c3b9 100644
--- a/rcc/include/rcc/Support/Errno.h
+++ b/libbase/include/core/error/Errno.h
@@ -21,15 +21,15 @@
// This implementation is based on llvm/include/llvm/Support/Errno.h.
-#ifndef RCC_SUPPORT_ERRNO_H
-#define RCC_SUPPORT_ERRNO_H
+#ifndef LIBBASE_SUPPORT_ERRNO_H
+#define LIBBASE_SUPPORT_ERRNO_H
-#include "rcc/Support/Compiler.h"
+#include "core/compiler/Compiler.h"
#include <cerrno>
#include <string>
-namespace rcc
+namespace libbase
{
namespace sys
{
@@ -38,11 +38,11 @@ namespace sys
/// thread-safe variant of strerror() is available. Be sure to call this
/// immediately after the function that set errno, or errno may have been
/// overwritten by an intervening call.
-RCC_ABI std::string
+LIBBASE_ABI std::string
StrError ();
/// Like the no-argument version above, but uses \p errnum instead of errno.
-RCC_ABI std::string
+LIBBASE_ABI std::string
StrError (int errnum);
template <typename FailT, typename Fun, typename... Args>
@@ -59,6 +59,6 @@ RetryAfterSignal (const FailT &Fail, const Fun &F, const Args &...As)
}
} // namespace sys
-} // namespace rcc
+} // namespace libbase
-#endif // RCC_SUPPORT_ERRNO_H
+#endif // LIBBASE_SUPPORT_ERRNO_H
diff --git a/libbase/include/core/error/ErrorHandling.h b/libbase/include/core/error/ErrorHandling.h
new file mode 100644
index 0000000..67461c3
--- /dev/null
+++ b/libbase/include/core/error/ErrorHandling.h
@@ -0,0 +1,217 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- llvm/Support/ErrorHandling.h - Fatal error handling ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines an API used to indicate fatal error conditions. Non-fatal
+// errors (most of them) should be handled through LLVMContext.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_SUPPORT_ERRORHANDLING_H
+#define LIBBASE_SUPPORT_ERRORHANDLING_H
+
+#include "core/compiler/Compiler.h"
+
+namespace libbase
+{
+class StringRef;
+class Twine;
+
+/// An error handler callback.
+using fatal_error_handler_t = void (*) (void *user_data, const char *reason,
+ bool gen_crash_diag);
+
+/// install_fatal_error_handler - Installs a new error handler to be used
+/// whenever a serious (non-recoverable) error is encountered by LLVM.
+///
+/// If no error handler is installed the default is to print the error message
+/// to stderr, and call exit(1). If an error handler is installed then it is
+/// the handler's responsibility to log the message, it will no longer be
+/// printed to stderr. If the error handler returns, then exit(1) will be
+/// called.
+///
+/// It is dangerous to naively use an error handler which throws an exception.
+/// Even though some applications desire to gracefully recover from arbitrary
+/// faults, blindly throwing exceptions through unfamiliar code isn't a way to
+/// achieve this.
+///
+/// \param user_data - An argument which will be passed to the install error
+/// handler.
+LIBBASE_ABI void
+install_fatal_error_handler (fatal_error_handler_t handler,
+ void *user_data = nullptr);
+
+/// Restores default error handling behaviour.
+LIBBASE_ABI void
+remove_fatal_error_handler ();
+
+/// ScopedFatalErrorHandler - This is a simple helper class which just
+/// calls install_fatal_error_handler in its constructor and
+/// remove_fatal_error_handler in its destructor.
+struct ScopedFatalErrorHandler
+{
+ explicit ScopedFatalErrorHandler (fatal_error_handler_t handler,
+ void *user_data = nullptr)
+ {
+ install_fatal_error_handler (handler, user_data);
+ }
+
+ ~ScopedFatalErrorHandler ()
+ {
+ remove_fatal_error_handler ();
+ }
+};
+
+/// @deprecated Use reportFatalInternalError() or reportFatalUsageError()
+/// instead.
+[[noreturn]] LIBBASE_ABI void
+report_fatal_error (const char *reason, bool gen_crash_diag = true);
+[[noreturn]] LIBBASE_ABI void
+report_fatal_error (StringRef reason, bool gen_crash_diag = true);
+[[noreturn]] LIBBASE_ABI void
+report_fatal_error (const Twine &reason, bool gen_crash_diag = true);
+
+/// Report a fatal error that likely indicates a bug in LLVM. It serves a
+/// similar purpose as an assertion, but is always enabled, regardless of the
+/// value of NDEBUG.
+///
+/// This will call installed error handlers (or print the message by default)
+/// and then abort. This will produce a crash trace and *will* ask users to
+/// report an LLVM bug.
+[[noreturn]] LIBBASE_ABI void
+reportFatalInternalError (const char *reason);
+[[noreturn]] LIBBASE_ABI void
+reportFatalInternalError (StringRef reason);
+[[noreturn]] LIBBASE_ABI void
+reportFatalInternalError (const Twine &reason);
+
+/// Report a fatal error that does not indicate a bug in LLVM.
+///
+/// This can be used in contexts where a proper error reporting mechanism
+/// (such as Error/Expected or DiagnosticInfo) is currently not supported, and
+/// would be too involved to introduce at the moment.
+///
+/// Examples where this function should be used instead of
+/// reportFatalInternalError() include invalid inputs or options, but also
+/// environment error conditions outside LLVM's control. It should also be used
+/// for known unsupported/unimplemented functionality.
+///
+/// This will call installed error handlers (or print the message by default)
+/// and then exit with code 1. It will not produce a crash trace and will
+/// *not* ask users to report an LLVM bug.
+[[noreturn]] LIBBASE_ABI void
+reportFatalUsageError (const char *reason);
+[[noreturn]] LIBBASE_ABI void
+reportFatalUsageError (StringRef reason);
+[[noreturn]] LIBBASE_ABI void
+reportFatalUsageError (const Twine &reason);
+
+/// Installs a new bad alloc error handler that should be used whenever a
+/// bad alloc error, e.g. failing malloc/calloc, is encountered by LLVM.
+///
+/// The user can install a bad alloc handler, in order to define the behavior
+/// in case of failing allocations, e.g. throwing an exception. Note that this
+/// handler must not trigger any additional allocations itself.
+///
+/// If no error handler is installed the default is to print the error message
+/// to stderr, and call exit(1). If an error handler is installed then it is
+/// the handler's responsibility to log the message, it will no longer be
+/// printed to stderr. If the error handler returns, then exit(1) will be
+/// called.
+///
+///
+/// \param user_data - An argument which will be passed to the installed error
+/// handler.
+LIBBASE_ABI void
+install_bad_alloc_error_handler (fatal_error_handler_t handler,
+ void *user_data = nullptr);
+
+/// Restores default bad alloc error handling behavior.
+LIBBASE_ABI void
+remove_bad_alloc_error_handler ();
+
+LIBBASE_ABI void
+install_out_of_memory_new_handler ();
+
+/// Reports a bad alloc error, calling any user defined bad alloc
+/// error handler. In contrast to the generic 'report_fatal_error'
+/// functions, this function might not terminate, e.g. the user
+/// defined error handler throws an exception, but it won't return.
+///
+/// Note: When throwing an exception in the bad alloc handler, make sure that
+/// the following unwind succeeds, e.g. do not trigger additional allocations
+/// in the unwind chain.
+///
+/// If no error handler is installed (default), throws a bad_alloc exception
+/// if LLVM is compiled with exception support. Otherwise prints the error
+/// to standard error and calls abort().
+[[noreturn]] LIBBASE_ABI void
+report_bad_alloc_error (const char *Reason, bool GenCrashDiag = true);
+
+/// This function calls abort(), and prints the optional message to stderr.
+/// Use the llvm_unreachable macro (that adds location info), instead of
+/// calling this function directly.
+[[noreturn]] LIBBASE_ABI void
+libbase_unreachable_internal (const char *msg = nullptr,
+ const char *file = nullptr, unsigned line = 0);
+} // namespace libbase
+
+/// Marks that the current location is not supposed to be reachable.
+/// In !NDEBUG builds, prints the message and location info to stderr.
+/// In NDEBUG builds, if the platform does not support a builtin unreachable
+/// then we call an internal LLVM runtime function. Otherwise the behavior is
+/// controlled by the CMake flag
+/// -DLLVM_UNREACHABLE_OPTIMIZE
+/// * When "ON" (default) llvm_unreachable() becomes an optimizer hint
+/// that the current location is not supposed to be reachable: the hint
+/// turns such code path into undefined behavior. On compilers that don't
+/// support such hints, prints a reduced message instead and aborts the
+/// program.
+/// * When "OFF", a builtin_trap is emitted instead of an
+// optimizer hint or printing a reduced message.
+///
+/// Use this instead of assert(0). It conveys intent more clearly, suppresses
+/// diagnostics for unreachable code paths, and allows compilers to omit
+/// unnecessary code.
+#ifndef NDEBUG
+#define libbase_unreachable(msg) \
+ ::libbase::libbase_unreachable_internal (msg, __FILE__, __LINE__)
+#elif !defined(LIBBASE_BUILTIN_UNREACHABLE)
+#define libbase_unreachable(msg) ::libbase::libbase_unreachable_internal ()
+#elif LIBBASE_UNREACHABLE_OPTIMIZE
+#define libbase_unreachable(msg) LIBBASE_BUILTIN_UNREACHABLE
+#else
+#define libbase_unreachable(msg) \
+ do \
+ { \
+ LIBBASE_BUILTIN_TRAP; \
+ LIBBASE_BUILTIN_UNREACHABLE; \
+ } while (false)
+#endif
+
+#endif
diff --git a/rcc/include/rcc/Support/MathExtras.h b/libbase/include/core/math/MathExtras.h
index 6ea2a1f..732f82c 100644
--- a/rcc/include/rcc/Support/MathExtras.h
+++ b/libbase/include/core/math/MathExtras.h
@@ -31,10 +31,10 @@
//
//===----------------------------------------------------------------------===//
-#ifndef RCC_SUPPORT_MATHEXTRAS_H
-#define RCC_SUPPORT_MATHEXTRAS_H
+#ifndef LIBBASE_SUPPORT_MATHEXTRAS_H
+#define LIBBASE_SUPPORT_MATHEXTRAS_H
-#include "rcc/Support/Compiler.h"
+#include "core/compiler/Compiler.h"
#include <bit>
#include <cassert>
#include <climits>
@@ -45,7 +45,7 @@
#include <type_traits>
#include <utility>
-namespace rcc
+namespace libbase
{
/// Some template parameter helpers to optimize for bitwidth, for functions that
/// take multiple arguments.
@@ -432,7 +432,7 @@ template <size_t kValue>
constexpr size_t
ConstantLog2 ()
{
- static_assert (rcc::isPowerOf2_64 (kValue),
+ static_assert (libbase::isPowerOf2_64 (kValue),
"Value is not a valid power of 2");
return std::countr_zero (kValue);
}
@@ -894,7 +894,7 @@ SaturatingMultiplyAdd (T X, T Y, T A, bool *ResultOverflowed = nullptr)
}
/// Use this rather than HUGE_VALF; the latter causes warnings on MSVC.
-RCC_ABI extern const float huge_valf;
+LIBBASE_ABI extern const float huge_valf;
/// Add two signed integers, computing the two's complement truncated result,
/// returning a pair {result, overflow}, where "overflow" is a boolean value
@@ -1042,9 +1042,9 @@ using stack_float_t = float;
#endif
/// Returns the number of digits in the given integer.
-RCC_ABI int
+LIBBASE_ABI int
NumDigitsBase10 (uint64_t X);
-} // namespace rcc
+} // namespace libbase
#endif
diff --git a/libbase/include/core/memory/Alignment.h b/libbase/include/core/memory/Alignment.h
new file mode 100644
index 0000000..1391e06
--- /dev/null
+++ b/libbase/include/core/memory/Alignment.h
@@ -0,0 +1,497 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===-- rcc/Memory/Alignment.h - Useful alignment functions ---*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file contains types to represent alignments.
+// They are instrumented to guarantee some invariants are preserved and prevent
+// invalid manipulations.
+//
+// - Align represents an alignment in bytes, it is always set and always a valid
+// power of two, its minimum value is 1 which means no alignment requirements.
+//
+// - MaybeAlign is an optional type, it may be undefined or set. When it's set
+// you can get the underlying Align type by using the value() method.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_SUPPORT_ALIGNMENT_H_
+#define LIBBASE_SUPPORT_ALIGNMENT_H_
+
+#include "core/math/MathExtras.h"
+#include <cassert>
+#include <optional>
+#ifndef NDEBUG
+#include <string>
+#endif // NDEBUG
+
+namespace libbase
+{
+
+#define ALIGN_CHECK_ISPOSITIVE(decl) \
+ assert (decl > 0 && (#decl " should be defined"))
+
+/// This struct is a compact representation of a valid (non-zero power of two)
+/// alignment.
+/// It is suitable for use as static global constants.
+struct Align
+{
+private:
+ uint8_t ShiftValue = 0; /// The log2 of the required alignment.
+ /// ShiftValue is less than 64 by construction.
+
+ friend struct MaybeAlign;
+ friend unsigned Log2 (Align);
+ friend bool
+ operator== (Align Lhs, Align Rhs);
+ friend bool
+ operator!= (Align Lhs, Align Rhs);
+ friend bool
+ operator<= (Align Lhs, Align Rhs);
+ friend bool
+ operator>= (Align Lhs, Align Rhs);
+ friend bool
+ operator< (Align Lhs, Align Rhs);
+ friend bool
+ operator> (Align Lhs, Align Rhs);
+ friend unsigned
+ encode (struct MaybeAlign A);
+ friend struct MaybeAlign
+ decodeMaybeAlign (unsigned Value);
+
+public:
+ /// Default is byte-aligned.
+ constexpr Align () = default;
+ /// Do not perform checks in case of copy/move construct/assign, because the
+ /// checks have been performed when building `Other`.
+ constexpr Align (const Align &Other) = default;
+ constexpr Align (Align &&Other) = default;
+ constexpr Align &
+ operator= (const Align &Other) = default;
+ constexpr Align &
+ operator= (Align &&Other) = default;
+
+ explicit Align (uint64_t Value)
+ {
+ assert (Value > 0 && "Value must not be 0");
+ assert (libbase::isPowerOf2_64 (Value) && "Alignment is not a power of 2");
+ ShiftValue = Log2_64 (Value);
+ assert (ShiftValue < 64 && "Broken invariant");
+ }
+
+ /// Returns an alignment of `1 << Log2Value` bytes.
+ constexpr static Align
+ fromLog2 (unsigned Log2Value)
+ {
+ assert (Log2Value < 64 && "Alignment exponent is too large");
+ Align Out;
+ Out.ShiftValue = Log2Value;
+ return Out;
+ }
+
+ /// This is a hole in the type system and should not be abused.
+ /// Needed to interact with C for instance.
+ constexpr uint64_t
+ value () const
+ {
+ return uint64_t (1) << ShiftValue;
+ }
+
+ // Returns the previous alignment.
+ Align
+ previous () const
+ {
+ assert (ShiftValue != 0 && "Undefined operation");
+ Align Out;
+ Out.ShiftValue = ShiftValue - 1;
+ return Out;
+ }
+
+ /// Allow constructions of constexpr Align.
+ template <size_t kValue>
+ constexpr static Align
+ Constant ()
+ {
+ return fromLog2 (ConstantLog2<kValue> ());
+ }
+
+ /// Allow constructions of constexpr Align from types.
+ /// Compile time equivalent to Align(alignof(T)).
+ template <typename T>
+ constexpr static Align
+ Of ()
+ {
+ return Constant<std::alignment_of_v<T>> ();
+ }
+};
+
+/// Treats the value 0 as a 1, so Align is always at least 1.
+inline Align
+assumeAligned (uint64_t Value)
+{
+ return Value ? Align (Value) : Align ();
+}
+
+/// This struct is a compact representation of a valid (power of two) or
+/// undefined (0) alignment.
+struct MaybeAlign : std::optional<Align>
+{
+private:
+ using UP = std::optional<Align>;
+
+public:
+ /// Default is undefined.
+ MaybeAlign () = default;
+ /// Do not perform checks in case of copy/move construct/assign, because the
+ /// checks have been performed when building `Other`.
+ MaybeAlign (const MaybeAlign &Other) = default;
+ MaybeAlign &
+ operator= (const MaybeAlign &Other) = default;
+ MaybeAlign (MaybeAlign &&Other) = default;
+ MaybeAlign &
+ operator= (MaybeAlign &&Other) = default;
+
+ constexpr MaybeAlign (std::nullopt_t None) : UP (None)
+ {
+ }
+ constexpr MaybeAlign (Align Value) : UP (Value)
+ {
+ }
+ explicit MaybeAlign (uint64_t Value)
+ {
+ assert ((Value == 0 || libbase::isPowerOf2_64 (Value)) &&
+ "Alignment is neither 0 nor a power of 2");
+ if (Value)
+ {
+ emplace (Value);
+ }
+ }
+
+ /// For convenience, returns a valid alignment or 1 if undefined.
+ Align
+ valueOrOne () const
+ {
+ return value_or (Align ());
+ }
+};
+
+/// Checks that SizeInBytes is a multiple of the alignment.
+inline bool
+isAligned (Align Lhs, uint64_t SizeInBytes)
+{
+ return SizeInBytes % Lhs.value () == 0;
+}
+
+/// Checks that Addr is a multiple of the alignment.
+inline bool
+isAddrAligned (Align Lhs, const void *Addr)
+{
+ return isAligned (Lhs, reinterpret_cast<uintptr_t> (Addr));
+}
+
+/// Returns a multiple of A needed to store `Size` bytes.
+constexpr inline uint64_t
+alignTo (uint64_t Size, Align A)
+{
+ const uint64_t Value = A.value ();
+ // The following line is equivalent to `(Size + Value - 1) / Value * Value`.
+
+ // The division followed by a multiplication can be thought of as a right
+ // shift followed by a left shift which zeros out the extra bits produced in
+ // the bump; `~(Value - 1)` is a mask where all those bits being zeroed out
+ // are just zero.
+
+ // Most compilers can generate this code but the pattern may be missed when
+ // multiple functions gets inlined.
+ return (Size + Value - 1) & ~(Value - 1U);
+}
+
+/// If non-zero \p Skew is specified, the return value will be a minimal integer
+/// that is greater than or equal to \p Size and equal to \p A * N + \p Skew for
+/// some integer N. If \p Skew is larger than \p A, its value is adjusted to '\p
+/// Skew mod \p A'.
+///
+/// Examples:
+/// \code
+/// alignTo(5, Align(8), 7) = 7
+/// alignTo(17, Align(8), 1) = 17
+/// alignTo(~0LL, Align(8), 3) = 3
+/// \endcode
+inline uint64_t
+alignTo (uint64_t Size, Align A, uint64_t Skew)
+{
+ const uint64_t Value = A.value ();
+ Skew %= Value;
+ return alignTo (Size - Skew, A) + Skew;
+}
+
+/// Aligns `Addr` to `Alignment` bytes, rounding up.
+inline uintptr_t
+alignAddr (const void *Addr, Align Alignment)
+{
+ uintptr_t ArithAddr = reinterpret_cast<uintptr_t> (Addr);
+ assert (static_cast<uintptr_t> (ArithAddr + Alignment.value () - 1) >=
+ ArithAddr &&
+ "Overflow");
+ return alignTo (ArithAddr, Alignment);
+}
+
+/// Returns the offset to the next integer (mod 2**64) that is greater than
+/// or equal to \p Value and is a multiple of \p Align.
+inline uint64_t
+offsetToAlignment (uint64_t Value, Align Alignment)
+{
+ return alignTo (Value, Alignment) - Value;
+}
+
+/// Returns the necessary adjustment for aligning `Addr` to `Alignment`
+/// bytes, rounding up.
+inline uint64_t
+offsetToAlignedAddr (const void *Addr, Align Alignment)
+{
+ return offsetToAlignment (reinterpret_cast<uintptr_t> (Addr), Alignment);
+}
+
+/// Returns the log2 of the alignment.
+inline unsigned
+Log2 (Align A)
+{
+ return A.ShiftValue;
+}
+
+/// Returns the alignment that satisfies both alignments.
+/// Same semantic as MinAlign.
+inline Align
+commonAlignment (Align A, uint64_t Offset)
+{
+ return Align (MinAlign (A.value (), Offset));
+}
+
+/// Returns a representation of the alignment that encodes undefined as 0.
+inline unsigned
+encode (MaybeAlign A)
+{
+ return A ? A->ShiftValue + 1 : 0;
+}
+
+/// Dual operation of the encode function above.
+inline MaybeAlign
+decodeMaybeAlign (unsigned Value)
+{
+ if (Value == 0)
+ {
+ return MaybeAlign ();
+ }
+ Align Out;
+ Out.ShiftValue = Value - 1;
+ return Out;
+}
+
+/// Returns a representation of the alignment, the encoded value is positive by
+/// definition.
+inline unsigned
+encode (Align A)
+{
+ return encode (MaybeAlign (A));
+}
+
+/// Comparisons between Align and scalars. Rhs must be positive.
+inline bool
+operator== (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () == Rhs;
+}
+inline bool
+operator!= (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () != Rhs;
+}
+inline bool
+operator<= (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () <= Rhs;
+}
+inline bool
+operator>= (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () >= Rhs;
+}
+inline bool
+operator< (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () < Rhs;
+}
+inline bool
+operator> (Align Lhs, uint64_t Rhs)
+{
+ ALIGN_CHECK_ISPOSITIVE (Rhs);
+ return Lhs.value () > Rhs;
+}
+
+/// Comparisons operators between Align.
+inline bool
+operator== (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue == Rhs.ShiftValue;
+}
+inline bool
+operator!= (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue != Rhs.ShiftValue;
+}
+inline bool
+operator<= (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue <= Rhs.ShiftValue;
+}
+inline bool
+operator>= (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue >= Rhs.ShiftValue;
+}
+inline bool
+operator< (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue < Rhs.ShiftValue;
+}
+inline bool
+operator> (Align Lhs, Align Rhs)
+{
+ return Lhs.ShiftValue > Rhs.ShiftValue;
+}
+
+// Don't allow relational comparisons with MaybeAlign.
+bool
+operator<= (Align Lhs, MaybeAlign Rhs) = delete;
+bool
+operator>= (Align Lhs, MaybeAlign Rhs) = delete;
+bool
+operator< (Align Lhs, MaybeAlign Rhs) = delete;
+bool
+operator> (Align Lhs, MaybeAlign Rhs) = delete;
+
+bool
+operator<= (MaybeAlign Lhs, Align Rhs) = delete;
+bool
+operator>= (MaybeAlign Lhs, Align Rhs) = delete;
+bool
+operator< (MaybeAlign Lhs, Align Rhs) = delete;
+bool
+operator> (MaybeAlign Lhs, Align Rhs) = delete;
+
+bool
+operator<= (MaybeAlign Lhs, MaybeAlign Rhs) = delete;
+bool
+operator>= (MaybeAlign Lhs, MaybeAlign Rhs) = delete;
+bool
+operator< (MaybeAlign Lhs, MaybeAlign Rhs) = delete;
+bool
+operator> (MaybeAlign Lhs, MaybeAlign Rhs) = delete;
+
+// Allow equality comparisons between Align and MaybeAlign.
+inline bool
+operator== (MaybeAlign Lhs, Align Rhs)
+{
+ return Lhs && *Lhs == Rhs;
+}
+inline bool
+operator!= (MaybeAlign Lhs, Align Rhs)
+{
+ return !(Lhs == Rhs);
+}
+inline bool
+operator== (Align Lhs, MaybeAlign Rhs)
+{
+ return Rhs == Lhs;
+}
+inline bool
+operator!= (Align Lhs, MaybeAlign Rhs)
+{
+ return !(Rhs == Lhs);
+}
+// Allow equality comparisons with MaybeAlign.
+inline bool
+operator== (MaybeAlign Lhs, MaybeAlign Rhs)
+{
+ return (Lhs && Rhs && (*Lhs == *Rhs)) || (!Lhs && !Rhs);
+}
+inline bool
+operator!= (MaybeAlign Lhs, MaybeAlign Rhs)
+{
+ return !(Lhs == Rhs);
+}
+// Allow equality comparisons with std::nullopt.
+inline bool
+operator== (MaybeAlign Lhs, std::nullopt_t)
+{
+ return !bool (Lhs);
+}
+inline bool
+operator!= (MaybeAlign Lhs, std::nullopt_t)
+{
+ return bool (Lhs);
+}
+inline bool
+operator== (std::nullopt_t, MaybeAlign Rhs)
+{
+ return !bool (Rhs);
+}
+inline bool
+operator!= (std::nullopt_t, MaybeAlign Rhs)
+{
+ return bool (Rhs);
+}
+
+#ifndef NDEBUG
+// For usage in LLVM_DEBUG macros.
+inline std::string
+DebugStr (const Align &A)
+{
+ return std::to_string (A.value ());
+}
+// For usage in LLVM_DEBUG macros.
+inline std::string
+DebugStr (const MaybeAlign &MA)
+{
+ if (MA)
+ {
+ return std::to_string (MA->value ());
+ }
+ return "None";
+}
+#endif // NDEBUG
+
+#undef ALIGN_CHECK_ISPOSITIVE
+
+} // namespace libbase
+
+#endif // LIBBASE_SUPPORT_ALIGNMENT_H_
diff --git a/libbase/include/core/memory/AllocatorBase.h b/libbase/include/core/memory/AllocatorBase.h
new file mode 100644
index 0000000..af6f5ca
--- /dev/null
+++ b/libbase/include/core/memory/AllocatorBase.h
@@ -0,0 +1,200 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- AllocatorBase.h - Simple memory allocation abstraction ---*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+/// \file
+///
+/// This file defines MallocAllocator. MallocAllocator conforms to the LLVM
+/// "Allocator" concept which consists of an Allocate method accepting a size
+/// and alignment, and a Deallocate accepting a pointer and size. Further, the
+/// LLVM "Allocator" concept has overloads of Allocate and Deallocate for
+/// setting size and alignment based on the final type. These overloads are
+/// typically provided by a base class template \c AllocatorBase.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_SUPPORT_ALLOCATORBASE_H
+#define LIBBASE_SUPPORT_ALLOCATORBASE_H
+
+#ifdef _MSC_VER
+#define LIBBASE_ALLOCATORHOLDER_EMPTYBASE __declspec (empty_bases)
+#else
+#define LIBBASE_ALLOCATORHOLDER_EMPTYBASE
+#endif // _MSC_VER
+
+#include "core/compiler/Compiler.h"
+#include "core/memory/MemAlloc.h"
+#include <type_traits>
+#include <utility>
+
+namespace libbase
+{
+
+/// CRTP base class providing obvious overloads for the core \c
+/// Allocate() methods of LLVM-style allocators.
+///
+/// This base class both documents the full public interface exposed by all
+/// LLVM-style allocators, and redirects all of the overloads to a single core
+/// set of methods which the derived class must define.
+template <typename DerivedT> class AllocatorBase
+{
+public:
+ /// Allocate \a Size bytes of \a Alignment aligned memory. This method
+ /// must be implemented by \c DerivedT.
+ void *
+ Allocate (size_t Size, size_t Alignment)
+ {
+#ifdef __clang__
+ static_assert (static_cast<void *(AllocatorBase::*) (size_t, size_t)> (
+ &AllocatorBase::Allocate) !=
+ static_cast<void *(DerivedT::*) (size_t, size_t)> (
+ &DerivedT::Allocate),
+ "Class derives from AllocatorBase without implementing the "
+ "core Allocate(size_t, size_t) overload!");
+#endif
+ return static_cast<DerivedT *> (this)->Allocate (Size, Alignment);
+ }
+
+ /// Deallocate \a Ptr to \a Size bytes of memory allocated by this
+ /// allocator.
+ void
+ Deallocate (const void *Ptr, size_t Size, size_t Alignment)
+ {
+#ifdef __clang__
+ static_assert (
+ static_cast<void (AllocatorBase::*) (const void *, size_t, size_t)> (
+ &AllocatorBase::Deallocate) !=
+ static_cast<void (DerivedT::*) (const void *, size_t, size_t)> (
+ &DerivedT::Deallocate),
+ "Class derives from AllocatorBase without implementing the "
+ "core Deallocate(void *) overload!");
+#endif
+ return static_cast<DerivedT *> (this)->Deallocate (Ptr, Size, Alignment);
+ }
+
+ // The rest of these methods are helpers that redirect to one of the above
+ // core methods.
+
+ /// Allocate space for a sequence of objects without constructing them.
+ template <typename T>
+ T *
+ Allocate (size_t Num = 1)
+ {
+ return static_cast<T *> (Allocate (Num * sizeof (T), alignof (T)));
+ }
+
+ /// Deallocate space for a sequence of objects without constructing them.
+ template <typename T>
+ std::enable_if_t<!std::is_same_v<std::remove_cv_t<T>, void>, void>
+ Deallocate (T *Ptr, size_t Num = 1)
+ {
+ Deallocate (static_cast<const void *> (Ptr), Num * sizeof (T), alignof (T));
+ }
+};
+
+class MallocAllocator : public AllocatorBase<MallocAllocator>
+{
+public:
+ void
+ Reset ()
+ {
+ }
+
+ LIBBASE_ATTRIBUTE_RETURNS_NONNULL void *
+ Allocate (size_t Size, size_t Alignment)
+ {
+ return allocate_buffer (Size, Alignment);
+ }
+
+ // Pull in base class overloads.
+ using AllocatorBase<MallocAllocator>::Allocate;
+
+ void
+ Deallocate (const void *Ptr, size_t Size, size_t Alignment)
+ {
+ deallocate_buffer (const_cast<void *> (Ptr), Size, Alignment);
+ }
+
+ // Pull in base class overloads.
+ using AllocatorBase<MallocAllocator>::Deallocate;
+
+ void
+ PrintStats () const
+ {
+ }
+};
+
+namespace detail
+{
+
+template <typename Alloc> class AllocatorHolder : Alloc
+{
+public:
+ AllocatorHolder () = default;
+ AllocatorHolder (const Alloc &A) : Alloc (A)
+ {
+ }
+ AllocatorHolder (Alloc &&A) : Alloc (std::move (A))
+ {
+ }
+ Alloc &
+ getAllocator ()
+ {
+ return *this;
+ }
+ const Alloc &
+ getAllocator () const
+ {
+ return *this;
+ }
+};
+
+template <typename Alloc> class AllocatorHolder<Alloc &>
+{
+ Alloc &A;
+
+public:
+ AllocatorHolder (Alloc &A) : A (A)
+ {
+ }
+ Alloc &
+ getAllocator ()
+ {
+ return A;
+ }
+ const Alloc &
+ getAllocator () const
+ {
+ return A;
+ }
+};
+
+} // namespace detail
+
+} // namespace libbase
+
+#endif // LIBBASE_SUPPORT_ALLOCATORBASE_H
diff --git a/libbase/include/core/memory/MemAlloc.h b/libbase/include/core/memory/MemAlloc.h
new file mode 100644
index 0000000..be7828e
--- /dev/null
+++ b/libbase/include/core/memory/MemAlloc.h
@@ -0,0 +1,125 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- MemAlloc.h - Memory allocation functions -----------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+/// \file
+///
+/// This file defines counterparts of C library allocation functions defined in
+/// the namespace 'std'. The new allocation functions crash on allocation
+/// failure instead of returning null pointer.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LIBBASE_SUPPORT_MEMALLOC_H
+#define LIBBASE_SUPPORT_MEMALLOC_H
+
+#include "core/compiler/Compiler.h"
+#include "core/error/ErrorHandling.h"
+#include <cstdlib>
+
+namespace libbase
+{
+
+LIBBASE_ATTRIBUTE_RETURNS_NONNULL inline void *
+safe_malloc (size_t Sz)
+{
+ void *Result = std::malloc (Sz);
+ if (Result == nullptr)
+ {
+ // It is implementation-defined whether allocation occurs if the space
+ // requested is zero (ISO/IEC 9899:2018 7.22.3). Retry, requesting
+ // non-zero, if the space requested was zero.
+ if (Sz == 0)
+ {
+ return safe_malloc (1);
+ }
+ report_bad_alloc_error ("Allocation failed");
+ }
+ return Result;
+}
+
+LIBBASE_ATTRIBUTE_RETURNS_NONNULL inline void *
+safe_calloc (size_t Count, size_t Sz)
+{
+ void *Result = std::calloc (Count, Sz);
+ if (Result == nullptr)
+ {
+ // It is implementation-defined whether allocation occurs if the space
+ // requested is zero (ISO/IEC 9899:2018 7.22.3). Retry, requesting
+ // non-zero, if the space requested was zero.
+ if (Count == 0 || Sz == 0)
+ {
+ return safe_malloc (1);
+ }
+ report_bad_alloc_error ("Allocation failed");
+ }
+ return Result;
+}
+
+LIBBASE_ATTRIBUTE_RETURNS_NONNULL inline void *
+safe_realloc (void *Ptr, size_t Sz)
+{
+ void *Result = std::realloc (Ptr, Sz);
+ if (Result == nullptr)
+ {
+ // It is implementation-defined whether allocation occurs if the space
+ // requested is zero (ISO/IEC 9899:2018 7.22.3). Retry, requesting
+ // non-zero, if the space requested was zero.
+ if (Sz == 0)
+ {
+ return safe_malloc (1);
+ }
+ report_bad_alloc_error ("Allocation failed");
+ }
+ return Result;
+}
+
+/// Allocate a buffer of memory with the given size and alignment.
+///
+/// When the compiler supports aligned operator new, this will use it to
+/// handle even over-aligned allocations.
+///
+/// However, this doesn't make any attempt to leverage the fancier techniques
+/// like posix_memalign due to portability. It is mostly intended to allow
+/// compatibility with platforms that, after aligned allocation was added, use
+/// reduced default alignment.
+LIBBASE_ABI
+ LIBBASE_ATTRIBUTE_RETURNS_NONNULL LIBBASE_ATTRIBUTE_RETURNS_NOALIAS void *
+ allocate_buffer (size_t Size, size_t Alignment);
+
+/// Deallocate a buffer of memory with the given size and alignment.
+///
+/// If supported, this will used the sized delete operator. Also if supported,
+/// this will pass the alignment to the delete operator.
+///
+/// The pointer must have been allocated with the corresponding new operator,
+/// most likely using the above helper.
+LIBBASE_ABI void
+deallocate_buffer (void *Ptr, size_t Size, size_t Alignment);
+
+} // namespace libbase
+#endif
diff --git a/libbase/src/core/adt/SmallVector.cpp b/libbase/src/core/adt/SmallVector.cpp
new file mode 100644
index 0000000..c28680c
--- /dev/null
+++ b/libbase/src/core/adt/SmallVector.cpp
@@ -0,0 +1,234 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+*/
+
+//===- core/adt/SmallVector.cpp - 'Normally small' vectors ----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the SmallVector class.
+//
+//===----------------------------------------------------------------------===//
+
+#include "core/adt/SmallVector.h"
+#include "core/adt/Twine.h"
+#include "core/memory/MemAlloc.h"
+#include <cstdint>
+#if LIBBASE_ENABLE_EXCEPTIONS
+#include <stdexcept>
+#endif
+using namespace libbase;
+
+// Check that no bytes are wasted and everything is well-aligned.
+namespace
+{
+// These structures may cause binary compat warnings on AIX. Suppress the
+// warning since we are only using these types for the static assertions below.
+#if defined(_AIX)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Waix-compat"
+#endif
+struct Struct16B
+{
+ alignas (16) void *X;
+};
+struct Struct32B
+{
+ alignas (32) void *X;
+};
+#if defined(_AIX)
+#pragma GCC diagnostic pop
+#endif
+} // namespace
+static_assert (sizeof (SmallVector<void *, 0>) ==
+ sizeof (unsigned) * 2 + sizeof (void *),
+ "wasted space in SmallVector size 0");
+static_assert (alignof (SmallVector<Struct16B, 0>) >= alignof (Struct16B),
+ "wrong alignment for 16-byte aligned T");
+static_assert (alignof (SmallVector<Struct32B, 0>) >= alignof (Struct32B),
+ "wrong alignment for 32-byte aligned T");
+static_assert (sizeof (SmallVector<Struct16B, 0>) >= alignof (Struct16B),
+ "missing padding for 16-byte aligned T");
+static_assert (sizeof (SmallVector<Struct32B, 0>) >= alignof (Struct32B),
+ "missing padding for 32-byte aligned T");
+static_assert (sizeof (SmallVector<void *, 1>) ==
+ sizeof (unsigned) * 2 + sizeof (void *) * 2,
+ "wasted space in SmallVector size 1");
+
+static_assert (sizeof (SmallVector<char, 0>) ==
+ sizeof (void *) * 2 + sizeof (void *),
+ "1 byte elements have word-sized type for size and capacity");
+
+/// Report that MinSize doesn't fit into this vector's size type. Throws
+/// std::length_error or calls report_fatal_error.
+[[noreturn]] static void
+report_size_overflow (size_t MinSize, size_t MaxSize);
+static void
+report_size_overflow (size_t MinSize, size_t MaxSize)
+{
+ std::string Reason = "SmallVector unable to grow. Requested capacity (" +
+ std::to_string (MinSize) +
+ ") is larger than maximum value for size type (" +
+ std::to_string (MaxSize) + ")";
+#if LIBBASE_ENABLE_EXCEPTIONS
+ throw std::length_error (Reason);
+#else
+ report_fatal_error (Twine (Reason));
+#endif
+}
+
+/// Report that this vector is already at maximum capacity. Throws
+/// std::length_error or calls report_fatal_error.
+[[noreturn]] static void
+report_at_maximum_capacity (size_t MaxSize);
+static void
+report_at_maximum_capacity (size_t MaxSize)
+{
+ std::string Reason =
+ "SmallVector capacity unable to grow. Already at maximum size " +
+ std::to_string (MaxSize);
+#ifdef LIBBASE_ENABLE_EXCEPTIONS
+ throw std::length_error (Reason);
+#else
+ report_fatal_error (Twine (Reason));
+#endif
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <class Size_T>
+static size_t
+getNewCapacity (size_t MinSize, size_t TSize, size_t OldCapacity)
+{
+ constexpr size_t MaxSize = std::numeric_limits<Size_T>::max ();
+
+ // Ensure we can fit the new capacity.
+ // This is only going to be applicable when the capacity is 32 bit.
+ if (MinSize > MaxSize)
+ {
+ report_size_overflow (MinSize, MaxSize);
+ }
+
+ // Ensure we can meet the guarantee of space for at least one more element.
+ // The above check alone will not catch the case where grow is called with a
+ // default MinSize of 0, but the current capacity cannot be increased.
+ // This is only going to be applicable when the capacity is 32 bit.
+ if (OldCapacity == MaxSize)
+ {
+ report_at_maximum_capacity (MaxSize);
+ }
+
+ // In theory 2*capacity can overflow if the capacity is 64 bit, but the
+ // original capacity would never be large enough for this to be a problem.
+ size_t NewCapacity = 2 * OldCapacity + 1; // Always grow.
+ return std::clamp (NewCapacity, MinSize, MaxSize);
+}
+
+/// If vector was first created with capacity 0, getFirstEl() points to the
+/// memory right after, an area unallocated. If a subsequent allocation,
+/// that grows the vector, happens to return the same pointer as getFirstEl(),
+/// get a new allocation, otherwise isSmall() will falsely return that no
+/// allocation was done (true) and the memory will not be freed in the
+/// destructor. If a VSize is given (vector size), also copy that many
+/// elements to the new allocation - used if realloca fails to increase
+/// space, and happens to allocate precisely at BeginX.
+/// This is unlikely to be called often, but resolves a memory leak when the
+/// situation does occur.
+static void *
+replaceAllocation (void *NewElts, size_t TSize, size_t NewCapacity,
+ size_t VSize = 0)
+{
+ void *NewEltsReplace = libbase::safe_malloc (NewCapacity * TSize);
+ if (VSize)
+ {
+ memcpy (NewEltsReplace, NewElts, VSize * TSize);
+ }
+ free (NewElts);
+ return NewEltsReplace;
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <class Size_T>
+void *
+SmallVectorBase<Size_T>::mallocForGrow (void *FirstEl, size_t MinSize,
+ size_t TSize, size_t &NewCapacity)
+{
+ NewCapacity = getNewCapacity<Size_T> (MinSize, TSize, this->capacity ());
+ // Even if capacity is not 0 now, if the vector was originally created with
+ // capacity 0, it's possible for the malloc to return FirstEl.
+ void *NewElts = libbase::safe_malloc (NewCapacity * TSize);
+ if (NewElts == FirstEl)
+ {
+ NewElts = replaceAllocation (NewElts, TSize, NewCapacity);
+ }
+ return NewElts;
+}
+
+// Note: Moving this function into the header may cause performance regression.
+template <class Size_T>
+void
+SmallVectorBase<Size_T>::grow_pod (void *FirstEl, size_t MinSize, size_t TSize)
+{
+ size_t NewCapacity =
+ getNewCapacity<Size_T> (MinSize, TSize, this->capacity ());
+ void *NewElts;
+ if (BeginX == FirstEl)
+ {
+ NewElts = libbase::safe_malloc (NewCapacity * TSize);
+ if (NewElts == FirstEl)
+ {
+ NewElts = replaceAllocation (NewElts, TSize, NewCapacity);
+ }
+
+ // Copy the elements over. No need to run dtors on PODs.
+ memcpy (NewElts, this->BeginX, size () * TSize);
+ }
+ else
+ {
+ // If this wasn't grown from the inline copy, grow the allocated space.
+ NewElts = libbase::safe_realloc (this->BeginX, NewCapacity * TSize);
+ if (NewElts == FirstEl)
+ {
+ NewElts = replaceAllocation (NewElts, TSize, NewCapacity, size ());
+ }
+ }
+
+ this->set_allocation_range (NewElts, NewCapacity);
+}
+
+template class libbase::SmallVectorBase<uint32_t>;
+
+// Disable the uint64_t instantiation for 32-bit builds.
+// Both uint32_t and uint64_t instantiations are needed for 64-bit builds.
+// This instantiation will never be used in 32-bit builds, and will cause
+// warnings when sizeof(Size_T) > sizeof(size_t).
+#if SIZE_MAX > UINT32_MAX
+template class libbase::SmallVectorBase<uint64_t>;
+
+// Assertions to ensure this #if stays in sync with SmallVectorSizeType.
+static_assert (sizeof (SmallVectorSizeType<char>) == sizeof (uint64_t),
+ "Expected SmallVectorBase<uint64_t> variant to be in use.");
+#else
+static_assert (sizeof (SmallVectorSizeType<char>) == sizeof (uint32_t),
+ "Expected SmallVectorBase<uint32_t> variant to be in use.");
+#endif
diff --git a/rcc/lib/Support/Errno.cc b/libbase/src/core/error/Errno.cc
index f0161a6..c861e46 100644
--- a/rcc/lib/Support/Errno.cc
+++ b/libbase/src/core/error/Errno.cc
@@ -21,13 +21,13 @@
// This implementation is based on llvm/lib/Support/Errno.cpp.
-#include "rcc/Support/Errno.h"
-#include "rcc/Config/config.h"
+#include "core/error/Errno.h"
+#include "core/config/config.h"
#include <cstring>
#include <errno.h>
-namespace rcc
+namespace libbase
{
namespace sys
{
@@ -76,4 +76,4 @@ StrError (int errnum)
}
} // namespace sys
-} // namespace rcc
+} // namespace libbase
diff --git a/rcc/lib/Support/MathExtras.cc b/libbase/src/core/math/MathExtras.cc
index f0db3a4..e691a3c 100644
--- a/rcc/lib/Support/MathExtras.cc
+++ b/libbase/src/core/math/MathExtras.cc
@@ -31,7 +31,7 @@
//
//===----------------------------------------------------------------------===//
-#include "rcc/Support/MathExtras.h"
+#include "core/math/MathExtras.h"
#ifdef _MSC_VER
#include <limits>
@@ -39,7 +39,7 @@
#include <cmath>
#endif
-namespace rcc
+namespace libbase
{
#if defined(_MSC_VER)
@@ -94,4 +94,4 @@ NumDigitsBase10 (uint64_t X)
return Digits + (X >= Data.Boundaries[Digits]);
}
-} // namespace rcc
+} // namespace libbase
diff --git a/libbase/src/core/memory/MemAlloc.cpp b/libbase/src/core/memory/MemAlloc.cpp
new file mode 100644
index 0000000..38e3bfd
--- /dev/null
+++ b/libbase/src/core/memory/MemAlloc.cpp
@@ -0,0 +1,63 @@
+/*
+ RCC-Project
+
+ Copyright (C) 2026 Richard Qin
+
+ This file is part of RCC-Project.
+
+ RCC-Project is free software; you can redistribute it and/or modify it under
+ the terms of the GNU General Public License as published by the Free
+ Software Foundation; either version 3 of the License, or (at your option) any
+ later version.
+
+ RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ RCC-Project; see the file COPYING. If not see
+ <http://www.gnu.org/licenses/>.
+ */
+
+//===- MemAlloc.cpp - Memory allocation functions -------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "core/memory/MemAlloc.h"
+#include <new>
+
+// These are out of line to have __cpp_aligned_new not affect ABI.
+
+LIBBASE_ATTRIBUTE_RETURNS_NONNULL LIBBASE_ATTRIBUTE_RETURNS_NOALIAS void *
+libbase::allocate_buffer (size_t Size, size_t Alignment)
+{
+ void *Result = ::operator new (Size,
+#ifdef __cpp_aligned_new
+ std::align_val_t (Alignment),
+#endif
+ std::nothrow);
+ if (Result == nullptr)
+ {
+ report_bad_alloc_error ("Buffer allocation failed");
+ }
+ return Result;
+}
+
+void
+libbase::deallocate_buffer (void *Ptr, size_t Size, size_t Alignment)
+{
+ ::operator delete (Ptr
+#ifdef __cpp_sized_deallocation
+ ,
+ Size
+#endif
+#ifdef __cpp_aligned_new
+ ,
+ std::align_val_t (Alignment)
+#endif
+ );
+}
diff --git a/proofsearch/.clang-format b/proofsearch/.clang-format
new file mode 100644
index 0000000..64f782a
--- /dev/null
+++ b/proofsearch/.clang-format
@@ -0,0 +1,62 @@
+---
+Language: Cpp
+BasedOnStyle: Google
+
+# Indentation
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+AccessModifierOffset: -2
+NamespaceIndentation: None
+IndentCaseLabels: true
+
+# Line width
+ColumnLimit: 100
+
+# Braces
+BreakBeforeBraces: Attach
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AllowShortBlocksOnASingleLine: Empty
+
+# Includes
+SortIncludes: CaseSensitive
+IncludeBlocks: Regroup
+IncludeCategories:
+ # Project headers
+ - Regex: '^"atp/'
+ Priority: 1
+ # System / C++ standard headers
+ - Regex: '^<'
+ Priority: 2
+ # Catch-all
+ - Regex: '.*'
+ Priority: 3
+
+# Alignment
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignOperands: Align
+AlignTrailingComments: true
+
+# Spacing
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpacesInAngles: Never
+
+# Pointer alignment
+PointerAlignment: Left
+ReferenceAlignment: Left
+DerivePointerAlignment: false
+
+# Other
+ReflowComments: true
+FixNamespaceComments: true
+CompactNamespaces: false
+AllowAllParametersOfDeclarationOnNextLine: true
+BinPackArguments: true
+BinPackParameters: true
diff --git a/proofsearch/.clang-tidy b/proofsearch/.clang-tidy
new file mode 100644
index 0000000..412e8a8
--- /dev/null
+++ b/proofsearch/.clang-tidy
@@ -0,0 +1,76 @@
+---
+# ATP Engine clang-tidy configuration
+#
+# Enable categories incrementally — start strict on the code we write,
+# avoid noise from auto-generated code (ANTLR4).
+
+Checks: >
+ -*,
+ bugprone-*,
+ -bugprone-easily-swappable-parameters,
+ clang-analyzer-*,
+ cppcoreguidelines-*,
+ -cppcoreguidelines-avoid-const-or-ref-data-members,
+ -cppcoreguidelines-avoid-magic-numbers,
+ -cppcoreguidelines-pro-type-reinterpret-cast,
+ -cppcoreguidelines-pro-bounds-pointer-arithmetic,
+ -cppcoreguidelines-owning-memory,
+ misc-*,
+ -misc-no-recursion,
+ -misc-non-private-member-variables-in-classes,
+ modernize-*,
+ -modernize-use-trailing-return-type,
+ performance-*,
+ readability-*,
+ -readability-magic-numbers,
+ -readability-identifier-length,
+
+WarningsAsErrors: ''
+
+HeaderFilterRegex: 'include/atp/.*\.h$'
+
+CheckOptions:
+ - key: readability-identifier-naming.NamespaceCase
+ value: lower_case
+ - key: readability-identifier-naming.ClassCase
+ value: CamelCase
+ - key: readability-identifier-naming.StructCase
+ value: CamelCase
+ - key: readability-identifier-naming.FunctionCase
+ value: camelBack
+ - key: readability-identifier-naming.VariableCase
+ value: lower_case
+ - key: readability-identifier-naming.MemberCase
+ value: lower_case
+ - key: readability-identifier-naming.MemberSuffix
+ value: '_'
+ - key: readability-identifier-naming.ConstantCase
+ value: CamelCase
+ - key: readability-identifier-naming.ConstantPrefix
+ value: k
+ - key: readability-identifier-naming.LocalConstantCase
+ value: lower_case
+ - key: readability-identifier-naming.LocalConstantPrefix
+ value: ''
+ - key: readability-identifier-naming.LocalConstantSuffix
+ value: ''
+ - key: readability-identifier-naming.EnumConstantCase
+ value: CamelCase
+ - key: readability-identifier-naming.EnumConstantPrefix
+ value: k
+ - key: readability-identifier-naming.ConstexprVariableCase
+ value: CamelCase
+ - key: readability-identifier-naming.ConstexprVariablePrefix
+ value: k
+ - key: readability-identifier-naming.GlobalConstantCase
+ value: CamelCase
+ - key: readability-identifier-naming.GlobalConstantPrefix
+ value: k
+ - key: readability-identifier-naming.TemplateParameterCase
+ value: CamelCase
+ - key: performance-move-const-arg.CheckTriviallyCopyableMove
+ value: false
+ - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
+ value: true
+ - key: readability-function-cognitive-complexity.Threshold
+ value: 25
diff --git a/proofsearch/.gitignore b/proofsearch/.gitignore
new file mode 100644
index 0000000..2a1b7e2
--- /dev/null
+++ b/proofsearch/.gitignore
@@ -0,0 +1,15 @@
+# Build
+build/
+cmake-build-*/
+.cache/
+compile_commands.json
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*~
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/proofsearch/CMakeLists.txt b/proofsearch/CMakeLists.txt
new file mode 100644
index 0000000..2257766
--- /dev/null
+++ b/proofsearch/CMakeLists.txt
@@ -0,0 +1,111 @@
+cmake_minimum_required(VERSION 3.20)
+project(atp_engine
+ VERSION 0.1.0
+ LANGUAGES CXX
+)
+
+# ─── C++ Standard ────────────────────────────────────────────────────────────
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+# Export compile_commands.json for clang-tidy and IDE support
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# ─── Compiler Warnings ──────────────────────────
+set(ATP_PROJECT_WARNING_FLAGS
+ -Wall -Wextra -Wpedantic
+ -Wshadow -Wnon-virtual-dtor
+ -Wold-style-cast -Wcast-align -Wunused
+ -Woverloaded-virtual -Wconversion -Wsign-conversion
+ -Wnull-dereference -Wdouble-promotion
+ -Wformat=2 -Wimplicit-fallthrough
+)
+
+# ─── Sanitizers (Debug builds) ───────────────────────────────────────────────
+option(ATP_ENABLE_SANITIZERS "Enable ASan + UBSan in Debug builds" ON)
+if(ATP_ENABLE_SANITIZERS AND CMAKE_BUILD_TYPE STREQUAL "Debug")
+ add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
+ add_link_options(-fsanitize=address,undefined)
+endif()
+
+# ─── clang-tidy Integration ─────────────────────────────────────────────────
+option(ATP_ENABLE_CLANG_TIDY "Enable clang-tidy static analysis" OFF)
+if(ATP_ENABLE_CLANG_TIDY)
+ find_program(CLANG_TIDY_EXE NAMES clang-tidy)
+ if(CLANG_TIDY_EXE)
+ message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
+ else()
+ message(WARNING "clang-tidy requested but not found")
+ endif()
+endif()
+
+# ─── clang-format Custom Target ─────────────────────────────────────────────
+find_program(CLANG_FORMAT_EXE NAMES clang-format)
+if(CLANG_FORMAT_EXE)
+ file(GLOB_RECURSE ALL_CXX_SOURCES
+ ${PROJECT_SOURCE_DIR}/src/*.cc
+ ${PROJECT_SOURCE_DIR}/include/*.h
+ ${PROJECT_SOURCE_DIR}/tests/*.cc
+ )
+ add_custom_target(format
+ COMMAND ${CLANG_FORMAT_EXE} -i ${ALL_CXX_SOURCES}
+ COMMENT "Running clang-format on all source files"
+ VERBATIM
+ )
+ add_custom_target(format-check
+ COMMAND ${CLANG_FORMAT_EXE} --dry-run --Werror ${ALL_CXX_SOURCES}
+ COMMENT "Checking clang-format compliance"
+ VERBATIM
+ )
+ message(STATUS "clang-format found: ${CLANG_FORMAT_EXE}")
+ message(STATUS " Use 'cmake --build . --target format' to auto-format")
+ message(STATUS " Use 'cmake --build . --target format-check' to check format")
+else()
+ message(WARNING "clang-format not found, format targets disabled")
+endif()
+
+# ─── Source Files ────────────────────────────────────────────────────────────
+set(ATP_SOURCES
+ src/core/symbol_table.cc
+ src/core/term_bank.cc
+ src/core/clause_store.cc
+ src/normalize/clausifier.cc
+ src/infer/substitution.cc
+ src/infer/unification.cc
+ src/infer/resolution.cc
+ src/index/discrim_tree.cc
+ src/simplify/subsumption.cc
+ src/simplify/tautology.cc
+ src/search/prover.cc
+ src/proof/proof_trace.cc
+ src/rewrite/ordering.cc
+ src/rewrite/paramodulation.cc
+ src/strategy/problem_analyzer.cc
+ src/frontend/tptp_parser.cc
+)
+
+# ─── Core Library ────────────────────────────────────────────────────────────
+add_library(atp_core STATIC ${ATP_SOURCES})
+target_include_directories(atp_core PUBLIC
+ ${PROJECT_SOURCE_DIR}/include
+)
+target_compile_options(atp_core PRIVATE ${ATP_PROJECT_WARNING_FLAGS})
+if(ATP_ENABLE_CLANG_TIDY AND CLANG_TIDY_EXE)
+ set_property(TARGET atp_core PROPERTY CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
+endif()
+
+# ─── Main Executable ────────────────────────────────────────────────────────
+add_executable(atp src/main.cc)
+target_link_libraries(atp PRIVATE atp_core)
+target_compile_options(atp PRIVATE ${ATP_PROJECT_WARNING_FLAGS})
+if(ATP_ENABLE_CLANG_TIDY AND CLANG_TIDY_EXE)
+ set_property(TARGET atp PROPERTY CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
+endif()
+
+# ─── Tests ───────────────────────────────────────────────────────────────────
+option(ATP_BUILD_TESTS "Build unit tests" ON)
+if(ATP_BUILD_TESTS)
+ enable_testing()
+ add_subdirectory(tests)
+endif()
diff --git a/proofsearch/LICENSE b/proofsearch/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/proofsearch/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/proofsearch/README.md b/proofsearch/README.md
new file mode 100644
index 0000000..ac0ccd6
--- /dev/null
+++ b/proofsearch/README.md
@@ -0,0 +1,49 @@
+# ATP Engine
+
+A saturation-based automated theorem prover for first-order logic, written in C++20.
+
+## Overview
+
+ATP Engine implements the **Given Clause Loop** (Otter/E-style) with hash-consed term sharing, discrimination tree indexing, and pluggable search heuristics. It reads problems in [TPTP](https://www.tptp.org/) format and produces human-readable proofs via resolution and factoring.
+
+The architecture is designed for extensibility toward equality reasoning (superposition), sorted logic, and theory integration. See [`docs/architecture.md`](docs/architecture.md) for full design documentation.
+
+## Build
+
+Requires CMake 3.20+ and a C++20 compiler (GCC 11+ / Clang 14+).
+
+```bash
+cmake -B build -DCMAKE_BUILD_TYPE=Debug
+cmake --build build -j$(nproc)
+```
+
+### Options
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `ATP_ENABLE_CLANG_TIDY` | `OFF` | Run clang-tidy during compilation |
+| `ATP_ENABLE_SANITIZERS` | `ON` | ASan + UBSan in Debug builds |
+| `ATP_BUILD_TESTS` | `ON` | Build GoogleTest unit tests |
+
+### Format & Lint
+
+```bash
+cmake --build build --target format # auto-format all sources
+cmake --build build --target format-check # check format compliance
+```
+
+## Run
+
+```bash
+./build/atp data/monkey_banana.p
+```
+
+## Test
+
+```bash
+cd build && ctest --output-on-failure
+```
+
+## License
+
+This project is licensed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html). Also see [LICENSE](LICENSE)
diff --git a/proofsearch/data/monkey_banana.p b/proofsearch/data/monkey_banana.p
new file mode 100644
index 0000000..446c20a
--- /dev/null
+++ b/proofsearch/data/monkey_banana.p
@@ -0,0 +1,27 @@
+% =========================================================================
+% Monkey and Banana Problem
+% State representation: state(Monkey, Box, Banana, Height, Situation)
+% =========================================================================
+
+% 1. Initial state (corresponding sample input: A B C 0)
+cnf(init_state, axiom, state(a, b, c, h0, s0)).
+
+% 2. State transition axioms (action rules)
+% Action: Go(X)
+cnf(action_go, axiom, ~state(M, B, G, h0, S) | state(X, B, G, h0, do(go(X), S)) ).
+
+% Action: Push(X) (Note M and B use the same variable M representing M=B)
+cnf(action_push, axiom, ~state(M, M, G, h0, S) | state(X, X, G, h0, do(push(X), S)) ).
+
+% Action: Climb()
+cnf(action_climb, axiom, ~state(M, M, G, h0, S) | state(M, M, G, h1, do(climb, S)) ).
+
+% Action: Down()
+cnf(action_down, axiom, ~state(M, B, G, h1, S) | state(M, B, G, h0, do(down, S)) ).
+
+% Action: Reach() (Note M, B, G all use G uniformly, representing M=G and B=G)
+cnf(action_reach, axiom, ~state(G, G, G, h1, S) | goal_reached(do(reach, S)) ).
+
+% 3. Goal theorem (using proof by contradiction)
+% Goal: There exists a state S such that goal_reached(S) is true.
+cnf(negated_goal, negated_conjecture, ~goal_reached(S) ). \ No newline at end of file
diff --git a/proofsearch/data/simple_propositional.p b/proofsearch/data/simple_propositional.p
new file mode 100644
index 0000000..283c326
--- /dev/null
+++ b/proofsearch/data/simple_propositional.p
@@ -0,0 +1,5 @@
+% Simple propositional-style test
+% P(a) and (P(X) → Q(X)) and ¬Q(a) should be unsatisfiable.
+cnf(fact, axiom, p(a)).
+cnf(rule, axiom, ~p(X) | q(X)).
+cnf(goal, negated_conjecture, ~q(a)).
diff --git a/proofsearch/docs/architecture.md b/proofsearch/docs/architecture.md
new file mode 100644
index 0000000..333fa99
--- /dev/null
+++ b/proofsearch/docs/architecture.md
@@ -0,0 +1,630 @@
+# ATP Engine — Architecture Design Document
+
+> **Version**: 1.0
+> **Target**: Extensible first-order logic theorem prover (saturation-based)
+> **Language**: C++20
+> **Build**: CMake 3.20+, GoogleTest, ANTLR4 (C++ target)
+
+---
+
+## Table of Contents
+
+1. [Design Philosophy](#1-design-philosophy)
+2. [System Architecture](#2-system-architecture)
+3. [Directory Structure](#3-directory-structure)
+4. [Module Specifications](#4-module-specifications)
+5. [Data Flow](#5-data-flow)
+6. [Known Limitations & Future Work](#6-known-limitations--future-work)
+7. [References](#7-references)
+
+---
+
+## 1. Design Philosophy
+
+### 1.1 Core Principles
+
+| Principle | Rationale |
+|-----------|-----------|
+| **Bottom-up construction** | Build from the memory layer upward. A stable Term Bank is the foundation everything else rests on. |
+| **Strict layered dependencies** | Dependencies flow in one direction only: `core → infer → index → simplify → search → proof`. No cycles. |
+| **Interface-implementation separation** | Headers in `include/atp/` define interfaces; `src/` contains implementations. Modules communicate through stable APIs. |
+| **Premature abstraction avoidance** | V1 hardcodes resolution + factoring. Abstract strategy selection (`strategy/`) is scaffolded but not wired in until V2. |
+| **Parse once, reason forever** | Text parsing (ANTLR4) is a one-shot frontend concern. The core engine operates entirely on integer IDs, never on strings. |
+
+### 1.2 Design Influences
+
+This architecture draws from three production-grade theorem provers:
+
+- **E Prover** (Stephan Schulz) — Given Clause Loop, term sharing via hash consing, age-weight clause selection.
+- **Vampire** (Andrei Voronkov et al.) — AVATAR splitting, superposition calculus, discrimination tree indexing.
+- **Otter** (William McCune) — Original set-of-support strategy, subsumption, demodulation.
+
+### 1.3 Versioning Strategy
+
+```
+V1 → Pure first-order resolution (no equality). Solve the Monkey-Banana problem.
+V2 → Pluggable inference rules (Calculus), age-weight scheduling, variable standardization.
+V3 → Equality reasoning (paramodulation/superposition), term orderings (KBO/LPO).
+V4 → AVATAR splitting, SAT solver integration, TPTP competition readiness.
+```
+
+---
+
+## 2. System Architecture
+
+### 2.1 Layered Architecture Overview
+
+```
+┌──────────────────────────────────────────────────────────────────────┐
+│ Frontend Layer │
+│ TPTP File → ANTLR4 Lexer/Parser → AST Builder → tptp_parser.h │
+└────────────────────────────────┬─────────────────────────────────────┘
+ │ Formula (internal AST)
+┌────────────────────────────────▼─────────────────────────────────────┐
+│ Normalization Layer │
+│ formula.h → clausifier.h (NNF → Skolem → CNF) │
+└────────────────────────────────┬─────────────────────────────────────┘
+ │ Clause set
+┌────────────────────────────────▼─────────────────────────────────────┐
+│ Search Layer (Control) │
+│ │
+│ ┌──────────────┐ select ┌─────────────────┐ resolve with all │
+│ │ Unprocessed │─────────→ │ Given Clause │─────────────────→ │
+│ │ (pri queue) │ │ (selected) │ │
+│ └──────▲───────┘ └─────────────────┘ │
+│ │ enqueue survivors │
+│ ┌──────┴───────┐ ┌─────────────────┐ │
+│ │ Simplify & │←──────────│ New Clauses │ │
+│ │ Filter │ │ (resolvents) │ │
+│ └──────────────┘ └─────────────────┘ │
+│ │ if empty clause found │
+└──────────┼───────────────────────────────────────────────────────────┘
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ Output Layer │
+│ proof_trace.h → DFS backtrack → format & print action sequence │
+└──────────────────────────────────────────────────────────────────────┘
+
+═══════════════════════ Shared Foundation ══════════════════════════════
+┌──────────────────────────────────────────────────────────────────────┐
+│ Core Layer (Data Plane) │
+│ Symbol Table │ Term Bank (hash consing) │ Clause Store │ Types │
+└──────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 Dependency Graph
+
+```
+frontend ──→ normalize ──→ core
+ ↑
+search ──→ simplify ──→ index ──→ infer ──→ core
+ ↑
+ rewrite ─┘
+ proof ──→ core/clause_store
+
+ strategy/ ──→ (V2: wired into search)
+```
+
+All arrows point downward/inward. `proof` depends only on `clause_store`, not on `search` (no reverse dependency).
+
+---
+
+## 3. Directory Structure
+
+```
+richard-atp-engine/
+├── CMakeLists.txt # Top-level build (C++20, clang-tidy, clang-format, sanitizers)
+├── .clang-format # Google-based, 4-space indent, 100-col
+├── .clang-tidy # Strict checks, filtered for generated code
+├── .gitignore
+│
+├── grammar/
+│ └── TPTP.g4 # ANTLR4 grammar for TPTP fof/cnf syntax
+│
+├── docs/
+│ └── architecture.md # This document
+│
+├── data/
+│ └── monkey_banana.p # First integration test (TPTP format)
+│
+├── include/atp/
+│ ├── core/ # Data plane — memory foundation
+│ │ ├── types.h # TermId, ClauseId, SymbolId, SortId, SymbolKind, InferenceRule
+│ │ ├── symbol_table.h # String interning with kind/sort metadata
+│ │ ├── term.h # Flat term struct (symbol_id + args)
+│ │ ├── literal.h # Polarity + atom TermId
+│ │ ├── clause.h # Literal vector + provenance (parents, rule, depth)
+│ │ ├── clause_store.h # Central clause ownership, ClauseId allocation
+│ │ └── term_bank.h # Hash-consing pool for structural sharing
+│ │
+│ ├── normalize/ # Normalization — FOL formula to CNF
+│ │ ├── formula.h # FOL AST nodes (And, Or, Not, Forall, Exists, ...)
+│ │ └── clausifier.h # Eliminate →/↔ → NNF → Skolemize → distribute → flatten
+│ │
+│ ├── infer/ # Compute plane — stateless inference operators
+│ │ ├── substitution.h # Variable binding map (stack-lifetime)
+│ │ ├── unification.h # MGU with optional occurs check
+│ │ └── resolution.h # Binary resolution + factoring
+│ │
+│ ├── index/ # Index plane — fast term retrieval
+│ │ └── discrim_tree.h # Discrimination tree (unifiable / instance / generalization queries)
+│ │
+│ ├── simplify/ # Redundancy elimination
+│ │ ├── tautology.h # Complementary literal detection
+│ │ └── subsumption.h # Forward/backward subsumption (backed by discrim tree)
+│ │
+│ ├── search/ # Control plane — proof search scheduling
+│ │ ├── clause_queue.h # Comparators (BFS, weight-based)
+│ │ └── prover.h # Given Clause Loop (V1: hardcoded rules; V2: Calculus-driven)
+│ │
+│ ├── proof/ # Output — proof reconstruction
+│ │ └── proof_trace.h # DFS backtrack from empty clause, formatting
+│ │
+│ ├── rewrite/ # Equality reasoning (V3 extension point)
+│ │ ├── ordering.h # Abstract TermOrdering (KBO, LPO)
+│ │ └── paramodulation.h # Paramodulation + demodulation
+│ │
+│ ├── strategy/ # Dynamic algorithm selection (V2 extension point)
+│ │ ├── inference_rule.h # GeneratingRule / SimplifyingRule interfaces
+│ │ ├── calculus.h # Bundles rules + heuristics + ordering
+│ │ └── problem_analyzer.h # Feature detection + calculus selection
+│ │
+│ └── utils/
+│ ├── hash_utils.h # Hash combination (boost::hash_combine equivalent)
+│ └── logger.h # Compile-time-filtered logging macros
+│
+├── src/ # Implementation files (mirror include/ structure)
+│ ├── core/
+│ ├── normalize/
+│ ├── infer/
+│ ├── index/
+│ ├── simplify/
+│ ├── search/
+│ ├── proof/
+│ ├── rewrite/
+│ ├── strategy/
+│ ├── frontend/
+│ └── main.cpp
+│
+└── tests/ # GoogleTest unit tests
+ ├── CMakeLists.txt
+ ├── test_term_bank.cc
+ ├── test_unification.cc
+ ├── test_clausifier.cc
+ ├── test_resolution.cc
+ ├── test_discrim_tree.cc
+ └── test_prover.cc # End-to-end: Monkey-Banana problem
+```
+
+---
+
+## 4. Module Specifications
+
+### 4.1 Core Layer (`core/`)
+
+The core layer owns all persistent data. Every other module receives `const&` or `&` references to core objects — never raw pointers, never ownership.
+
+#### `types.h` — Type System Foundation
+
+```cpp
+using SymbolId = uint32_t; // Index into SymbolTable
+using TermId = uint32_t; // Index into TermBank (hash-consed)
+using ClauseId = uint32_t; // Index into ClauseStore
+using SortId = uint16_t; // For sorted logic (V3+), default = kUnsorted
+
+enum class SymbolKind : uint8_t {
+ kVariable, kConstant, kFunction, kPredicate, kSkolem
+};
+
+enum class InferenceRule : uint16_t {
+ kInput = 0, kResolution = 1, kFactoring = 2, // Core
+ kParamodulation = 100, kDemodulation = 101, ... // Equality (V3)
+ kSubsumption = 200, kTautologyElim = 201, ... // Simplification
+ kTheoryResolution = 300, ... // Theory (V4)
+};
+```
+
+**Design decisions**:
+- `uint32_t` IDs instead of pointers — enables flat array storage, trivial comparison, and cache-friendly iteration.
+- `InferenceRule` uses `uint16_t` with reserved ranges (0-99 core, 100-199 equality, 200-299 simplification, 300+ theory) to avoid enum churn as the system grows.
+- `SymbolKind` enables distinguishing variables from function symbols at the symbol level, critical for unification and sorted logic.
+- `SortId` is present but unused in V1 (`kUnsorted`), ready for sorted unification in V3.
+
+#### `symbol_table.h` — String Interning
+
+```cpp
+struct SymbolInfo {
+ std::string name;
+ SymbolKind kind;
+ uint16_t arity;
+ SortId sort;
+};
+
+class SymbolTable {
+ SymbolId intern(string_view name, SymbolKind kind, uint16_t arity, SortId sort);
+ string_view getName(SymbolId id) const;
+ const SymbolInfo& getInfo(SymbolId id) const;
+ bool isVariable(SymbolId id) const;
+};
+```
+
+Each unique symbol string is stored exactly once. All downstream modules work with `SymbolId` integers. The `SymbolInfo` metadata (kind, arity, sort) enables type-checking during unification without string inspection.
+
+#### `term_bank.h` — Hash Consing
+
+```cpp
+class TermBank {
+ TermId makeTerm(SymbolId symbol, span<const TermId> args);
+ TermId makeVar(SymbolId var_symbol);
+ const Term& getTerm(TermId id) const;
+ bool isVariable(TermId id) const;
+};
+```
+
+**Hash consing guarantee**: If `makeTerm(f, {a, b})` is called twice with the same `f`, `a`, `b`, it returns the same `TermId`. This makes structural equality a single integer comparison (`O(1)`), which is the foundation for efficient indexing and subsumption.
+
+**Memory layout**: Terms are stored in a flat arena. Each `Term` header (`symbol_id` + `arity`) is followed by its argument `TermId` array in contiguous memory. No heap allocation per term.
+
+#### `clause_store.h` — Clause Ownership
+
+```cpp
+class ClauseStore {
+ ClauseId addClause(Clause clause);
+ const Clause& getClause(ClauseId id) const;
+ Clause& getMutableClause(ClauseId id);
+ void forEach(const function<void(const Clause&)>& visitor) const;
+};
+```
+
+Separated from `Prover` so that proof extraction, simplification, and search all access clauses through the same stable interface. Changing the search strategy (OTTER loop → DISCOUNT loop) does not affect clause storage.
+
+---
+
+### 4.2 Normalization Layer (`normalize/`)
+
+#### `formula.h` — First-Order Logic AST
+
+```cpp
+enum class FormulaKind : uint8_t {
+ kAtom, kNot, kAnd, kOr, kImplies, kIff, kForall, kExists
+};
+
+struct Formula {
+ FormulaKind kind;
+ TermId atom; // for kAtom
+ string var_name; // for quantifiers
+ vector<unique_ptr<Formula>> children; // sub-formulas
+};
+```
+
+This is the **pre-clausification** representation. The parser builds `Formula` trees; the clausifier destroys them and produces `Clause` vectors. Formula nodes are short-lived — they exist only during the normalization phase.
+
+#### `clausifier.h` — CNF Conversion Pipeline
+
+```
+Input Formula
+ │
+ ▼ eliminateImplication() — P → Q becomes ¬P ∨ Q
+ ▼ eliminateBiconditional() — P ↔ Q becomes (¬P ∨ Q) ∧ (P ∨ ¬Q)
+ ▼ pushNegation() — ¬(P ∧ Q) becomes ¬P ∨ ¬Q (NNF)
+ ▼ skolemize() — ∀x∃y.P(x,y) becomes ∀x.P(x, sk(x))
+ ▼ dropUniversal() — All remaining vars are implicitly ∀
+ ▼ distribute() — (P ∨ (Q ∧ R)) becomes (P ∨ Q) ∧ (P ∨ R)
+ ▼ flatten() — Nested ∧ at top level → separate clauses
+ │
+ ▼
+Output: vector<Clause>
+```
+
+**Critical**: `skolemize()` must correctly handle nested quantifier scopes. Given `∀x ∃y ∀z ∃w. P(x,y,z,w)`, the Skolem functions are `y = sk1(x)` and `w = sk2(x, z)` — each existential variable becomes a function of all universally quantified variables in its scope.
+
+---
+
+### 4.3 Inference Layer (`infer/`)
+
+#### `substitution.h` — Variable Bindings
+
+A substitution `σ = {X → f(a), Y → Z}` maps variables to terms. Key properties:
+
+- **Stack lifetime**: Created during unification, applied to build the resolvent, then discarded. Never stored long-term.
+- **Chain resolution**: `resolve(X)` walks the binding chain (`X → Y → f(a)`) to the final value.
+- **Idempotence**: After applying a substitution, no variables in the result have bindings in the same substitution.
+
+#### `unification.h` — Most General Unifier
+
+```cpp
+bool unify(const TermBank& bank, TermId t1, TermId t2, Substitution& subst,
+ const UnificationConfig& config);
+```
+
+The Robinson unification algorithm, operating on `TermId` values via the `TermBank`. Returns the Most General Unifier (MGU) if one exists.
+
+**Occurs check**: Configurable via `UnificationConfig::enable_occurs_check`. Disabled for situation calculus planning (performance); enabled for general mathematical reasoning (soundness — prevents infinite terms like `X = f(X)`).
+
+#### `resolution.h` — Inference Rules
+
+```cpp
+optional<Clause> resolve(bank, clause1, lit_idx1, clause2, lit_idx2);
+vector<Clause> allResolvents(bank, clause1, clause2);
+vector<Clause> factor(bank, clause);
+```
+
+**Binary resolution**: Given clauses `{P(x), Q(x)}` and `{¬P(a), R(a)}`, if `P(x)` and `P(a)` unify under `σ = {x→a}`, produce resolvent `{Q(a), R(a)}`.
+
+**Factoring**: Given clause `{P(x), P(a), Q(x)}`, if `P(x)` and `P(a)` unify, produce `{P(a), Q(a)}`. Factoring is necessary for completeness of binary resolution.
+
+---
+
+### 4.4 Index Layer (`index/`)
+
+#### `discrim_tree.h` — Discrimination Tree
+
+A trie-based index over the flattened structure of terms. Supports three query types:
+
+| Query | Use Case | Direction |
+|-------|----------|-----------|
+| `queryUnifiable(t)` | Find resolution partners | Bidirectional matching |
+| `queryInstances(t)` | Backward subsumption | Pattern more general than indexed |
+| `queryGeneralizations(t)` | Forward subsumption | Indexed more general than query |
+
+The tree is built by linearizing terms in pre-order: `f(g(a), b)` becomes the key sequence `[f, g, a, *, b]` where `*` represents a variable (wildcard). Lookups traverse the trie, branching at decision points.
+
+---
+
+### 4.5 Simplification Layer (`simplify/`)
+
+#### `tautology.h`
+
+Detects clauses containing both `P` and `¬P` for the same atom. These are logically true and can be safely discarded without affecting completeness.
+
+#### `subsumption.h`
+
+Backed by the discrimination tree. Two operations:
+
+- **Forward subsumption**: Before enqueuing a new clause `C`, check if any existing clause `D` subsumes it (`D ⊆ C` — every literal of `D` matches a literal of `C`). If yes, discard `C`.
+- **Backward subsumption**: After adding `C`, check if `C` subsumes any existing clause `D`. If yes, remove `D`.
+
+Subsumption is the single most important simplification rule for keeping the clause set manageable.
+
+---
+
+### 4.6 Search Layer (`search/`)
+
+#### `prover.h` — The Given Clause Loop
+
+The main proof search loop, following the OTTER/E paradigm:
+
+```
+procedure GivenClauseLoop(initial_clauses):
+ Unprocessed ← priority_queue(initial_clauses)
+ Processed ← empty_set
+
+ while Unprocessed is not empty:
+ given ← Unprocessed.pop() // clause selection
+
+ if given is tautology: continue
+ if given is forward-subsumed: continue
+
+ // Backward simplification
+ remove from Processed all clauses subsumed by given
+
+ // Inference
+ for each clause C in Processed:
+ for each resolvent R of (given, C):
+ if R is empty clause: RETURN PROOF FOUND
+ if R is not tautology and not forward-subsumed:
+ Unprocessed.push(R)
+
+ // Generate factors of given
+ for each factor F of given:
+ if F is empty clause: RETURN PROOF FOUND
+ Unprocessed.push(F)
+
+ Processed.add(given)
+
+ RETURN SATURATED (no proof exists)
+```
+
+**V1 configuration**:
+```cpp
+struct ProverConfig {
+ ClauseComparator comparator = bfsCompare; // BFS for shortest proof
+ size_t max_clauses = 100000;
+ size_t max_iterations = 500000;
+ bool enable_occurs_check = false;
+};
+```
+
+---
+
+### 4.7 Proof Layer (`proof/`)
+
+#### `proof_trace.h`
+
+When the empty clause `□` is derived, the prover halts. Proof extraction walks the `parent1`/`parent2` pointers stored in each `Clause` via DFS, reconstructing the derivation tree from `□` back to input axioms.
+
+The output is a sequence of `ProofStep` records, each containing the inference rule used and the parent clause IDs. This can be formatted as a human-readable proof or (future) TSTP-compliant output.
+
+---
+
+### 4.8 Extension Point: Rewriting (`rewrite/`)
+
+Scaffolded for V3. Not compiled in V1.
+
+#### `ordering.h` — Abstract Term Ordering
+
+```cpp
+class TermOrdering {
+ virtual OrderResult compare(const TermBank& bank, TermId t1, TermId t2) const = 0;
+};
+```
+
+Concrete implementations (future): Knuth-Bendix Ordering (KBO), Lexicographic Path Ordering (LPO). These are required for ordered resolution and superposition calculus — they ensure that rewriting always terminates.
+
+#### `paramodulation.h` — Equality Reasoning
+
+Paramodulation replaces a subterm in one clause with an equal term from another. Combined with a term ordering, this becomes the **superposition calculus** — the dominant approach for first-order logic with equality.
+
+---
+
+### 4.9 Extension Point: Strategy (`strategy/`)
+
+Scaffolded for V2. Not compiled in V1.
+
+The strategy module will enable dynamic algorithm selection based on problem analysis:
+
+```
+Input clauses → ProblemAnalyzer → ProblemFeatures → selectCalculus() → Calculus
+ │
+ ┌────────────────────────────────────────────────┘
+ ▼
+ Calculus = { generating_rules[], simplifying_rules[],
+ comparator, ordering, resource_limits }
+ │
+ ▼
+ Prover.prove(calculus)
+```
+
+This prevents search space explosion by loading only the inference rules relevant to the problem. A pure propositional problem would never load the paramodulation rule; an equality-heavy problem would skip pure resolution in favor of superposition.
+
+---
+
+## 5. Data Flow
+
+### 5.1 End-to-End Pipeline
+
+```
+monkey_banana.p (TPTP text file)
+ │
+ ▼
+ [tptp_parser] ANTLR4 lexer/parser → parse tree → Formula AST
+ │
+ ▼
+ [clausifier] NNF → Skolemize → CNF → vector<Clause>
+ │
+ ▼
+ [prover] Given Clause Loop (resolution + factoring + subsumption)
+ │
+ ├── Normal: derive new clauses, enqueue, repeat
+ │
+ └── Empty clause found!
+ │
+ ▼
+ [proof_trace] DFS backtrack through parent pointers
+ │
+ ▼
+ Human-readable proof output
+```
+
+### 5.2 Term Lifecycle
+
+```
+String "do(walk, S0)"
+ │
+ ▼ SymbolTable::intern()
+ SymbolIds: { do=0, walk=1, S0=2 }
+ │
+ ▼ TermBank::makeTerm()
+ TermIds: S0_term=0, walk_term=1, do_walk_S0=2
+ │ (hash-consed: calling makeTerm(do, [walk, S0]) again returns 2)
+ │
+ ▼ Used everywhere as integer IDs
+ Clause { literals: [ Literal{atom=2, positive=true} ] }
+```
+
+### 5.3 Substitution Lifecycle
+
+```
+Unify( P(X, f(Y)), P(a, f(b)) )
+ │
+ ▼ unify() creates stack-local Substitution
+ σ = { X → a, Y → b }
+ │
+ ▼ Apply σ to build resolvent clause
+ New terms created in TermBank (hash-consed)
+ │
+ ▼ Substitution goes out of scope — destroyed
+ Only the new TermIds persist
+```
+
+---
+
+## 6. Known Limitations & Future Work
+
+### 6.1 Critical Gaps (Ordered by Priority)
+
+#### Gap 1: No Variable Standardization Apart
+
+**Impact**: Correctness bug if not addressed before first real resolution.
+
+When resolving clause `{P(X)}` with clause `{¬P(X), Q(X)}`, variable `X` in the two clauses must be treated as distinct. Without renaming one clause's variables (e.g., `X → X'`), the unifier incorrectly constrains both clauses' variables.
+
+**Fix**: Add `renameVariables(Clause&, TermBank&, uint32_t& next_var_id)` to `infer/` before Phase 2 resolution implementation.
+
+#### Gap 2: No Literal Selection Function
+
+**Impact**: Search space is ~10x larger than necessary.
+
+Ordered resolution restricts inference to the **maximal literal** (under a term ordering) in each clause. Our `Prover` currently has no concept of selecting which literal to resolve on — it tries all pairs.
+
+**Fix**: V2 — add `LiteralSelector` interface, parameterized by `TermOrdering`.
+
+#### Gap 3: Single Clause Queue (No Age-Weight Interleaving)
+
+**Impact**: Incomplete or extremely slow on hard problems.
+
+A single `ClauseComparator` cannot implement the age-weight ratio that E Prover uses: "pick the lightest clause 5 times, then the oldest clause once." This interleaving is crucial for both performance and completeness.
+
+**Fix**: V2 — replace `ClauseComparator` with a `ClauseQueue` abstract class supporting multi-queue round-robin.
+
+#### Gap 4: Single Index Type
+
+**Impact**: Suboptimal retrieval performance.
+
+The discrimination tree handles forward matching well but is not ideal for all query patterns. Modern ATPs maintain multiple complementary indices:
+
+| Index Type | Best For |
+|-----------|----------|
+| Discrimination Tree | Forward matching (generalization queries) |
+| Substitution Tree | Backward matching + unification candidates |
+| Fingerprint Index | O(1) pre-filtering of non-unifiable pairs |
+| Feature Vector Index | Clause-level subsumption checks |
+
+**Fix**: V3 — abstract `TermIndex` interface, multiple implementations.
+
+#### Gap 5: No Preprocessing Pipeline
+
+**Impact**: Missed optimization opportunities.
+
+Between clausification and the main loop, modern ATPs perform definition unfolding, pure literal elimination, equality axiom injection, and miniscoping. Our `normalize/` layer has only the clausifier.
+
+**Fix**: V2 — extend `normalize/` into a transform pipeline with pluggable passes.
+
+#### Gap 6: No AVATAR Splitting
+
+**Impact**: Cannot compete on structured problems.
+
+AVATAR splits clauses with independent variable sets into propositional components managed by an embedded SAT solver. This is the single largest performance innovation in ATP in the last decade (10-100x speedup on structured problems).
+
+**Fix**: V4 — add `sat/` module with MiniSat/CaDiCaL integration + splitting logic in `search/`.
+
+### 6.2 Non-Critical Gaps
+
+| Gap | Category | Target Version |
+|-----|----------|----------------|
+| TSTP proof output format | Interoperability | V2 |
+| Watched literals for unit propagation | Performance | V3 |
+| Clause arena (replace `vector<Literal>`) | Memory | V3 |
+| ML-guided clause selection | Research | V5+ |
+| Parallel portfolio mode | Performance | V5+ |
+
+---
+
+## 7. References
+
+1. Schulz, S. (2013). *System Description: E 1.8*. CADE-24.
+2. Kovács, L., Voronkov, A. (2013). *First-Order Theorem Proving and Vampire*. CAV 2013.
+3. Voronkov, A. (2014). *AVATAR: The Architecture for First-Order Theorem Provers*. CAV 2014.
+4. Nieuwenhuis, R., Rubio, A. (2001). *Paramodulation-Based Theorem Proving*. Handbook of Automated Reasoning.
+5. Sutcliffe, G. (2009). *The TPTP Problem Library and Associated Infrastructure*. Journal of Automated Reasoning.
+6. Robinson, J.A. (1965). *A Machine-Oriented Logic Based on the Resolution Principle*. JACM.
diff --git a/proofsearch/grammar/TPTP.g4 b/proofsearch/grammar/TPTP.g4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/proofsearch/grammar/TPTP.g4
diff --git a/proofsearch/include/atp/core/clause.h b/proofsearch/include/atp/core/clause.h
new file mode 100644
index 0000000..6387adf
--- /dev/null
+++ b/proofsearch/include/atp/core/clause.h
@@ -0,0 +1,52 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file clause.h
+/// @brief A clause is a disjunction of literals, with provenance tracking.
+///
+/// NOTE: V1 uses std::vector<Literal> for simplicity.
+/// Future optimization: migrate to a Clause Arena with contiguous storage.
+
+#include "atp/core/literal.h"
+#include "atp/core/types.h"
+
+#include <vector>
+
+namespace atp {
+
+/// A clause: a disjunction of literals plus provenance for proof reconstruction.
+struct Clause {
+ ClauseId id_ = kInvalidId;
+ std::vector<Literal> literals_;
+
+ // Provenance (for proof trace)
+ InferenceRule rule_ = InferenceRule::kInput;
+ ClauseId parent1_ = kInvalidId;
+ ClauseId parent2_ = kInvalidId;
+ uint16_t depth_ = 0; ///< Derivation depth (for BFS scheduling)
+
+ /// True if the clause has no literals (contradiction).
+ [[nodiscard]] bool isEmpty() const { return literals_.empty(); }
+
+ /// Number of literals.
+ [[nodiscard]] size_t size() const { return literals_.size(); }
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/clause_store.h b/proofsearch/include/atp/core/clause_store.h
new file mode 100644
index 0000000..5d6562a
--- /dev/null
+++ b/proofsearch/include/atp/core/clause_store.h
@@ -0,0 +1,64 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file clause_store.h
+/// @brief Central clause storage — owns all clauses, assigns ClauseIds.
+///
+/// Extracted from Prover to separate storage responsibility from search logic.
+/// This enables swapping search strategies (OTTER loop / DISCOUNT loop)
+/// without touching clause lifecycle management.
+
+#include "atp/core/clause.h"
+#include "atp/core/types.h"
+
+#include <functional>
+#include <vector>
+
+namespace atp {
+
+/// Central clause store. Owns all clauses and assigns unique ClauseIds.
+/// All modules access clauses through this store via ClauseId references.
+class ClauseStore {
+ public:
+ ClauseStore() = default;
+
+ /// Add a clause, assign it a ClauseId, and return the ID.
+ ClauseId addClause(Clause clause);
+
+ /// Access a clause by its ID. UB if id is out of range.
+ [[nodiscard]] const Clause& getClause(ClauseId id) const;
+
+ /// Mutable access (for marking clauses as deleted, etc.)
+ [[nodiscard]] Clause& getMutableClause(ClauseId id);
+
+ /// Total number of stored clauses.
+ [[nodiscard]] size_t size() const;
+
+ /// Iterate over all clauses with a visitor.
+ void forEach(const std::function<void(const Clause&)>& visitor) const;
+ // function may have type erasure overhead, consider using a template
+ // template <typename Visitor>
+ // void forEach(Visitor&& visitor) const;
+
+ private:
+ std::vector<Clause> clauses_;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/literal.h b/proofsearch/include/atp/core/literal.h
new file mode 100644
index 0000000..e58b185
--- /dev/null
+++ b/proofsearch/include/atp/core/literal.h
@@ -0,0 +1,39 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file literal.h
+/// @brief A literal is a (possibly negated) atomic formula.
+
+#include "atp/core/types.h"
+
+namespace atp {
+
+/// A literal: a polarity flag plus the TermId of the atom.
+struct Literal {
+ TermId atom_; ///< TermId of the predicate application
+ bool is_positive_; ///< true = positive, false = negated
+
+ /// Complement: flip polarity, same atom.
+ [[nodiscard]] Literal complement() const { return {.atom_ = atom_, .is_positive_ = !is_positive_}; }
+
+ bool operator==(const Literal& other) const = default;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/symbol_table.h b/proofsearch/include/atp/core/symbol_table.h
new file mode 100644
index 0000000..a646f78
--- /dev/null
+++ b/proofsearch/include/atp/core/symbol_table.h
@@ -0,0 +1,71 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file symbol_table.h
+/// @brief Bidirectional mapping between symbol names and SymbolIds,
+/// with kind and sort metadata.
+
+#include "atp/core/types.h"
+
+#include <deque>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+namespace atp {
+
+/// Metadata associated with an interned symbol.
+struct SymbolInfo {
+ std::string name_;
+ SymbolKind kind_ = SymbolKind::kFunction;
+ uint16_t arity_ = 0;
+ SortId sort_ = kUnsorted; ///< Result sort (for sorted logic)
+};
+
+/// Interns symbol names so that each unique string is stored exactly once.
+/// Also tracks symbol metadata (kind, arity, sort).
+/// Thread-safety: NOT thread-safe (single-threaded prover design).
+class SymbolTable {
+ public:
+ SymbolTable() = default;
+
+ /// Intern a symbol name with metadata. Returns existing ID if already present.
+ SymbolId intern(std::string_view name, SymbolKind kind, uint16_t arity = 0,
+ SortId sort = kUnsorted);
+
+ /// Look up the name for a given SymbolId. UB if id is out of range.
+ [[nodiscard]] std::string_view getName(SymbolId id) const;
+
+ /// Look up full metadata for a SymbolId.
+ [[nodiscard]] const SymbolInfo& getInfo(SymbolId id) const;
+
+ /// Check if a symbol is a variable.
+ [[nodiscard]] bool isVariable(SymbolId id) const;
+
+ /// Number of interned symbols.
+ [[nodiscard]] size_t size() const;
+
+ private:
+ // TODO: Phase 1 implementation
+ std::deque<SymbolInfo> infos_;
+ std::unordered_map<std::string_view, SymbolId> name_to_id_;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/term.h b/proofsearch/include/atp/core/term.h
new file mode 100644
index 0000000..d35c753
--- /dev/null
+++ b/proofsearch/include/atp/core/term.h
@@ -0,0 +1,43 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file term.h
+/// @brief Core Term data structure — hash-consed, immutable.
+///
+/// V1 uses vector<TermId> for args.
+/// Future: flat arena with contiguous arg storage for cache locality.
+
+#include "atp/core/types.h"
+
+#include <vector>
+
+namespace atp {
+
+/// A term is a function/predicate application: f(t1, t2, ..., tn).
+/// Variables are 0-arity terms whose symbol has SymbolKind::kVariable.
+struct Term {
+ SymbolId symbol_id_; ///< Index into the SymbolTable
+ std::vector<TermId> args_; ///< Argument sub-term IDs
+
+ /// Number of arguments.
+ [[nodiscard]] uint16_t arity() const { return static_cast<uint16_t>(args_.size()); }
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/term_bank.h b/proofsearch/include/atp/core/term_bank.h
new file mode 100644
index 0000000..ca12759
--- /dev/null
+++ b/proofsearch/include/atp/core/term_bank.h
@@ -0,0 +1,86 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file term_bank.h
+/// @brief Hash-consing term pool — guarantees structural sharing.
+
+#include "atp/core/symbol_table.h"
+#include "atp/core/term.h"
+#include "atp/core/types.h"
+
+#include <span>
+#include <unordered_map>
+#include <vector>
+
+namespace atp {
+
+/// Global term pool with hash-consing.
+/// Every structurally identical term maps to the same TermId.
+///
+/// Holds a reference to SymbolTable for variable detection.
+class TermBank {
+ public:
+ /// TermBank needs the SymbolTable to distinguish variables from constants
+ /// (both have 0 args, but SymbolKind differs).
+ explicit TermBank(SymbolTable& symbols) : symbols_(&symbols) {}
+
+ /// Create or retrieve a term. Returns the canonical TermId.
+ TermId makeTerm(SymbolId symbol, std::span<const TermId> args);
+
+ /// Create a variable term. Convenience for makeTerm(var, {}).
+ TermId makeVar(SymbolId var_symbol);
+
+ /// Look up a term by its ID.
+ [[nodiscard]] const Term& getTerm(TermId id) const;
+
+ /// Check if a TermId represents a variable (delegates to SymbolTable).
+ [[nodiscard]] bool isVariable(TermId id) const;
+
+ /// Total number of unique terms stored.
+ [[nodiscard]] size_t size() const;
+
+ /// Access the underlying SymbolTable.
+ [[nodiscard]] const SymbolTable& symbols() const { return *symbols_; }
+ [[nodiscard]] SymbolTable& mutableSymbols() { return *symbols_; }
+
+ private:
+ struct TermKey {
+ SymbolId symbol_;
+ std::vector<TermId> args_;
+
+ bool operator==(const TermKey& other) const = default;
+ };
+
+ struct TermKeyHash {
+ size_t operator()(const TermKey& key) const {
+ size_t seed = std::hash<uint32_t>{}(key.symbol_);
+ for (const TermId arg : key.args_) {
+ seed ^= std::hash<TermId>{}(arg) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+ }
+ return seed;
+ }
+ };
+
+ SymbolTable* symbols_;
+ std::vector<Term> terms_;
+ std::unordered_map<TermKey, TermId, TermKeyHash> term_to_id_;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/core/types.h b/proofsearch/include/atp/core/types.h
new file mode 100644
index 0000000..23f2322
--- /dev/null
+++ b/proofsearch/include/atp/core/types.h
@@ -0,0 +1,79 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file types.h
+/// @brief Core type aliases for the ATP engine.
+
+#include <cstdint>
+
+namespace atp {
+
+/// Unique identifier for an interned symbol (predicate/function/constant name).
+using SymbolId = uint32_t;
+
+/// Unique identifier for a term in the Term Bank (hash-consed).
+using TermId = uint32_t;
+
+/// Unique identifier for a clause in the Clause Store.
+using ClauseId = uint32_t;
+
+/// Unique identifier for a sort/type (for sorted logic).
+using SortId = uint16_t;
+
+/// Sentinel value indicating an invalid / unassigned ID.
+inline constexpr uint32_t kInvalidId = UINT32_MAX;
+
+/// Default sort (unsorted logic).
+inline constexpr SortId kUnsorted = 0;
+
+/// Symbol kind — distinguishes variables from function/predicate symbols.
+enum class SymbolKind : uint8_t {
+ kVariable, ///< Universally quantified variable
+ kConstant, ///< 0-arity function symbol (constant)
+ kFunction, ///< Function symbol (arity >= 1)
+ kPredicate, ///< Predicate symbol (returns bool)
+ kSkolem, ///< Skolem function/constant (introduced by Skolemization)
+};
+
+/// Inference rule that produced a clause.
+/// Deliberately uses uint16_t for extensibility toward algebra/analysis rules.
+enum class InferenceRule : uint16_t {
+ // ── Core resolution rules ──
+ kInput = 0, ///< Original input clause (axiom / negated conjecture)
+ kResolution = 1, ///< Binary resolution
+ kFactoring = 2, ///< Factoring
+
+ // ── Equality rules (algebra) ──
+ kParamodulation = 100, ///< Paramodulation (equality resolution)
+ kDemodulation = 101, ///< Demodulation (simplification by rewriting)
+ kEqualityResolution = 102, ///< s != t resolved with unifier
+ kEqualityFactoring = 103, ///< Factoring with equality
+
+ // ── Simplification rules ──
+ kSubsumption = 200, ///< Removed by subsumption
+ kTautologyElim = 201, ///< Removed as tautology
+ kPureLiteralElim = 202, ///< Pure literal elimination
+
+ // ── Theory rules (analysis, arithmetic) ──
+ kTheoryResolution = 300, ///< Theory-specific resolution
+ kTheoryRewrite = 301, ///< Theory-driven rewriting
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/frontend/tptp_parser.h b/proofsearch/include/atp/frontend/tptp_parser.h
new file mode 100644
index 0000000..68a5564
--- /dev/null
+++ b/proofsearch/include/atp/frontend/tptp_parser.h
@@ -0,0 +1,99 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file tptp_parser.h
+/// @brief TPTP format parser — converts .p files to internal representations.
+///
+/// The parser interns all symbols (predicates, functions, variables) into the
+/// SymbolTable and builds terms in the TermBank during parsing. This is the
+/// E Prover approach: parse and intern simultaneously, avoiding a separate
+/// resolution pass.
+///
+/// V1: Hand-written recursive descent for TPTP CNF/FOF subset.
+/// (ANTLR4 integration deferred — avoids build complexity for V1.)
+
+#include "atp/core/clause.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/normalize/formula.h"
+
+#include <string>
+#include <vector>
+
+namespace atp {
+
+/// Role of a TPTP formula.
+/// See TPTP spec: https://www.tptp.org/TPTP/SyntaxBNF.html
+enum class FormulaRole : uint8_t {
+ kAxiom, ///< Background truth
+ kHypothesis, ///< Problem-specific assumption
+ kDefinition, ///< Symbol definition (can be unfolded)
+ kAssumption, ///< Assumed for this proof attempt
+ kLemma, ///< Derived intermediate result
+ kTheorem, ///< Previously proved statement (used as axiom)
+ kConjecture, ///< The statement to prove
+ kNegatedConjecture, ///< Already negated conjecture (input was cnf)
+ kPlain, ///< No semantic role specified
+};
+
+/// Map a TPTP role string to enum. Returns kPlain for unknown roles.
+FormulaRole parseRole(std::string_view role_str);
+
+/// A parsed TPTP formula with its name and role.
+struct AnnotatedFormula {
+ std::string name_; ///< Formula name (e.g., "axiom1")
+ FormulaRole role_ = FormulaRole::kPlain; ///< Semantic role
+ std::unique_ptr<Formula> formula_; ///< The FOL formula (for fof(...))
+ std::vector<Clause> clauses_; ///< Direct clauses (for cnf(...))
+ bool is_cnf_ = false; ///< true if input was cnf(), false if fof()
+};
+
+/// A complete parsed TPTP problem.
+struct ParsedProblem {
+ std::vector<AnnotatedFormula> formulas_;
+
+ /// Does any formula contain an equality predicate?
+ bool has_equality_ = false;
+
+ /// Original file path (for error messages).
+ std::string source_file_;
+};
+
+/// Parse a TPTP file.
+///
+/// For `cnf(...)` entries: clauses are built directly (no clausification needed).
+/// For `fof(...)` entries: Formula AST is built (clausification happens later).
+///
+/// All symbols are interned into `symbols` and terms into `bank` during parsing.
+ParsedProblem parseTptpFile(const std::string& filename, TermBank& bank, SymbolTable& symbols);
+
+/// Parse a TPTP string (for testing).
+ParsedProblem parseTptpString(const std::string& input, TermBank& bank, SymbolTable& symbols);
+
+/// Prepare a parsed problem for proving:
+/// 1. Negate conjecture formulas.
+/// 2. Clausify all fof() formulas.
+/// 3. Collect all cnf() clauses directly.
+/// 4. Assign ClauseIds and set provenance to kInput.
+///
+/// Returns the final clause set ready for the Prover.
+std::vector<Clause> prepareForProving(ParsedProblem& problem, TermBank& bank, SymbolTable& symbols);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/index/discrim_tree.h b/proofsearch/include/atp/index/discrim_tree.h
new file mode 100644
index 0000000..5f8a206
--- /dev/null
+++ b/proofsearch/include/atp/index/discrim_tree.h
@@ -0,0 +1,76 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file discrim_tree.h
+/// @brief Perfect Discrimination Tree for fast term indexing.
+///
+/// Used by subsumption and resolution to quickly find candidate clauses
+/// whose literals unify with a query term. Supports three query modes:
+/// - Unifiable: find terms that can be unified with the query
+/// - Instances: find terms that are instances of the query (more specific)
+/// - Generalizations: find terms that are generalizations of the query (more general)
+
+#include "atp/core/clause.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <vector>
+
+namespace atp {
+
+/// A match result from the discrimination tree.
+struct IndexMatch {
+ ClauseId clause_id_;
+ size_t lit_index_;
+};
+
+/// A discrimination tree index over terms.
+/// Supports insertion and three types of retrieval queries.
+class DiscrimTree {
+ public:
+ explicit DiscrimTree(const TermBank& bank);
+
+ /// Insert a term (identified by its TermId) with an associated clause/literal ref.
+ void insert(TermId term, ClauseId clause_id, size_t lit_index);
+
+ /// Retrieve all entries whose indexed term is unifiable with `query`.
+ [[nodiscard]] std::vector<IndexMatch> queryUnifiable(TermId query) const;
+
+ /// Retrieve all entries whose indexed term is an instance of `query`.
+ /// (query is more general, indexed term is more specific)
+ [[nodiscard]] std::vector<IndexMatch> queryInstances(TermId query) const;
+
+ /// Retrieve all entries whose indexed term is a generalization of `query`.
+ /// (indexed term is more general, query is more specific)
+ /// This is the key operation for forward subsumption.
+ [[nodiscard]] std::vector<IndexMatch> queryGeneralizations(TermId query) const;
+
+ /// Remove all entries for a given clause.
+ void removeClause(ClauseId clause_id);
+
+ /// Number of indexed entries.
+ [[nodiscard]] size_t size() const;
+
+ private:
+ const TermBank& bank_;
+ // TODO: Phase 3 implementation — trie node arena
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/infer/resolution.h b/proofsearch/include/atp/infer/resolution.h
new file mode 100644
index 0000000..6e2954d
--- /dev/null
+++ b/proofsearch/include/atp/infer/resolution.h
@@ -0,0 +1,50 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file resolution.h
+/// @brief Binary resolution and factoring inference rules.
+
+#include "atp/core/clause.h"
+#include "atp/core/term_bank.h"
+#include "atp/infer/unification.h"
+
+#include <optional>
+#include <vector>
+
+namespace atp {
+
+/// Attempt binary resolution between two clauses on the specified literal indices.
+/// @return The resolvent clause if resolution succeeds, std::nullopt otherwise.
+std::optional<Clause> resolve(TermBank& bank, const Clause& c1, size_t lit_idx1, const Clause& c2,
+ size_t lit_idx2, const UnificationConfig& uconfig = {});
+
+/// Generate all possible resolvents between two clauses.
+std::vector<Clause> allResolvents(TermBank& bank, const Clause& c1, const Clause& c2,
+ const UnificationConfig& uconfig = {});
+
+/// Attempt factoring on a single clause (merge two unifiable literals).
+std::vector<Clause> factor(TermBank& bank, const Clause& c, const UnificationConfig& uconfig = {});
+
+/// Rename all variables in a clause to fresh ones, avoiding conflicts
+/// with variables already in the bank.
+Clause renameVariables(const Clause& c, TermBank& bank, SymbolTable& symbols,
+ uint32_t& next_var_id);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/infer/substitution.h b/proofsearch/include/atp/infer/substitution.h
new file mode 100644
index 0000000..671f6d7
--- /dev/null
+++ b/proofsearch/include/atp/infer/substitution.h
@@ -0,0 +1,76 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file substitution.h
+/// @brief Variable binding map: Var -> Term.
+///
+/// Short-lived object created during unification, applied to build resolvents,
+/// then discarded. Never stored long-term.
+
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <unordered_map>
+
+namespace atp {
+
+/// A substitution σ maps variable TermIds to their bound TermIds.
+///
+/// Example: σ = { X → f(a), Y → Z }
+/// lookup(X) = f(a) (direct binding)
+/// resolve(X) = f(a) (same — f(a) is not a variable)
+/// resolve(Y) = Z (if Z is unbound, stops at Z)
+/// resolve(Y) = f(b) (if Z → f(b) also in σ, walks the chain)
+class Substitution {
+ public:
+ Substitution() = default;
+
+ /// Bind a variable to a term. Returns false if already bound to a different term.
+ bool bind(TermId var, TermId term);
+
+ /// Look up what a variable is directly bound to. Returns kInvalidId if unbound.
+ [[nodiscard]] TermId lookup(TermId var) const;
+
+ /// Walk the binding chain to the final value.
+ /// If var → Y → f(a), returns f(a).
+ /// Requires TermBank to check whether intermediate values are variables.
+ [[nodiscard]] TermId resolve(TermId id, const TermBank& bank) const;
+
+ /// Apply this substitution to a term recursively, creating new terms in the bank.
+ /// Free (unbound) variables are left as-is.
+ [[nodiscard]] TermId apply(TermId term, TermBank& bank) const;
+
+ /// Check if a variable occurs anywhere inside a term (for occurs check).
+ [[nodiscard]] bool occursIn(TermId var, TermId term, const TermBank& bank) const;
+
+ /// Clear all bindings.
+ void clear();
+
+ /// Number of bindings.
+ [[nodiscard]] size_t size() const { return bindings_.size(); }
+
+ /// Check if empty.
+ [[nodiscard]] bool empty() const { return bindings_.empty(); }
+
+ private:
+ std::unordered_map<TermId, TermId> bindings_;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/infer/unification.h b/proofsearch/include/atp/infer/unification.h
new file mode 100644
index 0000000..870be66
--- /dev/null
+++ b/proofsearch/include/atp/infer/unification.h
@@ -0,0 +1,43 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file unification.h
+/// @brief Most General Unifier (MGU) computation.
+
+#include "atp/core/term_bank.h"
+#include "atp/infer/substitution.h"
+
+namespace atp {
+
+/// Configuration for the unification algorithm.
+struct UnificationConfig {
+ bool enable_occurs_check_ = false; ///< Set true for sound but slower unification
+};
+
+/// Attempt to unify two terms, producing a most general unifier.
+/// @return true if unification succeeds, with the MGU in `subst`.
+bool unify(const TermBank& bank, TermId t1, TermId t2, Substitution& subst,
+ const UnificationConfig& config = {});
+
+/// Apply a substitution to a term recursively, creating new terms in the bank.
+/// Free (unbound) variables are left unchanged.
+TermId applySubstitution(const Substitution& subst, TermId term, TermBank& bank);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/normalize/clausifier.h b/proofsearch/include/atp/normalize/clausifier.h
new file mode 100644
index 0000000..61d2505
--- /dev/null
+++ b/proofsearch/include/atp/normalize/clausifier.h
@@ -0,0 +1,69 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file clausifier.h
+/// @brief Converts FOL formulas to Clause Normal Form (CNF).
+///
+/// Clausifier is a stateful object because:
+/// 1. Skolem function counter must be shared across all formulas in a problem
+/// (otherwise different formulas generate conflicting Skolem names).
+/// 2. Variable renaming counter must be shared for the same reason.
+///
+/// Pipeline: eliminate ↔/→ → NNF → Skolemize → distribute ∨ over ∧ → flatten.
+
+#include "atp/core/clause.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/normalize/formula.h"
+
+#include <vector>
+
+namespace atp {
+
+/// Stateful clausifier. Create one per proof problem.
+///
+/// Usage:
+/// Clausifier clausifier(bank, symbols);
+/// auto clauses1 = clausifier.clausify(formula1);
+/// auto clauses2 = clausifier.clausify(formula2);
+/// // Skolem IDs are unique across both calls.
+class Clausifier {
+ public:
+ Clausifier(TermBank& bank, SymbolTable& symbols);
+
+ /// Convert a single formula to CNF clauses.
+ /// Skolem functions generated here are globally unique (counter persists).
+ [[nodiscard]] std::vector<Clause> clausify(const Formula& formula);
+
+ /// Negate a formula: wraps it in ¬(...)
+ /// Used to negate the conjecture before clausification.
+ [[nodiscard]] static std::unique_ptr<Formula> negate(std::unique_ptr<Formula> formula);
+
+ /// Number of Skolem functions generated so far.
+ [[nodiscard]] uint32_t skolemCount() const { return next_skolem_id_; }
+
+ private:
+ TermBank& bank_;
+ SymbolTable& symbols_;
+ uint32_t next_skolem_id_ = 0;
+ uint32_t next_var_id_ = 0; ///< For variable standardization apart
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/normalize/formula.h b/proofsearch/include/atp/normalize/formula.h
new file mode 100644
index 0000000..5ee9b94
--- /dev/null
+++ b/proofsearch/include/atp/normalize/formula.h
@@ -0,0 +1,84 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file formula.h
+/// @brief First-order logic formula AST (pre-clausification).
+///
+/// Design decision: atoms use TermId (interned), quantifier variables use
+/// SymbolId (interned). The parser interns all symbols during parsing.
+/// This avoids string→ID lookups during clausification and ensures
+/// consistent variable identity across quantifier scopes.
+
+#include "atp/core/types.h"
+
+#include <memory>
+#include <vector>
+
+namespace atp {
+
+/// AST node types for first-order logic formulas.
+enum class FormulaKind : uint8_t {
+ kAtom, ///< Predicate application: P(t1, ..., tn)
+ kEquality, ///< Equality atom: t1 = t2 (distinguished for equality reasoning)
+ kNot, ///< Negation: ¬F
+ kAnd, ///< Conjunction: F ∧ G
+ kOr, ///< Disjunction: F ∨ G
+ kImplies, ///< Implication: F → G
+ kIff, ///< Biconditional: F ↔ G
+ kForall, ///< Universal: ∀x. F
+ kExists, ///< Existential: ∃x. F
+};
+
+/// A first-order logic formula node.
+///
+/// All identifiers are interned: atoms as TermIds, quantifier variables as
+/// SymbolIds. No raw strings survive past the parser.
+struct Formula {
+ FormulaKind kind_;
+
+ /// For kAtom / kEquality: the predicate/equality term, already interned.
+ TermId atom_ = kInvalidId;
+
+ /// For kForall / kExists: the bound variable, as an interned SymbolId.
+ /// Using SymbolId (not string) so the clausifier can map bound variables
+ /// back to term-bank variables without string lookups.
+ SymbolId bound_var_ = kInvalidId;
+
+ /// Sub-formulas (1 for Not/Quantifiers, 2 for binary connectives).
+ std::vector<std::unique_ptr<Formula>> children_;
+
+ // ── Factory methods ──
+
+ static std::unique_ptr<Formula> makeAtom(TermId atom);
+ static std::unique_ptr<Formula> makeEquality(TermId lhs, TermId rhs, TermId eq_atom);
+ static std::unique_ptr<Formula> makeNot(std::unique_ptr<Formula> child);
+ static std::unique_ptr<Formula> makeAnd(std::unique_ptr<Formula> lhs,
+ std::unique_ptr<Formula> rhs);
+ static std::unique_ptr<Formula> makeOr(std::unique_ptr<Formula> lhs,
+ std::unique_ptr<Formula> rhs);
+ static std::unique_ptr<Formula> makeImplies(std::unique_ptr<Formula> lhs,
+ std::unique_ptr<Formula> rhs);
+ static std::unique_ptr<Formula> makeIff(std::unique_ptr<Formula> lhs,
+ std::unique_ptr<Formula> rhs);
+ static std::unique_ptr<Formula> makeForall(SymbolId var, std::unique_ptr<Formula> body);
+ static std::unique_ptr<Formula> makeExists(SymbolId var, std::unique_ptr<Formula> body);
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/proof/proof_trace.h b/proofsearch/include/atp/proof/proof_trace.h
new file mode 100644
index 0000000..4ea7aa1
--- /dev/null
+++ b/proofsearch/include/atp/proof/proof_trace.h
@@ -0,0 +1,55 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file proof_trace.h
+/// @brief Proof reconstruction from the empty clause back to axioms.
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/term_bank.h"
+
+#include <string>
+#include <vector>
+
+namespace atp {
+
+/// A single step in a reconstructed proof.
+struct ProofStep {
+ ClauseId clause_id_;
+ InferenceRule rule_;
+ ClauseId parent1_;
+ ClauseId parent2_;
+};
+
+/// Reconstruct the proof trace from the empty clause.
+/// Returns steps in topological order: axioms first, empty clause last.
+std::vector<ProofStep> extractProof(const ClauseStore& store, ClauseId empty_clause_id);
+
+/// Convert a TermId back to a human-readable string.
+std::string termToString(TermId id, const TermBank& bank);
+
+/// Convert a Clause to a human-readable string.
+std::string clauseToString(const Clause& clause, const TermBank& bank);
+
+/// Format a proof as a human-readable string.
+std::string formatProof(const std::vector<ProofStep>& proof, const ClauseStore& store,
+ const TermBank& bank);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/rewrite/ordering.h b/proofsearch/include/atp/rewrite/ordering.h
new file mode 100644
index 0000000..195c3c6
--- /dev/null
+++ b/proofsearch/include/atp/rewrite/ordering.h
@@ -0,0 +1,52 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file ordering.h
+/// @brief Term orderings for equality reasoning (KBO, LPO).
+///
+/// Term orderings are the foundation of equality-based theorem proving.
+/// They ensure that rewriting always terminates by enforcing a well-founded
+/// ordering on terms. Required by paramodulation, demodulation, and
+/// ordered resolution.
+
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+namespace atp {
+
+/// Ordering comparison result.
+enum class OrderResult : uint8_t {
+ kLessThan, ///< t1 < t2
+ kEqual, ///< t1 = t2
+ kGreaterThan, ///< t1 > t2
+ kIncomparable, ///< t1 and t2 are incomparable
+};
+
+/// Abstract base for term orderings.
+/// Concrete implementations: KBO (Knuth-Bendix), LPO (Lexicographic Path).
+class TermOrdering {
+ public:
+ virtual ~TermOrdering() = default;
+
+ /// Compare two terms under the ordering.
+ [[nodiscard]] virtual OrderResult compare(const TermBank& bank, TermId t1, TermId t2) const = 0;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/rewrite/paramodulation.h b/proofsearch/include/atp/rewrite/paramodulation.h
new file mode 100644
index 0000000..9dfbd66
--- /dev/null
+++ b/proofsearch/include/atp/rewrite/paramodulation.h
@@ -0,0 +1,47 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file paramodulation.h
+/// @brief Paramodulation inference rule for equality reasoning.
+///
+/// Paramodulation replaces a subterm in one clause with an equal term
+/// from an equation in another clause. Combined with term orderings,
+/// this gives a complete calculus for first-order logic with equality
+/// (the Superposition calculus).
+
+#include "atp/core/clause.h"
+#include "atp/core/term_bank.h"
+#include "atp/rewrite/ordering.h"
+
+#include <vector>
+
+namespace atp {
+
+/// Generate all paramodulants between two clauses.
+/// Uses the term ordering to restrict to ordered paramodulation (superposition).
+std::vector<Clause> paramodulate(const TermBank& bank, const Clause& c1, const Clause& c2,
+ const TermOrdering& ordering);
+
+/// Simplify a clause by demodulation (rewriting with unit equalities).
+/// Returns true if the clause was modified.
+bool demodulate(TermBank& bank, Clause& clause, const Clause& unit_eq,
+ const TermOrdering& ordering);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/search/clause_queue.h b/proofsearch/include/atp/search/clause_queue.h
new file mode 100644
index 0000000..dfb575c
--- /dev/null
+++ b/proofsearch/include/atp/search/clause_queue.h
@@ -0,0 +1,43 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file clause_queue.h
+/// @brief Priority queue for the Given Clause Loop.
+
+#include "atp/core/clause.h"
+
+#include <functional>
+
+namespace atp {
+
+/// Comparison function type for clause ordering.
+using ClauseComparator = std::function<bool(const Clause& a, const Clause& b)>;
+
+/// BFS comparator: prioritize shallower derivation depth.
+inline bool bfsCompare(const Clause& a, const Clause& b) {
+ return a.depth_ > b.depth_; // min-heap by depth
+}
+
+/// Weight comparator: prioritize shorter (fewer literals) clauses.
+inline bool weightCompare(const Clause& a, const Clause& b) {
+ return a.literals_.size() > b.literals_.size(); // min-heap by literal count
+}
+
+} // namespace atp
diff --git a/proofsearch/include/atp/search/prover.h b/proofsearch/include/atp/search/prover.h
new file mode 100644
index 0000000..65ec7f0
--- /dev/null
+++ b/proofsearch/include/atp/search/prover.h
@@ -0,0 +1,92 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file prover.h
+/// @brief The Given Clause Loop — main saturation-based proof search.
+///
+/// V1: Uses a simple ProverConfig with hardcoded resolution + factoring.
+/// V2: Will be refactored to accept a Calculus object from strategy/ module
+/// for dynamic algorithm loading per problem type.
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/term_bank.h"
+#include "atp/search/clause_queue.h"
+
+#include <optional>
+#include <queue>
+#include <vector>
+
+namespace atp {
+
+/// Result of a proof attempt.
+enum class ProverResult : uint8_t {
+ kTheorem, ///< Empty clause derived — conjecture is a theorem
+ kSaturation, ///< Search space saturated — cannot prove
+ kTimeout, ///< Resource limit exceeded
+};
+
+/// V1 configuration — simple and direct.
+/// Future: replaced by strategy::Calculus for dynamic rule loading.
+struct ProverConfig {
+ ClauseComparator comparator_ = bfsCompare;
+ size_t max_clauses_ = 100000; ///< Safety limit
+ size_t max_iterations_ = 500000; ///< Safety limit
+ bool enable_occurs_check_ = false;
+};
+
+/// Main prover class implementing the Given Clause Loop.
+/// V1: hardcodes resolution + factoring + subsumption.
+/// V2: will accept a Calculus object for pluggable inference rules.
+class Prover {
+ public:
+ Prover(TermBank& bank, ClauseStore& store, ProverConfig config = {});
+
+ /// Add initial clauses (axioms + negated conjecture).
+ void addClauses(std::vector<Clause> clauses);
+
+ /// Run the proof search.
+ ProverResult prove();
+
+ /// After a successful proof, retrieve the empty clause ID.
+ [[nodiscard]] std::optional<ClauseId> getEmptyClauseId() const;
+
+ /// Access the underlying clause store.
+ [[nodiscard]] const ClauseStore& clauseStore() const { return store_; }
+
+ private:
+ [[nodiscard]] bool hitResourceLimit(size_t iterations) const;
+ [[nodiscard]] Clause popGivenClause();
+ [[nodiscard]] bool isForwardSubsumedByProcessed(const Clause& clause) const;
+ [[nodiscard]] bool shouldSkipGiven(const Clause& given) const;
+ void applyBackwardSubsumption(const Clause& given);
+ [[nodiscard]] std::vector<Clause> generateNewClauses(const Clause& given) const;
+ [[nodiscard]] uint16_t computeDepth(const Clause& clause) const;
+ [[nodiscard]] std::optional<ClauseId> processNewClauses(std::vector<Clause> new_clauses);
+
+ TermBank& bank_;
+ ClauseStore& store_;
+ ProverConfig config_;
+ std::priority_queue<Clause, std::vector<Clause>, ClauseComparator> unprocessed_;
+ std::vector<ClauseId> processed_;
+ std::optional<ClauseId> empty_clause_id_;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/simplify/subsumption.h b/proofsearch/include/atp/simplify/subsumption.h
new file mode 100644
index 0000000..970b6d8
--- /dev/null
+++ b/proofsearch/include/atp/simplify/subsumption.h
@@ -0,0 +1,56 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file subsumption.h
+/// @brief Forward and backward subsumption checks (backed by discrimination tree).
+
+#include "atp/core/clause.h"
+#include "atp/index/discrim_tree.h"
+
+#include <vector>
+
+namespace atp {
+
+/// Subsumption engine: checks if a new clause is subsumed by existing ones
+/// (forward) or subsumes existing ones (backward).
+class SubsumptionEngine {
+ public:
+ explicit SubsumptionEngine(const TermBank& bank);
+
+ /// Returns true if `candidate` is subsumed by any existing clause.
+ [[nodiscard]] bool isForwardSubsumed(const Clause& candidate) const;
+
+ /// Returns IDs of existing clauses that are subsumed by `new_clause`.
+ [[nodiscard]] std::vector<ClauseId> backwardSubsumed(const Clause& new_clause) const;
+
+ /// Add a clause to the subsumption index.
+ void addClause(const Clause& clause);
+
+ /// Remove a clause from the subsumption index.
+ void removeClause(ClauseId id);
+
+ private:
+ DiscrimTree index_;
+ // TODO: Phase 5 implementation
+};
+
+bool subsumes(const Clause& general, const Clause& specific);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/simplify/tautology.h b/proofsearch/include/atp/simplify/tautology.h
new file mode 100644
index 0000000..f17f0c7
--- /dev/null
+++ b/proofsearch/include/atp/simplify/tautology.h
@@ -0,0 +1,31 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file tautology.h
+/// @brief Tautology detection for clauses.
+
+#include "atp/core/clause.h"
+
+namespace atp {
+
+/// Returns true if the clause is a tautology (contains complementary literals).
+[[nodiscard]] bool isTautology(const Clause& clause);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/strategy/calculus.h b/proofsearch/include/atp/strategy/calculus.h
new file mode 100644
index 0000000..e06988e
--- /dev/null
+++ b/proofsearch/include/atp/strategy/calculus.h
@@ -0,0 +1,67 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file calculus.h
+/// @brief A Calculus bundles the exact set of inference rules + heuristics
+/// to use for a specific problem class.
+///
+/// Think of it as the "algorithm profile" that the Prover loads.
+/// Different problems get different calculi:
+/// - Pure propositional: resolution + factoring only
+/// - FOL without equality: + full unification
+/// - FOL with equality: + paramodulation + KBO ordering
+/// - Arithmetic: + theory solver integration
+///
+/// This is the single point where search space control happens.
+
+#include "atp/rewrite/ordering.h"
+#include "atp/search/clause_queue.h"
+#include "atp/strategy/inference_rule.h"
+
+#include <memory>
+#include <string_view>
+#include <vector>
+
+namespace atp {
+
+/// A Calculus = a complete specification of how to search for a proof.
+/// The Prover is parameterized by this — it never hardcodes which rules to use.
+struct Calculus {
+ /// Name for logging (e.g., "superposition", "pure_resolution").
+ std::string name;
+
+ /// Generating rules (executed in order during the main loop).
+ std::vector<std::unique_ptr<GeneratingRule>> generating_rules;
+
+ /// Simplifying rules (applied to each new clause before enqueuing).
+ std::vector<std::unique_ptr<SimplifyingRule>> simplifying_rules;
+
+ /// Clause selection heuristic.
+ ClauseComparator comparator = bfsCompare;
+
+ /// Term ordering (nullptr if equality reasoning is disabled).
+ std::unique_ptr<TermOrdering> ordering = nullptr;
+
+ /// Resource limits.
+ size_t max_clauses = 100000;
+ size_t max_iterations = 500000;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/strategy/inference_rule.h b/proofsearch/include/atp/strategy/inference_rule.h
new file mode 100644
index 0000000..88b6fe0
--- /dev/null
+++ b/proofsearch/include/atp/strategy/inference_rule.h
@@ -0,0 +1,80 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file inference_rule.h
+/// @brief Abstract interface for pluggable inference rules.
+///
+/// Each inference rule (resolution, factoring, paramodulation, etc.)
+/// is a self-contained object that the Prover can selectively load.
+/// This prevents search space explosion by only enabling rules relevant
+/// to the problem at hand.
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/term_bank.h"
+
+#include <string_view>
+#include <vector>
+
+namespace atp {
+
+/// Abstract base class for all inference rules.
+/// The Prover invokes only the rules present in its active calculus.
+class InferenceRuleBase {
+ public:
+ virtual ~InferenceRuleBase() = default;
+
+ /// Human-readable name for logging and proof output.
+ [[nodiscard]] virtual std::string_view name() const = 0;
+
+ /// Is this a generating rule (produces new clauses) or simplifying rule
+ /// (rewrites/deletes existing clauses)?
+ enum class Kind : uint8_t { kGenerating, kSimplifying };
+ [[nodiscard]] virtual Kind kind() const = 0;
+};
+
+/// Generating inference rule: given a "given clause" and the set of processed
+/// clauses, produce new clauses.
+class GeneratingRule : public InferenceRuleBase {
+ public:
+ [[nodiscard]] Kind kind() const final { return Kind::kGenerating; }
+
+ /// Generate new clauses from the given clause against all processed clauses.
+ /// @param given The selected clause from the Unprocessed queue.
+ /// @param processed All previously processed clauses (accessed via store).
+ /// @param bank The global term bank.
+ /// @return Newly generated clauses (not yet added to the store).
+ [[nodiscard]] virtual std::vector<Clause> generate(const Clause& given,
+ const std::vector<ClauseId>& processed,
+ const ClauseStore& store,
+ TermBank& bank) const = 0;
+};
+
+/// Simplifying inference rule: rewrite or delete a clause in-place.
+class SimplifyingRule : public InferenceRuleBase {
+ public:
+ [[nodiscard]] Kind kind() const final { return Kind::kSimplifying; }
+
+ /// Attempt to simplify a clause. Returns true if the clause was modified.
+ /// If the clause should be deleted entirely, clear its literals.
+ virtual bool simplify(Clause& clause, const ClauseStore& store, TermBank& bank) const = 0;
+};
+
+} // namespace atp
diff --git a/proofsearch/include/atp/strategy/problem_analyzer.h b/proofsearch/include/atp/strategy/problem_analyzer.h
new file mode 100644
index 0000000..5e6c5cf
--- /dev/null
+++ b/proofsearch/include/atp/strategy/problem_analyzer.h
@@ -0,0 +1,61 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file problem_analyzer.h
+/// @brief Analyzes input clauses and selects the optimal Calculus.
+///
+/// Before the Prover starts its main loop, the ProblemAnalyzer inspects
+/// the input clause set to determine problem characteristics:
+/// - Contains equality? → enable paramodulation
+/// - Pure Horn clauses? → use SLD resolution
+/// - Contains arithmetic literals? → enable theory solver
+/// - Problem size / clause density → tune resource limits
+///
+/// This is the "auto mode" that mature ATPs use to avoid loading
+/// unnecessary inference rules that would explode the search space.
+
+#include "atp/core/clause.h"
+#include "atp/strategy/calculus.h"
+
+#include <vector>
+
+namespace atp {
+
+/// Features detected in the input problem.
+struct ProblemFeatures {
+ bool has_equality = false; ///< Contains '=' predicate
+ bool is_purely_propositional = false; ///< No function symbols, no variables
+ bool is_horn = false; ///< All clauses are Horn clauses
+ bool has_existential = false; ///< Had existential quantifiers (Skolemized)
+ size_t num_clauses = 0;
+ size_t num_predicates = 0;
+ size_t num_functions = 0;
+ size_t max_clause_size = 0;
+ size_t max_term_depth = 0;
+};
+
+/// Analyze the input clauses and extract problem features.
+ProblemFeatures analyzeFeatures(const std::vector<Clause>& clauses, const TermBank& bank);
+
+/// Select the optimal calculus based on detected features.
+/// This is the core strategy dispatch logic.
+std::unique_ptr<Calculus> selectCalculus(const ProblemFeatures& features);
+
+} // namespace atp
diff --git a/proofsearch/include/atp/utils/hash_utils.h b/proofsearch/include/atp/utils/hash_utils.h
new file mode 100644
index 0000000..ae31f43
--- /dev/null
+++ b/proofsearch/include/atp/utils/hash_utils.h
@@ -0,0 +1,46 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file hash_utils.h
+/// @brief Hash combination utilities.
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+
+namespace atp {
+
+/// Combine a hash with a new value (boost::hash_combine style).
+inline void hashCombine(size_t& seed, size_t value) {
+ seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+}
+
+/// Hash a span of values.
+template <typename T>
+size_t hashRange(const T* data, size_t count) {
+ size_t seed = 0;
+ std::hash<T> hasher;
+ for (size_t i = 0; i < count; ++i) {
+ hashCombine(seed, hasher(data[i]));
+ }
+ return seed;
+}
+
+} // namespace atp
diff --git a/proofsearch/include/atp/utils/logger.h b/proofsearch/include/atp/utils/logger.h
new file mode 100644
index 0000000..de77c54
--- /dev/null
+++ b/proofsearch/include/atp/utils/logger.h
@@ -0,0 +1,41 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/// @file logger.h
+/// @brief Minimal high-performance logging macros.
+
+#include <cstdio>
+
+#ifndef ATP_LOG_LEVEL
+#define ATP_LOG_LEVEL 2 // 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR
+#endif
+
+#define ATP_LOG(level, level_str, fmt, ...) \
+ do { \
+ if constexpr (level >= ATP_LOG_LEVEL) { \
+ std::fprintf(stderr, "[%s] " fmt "\n", level_str, ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+#define ATP_TRACE(fmt, ...) ATP_LOG(0, "TRACE", fmt, ##__VA_ARGS__)
+#define ATP_DEBUG(fmt, ...) ATP_LOG(1, "DEBUG", fmt, ##__VA_ARGS__)
+#define ATP_INFO(fmt, ...) ATP_LOG(2, "INFO", fmt, ##__VA_ARGS__)
+#define ATP_WARN(fmt, ...) ATP_LOG(3, "WARN", fmt, ##__VA_ARGS__)
+#define ATP_ERROR(fmt, ...) ATP_LOG(4, "ERROR", fmt, ##__VA_ARGS__)
diff --git a/proofsearch/src/core/clause_store.cc b/proofsearch/src/core/clause_store.cc
new file mode 100644
index 0000000..f8ee73d
--- /dev/null
+++ b/proofsearch/src/core/clause_store.cc
@@ -0,0 +1,59 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause_store.h"
+
+#include "atp/core/clause.h"
+#include "atp/core/types.h"
+
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <utility>
+
+namespace atp {
+
+ClauseId ClauseStore::addClause(Clause clause) {
+ auto new_id = static_cast<ClauseId>(clauses_.size());
+ clause.id_ = new_id;
+ clauses_.push_back(std::move(clause));
+ return new_id;
+}
+
+const Clause& ClauseStore::getClause(ClauseId id) const {
+ assert(id < clauses_.size() && "ClauseId out of bounds!");
+ return clauses_[id];
+}
+
+Clause& ClauseStore::getMutableClause(ClauseId id) {
+ assert(id < clauses_.size() && "ClauseId out of bounds!");
+ return clauses_[id];
+}
+
+size_t ClauseStore::size() const {
+ return clauses_.size();
+}
+
+void ClauseStore::forEach(const std::function<void(const Clause&)>& visitor) const {
+ for (const auto& clause : clauses_) {
+ // TODO: Tombstone mechanism
+ visitor(clause);
+ }
+}
+
+} // namespace atp
diff --git a/proofsearch/src/core/symbol_table.cc b/proofsearch/src/core/symbol_table.cc
new file mode 100644
index 0000000..b707132
--- /dev/null
+++ b/proofsearch/src/core/symbol_table.cc
@@ -0,0 +1,65 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/symbol_table.h"
+
+#include "atp/core/types.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+
+namespace atp {
+
+SymbolId SymbolTable::intern(std::string_view name, SymbolKind kind, uint16_t arity, SortId sort) {
+ auto it = name_to_id_.find(name);
+ if (it != name_to_id_.end()) {
+ SymbolId const id = it->second;
+ SymbolInfo const& info = infos_[id];
+ if (info.kind_ != kind || info.arity_ != arity) {
+ throw std::runtime_error("Symbol kind or arity mismatch for: " + std::string(name));
+ }
+ return id;
+ }
+ auto new_id = static_cast<SymbolId>(infos_.size());
+ infos_.push_back(SymbolInfo{
+ .name_ = std::string(name), .kind_ = kind, .arity_ = arity, .sort_ = sort});
+ std::string_view const stable_name_view = infos_.back().name_;
+ name_to_id_.emplace(stable_name_view, new_id);
+ return new_id;
+}
+
+std::string_view SymbolTable::getName(SymbolId id) const {
+ return infos_[id].name_;
+}
+
+const SymbolInfo& SymbolTable::getInfo(SymbolId id) const {
+ return infos_[id];
+}
+
+bool SymbolTable::isVariable(SymbolId id) const {
+ return infos_[id].kind_ == SymbolKind::kVariable;
+}
+
+size_t SymbolTable::size() const {
+ return infos_.size();
+}
+
+} // namespace atp
diff --git a/proofsearch/src/core/term_bank.cc b/proofsearch/src/core/term_bank.cc
new file mode 100644
index 0000000..0dc166f
--- /dev/null
+++ b/proofsearch/src/core/term_bank.cc
@@ -0,0 +1,63 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/term_bank.h"
+
+#include "atp/core/term.h"
+#include "atp/core/types.h"
+
+#include <cassert>
+#include <cstddef>
+#include <span>
+#include <utility>
+#include <vector>
+
+namespace atp {
+
+TermId TermBank::makeTerm(SymbolId symbol, std::span<const TermId> args) {
+ TermKey key{.symbol_ = symbol, .args_ = {args.begin(), args.end()}};
+ auto it = term_to_id_.find(key);
+ if (it != term_to_id_.end()) {
+ return it->second;
+ }
+ auto new_id = static_cast<TermId>(terms_.size());
+ Term new_term{.symbol_id_ = symbol, .args_ = key.args_};
+ terms_.push_back(std::move(new_term));
+ term_to_id_.emplace(std::move(key), new_id);
+ return new_id;
+}
+
+TermId TermBank::makeVar(SymbolId var_symbol) {
+ return makeTerm(var_symbol, {});
+}
+
+const Term& TermBank::getTerm(TermId id) const {
+ assert(id < terms_.size() && "TermId out of bounds!");
+ return terms_[id];
+}
+
+bool TermBank::isVariable(TermId id) const {
+ assert(id < terms_.size() && "TermId out of bounds!");
+ return symbols_->isVariable(terms_[id].symbol_id_);
+}
+
+size_t TermBank::size() const {
+ return terms_.size();
+}
+
+} // namespace atp
diff --git a/proofsearch/src/frontend/tptp_parser.cc b/proofsearch/src/frontend/tptp_parser.cc
new file mode 100644
index 0000000..10190b8
--- /dev/null
+++ b/proofsearch/src/frontend/tptp_parser.cc
@@ -0,0 +1,325 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/frontend/tptp_parser.h"
+
+#include "atp/core/clause.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <ios>
+#include <iterator>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace atp {
+
+FormulaRole parseRole(std::string_view role_str) {
+ if (role_str == "axiom") {
+ return FormulaRole::kAxiom;
+ }
+ if (role_str == "hypothesis") {
+ return FormulaRole::kHypothesis;
+ }
+ if (role_str == "definition") {
+ return FormulaRole::kDefinition;
+ }
+ if (role_str == "assumption") {
+ return FormulaRole::kAssumption;
+ }
+ if (role_str == "lemma") {
+ return FormulaRole::kLemma;
+ }
+ if (role_str == "theorem") {
+ return FormulaRole::kTheorem;
+ }
+ if (role_str == "conjecture") {
+ return FormulaRole::kConjecture;
+ }
+ if (role_str == "negated_conjecture") {
+ return FormulaRole::kNegatedConjecture;
+ }
+ if (role_str == "plain") {
+ return FormulaRole::kPlain;
+ }
+ return FormulaRole::kPlain;
+}
+
+enum class TokenType : std::uint8_t { kWord, kLParen, kRParen, kOr, kNot, kDot, kComma, kEOF, kUnkown };
+
+struct Token {
+ TokenType type_{};
+ std::string_view value_;
+ size_t line_{};
+ size_t column_{};
+};
+
+class TptpLexer {
+ public:
+ explicit TptpLexer(std::string_view input) : source_(input) {}
+
+ Token nextToken() {
+ skipWhitespaceAndComments();
+
+ if (pos_ >= source_.length()) {
+ return {.type_ = TokenType::kEOF, .value_ = "", .line_ = line_, .column_ = column_};
+ }
+
+ size_t const start_pos = pos_;
+ [[maybe_unused]] size_t const start_col = column_;
+ char const c = source_[pos_];
+
+ if ((std::isalpha(static_cast<unsigned char>(c)) != 0) || c == '_') {
+ while (
+ pos_ < source_.size() &&
+ ((std::isalnum(static_cast<unsigned char>(source_[pos_])) != 0) || source_[pos_] == '_')) {
+ advanceChar();
+ }
+ return {.type_ = TokenType::kWord,
+ .value_ = source_.substr(start_pos, pos_ - start_pos),
+ .line_ = line_,
+ .column_ = column_};
+ }
+
+ advanceChar();
+ switch (c) {
+ case '(':
+ return {.type_ = TokenType::kLParen, .value_ = "(", .line_ = line_, .column_ = column_};
+ case ')':
+ return {.type_ = TokenType::kRParen, .value_ = ")", .line_ = line_, .column_ = column_};
+ case ',':
+ return {.type_ = TokenType::kComma, .value_ = ",", .line_ = line_, .column_ = column_};
+ case '.':
+ return {.type_ = TokenType::kDot, .value_ = ".", .line_ = line_, .column_ = column_};
+ case '~':
+ return {.type_ = TokenType::kNot, .value_ = "~", .line_ = line_, .column_ = column_};
+ case '|':
+ return {.type_ = TokenType::kOr, .value_ = "|", .line_ = line_, .column_ = column_};
+ default:
+ return {.type_ = TokenType::kUnkown,
+ .value_ = source_.substr(start_pos, 1),
+ .line_ = line_,
+ .column_ = column_};
+ }
+ }
+
+ private:
+ std::string_view source_;
+ size_t pos_ = 0;
+ size_t line_ = 1;
+ size_t column_ = 1;
+
+ void advanceChar() {
+ if (pos_ < source_.size()) {
+ if (source_[pos_] == '\n') {
+ line_++;
+ column_ = 1;
+ } else {
+ column_++;
+ }
+ pos_++;
+ }
+ }
+
+ void skipWhitespaceAndComments() {
+ while (pos_ < source_.size()) {
+ char const c = source_[pos_];
+ if (std::isspace(static_cast<unsigned char>(c)) != 0) {
+ advanceChar();
+ } else if (c == '%') {
+ while (pos_ < source_.size() && source_[pos_] != '\n') {
+ advanceChar();
+ }
+ } else {
+ break;
+ }
+ }
+ }
+};
+
+class TptpParser {
+ public:
+ explicit TptpParser(std::string_view input, TermBank& bank, SymbolTable& symbols)
+ : lexer_(input), bank_(bank), symbols_(symbols) {
+ advance();
+ }
+
+ void parseProblem(ParsedProblem& problem) {
+ while (current_token_.type_ != TokenType::kEOF) {
+ if (current_token_.type_ == TokenType::kWord && current_token_.value_ == "cnf") {
+ advance();
+ expect(TokenType::kLParen);
+
+ AnnotatedFormula af;
+ af.is_cnf_ = true;
+
+ af.name_ = std::string(current_token_.value_);
+ expect(TokenType::kWord);
+ expect(TokenType::kComma);
+
+ af.role_ = parseRole(current_token_.value_);
+ expect(TokenType::kWord);
+ expect(TokenType::kComma);
+
+ Clause clause;
+ clause.literals_.push_back(parseLiteral());
+ while (current_token_.type_ == TokenType::kOr) {
+ advance();
+ clause.literals_.push_back(parseLiteral());
+ }
+ af.clauses_.push_back(std::move(clause));
+
+ expect(TokenType::kRParen);
+ expect(TokenType::kDot);
+ problem.formulas_.push_back(std::move(af));
+
+ variable_cache_.clear();
+ } else if (current_token_.value_ == "fof") {
+ throw std::runtime_error("FOF parsing not implemented. Line " +
+ std::to_string(current_token_.line_));
+ } else {
+ throw std::runtime_error("Unknown declaration: '" +
+ std::string(current_token_.value_) + "'");
+ }
+ }
+ }
+
+ private:
+ TptpLexer lexer_;
+ TermBank& bank_;
+ SymbolTable& symbols_;
+ Token current_token_{};
+ std::vector<Token> tokens_;
+ std::unordered_map<std::string_view, SymbolId> variable_cache_;
+
+ void advance() { current_token_ = lexer_.nextToken(); }
+
+ void expect(TokenType type) {
+ if (current_token_.type_ != type) {
+ throw std::runtime_error("Parse error at line " + std::to_string(current_token_.line_) +
+ ", col " + std::to_string(current_token_.column_) +
+ ": unexpected token '" + std::string(current_token_.value_) +
+ "'");
+ }
+ advance();
+ }
+
+ TermId parseTerm() {
+ if (current_token_.type_ != TokenType::kWord) {
+ throw std::runtime_error("Expected variable or function name at line " +
+ std::to_string(current_token_.line_) + ", col " +
+ std::to_string(current_token_.column_) +
+ ": unexpected token '" + std::string(current_token_.value_) +
+ "'");
+ }
+
+ std::string_view const name = current_token_.value_;
+ bool const is_variable = std::isupper(static_cast<unsigned char>(name[0])) != 0;
+ advance();
+
+ if (is_variable) {
+ // Check cache first — same variable name in one clause → same TermId
+ auto it = variable_cache_.find(name);
+ if (it != variable_cache_.end()) {
+ return it->second;
+ }
+ SymbolId const sym = symbols_.intern(name, SymbolKind::kVariable);
+ TermId const var_id = bank_.makeVar(sym);
+ variable_cache_[name] = var_id;
+ return var_id;
+ }
+
+ // Check if this is a function/predicate (has args) or a constant (no parens)
+ if (current_token_.type_ == TokenType::kLParen) {
+ advance(); // consume '('
+ std::vector<TermId> args;
+ if (current_token_.type_ != TokenType::kRParen) {
+ args.push_back(parseTerm());
+ while (current_token_.type_ == TokenType::kComma) {
+ advance();
+ args.push_back(parseTerm());
+ }
+ }
+ expect(TokenType::kRParen);
+ const SymbolId sym = symbols_.intern(name, SymbolKind::kFunction,
+ static_cast<uint16_t>(args.size()));
+ return bank_.makeTerm(sym, args);
+ }
+
+ // Constant: no parentheses (e.g., a, b, h0, s0)
+ const SymbolId sym = symbols_.intern(name, SymbolKind::kConstant, 0);
+ return bank_.makeTerm(sym, {});
+ }
+
+ Literal parseLiteral() {
+ bool is_positive = true;
+ if (current_token_.type_ == TokenType::kNot) {
+ is_positive = false;
+ advance();
+ }
+ TermId const term = parseTerm();
+ return Literal{.atom_ = term, .is_positive_ = is_positive};
+ }
+};
+
+ParsedProblem parseTptpString(const std::string& input, TermBank& bank, SymbolTable& symbols) {
+ ParsedProblem problem;
+ TptpParser parser(input, bank, symbols);
+ parser.parseProblem(problem);
+ return problem;
+}
+
+ParsedProblem parseTptpFile(const std::string& filename, TermBank& bank, SymbolTable& symbols) {
+ std::ifstream file(filename, std::ios::binary);
+ if (!file.is_open()) {
+ throw std::runtime_error("Failed to open file: " + filename);
+ }
+ std::string const content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+ ParsedProblem problem = parseTptpString(content, bank, symbols);
+ problem.source_file_ = filename;
+ return problem;
+}
+
+std::vector<Clause> prepareForProving(ParsedProblem& problem, TermBank& /*bank*/,
+ SymbolTable& /*symbols*/) {
+ std::vector<Clause> final_clauses;
+
+ for (auto& af : problem.formulas_) {
+ if (af.is_cnf_) {
+ for (auto& clause : af.clauses_) {
+ clause.rule_ = InferenceRule::kInput;
+ clause.parent1_ = kInvalidId;
+ clause.parent2_ = kInvalidId;
+ final_clauses.push_back(std::move(clause));
+ }
+ }
+ }
+ return final_clauses;
+}
+
+} // namespace atp
diff --git a/proofsearch/src/index/discrim_tree.cc b/proofsearch/src/index/discrim_tree.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/src/index/discrim_tree.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/src/infer/resolution.cc b/proofsearch/src/infer/resolution.cc
new file mode 100644
index 0000000..5fb0d4a
--- /dev/null
+++ b/proofsearch/src/infer/resolution.cc
@@ -0,0 +1,176 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/infer/resolution.h"
+
+#include "atp/core/clause.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/infer/substitution.h"
+#include "atp/infer/unification.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace atp {
+
+namespace {
+
+/// Recursively collect all variable TermIds appearing in a term.
+void collectVariables(TermId term, const TermBank& bank, std::unordered_map<TermId, bool>& seen) {
+ if (bank.isVariable(term)) {
+ seen[term] = true;
+ return;
+ }
+ // Copy args to avoid dangling ref (bank is const here, but be safe)
+ const Term& t = bank.getTerm(term);
+ for (TermId const arg : t.args_) {
+ collectVariables(arg, bank, seen);
+ }
+}
+
+} // namespace
+
+Clause renameVariables(const Clause& c, TermBank& bank, SymbolTable& symbols,
+ uint32_t& next_var_id) {
+ // 1. Collect all variables in the clause
+ std::unordered_map<TermId, bool> var_set;
+ for (const auto& lit : c.literals_) {
+ collectVariables(lit.atom_, bank, var_set);
+ }
+
+ if (var_set.empty()) {
+ return c; // Ground clause — nothing to rename
+ }
+
+ // 2. Create fresh variables and build a substitution
+ Substitution rename_subst;
+ for (auto& [old_var_id, _] : var_set) {
+ std::string const fresh_name = "_R" + std::to_string(next_var_id++);
+ SymbolId const fresh_sym = symbols.intern(fresh_name, SymbolKind::kVariable);
+ TermId const fresh_var = bank.makeVar(fresh_sym);
+ rename_subst.bind(old_var_id, fresh_var);
+ }
+
+ // 3. Apply the renaming substitution to all literals
+ Clause renamed;
+ renamed.id_ = c.id_;
+ renamed.rule_ = c.rule_;
+ renamed.parent1_ = c.parent1_;
+ renamed.parent2_ = c.parent2_;
+ renamed.depth_ = c.depth_;
+ for (const auto& lit : c.literals_) {
+ TermId const new_atom = applySubstitution(rename_subst, lit.atom_, bank);
+ renamed.literals_.push_back({.atom_ = new_atom, .is_positive_ = lit.is_positive_});
+ }
+ return renamed;
+}
+
+std::optional<Clause> resolve(TermBank& bank, const Clause& c1, size_t lit_idx1, const Clause& c2,
+ size_t lit_idx2, const UnificationConfig& uconfig) {
+ // Must have opposite polarity
+ if (c1.literals_[lit_idx1].is_positive_ == c2.literals_[lit_idx2].is_positive_) {
+ return std::nullopt;
+ }
+
+ // Rename c2's variables to avoid clashes with c1
+ // Use a static counter — safe for single-threaded prover
+ static uint32_t var_counter = 0;
+ auto& symbols = bank.mutableSymbols();
+ Clause c2r = renameVariables(c2, bank, symbols, var_counter);
+
+ // Unify the complementary atoms
+ Substitution subst;
+ if (!unify(bank, c1.literals_[lit_idx1].atom_, c2r.literals_[lit_idx2].atom_, subst, uconfig)) {
+ return std::nullopt;
+ }
+
+ // Build the resolvent: all literals except the resolved pair, with subst applied
+ Clause resolvent;
+ resolvent.rule_ = InferenceRule::kResolution;
+ resolvent.parent1_ = c1.id_;
+ resolvent.parent2_ = c2.id_;
+
+ for (size_t i = 0; i < c1.literals_.size(); ++i) {
+ if (i != lit_idx1) {
+ TermId const new_atom = applySubstitution(subst, c1.literals_[i].atom_, bank);
+ resolvent.literals_.push_back(
+ {.atom_ = new_atom, .is_positive_ = c1.literals_[i].is_positive_});
+ }
+ }
+ for (size_t i = 0; i < c2r.literals_.size(); ++i) {
+ if (i != lit_idx2) {
+ TermId const new_atom = applySubstitution(subst, c2r.literals_[i].atom_, bank);
+ resolvent.literals_.push_back(
+ {.atom_ = new_atom, .is_positive_ = c2r.literals_[i].is_positive_});
+ }
+ }
+ return resolvent;
+}
+
+std::vector<Clause> allResolvents(TermBank& bank, const Clause& c1, const Clause& c2,
+ const UnificationConfig& uconfig) {
+ std::vector<Clause> results;
+ for (size_t i = 0; i < c1.literals_.size(); ++i) {
+ for (size_t j = 0; j < c2.literals_.size(); ++j) {
+ if (c1.literals_[i].is_positive_ != c2.literals_[j].is_positive_) {
+ if (auto r = resolve(bank, c1, i, c2, j, uconfig)) {
+ results.push_back(std::move(*r));
+ }
+ }
+ }
+ }
+ return results;
+}
+
+std::vector<Clause> factor(TermBank& bank, const Clause& c, const UnificationConfig& uconfig) {
+ std::vector<Clause> results;
+ for (size_t i = 0; i < c.literals_.size(); ++i) {
+ for (size_t j = i + 1; j < c.literals_.size(); ++j) {
+ if (c.literals_[i].is_positive_ != c.literals_[j].is_positive_) {
+ continue; // Must have same polarity
+ }
+ Substitution subst;
+ if (!unify(bank, c.literals_[i].atom_, c.literals_[j].atom_, subst, uconfig)) {
+ continue;
+ }
+ // Build factored clause: drop literal j, apply subst to all
+ Clause factored;
+ factored.rule_ = InferenceRule::kFactoring;
+ factored.parent1_ = c.id_;
+ for (size_t k = 0; k < c.literals_.size(); ++k) {
+ if (k != j) {
+ TermId const new_atom = applySubstitution(subst, c.literals_[k].atom_, bank);
+ factored.literals_.push_back(
+ {.atom_ = new_atom, .is_positive_ = c.literals_[k].is_positive_});
+ }
+ }
+ results.push_back(std::move(factored));
+ }
+ }
+ return results;
+}
+
+} // namespace atp
diff --git a/proofsearch/src/infer/substitution.cc b/proofsearch/src/infer/substitution.cc
new file mode 100644
index 0000000..584008b
--- /dev/null
+++ b/proofsearch/src/infer/substitution.cc
@@ -0,0 +1,119 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/infer/substitution.h"
+
+#include "atp/core/term.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <algorithm>
+#include <vector>
+
+namespace atp {
+
+bool Substitution::bind(TermId var, TermId term) {
+ auto [it, inserted] = bindings_.emplace(var, term);
+ if (!inserted) {
+ // Already bound — succeed only if bound to the same term
+ return it->second == term;
+ }
+ return true;
+}
+
+TermId Substitution::lookup(TermId var) const {
+ auto it = bindings_.find(var);
+ if (it != bindings_.end()) {
+ return it->second;
+ }
+ return kInvalidId;
+}
+
+TermId Substitution::resolve(TermId id, const TermBank& bank) const {
+ // Walk the chain: X → Y → Z → f(a)
+ // Stop when we hit a non-variable or an unbound variable.
+ while (bank.isVariable(id)) {
+ auto it = bindings_.find(id);
+ if (it == bindings_.end()) {
+ break; // Unbound variable — stop here
+ }
+ id = it->second;
+ }
+ return id;
+}
+
+TermId Substitution::apply(TermId term, TermBank& bank) const {
+ // First, resolve through variable chain
+ term = resolve(term, bank);
+
+ if (bank.isVariable(term)) {
+ // Free variable (unbound) — leave as-is
+ return term;
+ }
+
+ // IMPORTANT: Copy symbol_id and args BEFORE recursing.
+ // Recursive apply() calls may trigger bank.makeTerm() → realloc → dangling ref.
+ const Term& t = bank.getTerm(term);
+ SymbolId const sym = t.symbol_id_;
+ std::vector<TermId> const old_args(t.args_); // copy!
+
+ if (old_args.empty()) {
+ // Constant or 0-arity — no substitution needed
+ return term;
+ }
+
+ // Recursively apply to all arguments
+ std::vector<TermId> new_args;
+ new_args.reserve(old_args.size());
+ bool changed = false;
+ for (TermId const arg : old_args) {
+ TermId const new_arg = apply(arg, bank);
+ new_args.push_back(new_arg);
+ if (new_arg != arg) {
+ changed = true;
+ }
+ }
+
+ if (!changed) {
+ return term;
+ }
+
+ return bank.makeTerm(sym, new_args);
+}
+
+bool Substitution::occursIn(TermId var, TermId term, const TermBank& bank) const {
+ term = resolve(term, bank);
+
+ if (var == term) {
+ return true;
+ }
+
+ if (bank.isVariable(term)) {
+ return false; // Different unbound variable
+ }
+
+ const Term& t = bank.getTerm(term);
+ return std::any_of(t.args_.begin(), t.args_.end(),
+ [&](TermId arg) { return occursIn(var, arg, bank); });
+}
+
+void Substitution::clear() {
+ bindings_.clear();
+}
+
+} // namespace atp \ No newline at end of file
diff --git a/proofsearch/src/infer/unification.cc b/proofsearch/src/infer/unification.cc
new file mode 100644
index 0000000..c42bf67
--- /dev/null
+++ b/proofsearch/src/infer/unification.cc
@@ -0,0 +1,92 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/infer/unification.h"
+
+#include "atp/core/term.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/infer/substitution.h"
+
+#include <cstddef>
+#include <vector>
+
+namespace atp {
+
+bool unify(const TermBank& bank, TermId t1, TermId t2, Substitution& subst,
+ const UnificationConfig& config) {
+ t1 = subst.resolve(t1, bank);
+ t2 = subst.resolve(t2, bank);
+
+ if (t1 == t2) {
+ return true;
+ }
+
+ if (bank.isVariable(t1)) {
+ if (config.enable_occurs_check_ && subst.occursIn(t1, t2, bank)) {
+ return false;
+ }
+ subst.bind(t1, t2);
+ return true;
+ }
+
+ if (bank.isVariable(t2)) {
+ if (config.enable_occurs_check_ && subst.occursIn(t2, t1, bank)) {
+ return false;
+ }
+ subst.bind(t2, t1);
+ return true;
+ }
+
+ const Term& term1 = bank.getTerm(t1);
+ const Term& term2 = bank.getTerm(t2);
+
+ if (term1.symbol_id_ != term2.symbol_id_ || term1.arity() != term2.arity()) {
+ return false;
+ }
+
+ for (size_t i = 0; i < term1.arity(); ++i) {
+ if (!unify(bank, term1.args_[i], term2.args_[i], subst, config)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+TermId applySubstitution(const Substitution& subst, TermId term, TermBank& bank) {
+ term = subst.resolve(term, bank);
+ if (bank.isVariable(term)) {
+ return term;
+ }
+
+ // IMPORTANT: Copy args and symbol_id BEFORE recursing.
+ // Recursive calls may call bank.makeTerm() → realloc terms_ → invalidate refs.
+ const Term& t = bank.getTerm(term);
+ SymbolId const sym = t.symbol_id_;
+ std::vector<TermId> const old_args(t.args_); // copy!
+
+ std::vector<TermId> new_args;
+ new_args.reserve(old_args.size());
+ for (TermId const arg : old_args) {
+ new_args.push_back(applySubstitution(subst, arg, bank));
+ }
+ return bank.makeTerm(sym, new_args);
+}
+
+} // namespace atp \ No newline at end of file
diff --git a/proofsearch/src/main.cc b/proofsearch/src/main.cc
new file mode 100644
index 0000000..c683d39
--- /dev/null
+++ b/proofsearch/src/main.cc
@@ -0,0 +1,99 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/// @file main.cc
+/// @brief ATP Engine command-line entry point.
+
+#include "atp/core/clause_store.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/frontend/tptp_parser.h"
+#include "atp/proof/proof_trace.h"
+#include "atp/search/prover.h"
+
+#include <cstdlib>
+#include <exception>
+#include <iostream>
+#include <string>
+#include <utility>
+
+int main(int argc, char* argv[]) {
+ std::cerr << "richard-atp-engine Copyright (C) 2026 Richard Qin\n"
+ << "This program comes with ABSOLUTELY NO WARRANTY.\n"
+ << "This is free software, and you are welcome to redistribute it\n"
+ << "under certain conditions; see LICENSE for details.\n\n";
+
+ if (argc < 2) {
+ std::cerr << "Usage: atp <file.p>\n";
+ return EXIT_FAILURE;
+ }
+
+ const std::string filepath = argv[1];
+
+ try {
+ // 1. Initialize core modules
+ atp::SymbolTable symbols;
+ atp::TermBank bank(symbols);
+ atp::ClauseStore store;
+
+ // 2. Parse TPTP input
+ auto problem = atp::parseTptpFile(filepath, bank, symbols);
+
+ std::cerr << "% Parsed " << problem.formulas_.size() << " formulas from " << filepath
+ << "\n";
+
+ // 3. Prepare clauses for proving
+ auto clauses = atp::prepareForProving(problem, bank, symbols);
+
+ std::cerr << "% " << clauses.size() << " clauses prepared for proving\n";
+
+ // 4. Run the prover
+ atp::Prover prover(bank, store);
+ prover.addClauses(std::move(clauses));
+ atp::ProverResult const result = prover.prove();
+
+ // 5. Output result (SZS format)
+ switch (result) {
+ case atp::ProverResult::kTheorem: {
+ std::cout << "% SZS status Theorem for " << filepath << "\n\n";
+
+ // Extract and print proof
+ auto eid = prover.getEmptyClauseId();
+ if (eid.has_value()) {
+ auto proof = atp::extractProof(store, *eid);
+ std::cout << "% SZS output start Proof for " << filepath << "\n";
+ std::cout << atp::formatProof(proof, store, bank);
+ std::cout << "% SZS output end Proof for " << filepath << "\n";
+ }
+ break;
+ }
+ case atp::ProverResult::kSaturation:
+ std::cout << "% SZS status Satisfiable for " << filepath << "\n";
+ break;
+ case atp::ProverResult::kTimeout:
+ std::cout << "% SZS status Timeout for " << filepath << "\n";
+ break;
+ }
+
+ } catch (const std::exception& e) {
+ std::cerr << "% Error: " << e.what() << "\n";
+ return EXIT_FAILURE;
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/proofsearch/src/normalize/clausifier.cc b/proofsearch/src/normalize/clausifier.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/src/normalize/clausifier.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/src/proof/proof_trace.cc b/proofsearch/src/proof/proof_trace.cc
new file mode 100644
index 0000000..5fa4b04
--- /dev/null
+++ b/proofsearch/src/proof/proof_trace.cc
@@ -0,0 +1,249 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/proof/proof_trace.h"
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <stack>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace atp {
+
+// extractProof
+// DFS from empty clause back to axioms, collect all participating clauses,
+// then reverse for topological order (axioms first).
+
+std::vector<ProofStep> extractProof(const ClauseStore& store, ClauseId empty_clause_id) {
+ std::vector<ProofStep> steps;
+ std::unordered_set<ClauseId> visited;
+ std::stack<ClauseId> worklist;
+ worklist.push(empty_clause_id);
+
+ while (!worklist.empty()) {
+ const ClauseId cid = worklist.top();
+ worklist.pop();
+
+ if (cid == kInvalidId) {
+ continue;
+ }
+ if (visited.contains(cid)) {
+ continue;
+ }
+ visited.insert(cid);
+
+ const Clause& c = store.getClause(cid);
+ steps.push_back({
+ .clause_id_ = cid,
+ .rule_ = c.rule_,
+ .parent1_ = c.parent1_,
+ .parent2_ = c.parent2_,
+ });
+
+ // Recurse into parents (axioms have kInvalidId parents, which are
+ // filtered by the check at the top of the loop)
+ worklist.push(c.parent1_);
+ worklist.push(c.parent2_);
+ }
+
+ // Reverse: axioms (leaves) first → empty clause (root) last
+ std::ranges::reverse(steps);
+ return steps;
+}
+
+// termToString
+
+std::string termToString(TermId id, const TermBank& bank) {
+ const Term& t = bank.getTerm(id);
+ std::string name(bank.symbols().getName(t.symbol_id_));
+
+ if (t.args_.empty()) {
+ return name;
+ }
+
+ std::string result = name + "(";
+ for (size_t i = 0; i < t.args_.size(); ++i) {
+ if (i > 0) {
+ result += ",";
+ }
+ result += termToString(t.args_[i], bank);
+ }
+ result += ")";
+ return result;
+}
+
+// Pretty variable names
+
+namespace {
+
+/// Generate a human-readable variable name from an index.
+/// 0→X, 1→Y, 2→Z, 3→W, 4→V, 5→U, 6→X1, 7→Y1, ...
+std::string prettyVarName(size_t index) {
+ constexpr size_t kBaseCount = 6;
+ const size_t base_index = index % kBaseCount;
+ const char base = [&]() {
+ switch (base_index) {
+ case 0:
+ return 'X';
+ case 1:
+ return 'Y';
+ case 2:
+ return 'Z';
+ case 3:
+ return 'W';
+ case 4:
+ return 'V';
+ case 5:
+ return 'U';
+ default:
+ return 'X';
+ }
+ }();
+
+ const size_t suffix = index / kBaseCount;
+ if (suffix == 0) {
+ return std::string{base};
+ }
+ return std::string{base} + std::to_string(suffix);
+}
+
+/// Pretty-print a term, renaming variables to readable names.
+/// `var_map` is built incrementally: each new variable gets the next name.
+std::string termToPretty(TermId id, const TermBank& bank,
+ std::unordered_map<TermId, std::string>& var_map) {
+ if (bank.isVariable(id)) {
+ auto it = var_map.find(id);
+ if (it != var_map.end()) {
+ return it->second;
+ }
+ const std::string name = prettyVarName(var_map.size());
+ var_map[id] = name;
+ return name;
+ }
+
+ const Term& t = bank.getTerm(id);
+ std::string name(bank.symbols().getName(t.symbol_id_));
+
+ if (t.args_.empty()) {
+ return name;
+ }
+
+ std::string result = name + "(";
+ for (size_t i = 0; i < t.args_.size(); ++i) {
+ if (i > 0) {
+ result += ",";
+ }
+ result += termToPretty(t.args_[i], bank, var_map);
+ }
+ result += ")";
+ return result;
+}
+
+// ruleToString
+
+std::string ruleToString(InferenceRule rule) {
+ switch (rule) {
+ case InferenceRule::kInput:
+ return "input";
+ case InferenceRule::kResolution:
+ return "resolution";
+ case InferenceRule::kFactoring:
+ return "factoring";
+ default:
+ return "unknown";
+ }
+}
+
+} // namespace
+
+// clauseToString
+
+std::string clauseToString(const Clause& clause, const TermBank& bank) {
+ if (clause.isEmpty()) {
+ return "□";
+ }
+
+ // Per-clause variable renaming: _R109 → X, _R286 → Y, etc.
+ std::unordered_map<TermId, std::string> var_map;
+
+ std::string result;
+ for (size_t i = 0; i < clause.literals_.size(); ++i) {
+ if (i > 0) {
+ result += " ∨ ";
+ }
+ if (!clause.literals_[i].is_positive_) {
+ result += "¬";
+ }
+ result += termToPretty(clause.literals_[i].atom_, bank, var_map);
+ }
+ return result;
+}
+
+// formatProof
+
+std::string formatProof(const std::vector<ProofStep>& proof, const ClauseStore& store,
+ const TermBank& bank) {
+ // Build a local renumbering: internal ClauseId → sequential proof step number
+ std::unordered_map<ClauseId, size_t> id_to_step;
+ for (size_t i = 0; i < proof.size(); ++i) {
+ id_to_step[proof[i].clause_id_] = i + 1; // 1-based
+ }
+
+ auto local_name = [&](ClauseId cid) -> std::string {
+ auto it = id_to_step.find(cid);
+ if (it != id_to_step.end()) {
+ return std::to_string(it->second);
+ }
+ // Fallback: shouldn't happen in a valid proof
+ return "?" + std::to_string(cid);
+ };
+
+ std::string result;
+ for (size_t i = 0; i < proof.size(); ++i) {
+ const auto& step = proof[i];
+ const Clause& c = store.getClause(step.clause_id_);
+
+ result += std::to_string(i + 1) + ". ";
+ result += "[" + ruleToString(step.rule_) + "] ";
+ result += clauseToString(c, bank);
+
+ // Show parents for derived clauses
+ if (step.parent1_ != kInvalidId) {
+ result += " [" + local_name(step.parent1_);
+ if (step.parent2_ != kInvalidId) {
+ result += ", " + local_name(step.parent2_);
+ }
+ result += "]";
+ }
+
+ result += "\n";
+ }
+ return result;
+}
+
+} // namespace atp
diff --git a/proofsearch/src/rewrite/ordering.cc b/proofsearch/src/rewrite/ordering.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/src/rewrite/ordering.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/src/rewrite/paramodulation.cc b/proofsearch/src/rewrite/paramodulation.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/src/rewrite/paramodulation.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/src/search/prover.cc b/proofsearch/src/search/prover.cc
new file mode 100644
index 0000000..f3f8bb0
--- /dev/null
+++ b/proofsearch/src/search/prover.cc
@@ -0,0 +1,167 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/search/prover.h"
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/infer/resolution.h"
+#include "atp/infer/unification.h"
+#include "atp/simplify/subsumption.h"
+#include "atp/simplify/tautology.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <utility>
+#include <vector>
+
+namespace atp {
+
+Prover::Prover(TermBank& bank, ClauseStore& store, ProverConfig config)
+ : bank_(bank), store_(store), config_(std::move(config)), unprocessed_(config_.comparator_) {}
+
+void Prover::addClauses(std::vector<Clause> clauses) {
+ for (auto& clause : clauses) {
+ clause.depth_ = 0;
+ ClauseId const id = store_.addClause(clause);
+ unprocessed_.push(store_.getClause(id));
+ }
+}
+
+bool Prover::hitResourceLimit(size_t iterations) const {
+ return iterations >= config_.max_iterations_ || store_.size() >= config_.max_clauses_;
+}
+
+Clause Prover::popGivenClause() {
+ Clause given = unprocessed_.top();
+ unprocessed_.pop();
+ return given;
+}
+
+bool Prover::isForwardSubsumedByProcessed(const Clause& clause) const {
+ return std::ranges::any_of(processed_,
+ [&](ClauseId pid) { return subsumes(store_.getClause(pid), clause); });
+}
+
+bool Prover::shouldSkipGiven(const Clause& given) const {
+ return isTautology(given) || isForwardSubsumedByProcessed(given);
+}
+
+void Prover::applyBackwardSubsumption(const Clause& given) {
+ std::erase_if(processed_, [&](ClauseId pid) { return subsumes(given, store_.getClause(pid)); });
+}
+
+std::vector<Clause> Prover::generateNewClauses(const Clause& given) const {
+ std::vector<Clause> new_clauses;
+ const UnificationConfig uconfig{.enable_occurs_check_ = config_.enable_occurs_check_};
+
+ for (ClauseId const pid : processed_) {
+ const Clause& pc = store_.getClause(pid);
+
+ auto resolvents1 = allResolvents(bank_, given, pc, uconfig);
+ auto resolvents2 = allResolvents(bank_, pc, given, uconfig);
+
+ for (auto& r : resolvents1) {
+ new_clauses.push_back(std::move(r));
+ }
+ for (auto& r : resolvents2) {
+ new_clauses.push_back(std::move(r));
+ }
+ }
+
+ auto factors = factor(bank_, given, uconfig);
+ for (auto& f : factors) {
+ new_clauses.push_back(std::move(f));
+ }
+
+ return new_clauses;
+}
+
+uint16_t Prover::computeDepth(const Clause& clause) const {
+ uint32_t const d1 = (clause.parent1_ != kInvalidId) ? store_.getClause(clause.parent1_).depth_ : 0;
+ uint32_t const d2 = (clause.parent2_ != kInvalidId) ? store_.getClause(clause.parent2_).depth_ : 0;
+ return static_cast<uint16_t>(std::max(d1, d2) + 1);
+}
+
+std::optional<ClauseId> Prover::processNewClauses(std::vector<Clause> new_clauses) {
+ for (auto& nc : new_clauses) {
+ if (nc.isEmpty()) {
+ return store_.addClause(std::move(nc));
+ }
+
+ if (isTautology(nc)) {
+ continue;
+ }
+
+ nc.depth_ = computeDepth(nc);
+
+ ClauseId const nid = store_.addClause(nc);
+ const Clause& stored_nc = store_.getClause(nid);
+ if (isForwardSubsumedByProcessed(stored_nc)) {
+ continue;
+ }
+
+ unprocessed_.push(stored_nc);
+ }
+
+ return std::nullopt;
+}
+
+ProverResult Prover::prove() {
+ size_t iterations = 0;
+
+ while (!unprocessed_.empty()) {
+ if (hitResourceLimit(iterations)) {
+ return ProverResult::kTimeout;
+ }
+ ++iterations;
+
+ Clause given = popGivenClause();
+
+ if (given.isEmpty()) {
+ empty_clause_id_ = given.id_;
+ return ProverResult::kTheorem;
+ }
+
+ if (shouldSkipGiven(given)) {
+ continue;
+ }
+
+ applyBackwardSubsumption(given);
+ std::vector<Clause> new_clauses = generateNewClauses(given);
+ processed_.push_back(given.id_);
+
+ const std::optional<ClauseId> derived_empty = processNewClauses(std::move(new_clauses));
+ if (derived_empty.has_value()) {
+ empty_clause_id_ = derived_empty;
+ return ProverResult::kTheorem;
+ }
+ }
+
+ return ProverResult::kSaturation;
+}
+
+std::optional<ClauseId> Prover::getEmptyClauseId() const {
+ return empty_clause_id_;
+}
+
+} // namespace atp \ No newline at end of file
diff --git a/proofsearch/src/simplify/subsumption.cc b/proofsearch/src/simplify/subsumption.cc
new file mode 100644
index 0000000..0038ad1
--- /dev/null
+++ b/proofsearch/src/simplify/subsumption.cc
@@ -0,0 +1,43 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/simplify/subsumption.h"
+
+#include "atp/core/clause.h"
+
+namespace atp {
+
+bool subsumes(const Clause& general, const Clause& specific) {
+ if (general.size() > specific.size()) {
+ return false;
+ }
+ for (const auto& lit_g : general.literals_) {
+ bool found = false;
+ for (const auto& lit_s : specific.literals_) {
+ if (lit_g.atom_ == lit_s.atom_ && lit_g.is_positive_ == lit_s.is_positive_) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+ return true;
+}
+} // namespace atp
diff --git a/proofsearch/src/simplify/tautology.cc b/proofsearch/src/simplify/tautology.cc
new file mode 100644
index 0000000..e35d874
--- /dev/null
+++ b/proofsearch/src/simplify/tautology.cc
@@ -0,0 +1,37 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/simplify/tautology.h"
+
+#include "atp/core/clause.h"
+
+#include <cstddef>
+
+namespace atp {
+bool isTautology(const Clause& clause) {
+ for (size_t i = 0; i < clause.size(); ++i) {
+ for (size_t j = i + 1; j < clause.size(); ++j) {
+ if (clause.literals_[i].atom_ == clause.literals_[j].atom_ &&
+ clause.literals_[i].is_positive_ != clause.literals_[j].is_positive_) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+} // namespace atp \ No newline at end of file
diff --git a/proofsearch/src/strategy/problem_analyzer.cc b/proofsearch/src/strategy/problem_analyzer.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/src/strategy/problem_analyzer.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/tests/CMakeLists.txt b/proofsearch/tests/CMakeLists.txt
new file mode 100644
index 0000000..184357b
--- /dev/null
+++ b/proofsearch/tests/CMakeLists.txt
@@ -0,0 +1,31 @@
+# ─── GoogleTest ───────────────────────────────────────────────────────────────
+include(FetchContent)
+FetchContent_Declare(
+ googletest
+ GIT_REPOSITORY https://github.com/google/googletest.git
+ GIT_TAG v1.14.0
+)
+# Prevent GoogleTest from overriding our compiler/linker options
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+FetchContent_MakeAvailable(googletest)
+
+include(GoogleTest)
+
+# ─── Helper: define a test target ────────────────────────────────────────────
+function(atp_add_test TEST_NAME TEST_SOURCE)
+ add_executable(${TEST_NAME} ${TEST_SOURCE})
+ target_link_libraries(${TEST_NAME} PRIVATE atp_core GTest::gtest_main)
+ target_compile_options(${TEST_NAME} PRIVATE ${ATP_PROJECT_WARNING_FLAGS})
+ gtest_discover_tests(${TEST_NAME})
+endfunction()
+
+# ─── Test Targets ────────────────────────────────────────────────────────────
+atp_add_test(test_term_bank test_term_bank.cc)
+atp_add_test(test_unification test_unification.cc)
+atp_add_test(test_clausifier test_clausifier.cc)
+atp_add_test(test_resolution test_resolution.cc)
+atp_add_test(test_discrim_tree test_discrim_tree.cc)
+atp_add_test(test_prover test_prover.cc)
+atp_add_test(test_simplify test_simplify.cc)
+atp_add_test(test_proof_trace test_proof_trace.cc)
+atp_add_test(test_end_to_end test_end_to_end.cc)
diff --git a/proofsearch/tests/test_clausifier.cc b/proofsearch/tests/test_clausifier.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/tests/test_clausifier.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/tests/test_discrim_tree.cc b/proofsearch/tests/test_discrim_tree.cc
new file mode 100644
index 0000000..17c63f8
--- /dev/null
+++ b/proofsearch/tests/test_discrim_tree.cc
@@ -0,0 +1,17 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
diff --git a/proofsearch/tests/test_end_to_end.cc b/proofsearch/tests/test_end_to_end.cc
new file mode 100644
index 0000000..ceb9991
--- /dev/null
+++ b/proofsearch/tests/test_end_to_end.cc
@@ -0,0 +1,184 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/// @file test_end_to_end.cc
+/// @brief End-to-end tests: TPTP string → parse → prove → verify result.
+
+#include "atp/core/clause_store.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/frontend/tptp_parser.h"
+#include "atp/proof/proof_trace.h"
+#include "atp/search/prover.h"
+
+#include <gtest/gtest.h>
+#include <string>
+#include <utility>
+
+namespace atp {
+namespace {
+
+/// Helper: parse a TPTP string and run the prover, return the result.
+struct E2EResult {
+ ProverResult result_;
+ std::string proof_output_;
+};
+
+E2EResult runProver(const std::string& tptp_input) {
+ SymbolTable symbols;
+ TermBank bank(symbols);
+ ClauseStore store;
+
+ auto problem = parseTptpString(tptp_input, bank, symbols);
+ auto clauses = prepareForProving(problem, bank, symbols);
+
+ Prover prover(bank, store);
+ prover.addClauses(std::move(clauses));
+ ProverResult const result = prover.prove();
+
+ std::string proof_output;
+ if (result == ProverResult::kTheorem) {
+ auto eid = prover.getEmptyClauseId();
+ if (eid.has_value()) {
+ auto proof = extractProof(store, *eid);
+ proof_output = formatProof(proof, store, bank);
+ }
+ }
+
+ return {result, proof_output};
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Simple Propositional
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, SimplePropositional) {
+ auto [result, proof] = runProver(R"(
+ cnf(fact, axiom, p(a)).
+ cnf(rule, axiom, ~p(X) | q(X)).
+ cnf(goal, negated_conjecture, ~q(a)).
+ )");
+
+ EXPECT_EQ(result, ProverResult::kTheorem);
+ EXPECT_FALSE(proof.empty());
+ // Proof should contain the key elements
+ EXPECT_NE(proof.find("p(a)"), std::string::npos) << proof;
+ EXPECT_NE(proof.find("q(a)"), std::string::npos) << proof;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Trivial: direct contradiction
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, DirectContradiction) {
+ auto [result, proof] = runProver(R"(
+ cnf(pos, axiom, p(a)).
+ cnf(neg, negated_conjecture, ~p(a)).
+ )");
+
+ EXPECT_EQ(result, ProverResult::kTheorem);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Satisfiable: no contradiction possible
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, Satisfiable) {
+ auto [result, proof] = runProver(R"(
+ cnf(a1, axiom, p(a)).
+ cnf(a2, axiom, q(b)).
+ )");
+
+ EXPECT_EQ(result, ProverResult::kSaturation);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Three-step chain
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, ThreeStepChain) {
+ auto [result, proof] = runProver(R"(
+ cnf(a1, axiom, p(a)).
+ cnf(a2, axiom, ~p(X) | q(X)).
+ cnf(a3, axiom, ~q(X) | r(X)).
+ cnf(goal, negated_conjecture, ~r(a)).
+ )");
+
+ EXPECT_EQ(result, ProverResult::kTheorem);
+ EXPECT_NE(proof.find("r(a)"), std::string::npos) << proof;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Proof output format
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, ProofOutputContainsAllSteps) {
+ auto [result, proof] = runProver(R"(
+ cnf(fact, axiom, p(a)).
+ cnf(rule, axiom, ~p(X) | q(X)).
+ cnf(goal, negated_conjecture, ~q(a)).
+ )");
+
+ ASSERT_EQ(result, ProverResult::kTheorem);
+
+ // Must contain [input] lines for axioms
+ EXPECT_NE(proof.find("[input]"), std::string::npos) << proof;
+ // Must contain [resolution] for derived clauses
+ EXPECT_NE(proof.find("[resolution]"), std::string::npos) << proof;
+ // Must contain empty clause marker
+ std::string const empty_marker = "□";
+ EXPECT_NE(proof.find(empty_marker), std::string::npos) << proof;
+ // Must contain [X, Y] provenance (sequential numbers)
+ EXPECT_NE(proof.find('['), std::string::npos) << proof;
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Monkey and Banana Problem (the flagship test!)
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(EndToEnd, MonkeyBanana) {
+ auto [result, proof] = runProver(R"(
+ % Initial state
+ cnf(init_state, axiom, state(a, b, c, h0, s0)).
+
+ % Action: Go(X)
+ cnf(action_go, axiom, ~state(M, B, G, h0, S) | state(X, B, G, h0, do(go(X), S))).
+
+ % Action: Push(X)
+ cnf(action_push, axiom, ~state(M, M, G, h0, S) | state(X, X, G, h0, do(push(X), S))).
+
+ % Action: Climb
+ cnf(action_climb, axiom, ~state(M, M, G, h0, S) | state(M, M, G, h1, do(climb, S))).
+
+ % Action: Down
+ cnf(action_down, axiom, ~state(M, B, G, h1, S) | state(M, B, G, h0, do(down, S))).
+
+ % Action: Reach
+ cnf(action_reach, axiom, ~state(G, G, G, h1, S) | goal_reached(do(reach, S))).
+
+ % Negated goal
+ cnf(negated_goal, negated_conjecture, ~goal_reached(S)).
+ )");
+
+ EXPECT_EQ(result, ProverResult::kTheorem) << "ATP should prove the monkey can get the banana!\n"
+ << "Proof output:\n"
+ << proof;
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_proof_trace.cc b/proofsearch/tests/test_proof_trace.cc
new file mode 100644
index 0000000..f2c3c96
--- /dev/null
+++ b/proofsearch/tests/test_proof_trace.cc
@@ -0,0 +1,207 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/proof/proof_trace.h"
+#include "atp/search/prover.h"
+
+#include <gtest/gtest.h>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace atp {
+namespace {
+
+class ProofTraceTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+ ClauseStore store_;
+
+ SymbolId sym_a_{}, sym_b_{};
+ SymbolId sym_p_{}, sym_q_{};
+ SymbolId sym_x_{};
+
+ TermId a_term_{}, b_term_{}, x_term_{};
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_p_ = symbols_.intern("P", SymbolKind::kPredicate, 1);
+ sym_q_ = symbols_.intern("Q", SymbolKind::kPredicate, 1);
+ sym_x_ = symbols_.intern("X", SymbolKind::kVariable);
+
+ a_term_ = bank_.makeTerm(sym_a_, {});
+ b_term_ = bank_.makeTerm(sym_b_, {});
+ x_term_ = bank_.makeVar(sym_x_);
+ }
+
+ TermId p(TermId arg) { return bank_.makeTerm(sym_p_, {{arg}}); }
+ TermId q(TermId arg) { return bank_.makeTerm(sym_q_, {{arg}}); }
+
+ static Literal pos(TermId atom) { return {.atom_ = atom, .is_positive_ = true}; }
+ static Literal neg(TermId atom) { return {.atom_ = atom, .is_positive_ = false}; }
+
+ static Clause makeClause(std::vector<Literal> lits) {
+ Clause cl;
+ cl.literals_ = std::move(lits);
+ return cl;
+ }
+};
+
+// ═══════════════════════════════════════════════════════════════════════════
+// termToString Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProofTraceTest, TermToStringConstant) {
+ EXPECT_EQ(termToString(a_term_, bank_), "a");
+}
+
+TEST_F(ProofTraceTest, TermToStringVariable) {
+ EXPECT_EQ(termToString(x_term_, bank_), "X");
+}
+
+TEST_F(ProofTraceTest, TermToStringCompound) {
+ TermId const pa = p(a_term_);
+ EXPECT_EQ(termToString(pa, bank_), "P(a)");
+}
+
+TEST_F(ProofTraceTest, TermToStringNested) {
+ SymbolId const sym_f = symbols_.intern("f", SymbolKind::kFunction, 1);
+ TermId const fa = bank_.makeTerm(sym_f, {{a_term_}});
+ TermId const pfa = p(fa);
+ EXPECT_EQ(termToString(pfa, bank_), "P(f(a))");
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// clauseToString Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProofTraceTest, ClauseToStringEmpty) {
+ Clause const empty = makeClause({});
+ EXPECT_EQ(clauseToString(empty, bank_), "□");
+}
+
+TEST_F(ProofTraceTest, ClauseToStringSinglePositive) {
+ Clause const c = makeClause({pos(p(a_term_))});
+ EXPECT_EQ(clauseToString(c, bank_), "P(a)");
+}
+
+TEST_F(ProofTraceTest, ClauseToStringSingleNegative) {
+ Clause const c = makeClause({neg(p(a_term_))});
+ EXPECT_EQ(clauseToString(c, bank_), "¬P(a)");
+}
+
+TEST_F(ProofTraceTest, ClauseToStringMultipleLiterals) {
+ Clause const c = makeClause({neg(p(x_term_)), pos(q(x_term_))});
+ std::string const s = clauseToString(c, bank_);
+ EXPECT_EQ(s, "¬P(X) ∨ Q(X)");
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// extractProof Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProofTraceTest, ExtractProofTrivial) {
+ // Run a trivial proof: {P(a)} + {¬P(a)} → □
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_term_))}), makeClause({neg(p(a_term_))})});
+ ProverResult const result = prover.prove();
+ ASSERT_EQ(result, ProverResult::kTheorem);
+
+ auto eid = prover.getEmptyClauseId();
+ ASSERT_TRUE(eid.has_value());
+
+ auto proof = extractProof(store_, *eid);
+ ASSERT_GE(proof.size(), 3U); // At least: 2 axioms + 1 empty clause
+
+ // First steps should be axioms (kInput)
+ EXPECT_EQ(proof.front().rule_, InferenceRule::kInput);
+ // Last step should be the empty clause
+ EXPECT_EQ(proof.back().clause_id_, *eid);
+ // Last step should be resolution
+ EXPECT_EQ(proof.back().rule_, InferenceRule::kResolution);
+}
+
+TEST_F(ProofTraceTest, ExtractProofTwoStep) {
+ // {P(a)}, {¬P(X), Q(X)}, {¬Q(a)} → □ in 2 resolution steps
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_term_))}), makeClause({neg(p(x_term_)), pos(q(x_term_))}),
+ makeClause({neg(q(a_term_))})});
+ ProverResult const result = prover.prove();
+ ASSERT_EQ(result, ProverResult::kTheorem);
+
+ auto eid = prover.getEmptyClauseId();
+ ASSERT_TRUE(eid.has_value());
+
+ auto proof = extractProof(store_, *eid);
+ // Should have axioms + intermediate + empty clause
+ ASSERT_GE(proof.size(), 4U); // 3 axioms + at least 1 intermediate + empty
+
+ // Last step is the empty clause
+ EXPECT_EQ(proof.back().clause_id_, *eid);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// formatProof Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProofTraceTest, FormatProofContainsExpectedElements) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_term_))}), makeClause({neg(p(a_term_))})});
+ prover.prove();
+
+ auto eid = prover.getEmptyClauseId();
+ ASSERT_TRUE(eid.has_value());
+
+ auto proof = extractProof(store_, *eid);
+ std::string const output = formatProof(proof, store_, bank_);
+
+ // Should contain key elements
+ EXPECT_NE(output.find("input"), std::string::npos) << output;
+ EXPECT_NE(output.find("resolution"), std::string::npos) << output;
+ EXPECT_NE(output.find("P(a)"), std::string::npos) << output;
+ // Empty clause should appear
+ std::string const empty_marker = "□";
+ EXPECT_NE(output.find(empty_marker), std::string::npos) << output;
+}
+
+TEST_F(ProofTraceTest, FormatProofNoParentsForAxioms) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_term_))}), makeClause({neg(p(a_term_))})});
+ prover.prove();
+
+ auto eid = prover.getEmptyClauseId();
+ auto proof = extractProof(store_, *eid);
+ std::string const output = formatProof(proof, store_, bank_);
+
+ // The first line (an axiom) should NOT have "[from ...]"
+ // Find the first newline
+ auto first_line = output.substr(0, output.find('\n'));
+ EXPECT_EQ(first_line.find("[from"), std::string::npos)
+ << "Axiom should not have parent info: " << first_line;
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_prover.cc b/proofsearch/tests/test_prover.cc
new file mode 100644
index 0000000..74d02ad
--- /dev/null
+++ b/proofsearch/tests/test_prover.cc
@@ -0,0 +1,183 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/search/prover.h"
+
+#include <gtest/gtest.h>
+#include <utility>
+#include <vector>
+
+namespace atp {
+namespace {
+
+class ProverTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+ ClauseStore store_;
+
+ SymbolId sym_a_{}, sym_b_{};
+ SymbolId sym_f_{};
+ SymbolId sym_p_{}, sym_q_{}, sym_r_{};
+ SymbolId sym_x_{}, sym_y_{};
+
+ TermId a_{}, b_{}, x_{}, y_{};
+
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_f_ = symbols_.intern("f", SymbolKind::kFunction, 1);
+ sym_p_ = symbols_.intern("P", SymbolKind::kPredicate, 1);
+ sym_q_ = symbols_.intern("Q", SymbolKind::kPredicate, 1);
+ sym_r_ = symbols_.intern("R", SymbolKind::kPredicate, 2);
+ sym_x_ = symbols_.intern("X", SymbolKind::kVariable);
+ sym_y_ = symbols_.intern("Y", SymbolKind::kVariable);
+
+ a_ = bank_.makeTerm(sym_a_, {});
+ b_ = bank_.makeTerm(sym_b_, {});
+ x_ = bank_.makeVar(sym_x_);
+ y_ = bank_.makeVar(sym_y_);
+ }
+
+ TermId p(TermId arg) { return bank_.makeTerm(sym_p_, {{arg}}); }
+ TermId q(TermId arg) { return bank_.makeTerm(sym_q_, {{arg}}); }
+ TermId r(TermId a1, TermId a2) { return bank_.makeTerm(sym_r_, {{a1, a2}}); }
+ TermId f(TermId arg) { return bank_.makeTerm(sym_f_, {{arg}}); }
+
+ static Literal pos(TermId atom) { return {.atom_ = atom, .is_positive_ = true}; }
+ static Literal neg(TermId atom) { return {.atom_ = atom, .is_positive_ = false}; }
+
+ static Clause makeClause(std::vector<Literal> lits) {
+ Clause cl;
+ cl.literals_ = std::move(lits);
+ return cl;
+ }
+};
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Trivial refutation: {P(a)} and {¬P(a)} → empty clause
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, TrivialRefutation) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_))}), makeClause({neg(p(a_))})});
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTheorem);
+ EXPECT_TRUE(prover.getEmptyClauseId().has_value());
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Unification required: {P(X)} and {¬P(a)} → empty clause
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, RefutationWithUnification) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(x_))}), makeClause({neg(p(a_))})});
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTheorem);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Multi-step: {P(a)}, {¬P(X), Q(X)}, {¬Q(a)}
+// Step 1: resolve P(a) with ¬P(X)∨Q(X) → Q(a)
+// Step 2: resolve Q(a) with ¬Q(a) → empty
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, TwoStepRefutation) {
+ Prover prover(bank_, store_);
+ prover.addClauses(
+ {makeClause({pos(p(a_))}), makeClause({neg(p(x_)), pos(q(x_))}), makeClause({neg(q(a_))})});
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTheorem);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Saturation: {P(a)}, {Q(b)} — no complementary literals
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, Saturation) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({pos(p(a_))}), makeClause({pos(q(b_))})});
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kSaturation);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Timeout: set very low limits
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, Timeout) {
+ ProverConfig config;
+ config.max_iterations_ = 1;
+
+ // Need enough clauses to not saturate in 1 iteration
+ Prover prover(bank_, store_, config);
+ prover.addClauses({makeClause({pos(p(x_)), pos(q(x_))}), makeClause({neg(p(a_)), pos(q(a_))}),
+ makeClause({neg(q(x_)), pos(p(f(x_)))}), makeClause({neg(p(x_)), neg(q(x_))})});
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTimeout);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Chain reasoning: P(a), ¬P(X)∨Q(X), ¬Q(X)∨R(X), ¬R(a)
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, ThreeStepChain) {
+ const SymbolId kSymS = symbols_.intern("S", SymbolKind::kPredicate, 1);
+ auto s = [&](TermId arg) { return bank_.makeTerm(kSymS, {{arg}}); };
+
+ Prover prover(bank_, store_);
+ prover.addClauses({
+ makeClause({pos(p(a_))}), // P(a)
+ makeClause({neg(p(x_)), pos(q(x_))}), // ¬P(X) ∨ Q(X)
+ makeClause({neg(q(x_)), pos(s(x_))}), // ¬Q(X) ∨ S(X)
+ makeClause({neg(s(a_))}) // ¬S(a)
+ });
+
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTheorem);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Empty clause in input directly
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ProverTest, EmptyClauseInInput) {
+ Prover prover(bank_, store_);
+ prover.addClauses({makeClause({}), // empty clause directly
+ makeClause({pos(p(a_))})});
+
+ // Empty clause should be detected immediately when selected from queue
+ const ProverResult kResult = prover.prove();
+ EXPECT_EQ(kResult, ProverResult::kTheorem);
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_resolution.cc b/proofsearch/tests/test_resolution.cc
new file mode 100644
index 0000000..02250d9
--- /dev/null
+++ b/proofsearch/tests/test_resolution.cc
@@ -0,0 +1,257 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/infer/resolution.h"
+
+#include <gtest/gtest.h>
+#include <utility>
+#include <vector>
+
+namespace atp {
+namespace {
+
+class ResolutionTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+
+ SymbolId sym_a_{}, sym_b_{}, sym_c_{};
+ SymbolId sym_f_{};
+ SymbolId sym_p_{}, sym_q_{}, sym_r_{};
+ SymbolId sym_x_{}, sym_y_{}, sym_z_{};
+
+ TermId a_{}, b_{}, c_{};
+ TermId x_{}, y_{}, z_{};
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_c_ = symbols_.intern("c", SymbolKind::kConstant);
+ sym_f_ = symbols_.intern("f", SymbolKind::kFunction, 1);
+ sym_p_ = symbols_.intern("P", SymbolKind::kPredicate, 1);
+ sym_q_ = symbols_.intern("Q", SymbolKind::kPredicate, 1);
+ sym_r_ = symbols_.intern("R", SymbolKind::kPredicate, 2);
+ sym_x_ = symbols_.intern("X", SymbolKind::kVariable);
+ sym_y_ = symbols_.intern("Y", SymbolKind::kVariable);
+ sym_z_ = symbols_.intern("Z", SymbolKind::kVariable);
+
+ a_ = bank_.makeTerm(sym_a_, {});
+ b_ = bank_.makeTerm(sym_b_, {});
+ c_ = bank_.makeTerm(sym_c_, {});
+ x_ = bank_.makeVar(sym_x_);
+ y_ = bank_.makeVar(sym_y_);
+ z_ = bank_.makeVar(sym_z_);
+ }
+
+ TermId p(TermId arg) { return bank_.makeTerm(sym_p_, {{arg}}); }
+ TermId q(TermId arg) { return bank_.makeTerm(sym_q_, {{arg}}); }
+ TermId r(TermId a1, TermId a2) { return bank_.makeTerm(sym_r_, {{a1, a2}}); }
+ TermId f(TermId arg) { return bank_.makeTerm(sym_f_, {{arg}}); }
+
+ // Helper: make a clause from literals
+ static Clause makeClause(std::vector<Literal> lits, ClauseId id = kInvalidId) {
+ Clause c;
+ c.id_ = id;
+ c.literals_ = std::move(lits);
+ return c;
+ }
+
+ static Literal pos(TermId atom) { return {.atom_ = atom, .is_positive_ = true}; }
+ static Literal neg(TermId atom) { return {.atom_ = atom, .is_positive_ = false}; }
+};
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Basic Resolution Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ResolutionTest, SimpleGroundResolution) {
+ // {P(a)} and {¬P(a), Q(b)} → {Q(b)}
+ Clause const c1 = makeClause({pos(p(a_))}, 0);
+ Clause const c2 = makeClause({neg(p(a_)), pos(q(b_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->literals_.size(), 1);
+ EXPECT_EQ(result->literals_[0].atom_, q(b_));
+ EXPECT_TRUE(result->literals_[0].is_positive_);
+}
+
+TEST_F(ResolutionTest, ResolutionWithUnification) {
+ // {P(X)} and {¬P(a)} → {} (empty clause!)
+ Clause const c1 = makeClause({pos(p(x_))}, 0);
+ Clause const c2 = makeClause({neg(p(a_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_TRUE(result->isEmpty()) << "Should derive empty clause (contradiction)";
+}
+
+TEST_F(ResolutionTest, ResolutionFailsSamePolarity) {
+ // {P(a)} and {P(b)} → no resolution (both positive)
+ Clause const c1 = makeClause({pos(p(a_))}, 0);
+ Clause const c2 = makeClause({pos(p(b_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ EXPECT_FALSE(result.has_value());
+}
+
+TEST_F(ResolutionTest, ResolutionFailsNoUnifier) {
+ // {P(a)} and {¬P(b)} → can't unify a with b
+ Clause const c1 = makeClause({pos(p(a_))}, 0);
+ Clause const c2 = makeClause({neg(p(b_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ EXPECT_FALSE(result.has_value());
+}
+
+TEST_F(ResolutionTest, ResolutionAppliesSubstToRemainingLiterals) {
+ // {P(X), Q(X)} and {¬P(a)} → {Q(a)}
+ Clause const c1 = makeClause({pos(p(x_)), pos(q(x_))}, 0);
+ Clause const c2 = makeClause({neg(p(a_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->literals_.size(), 1);
+ EXPECT_EQ(result->literals_[0].atom_, q(a_));
+}
+
+TEST_F(ResolutionTest, ResolutionMultipleLiterals) {
+ // {P(X), Q(b)} and {¬P(a), R(a, c)} → {Q(b), R(a, c)}
+ Clause const c1 = makeClause({pos(p(x_)), pos(q(b_))}, 0);
+ Clause const c2 = makeClause({neg(p(a_)), pos(r(a_, c_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->literals_.size(), 2);
+}
+
+TEST_F(ResolutionTest, VariableRenamingWorks) {
+ // Both clauses use variable X
+ // {P(X)} and {¬P(X), Q(X)} → {Q(a)} only if X is renamed in c2
+ Clause const c1 = makeClause({pos(p(a_))}, 0);
+ Clause const c2 = makeClause({neg(p(x_)), pos(q(x_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->literals_.size(), 1);
+ // Q(X) should become Q(a) after substitution {X→a}
+ EXPECT_EQ(result->literals_[0].atom_, q(a_));
+}
+
+TEST_F(ResolutionTest, VariableRenamingPreventsClash) {
+ // {R(X, a)} and {¬R(b, X)}
+ // Without renaming: X would clash between c1 and c2
+ // With renaming: c2's X → _R0, then unify R(X,a) with R(b,_R0) → {X→b, _R0→a}
+ Clause const c1 = makeClause({pos(r(x_, a_))}, 0);
+ Clause const c2 = makeClause({neg(r(b_, x_))}, 1);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_TRUE(result->isEmpty());
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// AllResolvents Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ResolutionTest, AllResolventsFindsAll) {
+ // {P(a), Q(a)} and {¬P(a), ¬Q(a)}
+ // Can resolve on P or Q → 2 resolvents
+ Clause const c1 = makeClause({pos(p(a_)), pos(q(a_))}, 0);
+ Clause const c2 = makeClause({neg(p(a_)), neg(q(a_))}, 1);
+
+ auto results = allResolvents(bank_, c1, c2);
+ EXPECT_EQ(results.size(), 2);
+}
+
+TEST_F(ResolutionTest, AllResolventsEmpty) {
+ // {P(a)} and {Q(b)} → no complementary pairs
+ Clause const c1 = makeClause({pos(p(a_))}, 0);
+ Clause const c2 = makeClause({pos(q(b_))}, 1);
+
+ auto results = allResolvents(bank_, c1, c2);
+ EXPECT_TRUE(results.empty());
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Factoring Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ResolutionTest, SimpleFactoring) {
+ // {P(X), P(a), Q(X)} → factor P(X) with P(a): {P(a), Q(a)}
+ Clause const c = makeClause({pos(p(x_)), pos(p(a_)), pos(q(x_))}, 0);
+
+ auto results = factor(bank_, c);
+ ASSERT_EQ(results.size(), 1);
+ EXPECT_EQ(results[0].literals_.size(), 2);
+ // After factoring: {P(a), Q(a)}
+ EXPECT_EQ(results[0].literals_[0].atom_, p(a_));
+ EXPECT_EQ(results[0].literals_[1].atom_, q(a_));
+}
+
+TEST_F(ResolutionTest, NoFactoring) {
+ // {P(a), Q(b)} → different predicates, can't factor
+ Clause const c = makeClause({pos(p(a_)), pos(q(b_))}, 0);
+ auto results = factor(bank_, c);
+ EXPECT_TRUE(results.empty());
+}
+
+TEST_F(ResolutionTest, FactoringDifferentPolarity) {
+ // {P(a), ¬P(a)} → different polarity, can't factor
+ Clause const c = makeClause({pos(p(a_)), neg(p(a_))}, 0);
+ auto results = factor(bank_, c);
+ EXPECT_TRUE(results.empty());
+}
+
+TEST_F(ResolutionTest, FactoringNonUnifiable) {
+ // {P(a), P(b)} → can't unify a with b
+ Clause const c = makeClause({pos(p(a_)), pos(p(b_))}, 0);
+ auto results = factor(bank_, c);
+ EXPECT_TRUE(results.empty());
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Provenance Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(ResolutionTest, ResolventHasProvenance) {
+ Clause const c1 = makeClause({pos(p(x_))}, 5);
+ Clause const c2 = makeClause({neg(p(a_))}, 7);
+
+ auto result = resolve(bank_, c1, 0, c2, 0);
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(result->rule_, InferenceRule::kResolution);
+ EXPECT_EQ(result->parent1_, 5);
+ EXPECT_EQ(result->parent2_, 7);
+}
+
+TEST_F(ResolutionTest, FactoredHasProvenance) {
+ Clause const c = makeClause({pos(p(x_)), pos(p(a_))}, 3);
+ auto results = factor(bank_, c);
+ ASSERT_FALSE(results.empty());
+ EXPECT_EQ(results[0].rule_, InferenceRule::kFactoring);
+ EXPECT_EQ(results[0].parent1_, 3);
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_simplify.cc b/proofsearch/tests/test_simplify.cc
new file mode 100644
index 0000000..968ab3b
--- /dev/null
+++ b/proofsearch/tests/test_simplify.cc
@@ -0,0 +1,191 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/simplify/subsumption.h"
+#include "atp/simplify/tautology.h"
+
+#include <gtest/gtest.h>
+#include <utility>
+#include <vector>
+
+namespace atp {
+namespace {
+
+class SimplifyTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+
+ SymbolId sym_a_{}, sym_b_{}, sym_c_{};
+ SymbolId sym_p_{}, sym_q_{}, sym_r_{};
+ SymbolId sym_x_{};
+
+ TermId a_{}, b_{}, c_{}, x_{};
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_c_ = symbols_.intern("c", SymbolKind::kConstant);
+ sym_p_ = symbols_.intern("P", SymbolKind::kPredicate, 1);
+ sym_q_ = symbols_.intern("Q", SymbolKind::kPredicate, 1);
+ sym_r_ = symbols_.intern("R", SymbolKind::kPredicate, 2);
+ sym_x_ = symbols_.intern("X", SymbolKind::kVariable);
+
+ a_ = bank_.makeTerm(sym_a_, {});
+ b_ = bank_.makeTerm(sym_b_, {});
+ c_ = bank_.makeTerm(sym_c_, {});
+ x_ = bank_.makeVar(sym_x_);
+ }
+
+ TermId p(TermId arg) { return bank_.makeTerm(sym_p_, {{arg}}); }
+ TermId q(TermId arg) { return bank_.makeTerm(sym_q_, {{arg}}); }
+ TermId r(TermId a1, TermId a2) { return bank_.makeTerm(sym_r_, {{a1, a2}}); }
+
+ static Literal pos(TermId atom) { return {.atom_ = atom, .is_positive_ = true}; }
+ static Literal neg(TermId atom) { return {.atom_ = atom, .is_positive_ = false}; }
+
+ static Clause makeClause(std::vector<Literal> lits) {
+ Clause cl;
+ cl.literals_ = std::move(lits);
+ return cl;
+ }
+};
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Tautology Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(SimplifyTest, TautologyComplementaryLiterals) {
+ // {P(a), ¬P(a)} → tautology
+ Clause const cl = makeClause({pos(p(a_)), neg(p(a_))});
+ EXPECT_TRUE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, TautologyWithExtraLiterals) {
+ // {Q(b), P(a), ¬P(a)} → tautology
+ Clause const cl = makeClause({pos(q(b_)), pos(p(a_)), neg(p(a_))});
+ EXPECT_TRUE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologySamePolarity) {
+ // {P(a), P(a)} → NOT tautology (same polarity)
+ Clause const cl = makeClause({pos(p(a_)), pos(p(a_))});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologyDifferentAtoms) {
+ // {P(a), ¬P(b)} → NOT tautology (different atoms)
+ Clause const cl = makeClause({pos(p(a_)), neg(p(b_))});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologyDifferentPredicates) {
+ // {P(a), ¬Q(a)} → NOT tautology (different predicates)
+ Clause const cl = makeClause({pos(p(a_)), neg(q(a_))});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologyEmptyClause) {
+ Clause const cl = makeClause({});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologySingleLiteral) {
+ Clause const cl = makeClause({pos(p(a_))});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, TautologyVariableAtom) {
+ // {P(X), ¬P(X)} → tautology (same variable term)
+ Clause const cl = makeClause({pos(p(x_)), neg(p(x_))});
+ EXPECT_TRUE(isTautology(cl));
+}
+
+TEST_F(SimplifyTest, NotTautologyVarVsConst) {
+ // {P(X), ¬P(a)} → NOT tautology (syntactically different)
+ Clause const cl = makeClause({pos(p(x_)), neg(p(a_))});
+ EXPECT_FALSE(isTautology(cl));
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Subsumption Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(SimplifyTest, SubsumesSubset) {
+ // {P(a)} subsumes {P(a), Q(b)}
+ Clause const gen = makeClause({pos(p(a_))});
+ Clause const spec = makeClause({pos(p(a_)), pos(q(b_))});
+ EXPECT_TRUE(subsumes(gen, spec));
+}
+
+TEST_F(SimplifyTest, SubsumesIdentical) {
+ // {P(a), Q(b)} subsumes {P(a), Q(b)}
+ Clause const c1 = makeClause({pos(p(a_)), pos(q(b_))});
+ Clause const c2 = makeClause({pos(p(a_)), pos(q(b_))});
+ EXPECT_TRUE(subsumes(c1, c2));
+}
+
+TEST_F(SimplifyTest, SubsumesEmptyClause) {
+ // {} subsumes everything
+ Clause const empty = makeClause({});
+ Clause const cl = makeClause({pos(p(a_)), neg(q(b_))});
+ EXPECT_TRUE(subsumes(empty, cl));
+}
+
+TEST_F(SimplifyTest, SubsumesNotLarger) {
+ // {P(a), Q(b)} does NOT subsume {P(a)}
+ Clause const gen = makeClause({pos(p(a_)), pos(q(b_))});
+ Clause const spec = makeClause({pos(p(a_))});
+ EXPECT_FALSE(subsumes(gen, spec));
+}
+
+TEST_F(SimplifyTest, SubsumesRequiresSamePolarity) {
+ // {P(a)} does NOT subsume {¬P(a)}
+ Clause const gen = makeClause({pos(p(a_))});
+ Clause const spec = makeClause({neg(p(a_))});
+ EXPECT_FALSE(subsumes(gen, spec));
+}
+
+TEST_F(SimplifyTest, SubsumesRequiresSameAtom) {
+ // {P(a)} does NOT subsume {P(b)}
+ Clause const gen = makeClause({pos(p(a_))});
+ Clause const spec = makeClause({pos(p(b_))});
+ EXPECT_FALSE(subsumes(gen, spec));
+}
+
+TEST_F(SimplifyTest, SubsumesNegativeLiterals) {
+ // {¬P(a)} subsumes {¬P(a), Q(b)}
+ Clause const gen = makeClause({neg(p(a_))});
+ Clause const spec = makeClause({neg(p(a_)), pos(q(b_))});
+ EXPECT_TRUE(subsumes(gen, spec));
+}
+
+TEST_F(SimplifyTest, SubsumesOrderIndependent) {
+ // {Q(b), P(a)} subsumes {P(a), Q(b), R(a,b)} regardless of order
+ Clause const gen = makeClause({pos(q(b_)), pos(p(a_))});
+ Clause const spec = makeClause({pos(p(a_)), pos(q(b_)), pos(r(a_, b_))});
+ EXPECT_TRUE(subsumes(gen, spec));
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_term_bank.cc b/proofsearch/tests/test_term_bank.cc
new file mode 100644
index 0000000..0d4fcc7
--- /dev/null
+++ b/proofsearch/tests/test_term_bank.cc
@@ -0,0 +1,339 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/clause.h"
+#include "atp/core/clause_store.h"
+#include "atp/core/literal.h"
+#include "atp/core/symbol_table.h"
+#include "atp/core/term.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+
+#include <cstddef>
+#include <gtest/gtest.h>
+#include <span>
+#include <stdexcept>
+#include <utility>
+
+namespace atp {
+namespace {
+
+// ═══════════════════════════════════════════════════════════════════════════
+// SymbolTable Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(SymbolTableTest, InternReturnsConsistentId) {
+ SymbolTable st;
+ SymbolId const id1 = st.intern("foo", SymbolKind::kFunction, 2);
+ SymbolId const id2 = st.intern("foo", SymbolKind::kFunction, 2);
+ EXPECT_EQ(id1, id2);
+}
+
+TEST(SymbolTableTest, DifferentNamesGetDifferentIds) {
+ SymbolTable st;
+ SymbolId const a = st.intern("a", SymbolKind::kConstant);
+ SymbolId const b = st.intern("b", SymbolKind::kConstant);
+ EXPECT_NE(a, b);
+}
+
+TEST(SymbolTableTest, GetNameRoundTrips) {
+ SymbolTable st;
+ SymbolId const id = st.intern("hello", SymbolKind::kFunction, 1);
+ EXPECT_EQ(st.getName(id), "hello");
+}
+
+TEST(SymbolTableTest, GetInfoPreservesMetadata) {
+ SymbolTable st;
+ SymbolId const id = st.intern("P", SymbolKind::kPredicate, 3, 42);
+ const SymbolInfo& info = st.getInfo(id);
+ EXPECT_EQ(info.name_, "P");
+ EXPECT_EQ(info.kind_, SymbolKind::kPredicate);
+ EXPECT_EQ(info.arity_, 3);
+ EXPECT_EQ(info.sort_, 42);
+}
+
+TEST(SymbolTableTest, IsVariableDetectsKind) {
+ SymbolTable st;
+ SymbolId const var = st.intern("X", SymbolKind::kVariable);
+ SymbolId const con = st.intern("a", SymbolKind::kConstant);
+ SymbolId const fun = st.intern("f", SymbolKind::kFunction, 1);
+ EXPECT_TRUE(st.isVariable(var));
+ EXPECT_FALSE(st.isVariable(con));
+ EXPECT_FALSE(st.isVariable(fun));
+}
+
+TEST(SymbolTableTest, MismatchKindThrows) {
+ SymbolTable st;
+ st.intern("x", SymbolKind::kConstant);
+ EXPECT_THROW(st.intern("x", SymbolKind::kVariable), std::runtime_error);
+}
+
+TEST(SymbolTableTest, MismatchArityThrows) {
+ SymbolTable st;
+ st.intern("f", SymbolKind::kFunction, 1);
+ EXPECT_THROW(st.intern("f", SymbolKind::kFunction, 2), std::runtime_error);
+}
+
+TEST(SymbolTableTest, SizeTracksInternedSymbols) {
+ SymbolTable st;
+ EXPECT_EQ(st.size(), 0);
+ st.intern("a", SymbolKind::kConstant);
+ EXPECT_EQ(st.size(), 1);
+ st.intern("b", SymbolKind::kConstant);
+ EXPECT_EQ(st.size(), 2);
+ st.intern("a", SymbolKind::kConstant); // duplicate
+ EXPECT_EQ(st.size(), 2);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// TermBank Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+class TermBankTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+
+ SymbolId sym_a_{}, sym_b_{}, sym_f_{}, sym_g_{}, sym_x_id_{}, sym_y_id_{};
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_f_ = symbols_.intern("f", SymbolKind::kFunction, 1);
+ sym_g_ = symbols_.intern("g", SymbolKind::kFunction, 2);
+ sym_x_id_ = symbols_.intern("X", SymbolKind::kVariable);
+ sym_y_id_ = symbols_.intern("Y", SymbolKind::kVariable);
+ }
+};
+
+TEST_F(TermBankTest, HashConsingReturnsSameId) {
+ const TermId kA1 = bank_.makeTerm(sym_a_, {});
+ const TermId kA2 = bank_.makeTerm(sym_a_, {});
+ EXPECT_EQ(kA1, kA2) << "Hash consing failed: same term got different IDs";
+}
+
+TEST_F(TermBankTest, DifferentTermsGetDifferentIds) {
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ const TermId kB = bank_.makeTerm(sym_b_, {});
+ EXPECT_NE(kA, kB);
+}
+
+TEST_F(TermBankTest, CompoundTermHashConsing) {
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ const TermId kB = bank_.makeTerm(sym_b_, {});
+ // f(a)
+ const TermId kFA1 = bank_.makeTerm(sym_f_, {{kA}});
+ const TermId kFA2 = bank_.makeTerm(sym_f_, {{kA}});
+ EXPECT_EQ(kFA1, kFA2);
+ // f(b) != f(a)
+ const TermId kFB = bank_.makeTerm(sym_f_, {{kB}});
+ EXPECT_NE(kFA1, kFB);
+}
+
+TEST_F(TermBankTest, NestedTermHashConsing) {
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ const TermId kFA = bank_.makeTerm(sym_f_, {{kA}});
+ // g(f(a), a)
+ const TermId kGFAA1 = bank_.makeTerm(sym_g_, {{kFA, kA}});
+ const TermId kGFAA2 = bank_.makeTerm(sym_g_, {{kFA, kA}});
+ EXPECT_EQ(kGFAA1, kGFAA2);
+}
+
+TEST_F(TermBankTest, GetTermReturnsCorrectSymbol) {
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ const Term& t = bank_.getTerm(kA);
+ EXPECT_EQ(t.symbol_id_, sym_a_);
+ EXPECT_EQ(t.arity(), 0);
+}
+
+TEST_F(TermBankTest, GetTermReturnsCorrectArgs) {
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ const TermId kB = bank_.makeTerm(sym_b_, {});
+ const TermId kGAB = bank_.makeTerm(sym_g_, {{kA, kB}});
+
+ const Term& t = bank_.getTerm(kGAB);
+ EXPECT_EQ(t.symbol_id_, sym_g_);
+ EXPECT_EQ(t.arity(), 2);
+ ASSERT_EQ(t.args_.size(), 2);
+ EXPECT_EQ(t.args_[0], kA);
+ EXPECT_EQ(t.args_[1], kB);
+}
+
+TEST_F(TermBankTest, MakeVarCreatesZeroArityTerm) {
+ const TermId kX = bank_.makeVar(sym_x_id_);
+ const Term& t = bank_.getTerm(kX);
+ EXPECT_EQ(t.symbol_id_, sym_x_id_);
+ EXPECT_EQ(t.arity(), 0);
+ EXPECT_TRUE(t.args_.empty());
+}
+
+TEST_F(TermBankTest, IsVariableDistinguishesFromConstants) {
+ const TermId kX = bank_.makeVar(sym_x_id_);
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+
+ EXPECT_TRUE(bank_.isVariable(kX)) << "Variable should be detected as variable";
+ EXPECT_FALSE(bank_.isVariable(kA)) << "Constant should NOT be detected as variable";
+}
+
+TEST_F(TermBankTest, VariableHashConsing) {
+ const TermId kX1 = bank_.makeVar(sym_x_id_);
+ const TermId kX2 = bank_.makeVar(sym_x_id_);
+ EXPECT_EQ(kX1, kX2);
+
+ const TermId kY = bank_.makeVar(sym_y_id_);
+ EXPECT_NE(kX1, kY);
+}
+
+TEST_F(TermBankTest, SizeTracksUniqueTerms) {
+ EXPECT_EQ(bank_.size(), 0);
+ const TermId kA = bank_.makeTerm(sym_a_, {});
+ EXPECT_EQ(bank_.size(), 1);
+ bank_.makeTerm(sym_a_, {}); // duplicate
+ EXPECT_EQ(bank_.size(), 1);
+ bank_.makeTerm(sym_b_, {});
+ EXPECT_EQ(bank_.size(), 2);
+ bank_.makeTerm(sym_f_, {{kA}});
+ EXPECT_EQ(bank_.size(), 3);
+}
+
+TEST_F(TermBankTest, CompoundTermWithVariableArgs) {
+ // f(X) — a function applied to a variable
+ const TermId kX = bank_.makeVar(sym_x_id_);
+ const TermId kFX = bank_.makeTerm(sym_f_, {{kX}});
+
+ const Term& t = bank_.getTerm(kFX);
+ EXPECT_EQ(t.symbol_id_, sym_f_);
+ EXPECT_EQ(t.arity(), 1);
+ EXPECT_EQ(t.args_[0], kX);
+
+ EXPECT_FALSE(bank_.isVariable(kFX));
+ EXPECT_TRUE(bank_.isVariable(kX));
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// ClauseStore Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(ClauseStoreTest, AddAndRetrieve) {
+ ClauseStore store;
+ Clause c;
+ c.literals_.push_back(Literal{.atom_ = 0, .is_positive_ = true});
+ c.literals_.push_back(Literal{.atom_ = 1, .is_positive_ = false});
+
+ const ClauseId kId = store.addClause(std::move(c));
+ EXPECT_EQ(kId, 0);
+
+ const Clause& retrieved = store.getClause(kId);
+ EXPECT_EQ(retrieved.id_, kId);
+ EXPECT_EQ(retrieved.literals_.size(), 2);
+ EXPECT_EQ(retrieved.literals_[0].atom_, 0);
+ EXPECT_TRUE(retrieved.literals_[0].is_positive_);
+ EXPECT_EQ(retrieved.literals_[1].atom_, 1);
+ EXPECT_FALSE(retrieved.literals_[1].is_positive_);
+}
+
+TEST(ClauseStoreTest, AssignsSequentialIds) {
+ ClauseStore store;
+ const ClauseId kId0 = store.addClause(Clause{});
+ const ClauseId kId1 = store.addClause(Clause{});
+ const ClauseId kId2 = store.addClause(Clause{});
+ EXPECT_EQ(kId0, 0);
+ EXPECT_EQ(kId1, 1);
+ EXPECT_EQ(kId2, 2);
+ EXPECT_EQ(store.size(), 3);
+}
+
+TEST(ClauseStoreTest, StampsClauseWithId) {
+ ClauseStore store;
+ Clause c;
+ c.id_ = kInvalidId; // unset
+ const ClauseId kId = store.addClause(std::move(c));
+ EXPECT_EQ(store.getClause(kId).id_, kId);
+}
+
+TEST(ClauseStoreTest, MutableAccess) {
+ ClauseStore store;
+ Clause c;
+ c.depth_ = 0;
+ const ClauseId kId = store.addClause(std::move(c));
+
+ store.getMutableClause(kId).depth_ = 42;
+ EXPECT_EQ(store.getClause(kId).depth_, 42);
+}
+
+TEST(ClauseStoreTest, ForEachVisitsAll) {
+ ClauseStore store;
+ store.addClause(Clause{});
+ store.addClause(Clause{});
+ store.addClause(Clause{});
+
+ size_t count = 0;
+ store.forEach([&count](const Clause&) { count++; });
+ EXPECT_EQ(count, 3);
+}
+
+TEST(ClauseStoreTest, EmptyClauseDetection) {
+ Clause c;
+ EXPECT_TRUE(c.isEmpty());
+ c.literals_.push_back(Literal{.atom_ = 0, .is_positive_ = true});
+ EXPECT_FALSE(c.isEmpty());
+ EXPECT_EQ(c.size(), 1);
+}
+
+TEST(ClauseStoreTest, ProvenancePreserved) {
+ ClauseStore store;
+ Clause c;
+ c.rule_ = InferenceRule::kResolution;
+ c.parent1_ = 3;
+ c.parent2_ = 7;
+ c.depth_ = 5;
+
+ ClauseId const id = store.addClause(std::move(c));
+ const Clause& retrieved = store.getClause(id);
+ EXPECT_EQ(retrieved.rule_, InferenceRule::kResolution);
+ EXPECT_EQ(retrieved.parent1_, 3);
+ EXPECT_EQ(retrieved.parent2_, 7);
+ EXPECT_EQ(retrieved.depth_, 5);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Literal Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST(LiteralTest, Complement) {
+ const Literal kPos{.atom_ = 42, .is_positive_ = true};
+ const Literal kNeg = kPos.complement();
+ EXPECT_EQ(kNeg.atom_, 42);
+ EXPECT_FALSE(kNeg.is_positive_);
+
+ const Literal kBack = kNeg.complement();
+ EXPECT_EQ(kBack, kPos);
+}
+
+TEST(LiteralTest, Equality) {
+ const Literal kA{.atom_ = 1, .is_positive_ = true};
+ const Literal kB{.atom_ = 1, .is_positive_ = true};
+ const Literal kC{.atom_ = 1, .is_positive_ = false};
+ const Literal kD{.atom_ = 2, .is_positive_ = true};
+ EXPECT_EQ(kA, kB);
+ EXPECT_NE(kA, kC);
+ EXPECT_NE(kA, kD);
+}
+
+} // namespace
+} // namespace atp
diff --git a/proofsearch/tests/test_unification.cc b/proofsearch/tests/test_unification.cc
new file mode 100644
index 0000000..63ac9f0
--- /dev/null
+++ b/proofsearch/tests/test_unification.cc
@@ -0,0 +1,355 @@
+/*
+ * richard-atp-engine: A saturation-based automated theorem prover for first-order logic.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "atp/core/symbol_table.h"
+#include "atp/core/term_bank.h"
+#include "atp/core/types.h"
+#include "atp/infer/substitution.h"
+#include "atp/infer/unification.h"
+
+#include <gtest/gtest.h>
+
+namespace atp {
+namespace {
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Test Fixture
+// ═══════════════════════════════════════════════════════════════════════════
+
+class UnificationTest : public ::testing::Test {
+ protected:
+ SymbolTable symbols_;
+ TermBank bank_{symbols_};
+
+ // Symbols
+ SymbolId sym_a_{}, sym_b_{}, sym_c_{};
+ SymbolId sym_f_{}, sym_g_{}, sym_h_{};
+ SymbolId sym_p_{};
+ SymbolId sym_x_{}, sym_y_{}, sym_z_{};
+
+ // Terms (created in SetUp)
+ TermId a_{}, b_{}, c_{};
+ TermId x_{}, y_{}, z_{};
+
+ void SetUp() override {
+ sym_a_ = symbols_.intern("a", SymbolKind::kConstant);
+ sym_b_ = symbols_.intern("b", SymbolKind::kConstant);
+ sym_c_ = symbols_.intern("c", SymbolKind::kConstant);
+ sym_f_ = symbols_.intern("f", SymbolKind::kFunction, 1);
+ sym_g_ = symbols_.intern("g", SymbolKind::kFunction, 2);
+ sym_h_ = symbols_.intern("h", SymbolKind::kFunction, 1);
+ sym_p_ = symbols_.intern("P", SymbolKind::kPredicate, 2);
+ sym_x_ = symbols_.intern("X", SymbolKind::kVariable);
+ sym_y_ = symbols_.intern("Y", SymbolKind::kVariable);
+ sym_z_ = symbols_.intern("Z", SymbolKind::kVariable);
+
+ a_ = bank_.makeTerm(sym_a_, {});
+ b_ = bank_.makeTerm(sym_b_, {});
+ c_ = bank_.makeTerm(sym_c_, {});
+ x_ = bank_.makeVar(sym_x_);
+ y_ = bank_.makeVar(sym_y_);
+ z_ = bank_.makeVar(sym_z_);
+ }
+
+ // Helper: make f(arg)
+ TermId f(TermId arg) { return bank_.makeTerm(sym_f_, {{arg}}); }
+ // Helper: make g(arg1, arg2)
+ TermId g(TermId a1, TermId a2) { return bank_.makeTerm(sym_g_, {{a1, a2}}); }
+ // Helper: make h(arg)
+ TermId h(TermId arg) { return bank_.makeTerm(sym_h_, {{arg}}); }
+ // Helper: make P(arg1, arg2)
+ TermId p(TermId a1, TermId a2) { return bank_.makeTerm(sym_p_, {{a1, a2}}); }
+};
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Substitution Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(UnificationTest, SubstBindAndLookup) {
+ Substitution subst;
+ EXPECT_TRUE(subst.bind(x_, a_));
+ EXPECT_EQ(subst.lookup(x_), a_);
+ EXPECT_EQ(subst.lookup(y_), kInvalidId); // unbound
+}
+
+TEST_F(UnificationTest, SubstBindSameTermSucceeds) {
+ Substitution subst;
+ EXPECT_TRUE(subst.bind(x_, a_));
+ EXPECT_TRUE(subst.bind(x_, a_)); // same binding → OK
+}
+
+TEST_F(UnificationTest, SubstBindDifferentTermFails) {
+ Substitution subst;
+ EXPECT_TRUE(subst.bind(x_, a_));
+ EXPECT_FALSE(subst.bind(x_, b_)); // different binding → fail
+}
+
+TEST_F(UnificationTest, SubstResolveChain) {
+ // X → Y → a
+ Substitution subst;
+ subst.bind(x_, y_);
+ subst.bind(y_, a_);
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, SubstResolveUnbound) {
+ Substitution const subst;
+ EXPECT_EQ(subst.resolve(x_, bank_), x_); // unbound → returns itself
+}
+
+TEST_F(UnificationTest, SubstResolveNonVariable) {
+ Substitution const subst;
+ EXPECT_EQ(subst.resolve(a_, bank_), a_); // constant → returns itself
+}
+
+TEST_F(UnificationTest, SubstApplySimple) {
+ // σ = {X → a} apply to f(X) → f(a)
+ Substitution subst;
+ subst.bind(x_, a_);
+ TermId const fx = f(x_);
+ TermId const result = subst.apply(fx, bank_);
+ TermId const expected = f(a_);
+ EXPECT_EQ(result, expected);
+}
+
+TEST_F(UnificationTest, SubstApplyLeavesUnbound) {
+ // σ = {X → a} apply to g(X, Y) → g(a, Y)
+ Substitution subst;
+ subst.bind(x_, a_);
+ TermId const gxy = g(x_, y_);
+ TermId const result = subst.apply(gxy, bank_);
+ TermId const expected = g(a_, y_);
+ EXPECT_EQ(result, expected);
+}
+
+TEST_F(UnificationTest, SubstApplyNested) {
+ // σ = {X → f(a)} apply to g(X, b) → g(f(a), b)
+ Substitution subst;
+ subst.bind(x_, f(a_));
+ TermId const result = subst.apply(g(x_, b_), bank_);
+ EXPECT_EQ(result, g(f(a_), b_));
+}
+
+TEST_F(UnificationTest, SubstOccursIn) {
+ Substitution const subst;
+ // X occurs in f(X)
+ EXPECT_TRUE(subst.occursIn(x_, f(x_), bank_));
+ // X occurs in g(a, f(X))
+ EXPECT_TRUE(subst.occursIn(x_, g(a_, f(x_)), bank_));
+ // X does NOT occur in f(a)
+ EXPECT_FALSE(subst.occursIn(x_, f(a_), bank_));
+ // X does NOT occur in Y (different variable)
+ EXPECT_FALSE(subst.occursIn(x_, y_, bank_));
+}
+
+TEST_F(UnificationTest, SubstClearAndSize) {
+ Substitution subst;
+ EXPECT_TRUE(subst.empty());
+ subst.bind(x_, a_);
+ subst.bind(y_, b_);
+ EXPECT_EQ(subst.size(), 2);
+ subst.clear();
+ EXPECT_TRUE(subst.empty());
+ EXPECT_EQ(subst.lookup(x_), kInvalidId);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Unification Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(UnificationTest, UnifyIdenticalConstants) {
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, a_, a_, subst));
+ EXPECT_TRUE(subst.empty());
+}
+
+TEST_F(UnificationTest, UnifyDifferentConstants) {
+ Substitution subst;
+ EXPECT_FALSE(unify(bank_, a_, b_, subst));
+}
+
+TEST_F(UnificationTest, UnifyVarWithConstant) {
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, x_, a_, subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, UnifyConstantWithVar) {
+ // Symmetric: unify(a, X) should also work
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, a_, x_, subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, UnifyTwoVariables) {
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, x_, y_, subst));
+ // One should be bound to the other
+ TermId const rx = subst.resolve(x_, bank_);
+ TermId const ry = subst.resolve(y_, bank_);
+ EXPECT_EQ(rx, ry);
+}
+
+TEST_F(UnificationTest, UnifyCompoundTerms) {
+ // f(X) =? f(a) → {X → a}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, f(x_), f(a_), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, UnifyMultipleArgs) {
+ // g(X, Y) =? g(a, b) → {X → a, Y → b}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, g(x_, y_), g(a_, b_), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+ EXPECT_EQ(subst.resolve(y_, bank_), b_);
+}
+
+TEST_F(UnificationTest, UnifyConflictingBindings) {
+ // g(X, X) =? g(a, b) → fails (X can't be both a and b)
+ Substitution subst;
+ EXPECT_FALSE(unify(bank_, g(x_, x_), g(a_, b_), subst));
+}
+
+TEST_F(UnificationTest, UnifyConsistentRepeatedVar) {
+ // g(X, X) =? g(a, a) → {X → a}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, g(x_, x_), g(a_, a_), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, UnifyDifferentHeadSymbols) {
+ // f(X) =? h(X) → fails
+ Substitution subst;
+ EXPECT_FALSE(unify(bank_, f(x_), h(x_), subst));
+}
+
+TEST_F(UnificationTest, UnifyDifferentArity) {
+ // f(X) =? g(X, Y) → fails (arity 1 vs 2)
+ Substitution subst;
+ EXPECT_FALSE(unify(bank_, f(x_), g(x_, y_), subst));
+}
+
+TEST_F(UnificationTest, UnifyNestedTerms) {
+ // f(g(X, a)) =? f(Y) → {Y → g(X, a)}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, f(g(x_, a_)), f(y_), subst));
+ EXPECT_EQ(subst.resolve(y_, bank_), g(x_, a_));
+}
+
+TEST_F(UnificationTest, UnifyVarWithCompound) {
+ // X =? f(a) → {X → f(a)}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, x_, f(a_), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), f(a_));
+}
+
+TEST_F(UnificationTest, UnifyDeepNesting) {
+ // f(f(f(X))) =? f(f(f(a))) → {X → a}
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, f(f(f(x_))), f(f(f(a_))), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+}
+
+TEST_F(UnificationTest, UnifyChainedVariables) {
+ // g(X, Y) =? g(Y, a) → {X → Y, Y → a} effectively X → a
+ Substitution subst;
+ EXPECT_TRUE(unify(bank_, g(x_, y_), g(y_, a_), subst));
+ EXPECT_EQ(subst.resolve(x_, bank_), a_);
+ EXPECT_EQ(subst.resolve(y_, bank_), a_);
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Occurs Check Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(UnificationTest, OccursCheckBlocks) {
+ // X =? f(X) with occurs check → fails
+ Substitution subst;
+ UnificationConfig const config{.enable_occurs_check_ = true};
+ EXPECT_FALSE(unify(bank_, x_, f(x_), subst, config));
+}
+
+TEST_F(UnificationTest, OccursCheckDisabledAllows) {
+ // X =? f(X) without occurs check → succeeds (unsound but fast)
+ Substitution subst;
+ UnificationConfig const config{.enable_occurs_check_ = false};
+ EXPECT_TRUE(unify(bank_, x_, f(x_), subst, config));
+}
+
+TEST_F(UnificationTest, OccursCheckDeep) {
+ // X =? f(g(X, a)) with occurs check → fails
+ Substitution subst;
+ UnificationConfig const config{.enable_occurs_check_ = true};
+ EXPECT_FALSE(unify(bank_, x_, f(g(x_, a_)), subst, config));
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// applySubstitution Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+TEST_F(UnificationTest, ApplySubstitutionSimple) {
+ Substitution subst;
+ subst.bind(x_, a_);
+ EXPECT_EQ(applySubstitution(subst, f(x_), bank_), f(a_));
+}
+
+TEST_F(UnificationTest, ApplySubstitutionNested) {
+ Substitution subst;
+ subst.bind(x_, a_);
+ subst.bind(y_, b_);
+ EXPECT_EQ(applySubstitution(subst, g(f(x_), y_), bank_), g(f(a_), b_));
+}
+
+TEST_F(UnificationTest, ApplySubstitutionPreservesUnbound) {
+ Substitution subst;
+ subst.bind(x_, a_);
+ // Y is unbound → stays as Y
+ EXPECT_EQ(applySubstitution(subst, g(x_, y_), bank_), g(a_, y_));
+}
+
+TEST_F(UnificationTest, ApplySubstitutionToConstant) {
+ Substitution subst;
+ subst.bind(x_, a_);
+ // Constant is unchanged
+ EXPECT_EQ(applySubstitution(subst, b_, bank_), b_);
+}
+
+TEST_F(UnificationTest, ApplySubstitutionChain) {
+ // σ = {X → Y, Y → a} apply to f(X) → f(a)
+ Substitution subst;
+ subst.bind(x_, y_);
+ subst.bind(y_, a_);
+ EXPECT_EQ(applySubstitution(subst, f(x_), bank_), f(a_));
+}
+
+TEST_F(UnificationTest, UnifyThenApply) {
+ // Full round-trip: unify P(X, f(Y)) with P(a, f(b)), then apply
+ Substitution subst;
+ TermId const lhs = p(x_, f(y_));
+ TermId const rhs = p(a_, f(b_));
+ ASSERT_TRUE(unify(bank_, lhs, rhs, subst));
+
+ EXPECT_EQ(applySubstitution(subst, lhs, bank_), p(a_, f(b_)));
+ EXPECT_EQ(applySubstitution(subst, rhs, bank_), p(a_, f(b_)));
+ // Both sides should produce the same term after applying the MGU
+ EXPECT_EQ(applySubstitution(subst, lhs, bank_), applySubstitution(subst, rhs, bank_));
+}
+
+} // namespace
+} // namespace atp
diff --git a/raytracing/.clang-format b/raytracing/.clang-format
new file mode 100644
index 0000000..c49e66b
--- /dev/null
+++ b/raytracing/.clang-format
@@ -0,0 +1,23 @@
+---
+Language: Cpp
+BasedOnStyle: Mozilla
+ColumnLimit: 116
+SpacesInParentheses: true
+AlwaysBreakAfterReturnType: None
+AlwaysBreakAfterDefinitionReturnType: None
+SpaceBeforeCpp11BracedList: true
+BreakBeforeBinaryOperators: All
+Cpp11BracedListStyle: true
+AllowShortBlocksOnASingleLine: Always
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterClass: true
+ AfterControlStatement: Never
+ AfterFunction: true
+ AfterStruct: true
+ AfterEnum: true
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: false
+PackConstructorInitializers: NextLine
+...
diff --git a/raytracing/.clang-tidy b/raytracing/.clang-tidy
new file mode 100644
index 0000000..c1d3e9f
--- /dev/null
+++ b/raytracing/.clang-tidy
@@ -0,0 +1,9 @@
+Checks:
+ '*,-altera-*,-llvmlibc-*,-google-build-using-namespace,-modernize-use-trailing-return-type,-readability-implicit-bool-conversion,-*-non-private-member-variables-in-classes,-modernize-use-nodiscard,-*-magic-numbers,-readability-identifier-length,-concurrency-mt-unsafe,-llvm-header-guard,-fuchsia-default-arguments-calls,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-android-cloexec-accept,-cppcoreguidelines-pro-type-vararg,-hicpp-vararg,-hicpp-signed-bitwise,-misc-include-cleaner,-readability-redundant-member-init,-boost-use-ranges,-portability-template-virtual-member-function'
+
+WarningsAsErrors:
+ '*'
+
+CheckOptions:
+ hicpp-special-member-functions.AllowSoleDefaultDtor: true
+ cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true
diff --git a/raytracing/.gitignore b/raytracing/.gitignore
new file mode 100644
index 0000000..06d9da1
--- /dev/null
+++ b/raytracing/.gitignore
@@ -0,0 +1,269 @@
+
+macOS
+
+.DS_Store
+.AppleDouble
+.LSOverride
+Icon
+._*
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+Windows
+
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+*.cab
+*.msi
+*.msm
+*.msp
+*.lnk
+desktop.ini
+
+Linux
+
+~
+.fuse_hidden
+.directory
+.Trash-*
+.nfs*
+
+Visual Studio Code
+
+.vscode/*
+# !.vscode/settings.json
+# !.vscode/tasks.json
+# !.vscode/launch.json
+# !.vscode/extensions.json
+*.code-workspace
+.history/
+
+IntelliJ IDEA / WebStorm / PyCharm / PhpStorm (JetBrains)
+
+.idea/
+*.iml
+*.iws
+out/
+
+Eclipse
+
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+
+Vim / Emacs
+
+*.swp
+*.swo
+~
+##
+/.emacs.desktop
+/.emacs.desktop.lock
+.elc
+auto-save-list
+tramp
+.#
+
+
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+*.pem
+*.key
+*.cert
+*.cer
+*.crt
+id_rsa
+id_rsa.pub
+secrets.yml
+auth.json
+
+
+.log
+npm-debug.log
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+*.sqlite
+*.sqlite3
+*.db
+*.sql
+
+Node.js
+
+node_modules/
+jspm_packages/
+typings/
+.npm
+.eslintcache
+.stylelintcache
+.node_repl_history
+
+
+dist/
+build/
+.next/
+.nuxt/
+.vuepress/dist
+.serverless/
+.cache/
+
+
+coverage/
+.nyc_output/
+
+Python
+
+pycache/
+*.py[cod]
+*$py.class
+*.so
+
+
+
+venv/
+env/
+.env/
+.venv/
+ENV/
+env.bak/
+
+
+
+*.egg
+*.egg-info/
+dist/
+build/
+eggs/
+parts/
+var/
+sdist/
+develop-eggs/
+.installed.cfg
+
+
+
+.pytest_cache/
+.mypy_cache/
+.tox/
+htmlcov/
+
+Java / Maven / Gradle
+
+*.class
+*.log
+*.ctxt
+.mtj.tmp/
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+target/
+build/
+!gradle-wrapper.jar
+.gradle/
+
+C / C++
+
+
+*.o
+*.ko
+*.obj
+*.elf
+
+
+*.ilk
+*.map
+*.exp
+*.gch
+*.pch
+*.lib
+*.a
+*.la
+*.lo
+*.dll
+*.so
+.so.
+*.dylib
+
+
+*.exe
+*.out
+*.app
+.i86
+*.x86_64
+*.hex
+
+CMake / Make
+
+CMakeCache.txt
+CMakeFiles/
+CMakeScripts/
+Makefile.cmake
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+
+
+Go
+
+*.test
+*.out
+
+vendor/
+
+go.work
+
+Rust
+
+target/
+
+Cargo.lock
+
+**/*.rs.bk
+
+PHP / Composer
+
+vendor/
+.phpunit.result.cache
+
+Ruby
+
+.gem
+/.bundle/
+/vendor/bundle
+/log/
+/tmp/*
+/db/.sqlite3
+/db/.sqlite3-journal
+/public/system
+/coverage/
+/spec/tmp
+
+
+杂项
+
+.sass-cache/
+.jekyll-metadata
+*.bak
+*.tmp
+*.temp \ No newline at end of file
diff --git a/raytracing/CMakeLists.txt b/raytracing/CMakeLists.txt
new file mode 100644
index 0000000..c001585
--- /dev/null
+++ b/raytracing/CMakeLists.txt
@@ -0,0 +1,39 @@
+cmake_minimum_required(VERSION 3.24.2)
+
+project(RayTracer LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+
+if(${CMAKE_GENERATOR} STREQUAL "Unix Makefiles")
+ set(CMAKE_MAKE_PROGRAM "${PROJECT_SOURCE_DIR}/scripts/make-parallel.sh" CACHE STRING "" FORCE)
+endif()
+
+if(${PROJECT_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
+ message(FATAL_ERROR "RayTracer must be built outside its source directory, e.g. `cmake -B build`.")
+endif()
+
+# Default is set to Release if not specified
+include(${PROJECT_SOURCE_DIR}/etc/build_type.cmake)
+
+# Enable LTO
+include(${PROJECT_SOURCE_DIR}/etc/lto.cmake)
+
+# Enable ccache
+include(${PROJECT_SOURCE_DIR}/etc/ccache.cmake)
+
+
+# strict compile warnings
+include(${PROJECT_SOURCE_DIR}/etc/cflags.cmake)
+
+# clang-format clang-tidy
+
+include(${PROJECT_SOURCE_DIR}/etc/scanners.cmake)
+
+# ctest
+# include(${PROJECT_SOURCE_DIR}/etc/ctest.cmake)
+
+# License
+install(FILES LICENSE DESTINATION .)
+
+add_subdirectory("${PROJECT_SOURCE_DIR}/src")
+add_subdirectory("${PROJECT_SOURCE_DIR}/apps") \ No newline at end of file
diff --git a/raytracing/LICENSE b/raytracing/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/raytracing/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/raytracing/README.md b/raytracing/README.md
new file mode 100644
index 0000000..71f3ff5
--- /dev/null
+++ b/raytracing/README.md
@@ -0,0 +1,44 @@
+# Ray Tracer
+
+Inspired by the book _Ray Tracing in One Weekend_, I'd like to write this project using modern C++ (C++ 23) and attempt to use high-performance approaches.
+
+## Build & Run
+
+Make Sure your build system is properly set up:
+```bash
+cmake -S . -B build
+```
+
+Configure for Release:
+```bash
+# Release mode
+cmake -B build -DCMAKE_BUILD_TYPE=Release
+#Debug mode
+cmake -B build -DCMAKE_BUILD_TYPE=Debug
+```
+
+Compile the source code:
+```bash
+cmake --build build
+```
+
+To normalize the coding style
+```bash
+cmake --build build --target format
+```
+
+Suggests improvements to follow good C++ programming practices:
+```bash
+cmake --build build --target tidy
+cmake --build build --target tidy-all
+```
+
+Run the program
+```bash
+./build/apps/RayTracer > images/image.ppm
+display images/image.ppm
+```
+
+## License
+This project is licensed under the **GNU General Public License v3.0**.
+See the [LICENSE](LICENSE) file for the full text. \ No newline at end of file
diff --git a/raytracing/apps/CMakeLists.txt b/raytracing/apps/CMakeLists.txt
new file mode 100644
index 0000000..af2d213
--- /dev/null
+++ b/raytracing/apps/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_executable(RayTracer main.cc)
+
+target_link_libraries(RayTracer PRIVATE RayTracerCore) \ No newline at end of file
diff --git a/raytracing/apps/main.cc b/raytracing/apps/main.cc
new file mode 100644
index 0000000..2a73d94
--- /dev/null
+++ b/raytracing/apps/main.cc
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2026 Richard
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "color.h"
+#include "ray.h"
+#include "vec3.h"
+
+#include <iostream>
+
+double hit_sphere( const point3& center, double radius, const ray& r )
+{
+ vec3 oc = center - r.origin();
+ auto a = dot( r.direction(), r.direction() );
+ auto b = -2.0 * dot( r.direction(), oc );
+ auto c = dot( oc, oc ) - radius * radius;
+ auto discriminant = b * b - 4 * a * c;
+
+ if ( discriminant < 0 ) {
+ return -1.0;
+ } else {
+ return ( -b - std::sqrt( discriminant ) ) / ( 2.0 * a );
+ }
+}
+
+color ray_color( const ray& r )
+{
+ auto t = hit_sphere( point3( 0, 0, -1 ), 0.5, r );
+ if ( t > 0.0 ) {
+ vec3 N = unit_vector( r.at( t ) - vec3( 0, 0, -1 ) );
+ return 0.5 * color( N.x() + 1, N.y() + 1, N.z() + 1 );
+ }
+
+ vec3 unit_direction = unit_vector( r.direction() );
+ auto a = 0.5 * ( unit_direction.y() + 1.0 );
+ return ( 1.0 - a ) * color( 1.0, 1.0, 1.0 ) + a * color( 0.5, 0.7, 1.0 );
+}
+
+int main()
+{
+ // Image
+
+ auto aspect_ratio = 16.0 / 9.0;
+ const int image_width = 400;
+ // Calculate the image height, and ensure that it's at least 1.
+ int image_height = static_cast<int>( image_width / aspect_ratio );
+ image_height = ( image_height < 1 ) ? 1 : image_height;
+
+ // Camera
+ auto focal_length = 1.0;
+ auto viewport_height = 2.0;
+ auto viewport_width = viewport_height * ( double( image_width ) / image_height );
+ auto camera_center = point3( 0, 0, 0 );
+
+ // Calculate the vectors across the horizontal and down the vertical viewport edges.
+ auto viewport_u = vec3( viewport_width, 0, 0 );
+ auto viewport_v = vec3( 0, -viewport_height, 0 );
+
+ // Calculate the horizontal and vertical delta vectors from pixel to pixel.
+ auto pixel_delta_u = viewport_u / image_width;
+ auto pixel_delta_v = viewport_v / image_height;
+
+ // Calculate the location of the upper left pixel.
+ auto viewport_upper_left = camera_center - vec3( 0, 0, focal_length ) - viewport_u / 2 - viewport_v / 2;
+ auto pixel00_loc = viewport_upper_left + 0.5 * ( pixel_delta_u + pixel_delta_v );
+
+ // Render
+
+ std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
+ for ( int j = 0; j < image_height; j++ ) {
+ std::clog << "\rScanlines remaining: " << ( image_height - j ) << ' ' << std::flush;
+ for ( int i = 0; i < image_width; i++ ) {
+ auto pixel_center = pixel00_loc + ( i * pixel_delta_u ) + ( j * pixel_delta_v );
+ auto ray_direction = pixel_center - camera_center;
+ ray r( camera_center, ray_direction );
+
+ const color pixel_color = ray_color( r );
+ write_color( std::cout, pixel_color );
+ }
+ }
+ std::clog << "\rDone. \n";
+ return 0;
+} \ No newline at end of file
diff --git a/raytracing/etc/build_type.cmake b/raytracing/etc/build_type.cmake
new file mode 100644
index 0000000..69d019f
--- /dev/null
+++ b/raytracing/etc/build_type.cmake
@@ -0,0 +1,11 @@
+set(default_build_type "Debug")
+
+if (NOT (CMAKE_BUILD_TYPE_SHADOW STREQUAL CMAKE_BUILD_TYPE))
+ if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ message (STATUS "Setting build type to '${default_build_type}'")
+ set (CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE)
+ endif ()
+ set (CMAKE_BUILD_TYPE_SHADOW ${CMAKE_BUILD_TYPE} CACHE STRING "used to detect changes in build type" FORCE)
+endif ()
+
+message (STATUS "Building in '${CMAKE_BUILD_TYPE}' mode.")
diff --git a/raytracing/etc/ccache.cmake b/raytracing/etc/ccache.cmake
new file mode 100644
index 0000000..aeceb8e
--- /dev/null
+++ b/raytracing/etc/ccache.cmake
@@ -0,0 +1,5 @@
+find_program(CCACHE_PROGRAM ccache)
+if(CCACHE_PROGRAM)
+ set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
+ message(STATUS "Found ccache: ${CCACHE_PROGRAM} - Compilation will be cached.")
+endif() \ No newline at end of file
diff --git a/raytracing/etc/cflags.cmake b/raytracing/etc/cflags.cmake
new file mode 100644
index 0000000..ac08601
--- /dev/null
+++ b/raytracing/etc/cflags.cmake
@@ -0,0 +1,16 @@
+set (CMAKE_CXX_STANDARD 23)
+set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+set(SANITIZING_FLAGS -fno-sanitize-recover=all -fsanitize=undefined -fsanitize=address)
+
+# ask for more warnings from the compiler
+set (CMAKE_BASE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wpedantic -Wextra -Weffc++ -Werror -Wshadow -Wpointer-arith -Wcast-qual -Wformat=2 -Wno-unqualified-std-cast-call -Wno-non-virtual-dtor")
+
+# Sanitizers
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ message(STATUS "Enabling Address and Undefined Behavior Sanitizers")
+ set(SANITIZING_FLAGS "-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer")
+ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${SANITIZING_FLAGS} -g -O0")
+ set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} ${SANITIZING_FLAGS}")
+endif() \ No newline at end of file
diff --git a/raytracing/etc/lto.cmake b/raytracing/etc/lto.cmake
new file mode 100644
index 0000000..517ec14
--- /dev/null
+++ b/raytracing/etc/lto.cmake
@@ -0,0 +1,8 @@
+include(CheckIPOSupported)
+check_ipo_supported(RESULT ipo_supported OUTPUT error)
+if(ipo_supported)
+ message(STATUS "IPO / LTO supported and enabled.")
+ set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
+else()
+ message(STATUS "IPO / LTO not supported: <${error}>")
+endif() \ No newline at end of file
diff --git a/raytracing/etc/scanners.cmake b/raytracing/etc/scanners.cmake
new file mode 100644
index 0000000..0a86f19
--- /dev/null
+++ b/raytracing/etc/scanners.cmake
@@ -0,0 +1,52 @@
+# file (GLOB_RECURSE RayTracer_CC_FILES ${CMAKE_SOURCE_DIR}/src/*.cc)
+
+# file (GLOB_RECURSE ALL_SRC_FILES *.hh *.cc)
+
+# add_custom_target (format "clang-format" -i ${ALL_SRC_FILES} COMMENT "Formatting source code...")
+
+
+find_program(CLANG_FORMAT_EXE clang-format)
+if(CLANG_FORMAT_EXE)
+ file(GLOB_RECURSE ALL_FMT_FILES
+ "${CMAKE_SOURCE_DIR}/src/*.cc"
+ "${CMAKE_SOURCE_DIR}/src/*.cpp"
+ "${CMAKE_SOURCE_DIR}/src/*.h"
+ "${CMAKE_SOURCE_DIR}/src/*.hpp"
+ "${CMAKE_SOURCE_DIR}/apps/*.cc"
+ "${CMAKE_SOURCE_DIR}/apps/*.cpp"
+ )
+
+ add_custom_target(format
+ COMMAND ${CLANG_FORMAT_EXE} -i -style=file ${ALL_FMT_FILES}
+ COMMENT "Formatting source code..."
+ )
+else()
+ message(WARNING "clang-format not found! 'format' target will be disabled.")
+endif()
+
+# clang-tidy
+# find_program(CLANG_TIDY_EXE clang-tidy)
+# if(CLANG_TIDY_EXE)
+# message(STATUS "Clang-Tidy enabled: will run automatically during compilation.")
+# set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
+# else()
+# message(WARNING "clang-tidy not found! Static analysis will be disabled.")
+# endif()
+
+foreach (tidy_target ${ALL_SRC_FILES})
+ get_filename_component (basename ${tidy_target} NAME)
+ get_filename_component (dirname ${tidy_target} DIRECTORY)
+ get_filename_component (basedir ${dirname} NAME)
+ set (tidy_target_name "${basedir}__${basename}")
+ set (tidy_command clang-tidy --quiet -header-filter=.* -p=${PROJECT_BINARY_DIR} ${tidy_target})
+ add_custom_target (tidy_${tidy_target_name} ${tidy_command})
+ list (APPEND ALL_TIDY_TARGETS tidy_${tidy_target_name})
+
+ if (${tidy_target} IN_LIST RayTracer_CC_FILES)
+ list (APPEND RayTracer_TIDY_TARGETS tidy_${tidy_target_name})
+ endif ()
+endforeach (tidy_target)
+
+add_custom_target (tidy DEPENDS ${RayTracer_TIDY_TARGETS})
+
+add_custom_target (tidy-all DEPENDS ${ALL_TIDY_TARGETS})
diff --git a/raytracing/scripts/make-parallel.sh b/raytracing/scripts/make-parallel.sh
new file mode 100755
index 0000000..879af9a
--- /dev/null
+++ b/raytracing/scripts/make-parallel.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec make -j`nproc` "$@"
diff --git a/raytracing/src/CMakeLists.txt b/raytracing/src/CMakeLists.txt
new file mode 100644
index 0000000..c0dc29c
--- /dev/null
+++ b/raytracing/src/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(LIB_SOURCES
+ vec3.cc
+ color.cc
+ ray.cc
+)
+
+add_library(RayTracerCore STATIC ${LIB_SOURCES})
+
+target_include_directories(RayTracerCore PUBLIC .) \ No newline at end of file
diff --git a/raytracing/src/color.cc b/raytracing/src/color.cc
new file mode 100644
index 0000000..9a458bc
--- /dev/null
+++ b/raytracing/src/color.cc
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2026 Richard
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "color.h"
+
+void write_color( std::ostream& out, const color& pixel_color )
+{
+ auto red = pixel_color.x();
+ auto green = pixel_color.y();
+ auto blue = pixel_color.z();
+
+ // Translate the [0,1] component values to the byte range [0,255].
+ const int rbyte = static_cast<int>( 255.999 * red );
+ const int gbyte = static_cast<int>( 255.999 * green );
+ const int bbyte = static_cast<int>( 255.999 * blue );
+
+ // Write out the pixel color components.
+ out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n';
+} \ No newline at end of file
diff --git a/raytracing/src/color.h b/raytracing/src/color.h
new file mode 100644
index 0000000..3e96a5b
--- /dev/null
+++ b/raytracing/src/color.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2026 Richard
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef COLOR_H
+#define COLOR_H
+
+#include "vec3.h"
+
+#include <iostream>
+
+using color = vec3;
+
+void write_color( std::ostream& out, const color& pixel_color );
+
+#endif \ No newline at end of file
diff --git a/raytracing/src/ray.cc b/raytracing/src/ray.cc
new file mode 100644
index 0000000..718fee2
--- /dev/null
+++ b/raytracing/src/ray.cc
@@ -0,0 +1,16 @@
+#include "ray.h"
+
+const point3& ray::origin() const
+{
+ return this->orig;
+}
+
+const vec3& ray::direction() const
+{
+ return this->dir;
+}
+
+point3 ray::at( double t ) const
+{
+ return this->orig + t * this->dir;
+} \ No newline at end of file
diff --git a/raytracing/src/ray.h b/raytracing/src/ray.h
new file mode 100644
index 0000000..d455847
--- /dev/null
+++ b/raytracing/src/ray.h
@@ -0,0 +1,22 @@
+#ifndef RAY_H
+#define RAY_H
+
+#include "vec3.h"
+
+class ray
+{
+public:
+ ray() {}
+ ray( const point3& origin, const vec3& direction ) : orig( origin ), dir( direction ) {}
+
+ const point3& origin() const;
+ const vec3& direction() const;
+
+ point3 at( double t ) const;
+
+private:
+ point3 orig {};
+ vec3 dir {};
+};
+
+#endif \ No newline at end of file
diff --git a/raytracing/src/vec3.cc b/raytracing/src/vec3.cc
new file mode 100644
index 0000000..4332c56
--- /dev/null
+++ b/raytracing/src/vec3.cc
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2026 Richard
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "vec3.h"
+
+double vec3::x() const
+{
+ return e[0];
+}
+double vec3::y() const
+{
+ return e[1];
+}
+double vec3::z() const
+{
+ return e[2];
+}
+
+vec3 vec3::operator-() const
+{
+ return { -e[0], -e[1], -e[2] };
+}
+
+double vec3::operator[]( int i ) const
+{
+ return e.at( i );
+}
+
+double& vec3::operator[]( int i )
+{
+ return e.at( i );
+}
+
+vec3& vec3::operator+=( const vec3& other )
+{
+ e[0] += other.e[0];
+ e[1] += other.e[1];
+ e[2] += other.e[2];
+ return *this;
+}
+
+vec3& vec3::operator*=( double t )
+{
+ e[0] *= t;
+ e[1] *= t;
+ e[2] *= t;
+ return *this;
+}
+
+vec3& vec3::operator/=( double t )
+{
+ return *this *= 1 / t;
+}
+
+double vec3::length() const
+{
+ return std::sqrt( length_squared() );
+}
+
+double vec3::length_squared() const
+{
+ return e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
+}
diff --git a/raytracing/src/vec3.h b/raytracing/src/vec3.h
new file mode 100644
index 0000000..a8b8265
--- /dev/null
+++ b/raytracing/src/vec3.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2026 Richard
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef VEC_3
+#define VEC_3
+
+#include <array>
+#include <cmath>
+#include <iostream>
+
+class vec3
+{
+public:
+ std::array<double, 3> e;
+
+ vec3() : e { 0, 0, 0 } {}
+ vec3( double e0, double e1, double e2 ) : e { e0, e1, e2 } {}
+
+ double x() const;
+ double y() const;
+ double z() const;
+
+ vec3 operator-() const;
+ double operator[]( int i ) const;
+ double& operator[]( int i );
+ vec3& operator+=( const vec3& other );
+ vec3& operator*=( double t );
+ vec3& operator/=( double t );
+ double length() const;
+ double length_squared() const;
+};
+
+// point3 is just an alias for vec3, but useful for geometric clarity in the code.
+using point3 = vec3;
+
+// Vector Utility Functions
+
+inline std::ostream& operator<<( std::ostream& out, const vec3& v )
+{
+ return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2];
+}
+
+inline vec3 operator+( const vec3& u, const vec3& v )
+{
+ return { u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2] };
+}
+
+inline vec3 operator-( const vec3& u, const vec3& v )
+{
+ return { u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2] };
+}
+
+inline vec3 operator*( const vec3& u, const vec3& v )
+{
+ return { u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2] };
+}
+
+inline vec3 operator*( double t, const vec3& v )
+{
+ return { t * v.e[0], t * v.e[1], t * v.e[2] };
+}
+
+inline vec3 operator*( const vec3& v, double t )
+{
+ return t * v;
+}
+
+inline vec3 operator/( const vec3& v, double t )
+{
+ return ( 1 / t ) * v;
+}
+
+inline double dot( const vec3& u, const vec3& v )
+{
+ return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2];
+}
+
+inline vec3 cross( const vec3& u, const vec3& v )
+{
+ return {
+ u.e[1] * v.e[2] - u.e[2] * v.e[1], u.e[2] * v.e[0] - u.e[0] * v.e[2], u.e[0] * v.e[1] - u.e[1] * v.e[0] };
+}
+
+inline vec3 unit_vector( const vec3& v )
+{
+ return v / v.length();
+}
+
+#endif
diff --git a/rcc/CMakeLists.txt b/rcc/CMakeLists.txt
index 86f1c80..1fb86a0 100644
--- a/rcc/CMakeLists.txt
+++ b/rcc/CMakeLists.txt
@@ -53,20 +53,15 @@ configure_file(
"${RCC_MAIN_INCLUDE_DIR}/rcc/Config/rcc-config.h.cmake"
"${RCC_INCLUDE_DIR}/rcc/Config/rcc-config.h")
-add_library(RCCHeaders INTERFACE)
-target_compile_features(RCCHeaders INTERFACE cxx_std_23)
-target_include_directories(RCCHeaders INTERFACE
- "$<BUILD_INTERFACE:${RCC_SOURCE_DIR}/include>"
- "$<BUILD_INTERFACE:${RCC_BINARY_DIR}/include>"
- "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
-install(TARGETS RCCHeaders EXPORT RCCTargets)
install(DIRECTORY include/rcc DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" PATTERN "*.def")
-install(DIRECTORY "${RCC_BINARY_DIR}/include/rcc" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
+install(DIRECTORY "${RCC_BINARY_DIR}/include/rcc/Config" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/rcc"
FILES_MATCHING PATTERN "*.h" PATTERN "*.inc" PATTERN "*.def"
- PATTERN "CMakeFiles" EXCLUDE PATTERN "config.h" EXCLUDE)
+ PATTERN "config.h" EXCLUDE)
-add_subdirectory(lib)
+set(RCC_LIBBASE_SOURCE_DIR "${RCC_SOURCE_DIR}/../libbase")
+set(RCC_LIBBASE_BINARY_DIR "${RCC_BINARY_DIR}/libbase")
+add_subdirectory("${RCC_LIBBASE_SOURCE_DIR}" "${RCC_LIBBASE_BINARY_DIR}")
add_subdirectory(tools)
foreach(rccProject IN LISTS RCC_ENABLE_PROJECTS)
if(NOT rccProject STREQUAL "cfront")
diff --git a/rcc/cmake/modules/AddRCC.cmake b/rcc/cmake/modules/AddRCC.cmake
index 11bd92e..738df5f 100644
--- a/rcc/cmake/modules/AddRCC.cmake
+++ b/rcc/cmake/modules/AddRCC.cmake
@@ -162,7 +162,13 @@ endfunction()
function(add_rcc_library name)
cmake_parse_arguments(PARSE_ARGV 1 arg "DISABLE_PCH_REUSE;NO_INSTALL_RPATH" "" "LINK_LIBS;PRECOMPILE_HEADERS")
add_library(${name} ${arg_UNPARSED_ARGUMENTS})
- target_link_libraries(${name} PUBLIC RCCHeaders ${arg_LINK_LIBS})
+ target_link_libraries(${name} PUBLIC RCCCore ${arg_LINK_LIBS})
+ target_compile_features(${name} PUBLIC cxx_std_23)
+ target_compile_definitions(${name} PRIVATE RCC_EXPORTS)
+ target_include_directories(${name} PUBLIC
+ "$<BUILD_INTERFACE:${RCC_SOURCE_DIR}/include>"
+ "$<BUILD_INTERFACE:${RCC_BINARY_DIR}/include>"
+ "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
rcc_configure_target(${name})
install(TARGETS ${name} EXPORT RCCTargets
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
@@ -173,7 +179,11 @@ endfunction()
function(add_rcc_executable name)
cmake_parse_arguments(PARSE_ARGV 1 arg "DISABLE_PCH_REUSE;NO_INSTALL_RPATH" "" "LINK_LIBS;PRECOMPILE_HEADERS")
add_executable(${name} ${arg_UNPARSED_ARGUMENTS})
- target_link_libraries(${name} PRIVATE RCCHeaders ${arg_LINK_LIBS})
+ target_link_libraries(${name} PRIVATE RCCCore ${arg_LINK_LIBS})
+ target_compile_features(${name} PRIVATE cxx_std_23)
+ target_include_directories(${name} PRIVATE
+ "${RCC_SOURCE_DIR}/include"
+ "${RCC_BINARY_DIR}/include")
rcc_configure_target(${name})
endfunction()
diff --git a/rcc/include/rcc/Config/config.h.cmake b/rcc/include/rcc/Config/config.h.cmake
index 32761c1..8830412 100644
--- a/rcc/include/rcc/Config/config.h.cmake
+++ b/rcc/include/rcc/Config/config.h.cmake
@@ -1,5 +1,5 @@
-#ifndef CONFIG_H
-#define CONFIG_H
+#ifndef RCC_PRIVATE_CONFIG_H
+#define RCC_PRIVATE_CONFIG_H
// Include this header only under the rcc source tree.
// This is a private header.
@@ -10,38 +10,6 @@
/* Bug report URL. */
#define BUG_REPORT_URL "${BUG_REPORT_URL}"
-/* Define to 1 to enable backtraces, and to 0 otherwise. */
-#cmakedefine01 ENABLE_BACKTRACES
-
-/* Define to 1 to enable crash overrides, and to 0 otherwise. */
-#cmakedefine01 ENABLE_CRASH_OVERRIDES
-
-/* Define to 1 to enable crash memory dumps, and to 0 otherwise. */
-#cmakedefine01 RCC_ENABLE_CRASH_DUMPS
-
-/* Define to 1 to prefer forward slashes on Windows, and to 0 prefer
- backslashes. */
-#cmakedefine01 RCC_WINDOWS_PREFER_FORWARD_SLASH
-
-/* Define to 1 if you have the `backtrace' function. */
-#cmakedefine HAVE_BACKTRACE ${HAVE_BACKTRACE}
-
-#define BACKTRACE_HEADER <${BACKTRACE_HEADER}>
-
-/* can use __crashreporter_info__ */
-#cmakedefine01 HAVE_CRASHREPORTER_INFO
-
-/* Define to 1 if you have the declaration of `arc4random', and to 0 if you
- don't. */
-#cmakedefine01 HAVE_DECL_ARC4RANDOM
-
-/* Define to 1 if you have the declaration of `strerror_s', and to 0 if you
- don't. */
-#cmakedefine01 HAVE_DECL_STRERROR_S
-
-/* Define if dlopen() is available on this platform. */
-#cmakedefine HAVE_DLOPEN ${HAVE_DLOPEN}
-
/* Define to 1 if we can register EH frames on this platform. */
#cmakedefine HAVE_REGISTER_FRAME ${HAVE_REGISTER_FRAME}
@@ -60,177 +28,12 @@
/* Define to 1 if you have the <ffi.h> header file. */
#cmakedefine HAVE_FFI_H ${HAVE_FFI_H}
-/* Define to 1 if you have the `futimens' function. */
-#cmakedefine HAVE_FUTIMENS ${HAVE_FUTIMENS}
-
-/* Define to 1 if you have the `futimes' function. */
-#cmakedefine HAVE_FUTIMES ${HAVE_FUTIMES}
-
-/* Define to 1 if you have the `getpagesize' function. */
-#cmakedefine HAVE_GETPAGESIZE ${HAVE_GETPAGESIZE}
-
-/* Define to 1 if you have the `getrusage' function. */
-#cmakedefine HAVE_GETRUSAGE ${HAVE_GETRUSAGE}
-
-/* Define to 1 if you have the `isatty' function. */
-#cmakedefine HAVE_ISATTY 1
-
-/* Define to 1 if you have the `edit' library (-ledit). */
-#cmakedefine HAVE_LIBEDIT ${HAVE_LIBEDIT}
-
/* Define to 1 if you have the `pfm' library (-lpfm). */
#cmakedefine HAVE_LIBPFM ${HAVE_LIBPFM}
/* Define to 1 if the `perf_branch_entry' struct has field cycles. */
#cmakedefine LIBPFM_HAS_FIELD_CYCLES ${LIBPFM_HAS_FIELD_CYCLES}
-/* Define to 1 if you have the `psapi' library (-lpsapi). */
-#cmakedefine HAVE_LIBPSAPI ${HAVE_LIBPSAPI}
-
-/* Define to 1 if you have the `pthread' library (-lpthread). */
-#cmakedefine HAVE_LIBPTHREAD ${HAVE_LIBPTHREAD}
-
-/* Define to 1 if you have the `pthread_getname_np' function. */
-#cmakedefine HAVE_PTHREAD_GETNAME_NP ${HAVE_PTHREAD_GETNAME_NP}
-
-/* Define to 1 if you have the `pthread_setname_np' function. */
-#cmakedefine HAVE_PTHREAD_SETNAME_NP ${HAVE_PTHREAD_SETNAME_NP}
-
-/* Define to 1 if you have the `pthread_get_name_np' function. */
-#cmakedefine HAVE_PTHREAD_GET_NAME_NP ${HAVE_PTHREAD_GET_NAME_NP}
-
-/* Define to 1 if you have the `pthread_set_name_np' function. */
-#cmakedefine HAVE_PTHREAD_SET_NAME_NP ${HAVE_PTHREAD_SET_NAME_NP}
-
-/* Define to 1 if you have the <mach/mach.h> header file. */
-#cmakedefine HAVE_MACH_MACH_H ${HAVE_MACH_MACH_H}
-
-/* Define to 1 if you have the `mallctl' function. */
-#cmakedefine HAVE_MALLCTL ${HAVE_MALLCTL}
-
-/* Define to 1 if you have the `mallinfo' function. */
-#cmakedefine HAVE_MALLINFO ${HAVE_MALLINFO}
-
-/* Define to 1 if you have the `mallinfo2' function. */
-#cmakedefine HAVE_MALLINFO2 ${HAVE_MALLINFO2}
-
-/* Define to 1 if you have the <malloc/malloc.h> header file. */
-#cmakedefine HAVE_MALLOC_MALLOC_H ${HAVE_MALLOC_MALLOC_H}
-
-/* Define to 1 if you have the `malloc_zone_statistics' function. */
-#cmakedefine HAVE_MALLOC_ZONE_STATISTICS ${HAVE_MALLOC_ZONE_STATISTICS}
-
-/* Define to 1 if you have the `posix_spawn' function. */
-#cmakedefine HAVE_POSIX_SPAWN ${HAVE_POSIX_SPAWN}
-
-/* Define to 1 if you have the `pread' function. */
-#cmakedefine HAVE_PREAD ${HAVE_PREAD}
-
-/* Define to 1 if you have the <pthread.h> header file. */
-#cmakedefine HAVE_PTHREAD_H ${HAVE_PTHREAD_H}
-
-/* Have pthread_rwlock_init */
-#cmakedefine HAVE_PTHREAD_RWLOCK_INIT ${HAVE_PTHREAD_RWLOCK_INIT}
-
-/* Define to 1 if you have the `sbrk' function. */
-#cmakedefine HAVE_SBRK ${HAVE_SBRK}
-
-/* Define to 1 if you have the `setenv' function. */
-#cmakedefine HAVE_SETENV ${HAVE_SETENV}
-
-/* Define to 1 if you have the `sigaltstack' function. */
-#cmakedefine HAVE_SIGALTSTACK ${HAVE_SIGALTSTACK}
-
-/* Define to 1 if you have the `strerror_r' function. */
-#cmakedefine HAVE_STRERROR_R ${HAVE_STRERROR_R}
-
-/* Define to 1 if you have the `sysconf' function. */
-#cmakedefine HAVE_SYSCONF ${HAVE_SYSCONF}
-
-/* Define to 1 if you have the <sys/mman.h> header file. */
-#cmakedefine HAVE_SYS_MMAN_H ${HAVE_SYS_MMAN_H}
-
-/* Define to 1 if you have the <sys/ioctl.h> header file. */
-#cmakedefine HAVE_SYS_IOCTL_H ${HAVE_SYS_IOCTL_H}
-
-/* Define to 1 if stat struct has st_mtimespec member .*/
-#cmakedefine HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC ${HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC}
-
-/* Define to 1 if stat struct has st_mtim member. */
-#cmakedefine HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC ${HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC}
-
-/* Define to 1 if you have the <unistd.h> header file. */
-#cmakedefine HAVE_UNISTD_H ${HAVE_UNISTD_H}
-
-/* Have host's _alloca */
-#cmakedefine HAVE__ALLOCA ${HAVE__ALLOCA}
-
-/* Define to 1 if you have the `_chsize_s' function. */
-#cmakedefine HAVE__CHSIZE_S ${HAVE__CHSIZE_S}
-
-/* Define to 1 if you have the `_Unwind_Backtrace' function. */
-#cmakedefine HAVE__UNWIND_BACKTRACE ${HAVE__UNWIND_BACKTRACE}
-
-/* Have host's __alloca */
-#cmakedefine HAVE___ALLOCA ${HAVE___ALLOCA}
-
-/* Have host's __ashldi3 */
-#cmakedefine HAVE___ASHLDI3 ${HAVE___ASHLDI3}
-
-/* Have host's __ashrdi3 */
-#cmakedefine HAVE___ASHRDI3 ${HAVE___ASHRDI3}
-
-/* Have host's __chkstk */
-#cmakedefine HAVE___CHKSTK ${HAVE___CHKSTK}
-
-/* Have host's __chkstk_ms */
-#cmakedefine HAVE___CHKSTK_MS ${HAVE___CHKSTK_MS}
-
-/* Have host's __cmpdi2 */
-#cmakedefine HAVE___CMPDI2 ${HAVE___CMPDI2}
-
-/* Have host's __divdi3 */
-#cmakedefine HAVE___DIVDI3 ${HAVE___DIVDI3}
-
-/* Have host's __fixdfdi */
-#cmakedefine HAVE___FIXDFDI ${HAVE___FIXDFDI}
-
-/* Have host's __fixsfdi */
-#cmakedefine HAVE___FIXSFDI ${HAVE___FIXSFDI}
-
-/* Have host's __floatdidf */
-#cmakedefine HAVE___FLOATDIDF ${HAVE___FLOATDIDF}
-
-/* Have host's __lshrdi3 */
-#cmakedefine HAVE___LSHRDI3 ${HAVE___LSHRDI3}
-
-/* Have host's __main */
-#cmakedefine HAVE___MAIN ${HAVE___MAIN}
-
-/* Have host's __moddi3 */
-#cmakedefine HAVE___MODDI3 ${HAVE___MODDI3}
-
-/* Have host's __udivdi3 */
-#cmakedefine HAVE___UDIVDI3 ${HAVE___UDIVDI3}
-
-/* Have host's __umoddi3 */
-#cmakedefine HAVE___UMODDI3 ${HAVE___UMODDI3}
-
-/* Have host's ___chkstk */
-#cmakedefine HAVE____CHKSTK ${HAVE____CHKSTK}
-
-/* Have host's ___chkstk_ms */
-#cmakedefine HAVE____CHKSTK_MS ${HAVE____CHKSTK_MS}
-
-/* Define if ICU library is available */
-#cmakedefine01 HAVE_ICU
-
-/* Define if Windows vendored ICU is available */
-#cmakedefine01 HAVE_WINDOWS_ICU
-
-/* Define if iconv library is available */
-#cmakedefine01 HAVE_ICONV
-
/* Linker version detected at compile time. */
#cmakedefine HOST_LINK_VERSION "${HOST_LINK_VERSION}"
@@ -267,25 +70,13 @@
/* Define to the vendor of this package. */
#cmakedefine PACKAGE_VENDOR "${PACKAGE_VENDOR}"
-/* Define to a function implementing stricmp */
-#cmakedefine stricmp ${stricmp}
-
-/* Define to a function implementing strdup */
-#cmakedefine strdup ${strdup}
-
/* Whether GlobalISel rule coverage is being collected */
#cmakedefine01 RCC_GISEL_COV_ENABLED
/* Define to the default GlobalISel coverage file prefix */
#cmakedefine RCC_GISEL_COV_PREFIX "${RCC_GISEL_COV_PREFIX}"
-/* Whether Timers signpost passes in Xcode Instruments */
-#cmakedefine01 RCC_SUPPORT_XCODE_SIGNPOSTS
-
-#cmakedefine HAVE_PROC_PID_RUSAGE 1
-
+/* Have host's builtin thread pointer. */
#cmakedefine HAVE_BUILTIN_THREAD_POINTER ${HAVE_BUILTIN_THREAD_POINTER}
-#cmakedefine HAVE_GETAUXVAL ${HAVE_GETAUXVAL}
-
#endif
diff --git a/rcc/include/rcc/Config/rcc-config.h.cmake b/rcc/include/rcc/Config/rcc-config.h.cmake
index 9c0ba18..fb76d22 100644
--- a/rcc/include/rcc/Config/rcc-config.h.cmake
+++ b/rcc/include/rcc/Config/rcc-config.h.cmake
@@ -14,19 +14,10 @@
#ifndef RCC_CONFIG_H
#define RCC_CONFIG_H
-/* Define if RCC_ENABLE_DUMP is enabled */
-#cmakedefine RCC_ENABLE_DUMP
-
/* Target triple RCC will generate code for by default */
/* Doesn't use `cmakedefine` because it is allowed to be empty. */
#define RCC_DEFAULT_TARGET_TRIPLE "${RCC_DEFAULT_TARGET_TRIPLE}"
-/* Define if threads enabled */
-#cmakedefine01 RCC_ENABLE_THREADS
-
-/* Has gcc/MSVC atomic intrinsics */
-#cmakedefine01 RCC_HAS_ATOMICS
-
/* Host triple RCC will be executed on */
#cmakedefine RCC_HOST_TRIPLE "${RCC_HOST_TRIPLE}"
@@ -54,9 +45,6 @@
/* RCC name for the native target MCA init function, if available */
#cmakedefine RCC_NATIVE_TARGETMCA RCCInitialize${RCC_NATIVE_ARCH}TargetMCA
-/* Define if this is Unixish platform */
-#cmakedefine RCC_ON_UNIX ${RCC_ON_UNIX}
-
/* Define if we have the Intel JIT API runtime support library */
#cmakedefine01 RCC_USE_INTEL_JITEVENTS
@@ -78,69 +66,24 @@
/* RCC version string */
#define RCC_VERSION_STRING "${PACKAGE_VERSION}"
-/* Whether RCC records statistics for use with GetStatistics(),
- * PrintStatistics() or PrintStatisticsJSON()
- */
-#cmakedefine01 RCC_FORCE_ENABLE_STATS
-
-/* Define if we have z3 and want to build it */
-#cmakedefine RCC_WITH_Z3 ${RCC_WITH_Z3}
-
-/* Define if we have curl and want to use it */
-#cmakedefine RCC_ENABLE_CURL ${RCC_ENABLE_CURL}
-
-/* Define if we have cpp-httplib and want to use it */
-#cmakedefine RCC_ENABLE_HTTPLIB ${RCC_ENABLE_HTTPLIB}
-
-/* Define if zlib compression is available */
-#cmakedefine01 RCC_ENABLE_ZLIB
-
-/* Define if zstd compression is available */
-#cmakedefine01 RCC_ENABLE_ZSTD
-
-/* Define if xz (lzma) decompression is available */
-#cmakedefine01 RCC_ENABLE_LZMA
-
/* Define if RCC is using tflite */
#cmakedefine RCC_HAVE_TFLITE
/* Define if we want to check profile consistency in lit tests */
#cmakedefine RCC_ENABLE_PROFCHECK
-/* Define to 1 if you have the <sysexits.h> header file. */
-#cmakedefine HAVE_SYSEXITS_H ${HAVE_SYSEXITS_H}
-
/* Define if building libRCC shared library */
#cmakedefine RCC_BUILD_RCC_DYLIB
-/* Define if building RCC with BUILD_SHARED_LIBS */
-#cmakedefine RCC_BUILD_SHARED_LIBS
-
-/* Define if exporting RCC public interface for shared library */
-#cmakedefine RCC_ENABLE_RCC_EXPORT_ANNOTATIONS
-
/* Define if exporting RCC-C public interface for shared library */
#cmakedefine RCC_ENABLE_RCC_C_EXPORT_ANNOTATIONS
-/* Define if building RCC with RCC_FORCE_USE_OLD_TOOLCHAIN_LIBS */
-#cmakedefine RCC_FORCE_USE_OLD_TOOLCHAIN ${RCC_FORCE_USE_OLD_TOOLCHAIN}
-
-/* Define if rcc_unreachable should be optimized with undefined behavior
- * in non assert builds */
-#cmakedefine01 RCC_UNREACHABLE_OPTIMIZE
-
-/* Define if building RCC with RCC_ENABLE_IO_SANDBOX */
-#cmakedefine01 RCC_ENABLE_IO_SANDBOX
-
/* Define to 1 if you have the DIA SDK installed, and to 0 if you don't. */
#cmakedefine01 RCC_ENABLE_DIA_SDK
/* Define if plugins enabled */
#cmakedefine RCC_ENABLE_PLUGINS
-/* Define if logf128 is available */
-#cmakedefine RCC_HAS_LOGF128
-
/* Define if building RCC with RCC_ENABLE_TELEMETRY */
#cmakedefine01 RCC_ENABLE_TELEMETRY
@@ -152,7 +95,4 @@
coverage bugs, and to 0 otherwise. */
#cmakedefine01 RCC_ENABLE_DEBUGLOC_TRACKING_ORIGIN
-/* Define to 1 to enable RCC OnDisk Content Addressable Storage */
-#cmakedefine01 RCC_ENABLE_ONDISK_CAS
-
#endif
diff --git a/rcc/include/rcc/Support/Compiler.h b/rcc/include/rcc/Support/Compiler.h
index 511c4cd..1076827 100644
--- a/rcc/include/rcc/Support/Compiler.h
+++ b/rcc/include/rcc/Support/Compiler.h
@@ -19,154 +19,40 @@
<http://www.gnu.org/licenses/>.
*/
-//===-- llvm/Support/Compiler.h - Compiler abstraction support --*- C++ -*-===//
+//===-- rcc/Support/Compiler.h - RCC ABI support ---------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
-//
-// This file defines several macros, based on the current compiler. This allows
-// use of compiler-specific features in a way that remains portable. This header
-// can be included from either C or C++.
-//
-//===----------------------------------------------------------------------===//
#ifndef RCC_SUPPORT_COMPILER_H
#define RCC_SUPPORT_COMPILER_H
+#include "core/compiler/Compiler.h"
#include "rcc/Config/rcc-config.h"
-#include <stddef.h>
-
-#if defined(_MSC_VER)
-#include <sal.h>
-#endif
-
-#ifndef __has_feature
-#define __has_feature(x) 0
-#endif
-
-#ifndef __has_extension
-#define __has_extension(x) 0
-#endif
-
-#ifndef __has_attribute
-#define __has_attribute(x) 0
-#endif
-
-#ifndef __has_builtin
-#define __has_builtin(x) 0
-#endif
-
-#ifndef __has_warning
-#define __has_warning(x) 0
-#endif
-
-// Only use __has_cpp_attribute in C++ mode. GCC defines __has_cpp_attribute in
-// C mode, but the :: in __has_cpp_attribute(scoped::attribute) is invalid.
-#ifndef RCC_HAS_CPP_ATTRIBUTE
-#if defined(__cplusplus) && defined(__has_cpp_attribute)
-#define RCC_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute (x)
-#else
-#define RCC_HAS_CPP_ATTRIBUTE(x) 0
-#endif
-#endif
-
-/// \macro RCC_GNUC_PREREQ
-/// Extend the default __GNUC_PREREQ even if glibc's features.h isn't
-/// available.
-#ifndef RCC_GNUC_PREREQ
-#if defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__)
-#define RCC_GNUC_PREREQ(maj, min, patch) \
- ((__GNUC__ << 20) + (__GNUC_MINOR__ << 10) + __GNUC_PATCHLEVEL__ >= \
- ((maj) << 20) + ((min) << 10) + (patch))
-#elif defined(__GNUC__) && defined(__GNUC_MINOR__)
-#define RCC_GNUC_PREREQ(maj, min, patch) \
- ((__GNUC__ << 20) + (__GNUC_MINOR__ << 10) >= ((maj) << 20) + ((min) << 10))
-#else
-#define RCC_GNUC_PREREQ(maj, min, patch) 0
-#endif
-#endif
-
-/// \macro RCC_MSC_PREREQ
-/// Is the compiler MSVC of at least the specified version?
-/// The common \param version values to check for are:
-/// * 1910: VS2017, version 15.1 & 15.2
-/// * 1911: VS2017, version 15.3 & 15.4
-/// * 1912: VS2017, version 15.5
-/// * 1913: VS2017, version 15.6
-/// * 1914: VS2017, version 15.7
-/// * 1915: VS2017, version 15.8
-/// * 1916: VS2017, version 15.9
-/// * 1920: VS2019, version 16.0
-/// * 1921: VS2019, version 16.1
-/// * 1922: VS2019, version 16.2
-/// * 1923: VS2019, version 16.3
-/// * 1924: VS2019, version 16.4
-/// * 1925: VS2019, version 16.5
-/// * 1926: VS2019, version 16.6
-/// * 1927: VS2019, version 16.7
-/// * 1928: VS2019, version 16.8 + 16.9
-/// * 1929: VS2019, version 16.10 + 16.11
-/// * 1930: VS2022, version 17.0
-#ifdef _MSC_VER
-#define RCC_MSC_PREREQ(version) (_MSC_VER >= (version))
-
-// We require at least VS 2019.
-#if !defined(RCC_FORCE_USE_OLD_TOOLCHAIN)
-#if !RCC_MSC_PREREQ(1920)
-#error RCC requires at least VS 2019.
-#endif
-#endif
-
-#else
-#define RCC_MSC_PREREQ(version) 0
-#endif
-
-/// RCC_LIBRARY_VISIBILITY - If a class marked with this attribute is linked
-/// into a shared library, then the class should be private to the library and
-/// not accessible from outside it. Can also be used to mark variables and
-/// functions, making them private to any shared library they are linked into.
-/// On PE/COFF targets, library visibility is the default, so this isn't needed.
-///
/// RCC_EXTERNAL_VISIBILITY - classes, functions, and variables marked with
-/// this attribute will be made public and visible outside of any shared library
-/// they are linked in to.
-
-#if RCC_HAS_CPP_ATTRIBUTE(gnu::visibility) && defined(__GNUC__) && \
- !defined(__clang__)
-#define RCC_ATTRIBUTE_VISIBILITY_HIDDEN [[gnu::visibility ("hidden")]]
-#define RCC_ATTRIBUTE_VISIBILITY_DEFAULT [[gnu::visibility ("default")]]
-#elif __has_attribute(visibility)
-#define RCC_ATTRIBUTE_VISIBILITY_HIDDEN __attribute__ ((visibility ("hidden")))
-#define RCC_ATTRIBUTE_VISIBILITY_DEFAULT \
- __attribute__ ((visibility ("default")))
-#else
-#define RCC_ATTRIBUTE_VISIBILITY_HIDDEN
-#define RCC_ATTRIBUTE_VISIBILITY_DEFAULT
-#endif
-
+/// this attribute will be made public and visible outside of a shared library.
#if defined(RCC_BUILD_RCC_DYLIB) || defined(RCC_BUILD_SHARED_LIBS)
-#define RCC_EXTERNAL_VISIBILITY RCC_ATTRIBUTE_VISIBILITY_DEFAULT
+#define RCC_EXTERNAL_VISIBILITY LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
#else
#define RCC_EXTERNAL_VISIBILITY
#endif
-#if (!(defined(_WIN32) || defined(__CYGWIN__)) || \
+/// RCC_LIBRARY_VISIBILITY - Mark symbols private to the RCC shared library.
+#if (!(defined(_WIN32) || defined(__CYGWIN__)) || \
((defined(__MINGW32__) || defined(__CYGWIN__)) && defined(__clang__)))
-#define RCC_LIBRARY_VISIBILITY RCC_ATTRIBUTE_VISIBILITY_HIDDEN
-// Clang compilers older then 15 do not support gnu style attributes on
-// namespaces.
+#define RCC_LIBRARY_VISIBILITY LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN
#if defined(__clang__) && __clang_major__ < 15
-#define RCC_LIBRARY_VISIBILITY_NAMESPACE [[gnu::visibility ("hidden")]]
+#define RCC_LIBRARY_VISIBILITY_NAMESPACE [[gnu::visibility("hidden")]]
#else
-#define RCC_LIBRARY_VISIBILITY_NAMESPACE RCC_ATTRIBUTE_VISIBILITY_HIDDEN
+#define RCC_LIBRARY_VISIBILITY_NAMESPACE LIBBASE_ATTRIBUTE_VISIBILITY_HIDDEN
#endif
-#define RCC_ALWAYS_EXPORT RCC_ATTRIBUTE_VISIBILITY_DEFAULT
+#define RCC_ALWAYS_EXPORT LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
#elif defined(_WIN32)
-#define RCC_ALWAYS_EXPORT __declspec (dllexport)
+#define RCC_ALWAYS_EXPORT __declspec(dllexport)
#define RCC_LIBRARY_VISIBILITY
#define RCC_LIBRARY_VISIBILITY_NAMESPACE
#else
@@ -175,58 +61,33 @@
#define RCC_LIBRARY_VISIBILITY_NAMESPACE
#endif
-/// RCC_ABI is the main export/visibility macro to mark something as explicitly
-/// exported when rcc is built as a shared library with everything else that is
-/// unannotated will have internal visibility.
-///
-/// RCC_ABI_EXPORT is for the special case for things like plugin symbol
-/// declarations or definitions where we don't want the macro to be switching
-/// between dllexport and dllimport on windows based on what codebase is being
-/// built, it will only be dllexport. For non windows platforms this macro
-/// behaves the same as RCC_ABI.
-///
-/// RCC_EXPORT_TEMPLATE is used on explicit template instantiations in source
-/// files that were declared extern in a header. This macro is only set as a
-/// compiler export attribute on windows, on other platforms it does nothing.
-///
-/// RCC_TEMPLATE_ABI is for annotating extern template declarations in headers
-/// for both functions and classes. On windows its turned in to dllimport for
-/// library consumers, for other platforms its a default visibility attribute.
-///
-/// RCC_ABI_FOR_TEST is for annotating symbols that are exported from a
-/// library-internal header solely so that unit tests can link against them.
-/// Symbols in RCC's public headers are part of the RCC public interface and
-/// should use RCC_ABI. RCC_ABI_FOR_TEST is reserved for internal headers,
-/// whose symbols could be conditionally excluded when not building tests in the
-/// future.
-///
+/// RCC_ABI is the main export macro for the RCC public interface.
+/// RCC_ABI_EXPORT always exports a symbol from the defining library.
+/// RCC_EXPORT_TEMPLATE marks explicit template instantiations.
+/// RCC_TEMPLATE_ABI marks extern template declarations.
#ifndef RCC_ABI_GENERATING_ANNOTATIONS
-// Marker to add to classes or functions in public headers that should not have
-// export macros added to them by the clang tool
#define RCC_ABI_NOT_EXPORTED
-// TODO(https://github.com/rcc/rcc-project/issues/145406): eliminate need for
-// two preprocessor definitions to gate RCC_ABI macro definitions.
#if defined(RCC_ENABLE_RCC_EXPORT_ANNOTATIONS) && !defined(RCC_BUILD_STATIC)
#if defined(_WIN32) && !defined(__MINGW32__)
#if defined(RCC_EXPORTS)
-#define RCC_ABI __declspec (dllexport)
+#define RCC_ABI __declspec(dllexport)
#define RCC_TEMPLATE_ABI
-#define RCC_EXPORT_TEMPLATE __declspec (dllexport)
+#define RCC_EXPORT_TEMPLATE __declspec(dllexport)
#else
-#define RCC_ABI __declspec (dllimport)
-#define RCC_TEMPLATE_ABI __declspec (dllimport)
+#define RCC_ABI __declspec(dllimport)
+#define RCC_TEMPLATE_ABI __declspec(dllimport)
#define RCC_EXPORT_TEMPLATE
#endif
-#define RCC_ABI_EXPORT __declspec (dllexport)
+#define RCC_ABI_EXPORT __declspec(dllexport)
#elif __has_attribute(visibility)
#if defined(__ELF__) || defined(__MINGW32__) || defined(_AIX) || \
defined(__MVS__) || defined(__CYGWIN__)
-#define RCC_ABI __attribute__ ((visibility ("default")))
+#define RCC_ABI LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
#define RCC_TEMPLATE_ABI RCC_ABI
#define RCC_EXPORT_TEMPLATE
#define RCC_ABI_EXPORT RCC_ABI
#elif defined(__MACH__) || defined(__wasm__) || defined(__EMSCRIPTEN__)
-#define RCC_ABI __attribute__ ((visibility ("default")))
+#define RCC_ABI LIBBASE_ATTRIBUTE_VISIBILITY_DEFAULT
#define RCC_TEMPLATE_ABI
#define RCC_EXPORT_TEMPLATE
#define RCC_ABI_EXPORT RCC_ABI
@@ -242,612 +103,4 @@
#define RCC_ABI_FOR_TEST RCC_ABI
#endif
-#if defined(__GNUC__)
-#define RCC_PREFETCH(addr, rw, locality) __builtin_prefetch (addr, rw, locality)
-#else
-#define RCC_PREFETCH(addr, rw, locality)
-#endif
-
-#if __has_attribute(uninitialized)
-#define RCC_ATTRIBUTE_UNINITIALIZED __attribute__ ((uninitialized))
-#else
-#define RCC_ATTRIBUTE_UNINITIALIZED
-#endif
-
-#if __has_attribute(used)
-#define RCC_ATTRIBUTE_USED __attribute__ ((__used__))
-#else
-#define RCC_ATTRIBUTE_USED
-#endif
-
-#if __has_attribute(warn_unused)
-#define RCC_ATTRIBUTE_WARN_UNUSED __attribute__ ((warn_unused))
-#else
-#define RCC_ATTRIBUTE_WARN_UNUSED
-#endif
-
-// Only enabled for clang:
-// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99587
-// GCC may produce "warning: 'retain' attribute ignored" (despite
-// __has_attribute(retain) being 1).
-#if defined(__clang__) && __has_attribute(retain)
-#define RCC_ATTRIBUTE_RETAIN __attribute__ ((__retain__))
-#else
-#define RCC_ATTRIBUTE_RETAIN
-#endif
-
-// For deprecations that are not a simple rename, use [[deprecated(MSG)]]
-// instead.
-#if defined(__clang__)
-#define RCC_DEPRECATED_WITH_FIXIT(MSG, FIX) \
- __attribute__ ((deprecated (MSG, FIX)))
-#else
-#define RCC_DEPRECATED_WITH_FIXIT(MSG, FIX) [[deprecated (MSG)]]
-#endif
-
-// clang-format off
-#if defined(__clang__) || defined(__GNUC__)
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH \
- _Pragma("GCC diagnostic push") \
- _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_POP \
- _Pragma("GCC diagnostic pop")
-#elif defined(_MSC_VER)
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH \
- __pragma(warning(push)) \
- __pragma(warning(disable : 4996))
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_POP \
- __pragma(warning(pop))
-#else
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_PUSH
-#define RCC_SUPPRESS_DEPRECATED_DECLARATIONS_POP
-#endif
-// clang-format on
-
-// Indicate that a non-static, non-const C++ member function reinitializes
-// the entire object to a known state, independent of the previous state of
-// the object.
-//
-// The clang-tidy check bugprone-use-after-move recognizes this attribute as a
-// marker that a moved-from object has left the indeterminate state and can be
-// reused.
-#if RCC_HAS_CPP_ATTRIBUTE(clang::reinitializes)
-#define RCC_ATTRIBUTE_REINITIALIZES [[clang::reinitializes]]
-#else
-#define RCC_ATTRIBUTE_REINITIALIZES
-#endif
-
-// Some compilers warn about unused functions. When a function is sometimes
-// used or not depending on build settings (e.g. a function only called from
-// within "assert"), this attribute can be used to suppress such warnings.
-//
-// However, it shouldn't be used for unused *variables*, as those have a much
-// more portable solution:
-// (void)unused_var_name;
-// Prefer cast-to-void wherever it is sufficient.
-#if __has_attribute(unused)
-#define RCC_ATTRIBUTE_UNUSED __attribute__ ((__unused__))
-#else
-#define RCC_ATTRIBUTE_UNUSED
-#endif
-
-// FIXME: Provide this for PE/COFF targets.
-#if __has_attribute(weak) && !defined(__MINGW32__) && !defined(__CYGWIN__) && \
- !defined(_WIN32)
-#define RCC_HAS_ATTRIBUTE_WEAK 1
-#else
-#define RCC_HAS_ATTRIBUTE_WEAK 0
-#endif
-#if RCC_HAS_ATTRIBUTE_WEAK
-#define RCC_ATTRIBUTE_WEAK __attribute__ ((__weak__))
-#else
-#define RCC_ATTRIBUTE_WEAK
-#endif
-
-// Prior to clang 3.2, clang did not accept any spelling of
-// __has_attribute(const), so assume it is supported.
-#if defined(__clang__) || defined(__GNUC__)
-// aka 'CONST' but following RCC Conventions.
-#define RCC_READNONE __attribute__ ((__const__))
-#else
-#define RCC_READNONE
-#endif
-
-#if __has_attribute(pure) || defined(__GNUC__)
-// aka 'PURE' but following RCC Conventions.
-#define RCC_READONLY __attribute__ ((__pure__))
-#else
-#define RCC_READONLY
-#endif
-
-#if __has_attribute(minsize)
-#define RCC_ATTRIBUTE_MINSIZE __attribute__ ((minsize))
-#else
-#define RCC_ATTRIBUTE_MINSIZE
-#endif
-
-#if __has_builtin(__builtin_expect) || defined(__GNUC__)
-#define RCC_LIKELY(EXPR) __builtin_expect ((bool)(EXPR), true)
-#define RCC_UNLIKELY(EXPR) __builtin_expect ((bool)(EXPR), false)
-#else
-#define RCC_LIKELY(EXPR) (EXPR)
-#define RCC_UNLIKELY(EXPR) (EXPR)
-#endif
-
-/// RCC_ATTRIBUTE_NOINLINE - On compilers where we have a directive to do so,
-/// mark a method "not for inlining".
-#if __has_attribute(noinline)
-#define RCC_ATTRIBUTE_NOINLINE __attribute__ ((noinline))
-#elif defined(_MSC_VER)
-#define RCC_ATTRIBUTE_NOINLINE __declspec (noinline)
-#else
-#define RCC_ATTRIBUTE_NOINLINE
-#endif
-
-/// RCC_ATTRIBUTE_ALWAYS_INLINE - On compilers where we have a directive to do
-/// so, mark a method "always inline" because it is performance sensitive.
-#if __has_attribute(always_inline)
-#define RCC_ATTRIBUTE_ALWAYS_INLINE inline __attribute__ ((always_inline))
-#elif defined(_MSC_VER)
-#define RCC_ATTRIBUTE_ALWAYS_INLINE __forceinline
-#else
-#define RCC_ATTRIBUTE_ALWAYS_INLINE inline
-#endif
-
-/// RCC_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG - Like
-/// RCC_ATTRIBUTE_ALWAYS_INLINE but disabled in debug builds to avoid stack
-/// overflow with deep recursion.
-#if defined(NDEBUG)
-#define RCC_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG RCC_ATTRIBUTE_ALWAYS_INLINE
-#else
-#define RCC_ATTRIBUTE_ALWAYS_INLINE_UNLESS_DEBUG inline
-#endif
-
-/// RCC_ATTRIBUTE_NO_DEBUG - On compilers where we have a directive to do
-/// so, mark a method "no debug" because debug info makes the debugger
-/// experience worse.
-#if __has_attribute(nodebug)
-#define RCC_ATTRIBUTE_NODEBUG __attribute__ ((nodebug))
-#else
-#define RCC_ATTRIBUTE_NODEBUG
-#endif
-
-#if __has_attribute(returns_nonnull)
-#define RCC_ATTRIBUTE_RETURNS_NONNULL __attribute__ ((returns_nonnull))
-#elif defined(_MSC_VER)
-#define RCC_ATTRIBUTE_RETURNS_NONNULL _Ret_notnull_
-#else
-#define RCC_ATTRIBUTE_RETURNS_NONNULL
-#endif
-
-/// RCC_ATTRIBUTE_RESTRICT - Annotates a pointer to tell the compiler that
-/// it is not aliased in the current scope.
-#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER)
-#define RCC_ATTRIBUTE_RESTRICT __restrict
-#else
-#define RCC_ATTRIBUTE_RESTRICT
-#endif
-
-/// \macro RCC_ATTRIBUTE_RETURNS_NOALIAS Used to mark a function as returning a
-/// pointer that does not alias any other valid pointer.
-#ifdef __GNUC__
-#define RCC_ATTRIBUTE_RETURNS_NOALIAS __attribute__ ((__malloc__))
-#elif defined(_MSC_VER)
-#define RCC_ATTRIBUTE_RETURNS_NOALIAS __declspec (restrict)
-#else
-#define RCC_ATTRIBUTE_RETURNS_NOALIAS
-#endif
-
-/// RCC_FALLTHROUGH - Mark fallthrough cases in switch statements.
-#if defined(__cplusplus) && __cplusplus > 201402L && \
- RCC_HAS_CPP_ATTRIBUTE(fallthrough)
-#define RCC_FALLTHROUGH [[fallthrough]]
-#elif RCC_HAS_CPP_ATTRIBUTE(gnu::fallthrough)
-#define RCC_FALLTHROUGH [[gnu::fallthrough]]
-#elif __has_attribute(fallthrough)
-#define RCC_FALLTHROUGH __attribute__ ((fallthrough))
-#elif RCC_HAS_CPP_ATTRIBUTE(clang::fallthrough)
-#define RCC_FALLTHROUGH [[clang::fallthrough]]
-#else
-#define RCC_FALLTHROUGH
-#endif
-
-/// RCC_REQUIRE_CONSTANT_INITIALIZATION - Apply this to globals to ensure that
-/// they are constant initialized.
-#if RCC_HAS_CPP_ATTRIBUTE(clang::require_constant_initialization)
-#define RCC_REQUIRE_CONSTANT_INITIALIZATION \
- [[clang::require_constant_initialization]]
-#else
-#define RCC_REQUIRE_CONSTANT_INITIALIZATION
-#endif
-
-/// RCC_GSL_OWNER - Apply this to owning classes like SmallVector to enable
-/// lifetime warnings.
-#if RCC_HAS_CPP_ATTRIBUTE(gsl::Owner)
-#define RCC_GSL_OWNER [[gsl::Owner]]
-#else
-#define RCC_GSL_OWNER
-#endif
-
-/// RCC_GSL_POINTER - Apply this to non-owning classes like
-/// StringRef to enable lifetime warnings.
-#if RCC_HAS_CPP_ATTRIBUTE(gsl::Pointer)
-#define RCC_GSL_POINTER [[gsl::Pointer]]
-#else
-#define RCC_GSL_POINTER
-#endif
-
-#if RCC_HAS_CPP_ATTRIBUTE(clang::lifetimebound)
-#define RCC_LIFETIME_BOUND [[clang::lifetimebound]]
-#else
-#define RCC_LIFETIME_BOUND
-#endif
-
-#if RCC_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L
-#define RCC_CTOR_NODISCARD [[nodiscard]]
-#else
-#define RCC_CTOR_NODISCARD
-#endif
-
-// Macro to suppress the MSVC warning C4848:
-// "support for attribute [[msvc::no_unique_address]] in C++17 and earlier
-// is a vendor extension".
-// This warning is removed in versions >= 19.43.
-#if !defined(_MSC_VER) || _MSC_VER >= 1943 || defined(__clang__)
-#define RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH
-#define RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP
-#else // MSVC < 19.43
-#define RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH \
- __pragma (warning (push)) __pragma (warning (disable : 4848))
-#define RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP __pragma (warning (pop))
-#endif
-
-#if RCC_HAS_CPP_ATTRIBUTE(no_unique_address)
-#define RCC_NO_UNIQUE_ADDRESS [[no_unique_address]]
-#elif RCC_HAS_CPP_ATTRIBUTE(msvc::no_unique_address)
-#define RCC_NO_UNIQUE_ADDRESS \
- RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_PUSH \
- [[msvc::no_unique_address]] RCC_SUPPRESS_MSVC_ATTR_IS_VENDOR_EXT_POP
-#else
-#define RCC_NO_UNIQUE_ADDRESS
-#endif
-
-/// RCC_EXTENSION - Support compilers where we have a keyword to suppress
-/// pedantic diagnostics.
-#ifdef __GNUC__
-#define RCC_EXTENSION __extension__
-#else
-#define RCC_EXTENSION
-#endif
-
-/// RCC_BUILTIN_UNREACHABLE - On compilers which support it, expands
-/// to an expression which states that it is undefined behavior for the
-/// compiler to reach this point. Otherwise is not defined.
-///
-/// '#else' is intentionally left out so that other macro logic (e.g.,
-/// RCC_ASSUME_ALIGNED and rcc_unreachable()) can detect whether
-/// RCC_BUILTIN_UNREACHABLE has a definition.
-#if __has_builtin(__builtin_unreachable) || defined(__GNUC__)
-#define RCC_BUILTIN_UNREACHABLE __builtin_unreachable ()
-#elif defined(_MSC_VER)
-#define RCC_BUILTIN_UNREACHABLE __assume (false)
-#endif
-
-/// RCC_BUILTIN_TRAP - On compilers which support it, expands to an expression
-/// which causes the program to exit abnormally.
-#if __has_builtin(__builtin_trap) || defined(__GNUC__)
-#define RCC_BUILTIN_TRAP __builtin_trap ()
-#elif defined(_MSC_VER)
-// The __debugbreak intrinsic is supported by MSVC, does not require forward
-// declarations involving platform-specific typedefs (unlike RaiseException),
-// results in a call to vectored exception handlers, and encodes to a short
-// instruction that still causes the trapping behavior we want.
-#define RCC_BUILTIN_TRAP __debugbreak ()
-#else
-#define RCC_BUILTIN_TRAP *(volatile int *)0x11 = 0
-#endif
-
-/// RCC_BUILTIN_DEBUGTRAP - On compilers which support it, expands to
-/// an expression which causes the program to break while running
-/// under a debugger.
-#if __has_builtin(__builtin_debugtrap)
-#define RCC_BUILTIN_DEBUGTRAP __builtin_debugtrap ()
-#elif defined(_MSC_VER)
-// The __debugbreak intrinsic is supported by MSVC and breaks while
-// running under the debugger, and also supports invoking a debugger
-// when the OS is configured appropriately.
-#define RCC_BUILTIN_DEBUGTRAP __debugbreak ()
-#else
-// Just continue execution when built with compilers that have no
-// support. This is a debugging aid and not intended to force the
-// program to abort if encountered.
-#define RCC_BUILTIN_DEBUGTRAP
-#endif
-
-/// \macro RCC_ASSUME_ALIGNED
-/// Returns a pointer with an assumed alignment.
-#if __has_builtin(__builtin_assume_aligned) || defined(__GNUC__)
-#define RCC_ASSUME_ALIGNED(p, a) __builtin_assume_aligned (p, a)
-#elif defined(RCC_BUILTIN_UNREACHABLE)
-#define RCC_ASSUME_ALIGNED(p, a) \
- (((uintptr_t (p) % (a)) == 0) ? (p) : (RCC_BUILTIN_UNREACHABLE, (p)))
-#else
-#define RCC_ASSUME_ALIGNED(p, a) (p)
-#endif
-
-/// \macro RCC_PACKED
-/// Used to specify a packed structure.
-/// RCC_PACKED(
-/// struct A {
-/// int i;
-/// int j;
-/// int k;
-/// long long l;
-/// });
-///
-/// RCC_PACKED_START
-/// struct B {
-/// int i;
-/// int j;
-/// int k;
-/// long long l;
-/// };
-/// RCC_PACKED_END
-#ifdef _MSC_VER
-#define RCC_PACKED(d) __pragma (pack (push, 1)) d __pragma (pack (pop))
-#define RCC_PACKED_START __pragma (pack (push, 1))
-#define RCC_PACKED_END __pragma (pack (pop))
-#else
-#define RCC_PACKED(d) d __attribute__ ((packed))
-#define RCC_PACKED_START _Pragma ("pack(push, 1)")
-#define RCC_PACKED_END _Pragma ("pack(pop)")
-#endif
-
-/// \macro RCC_MEMORY_SANITIZER_BUILD
-/// Whether RCC itself is built with MemorySanitizer instrumentation.
-#if __has_feature(memory_sanitizer)
-#define RCC_MEMORY_SANITIZER_BUILD 1
-#include <sanitizer/msan_interface.h>
-#define RCC_NO_SANITIZE_MEMORY_ATTRIBUTE __attribute__ ((no_sanitize_memory))
-#else
-#define RCC_MEMORY_SANITIZER_BUILD 0
-#define __msan_allocated_memory(p, size)
-#define __msan_unpoison(p, size)
-#define RCC_NO_SANITIZE_MEMORY_ATTRIBUTE
-#endif
-
-/// \macro RCC_ADDRESS_SANITIZER_BUILD
-/// Whether RCC itself is built with AddressSanitizer instrumentation.
-#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
-#define RCC_ADDRESS_SANITIZER_BUILD 1
-#if __has_include(<sanitizer/asan_interface.h>)
-#include <sanitizer/asan_interface.h>
-#else
-// These declarations exist to support ASan with MSVC. If MSVC eventually ships
-// asan_interface.h in their headers, then we can remove this.
-#ifdef __cplusplus
-extern "C"
-{
-#endif
- void
- __asan_poison_memory_region (void const volatile *addr, size_t size);
- void
- __asan_unpoison_memory_region (void const volatile *addr, size_t size);
-#ifdef __cplusplus
-} // extern "C"
-#endif
-#endif
-#else
-#define RCC_ADDRESS_SANITIZER_BUILD 0
-#define __asan_poison_memory_region(p, size)
-#define __asan_unpoison_memory_region(p, size)
-#endif
-
-/// \macro RCC_HWADDRESS_SANITIZER_BUILD
-/// Whether RCC itself is built with HWAddressSanitizer instrumentation.
-#if __has_feature(hwaddress_sanitizer)
-#define RCC_HWADDRESS_SANITIZER_BUILD 1
-#else
-#define RCC_HWADDRESS_SANITIZER_BUILD 0
-#endif
-
-/// \macro RCC_THREAD_SANITIZER_BUILD
-/// Whether RCC itself is built with ThreadSanitizer instrumentation.
-#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__)
-#define RCC_THREAD_SANITIZER_BUILD 1
-#else
-#define RCC_THREAD_SANITIZER_BUILD 0
-#endif
-
-#if RCC_THREAD_SANITIZER_BUILD
-// Thread Sanitizer is a tool that finds races in code.
-// See http://code.google.com/p/data-race-test/wiki/DynamicAnnotations .
-// tsan detects these exact functions by name.
-#ifdef __cplusplus
-extern "C"
-{
-#endif
- void
- AnnotateHappensAfter (const char *file, int line, const volatile void *cv);
- void
- AnnotateHappensBefore (const char *file, int line, const volatile void *cv);
- void
- AnnotateIgnoreWritesBegin (const char *file, int line);
- void
- AnnotateIgnoreWritesEnd (const char *file, int line);
-#ifdef __cplusplus
-}
-#endif
-
-// This marker is used to define a happens-before arc. The race detector will
-// infer an arc from the begin to the end when they share the same pointer
-// argument.
-#define TsanHappensBefore(cv) AnnotateHappensBefore (__FILE__, __LINE__, cv)
-
-// This marker defines the destination of a happens-before arc.
-#define TsanHappensAfter(cv) AnnotateHappensAfter (__FILE__, __LINE__, cv)
-
-// Ignore any races on writes between here and the next TsanIgnoreWritesEnd.
-#define TsanIgnoreWritesBegin() AnnotateIgnoreWritesBegin (__FILE__, __LINE__)
-
-// Resume checking for racy writes.
-#define TsanIgnoreWritesEnd() AnnotateIgnoreWritesEnd (__FILE__, __LINE__)
-#else
-#define TsanHappensBefore(cv)
-#define TsanHappensAfter(cv)
-#define TsanIgnoreWritesBegin()
-#define TsanIgnoreWritesEnd()
-#endif
-
-/// \macro RCC_NO_SANITIZE
-/// Disable a particular sanitizer for a function.
-#if __has_attribute(no_sanitize)
-#define RCC_NO_SANITIZE(KIND) __attribute__ ((no_sanitize (KIND)))
-#else
-#define RCC_NO_SANITIZE(KIND)
-#endif
-
-/// Mark debug helper function definitions like dump() that should not be
-/// stripped from debug builds.
-/// Note that you should also surround dump() functions with
-/// `#if !defined(NDEBUG) || defined(RCC_ENABLE_DUMP)` so they do always
-/// get stripped in release builds.
-// FIXME: Move this to a private config.h as it's not usable in public headers.
-#if !defined(NDEBUG) || defined(RCC_ENABLE_DUMP)
-#define RCC_DUMP_METHOD \
- RCC_ATTRIBUTE_NOINLINE RCC_ATTRIBUTE_USED RCC_ATTRIBUTE_RETAIN
-#else
-#define RCC_DUMP_METHOD RCC_ATTRIBUTE_NOINLINE
-#endif
-
-/// \macro RCC_PRETTY_FUNCTION
-/// Gets a user-friendly looking function signature for the current scope
-/// using the best available method on each platform. The exact format of the
-/// resulting string is implementation specific and non-portable, so this should
-/// only be used, for example, for logging or diagnostics.
-#if defined(_MSC_VER)
-#define RCC_PRETTY_FUNCTION __FUNCSIG__
-#elif defined(__GNUC__) || defined(__clang__)
-#define RCC_PRETTY_FUNCTION __PRETTY_FUNCTION__
-#else
-#define RCC_PRETTY_FUNCTION __func__
-#endif
-
-/// \macro RCC_THREAD_LOCAL
-/// A thread-local storage specifier which can be used with globals,
-/// extern globals, and static globals.
-///
-/// This is essentially an extremely restricted analog to C++11's thread_local
-/// support. It uses thread_local if available, falling back on gcc __thread
-/// if not. __thread doesn't support many of the C++11 thread_local's
-/// features. You should only use this for PODs that you can statically
-/// initialize to some constant value. In almost all circumstances this is most
-/// appropriate for use with a pointer, integer, or small aggregation of
-/// pointers and integers.
-#if RCC_ENABLE_THREADS
-#if __has_feature(cxx_thread_local) || defined(_MSC_VER)
-#define RCC_THREAD_LOCAL thread_local
-#else
-// Clang, GCC, and other compatible compilers used __thread prior to C++11 and
-// we only need the restricted functionality that provides.
-#define RCC_THREAD_LOCAL __thread
-#endif
-#else // !RCC_ENABLE_THREADS
-// If threading is disabled entirely, this compiles to nothing and you get
-// a normal global variable.
-#define RCC_THREAD_LOCAL
-#endif
-
-/// \macro RCC_ENABLE_EXCEPTIONS
-/// Whether RCC is built with exception support.
-#if __has_feature(cxx_exceptions)
-#define RCC_ENABLE_EXCEPTIONS 1
-#elif defined(__GNUC__) && defined(__EXCEPTIONS)
-#define RCC_ENABLE_EXCEPTIONS 1
-#elif defined(_MSC_VER) && defined(_CPPUNWIND)
-#define RCC_ENABLE_EXCEPTIONS 1
-#endif
-
-/// \macro RCC_NO_PROFILE_INSTRUMENT_FUNCTION
-/// Disable the profile instrument for a function.
-#if __has_attribute(no_profile_instrument_function)
-#define RCC_NO_PROFILE_INSTRUMENT_FUNCTION \
- __attribute__ ((no_profile_instrument_function))
-#else
-#define RCC_NO_PROFILE_INSTRUMENT_FUNCTION
-#endif
-
-/// \macro RCC_PREFERRED_TYPE
-/// Adjust type of bit-field in debug info.
-#if __has_attribute(preferred_type)
-#define RCC_PREFERRED_TYPE(T) __attribute__ ((preferred_type (T)))
-#else
-#define RCC_PREFERRED_TYPE(T)
-#endif
-
-#if RCC_HAS_CPP_ATTRIBUTE(clang::ptrauth_vtable_pointer) && \
- (defined(__PTRAUTH__) || __has_feature(ptrauth_calls))
-#define RCC_MOVABLE_POLYMORPHIC_TYPE \
- [[clang::ptrauth_vtable_pointer (default_key, no_address_discrimination, \
- default_extra_discrimination)]]
-#else
-#define RCC_MOVABLE_POLYMORPHIC_TYPE
-#endif
-
-/// \macro RCC_VIRTUAL_ANCHOR_FUNCTION
-/// This macro is used to adhere to RCC's policy that each class with a vtable
-/// must have at least one out-of-line virtual function. This macro allows us
-/// to declare such a function in `final` classes without triggering a warning.
-// clang-format off
-// Autoformatting makes this look awful.
-#if defined(__clang__)
- // Make sure this is only parsed if __clang__ is defined
- #if __has_warning("-Wunnecessary-virtual-specifier")
- #define RCC_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
- _Pragma("clang diagnostic push") \
- _Pragma("clang diagnostic ignored \"-Wunnecessary-virtual-specifier\"") \
- virtual void anchor() \
- _Pragma("clang diagnostic pop")
- #else // __has_warning
- #define RCC_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
- virtual void anchor()
- #endif
-#else // defined(__clang__)
- #define RCC_DECLARE_VIRTUAL_ANCHOR_FUNCTION() \
- virtual void anchor()
-#endif
-// clang-format on
-
-/// \macro RCC_IS_X86
-/// Whether the target architecture is x86 / x86-64.
-#if defined(__x86_64__) || defined(__i386__)
-#define RCC_IS_X86 1
-#else
-#define RCC_IS_X86 0
-#endif
-
-/// \macro RCC_TARGET_SSE42
-/// Function attribute to compile a function with SSE4.2 enabled.
-#if defined(__has_attribute) && __has_attribute(target)
-#define RCC_TARGET_SSE42 __attribute__ ((target ("sse4.2")))
-#else
-#define RCC_TARGET_SSE42
-#endif
-
-#if __has_builtin(__builtin_cpu_supports) && \
- (defined(__linux__) || defined(__APPLE__))
-#define RCC_CPU_SUPPORTS(feature) __builtin_cpu_supports (feature)
-#else
-#define RCC_CPU_SUPPORTS(feature) 0
-#endif
-
-/// \macro RCC_CPU_SUPPORTS_SSE42
-/// Expands to true if the runtime cpu supports SSE4.2, or if compiled with
-/// SSE4.2 enabled.
-#if defined(__SSE4_2__)
-#define RCC_CPU_SUPPORTS_SSE42 1
-#else
-#define RCC_CPU_SUPPORTS_SSE42 RCC_CPU_SUPPORTS ("sse4.2")
-#endif
-
#endif
diff --git a/rcc/include/rcc/Support/ErrorOr.h b/rcc/include/rcc/Support/ErrorOr.h
deleted file mode 100644
index 0e17822..0000000
--- a/rcc/include/rcc/Support/ErrorOr.h
+++ /dev/null
@@ -1,378 +0,0 @@
-/*
- RCC-Project
-
- Copyright (C) 2026 Richard Qin
-
- This file is part of RCC-Project.
-
- RCC-Project is free software; you can redistribute it and/or modify it under
- the terms of the GNU General Public License as published by the Free
- Software Foundation; either version 3 of the License, or (at your option) any
- later version.
-
- RCC-Project is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- RCC-Project; see the file COPYING. If not see
- <http://www.gnu.org/licenses/>.
-*/
-
-//===- llvm/Support/ErrorOr.h - Error Smart Pointer -------------*- C++ -*-===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-///
-/// \file
-///
-/// Provides ErrorOr<T> smart pointer.
-///
-//===----------------------------------------------------------------------===//
-
-#ifndef RCC_SUPPORT_ERROROR_H
-#define RCC_SUPPORT_ERROROR_H
-
-#include <cassert>
-#include <functional> // for std::reference_wrapper
-#include <system_error>
-#include <type_traits>
-#include <utility>
-
-namespace rcc
-{
-
-/// Represents either an error or a value T.
-///
-/// ErrorOr<T> is a pointer-like class that represents the result of an
-/// operation. The result is either an error, or a value of type T. This is
-/// designed to emulate the usage of returning a pointer where nullptr indicates
-/// failure. However instead of just knowing that the operation failed, we also
-/// have an error_code that describes why it failed.
-///
-/// It is used like the following.
-/// \code
-/// ErrorOr<Buffer> getBuffer();
-///
-/// auto buffer = getBuffer();
-/// if (error_code ec = buffer.getError())
-/// return ec;
-/// buffer->write("adena");
-/// \endcode
-///
-///
-/// Implicit conversion to bool returns true if there is a usable value. The
-/// unary * and -> operators provide pointer like access to the value. Accessing
-/// the value when there is an error has undefined behavior.
-///
-/// When T is a reference type the behavior is slightly different. The reference
-/// is held in a std::reference_wrapper<std::remove_reference<T>::type>, and
-/// there is special handling to make operator -> work as if T was not a
-/// reference.
-///
-/// T cannot be a rvalue reference.
-template <class T> class ErrorOr
-{
- template <class OtherT> friend class ErrorOr;
-
- static constexpr bool isRef = std::is_reference_v<T>;
-
- using wrap = std::reference_wrapper<std::remove_reference_t<T>>;
-
-public:
- using storage_type = std::conditional_t<isRef, wrap, T>;
-
-private:
- using reference = std::remove_reference_t<T> &;
- using const_reference = const std::remove_reference_t<T> &;
- using pointer = std::remove_reference_t<T> *;
- using const_pointer = const std::remove_reference_t<T> *;
-
-public:
- template <class E>
- ErrorOr (E ErrorCode,
- std::enable_if_t<std::is_error_code_enum<E>::value ||
- std::is_error_condition_enum<E>::value,
- void *> = nullptr)
- : HasError (true)
- {
- new (getErrorStorage ()) std::error_code (make_error_code (ErrorCode));
- }
-
- ErrorOr (std::error_code EC) : HasError (true)
- {
- new (getErrorStorage ()) std::error_code (EC);
- }
-
- template <class OtherT>
- ErrorOr (OtherT &&Val,
- std::enable_if_t<std::is_convertible_v<OtherT, T>> * = nullptr)
- : HasError (false)
- {
- new (getStorage ()) storage_type (std::forward<OtherT> (Val));
- }
-
- ErrorOr (const ErrorOr &Other)
- {
- copyConstruct (Other);
- }
-
- template <class OtherT>
- ErrorOr (const ErrorOr<OtherT> &Other,
- std::enable_if_t<std::is_convertible_v<OtherT, T>> * = nullptr)
- {
- copyConstruct (Other);
- }
-
- template <class OtherT>
- explicit ErrorOr (
- const ErrorOr<OtherT> &Other,
- std::enable_if_t<!std::is_convertible_v<OtherT, const T &>> * = nullptr)
- {
- copyConstruct (Other);
- }
-
- ErrorOr (ErrorOr &&Other)
- {
- moveConstruct (std::move (Other));
- }
-
- template <class OtherT>
- ErrorOr (ErrorOr<OtherT> &&Other,
- std::enable_if_t<std::is_convertible_v<OtherT, T>> * = nullptr)
- {
- moveConstruct (std::move (Other));
- }
-
- // This might eventually need SFINAE but it's more complex than is_convertible
- // & I'm too lazy to write it right now.
- template <class OtherT>
- explicit ErrorOr (
- ErrorOr<OtherT> &&Other,
- std::enable_if_t<!std::is_convertible_v<OtherT, T>> * = nullptr)
- {
- moveConstruct (std::move (Other));
- }
-
- ErrorOr &
- operator= (const ErrorOr &Other)
- {
- copyAssign (Other);
- return *this;
- }
-
- ErrorOr &
- operator= (ErrorOr &&Other)
- {
- moveAssign (std::move (Other));
- return *this;
- }
-
- ~ErrorOr ()
- {
- if (!HasError)
- {
- getStorage ()->~storage_type ();
- }
- }
-
- /// Return false if there is an error.
- explicit
- operator bool () const
- {
- return !HasError;
- }
-
- reference
- get ()
- {
- return *getStorage ();
- }
- const_reference
- get () const
- {
- return const_cast<ErrorOr<T> *> (this)->get ();
- }
-
- std::error_code
- getError () const
- {
- return HasError ? *getErrorStorage () : std::error_code ();
- }
-
- pointer
- operator->()
- {
- return toPointer (getStorage ());
- }
-
- const_pointer
- operator->() const
- {
- return toPointer (getStorage ());
- }
-
- reference
- operator* ()
- {
- return *getStorage ();
- }
-
- const_reference
- operator* () const
- {
- return *getStorage ();
- }
-
-private:
- template <class OtherT>
- void
- copyConstruct (const ErrorOr<OtherT> &Other)
- {
- if (!Other.HasError)
- {
- // Get the other value.
- HasError = false;
- new (getStorage ()) storage_type (*Other.getStorage ());
- }
- else
- {
- // Get other's error.
- HasError = true;
- new (getErrorStorage ()) std::error_code (Other.getError ());
- }
- }
-
- template <class T1>
- static bool
- compareThisIfSameType (const T1 &a, const T1 &b)
- {
- return &a == &b;
- }
-
- template <class T1, class T2>
- static bool
- compareThisIfSameType (const T1 &, const T2 &)
- {
- return false;
- }
-
- template <class OtherT>
- void
- copyAssign (const ErrorOr<OtherT> &Other)
- {
- if (compareThisIfSameType (*this, Other))
- {
- return;
- }
-
- this->~ErrorOr ();
- new (this) ErrorOr (Other);
- }
-
- template <class OtherT>
- void
- moveConstruct (ErrorOr<OtherT> &&Other)
- {
- if (!Other.HasError)
- {
- // Get the other value.
- HasError = false;
- new (getStorage ()) storage_type (std::move (*Other.getStorage ()));
- }
- else
- {
- // Get other's error.
- HasError = true;
- new (getErrorStorage ()) std::error_code (Other.getError ());
- }
- }
-
- template <class OtherT>
- void
- moveAssign (ErrorOr<OtherT> &&Other)
- {
- if (compareThisIfSameType (*this, Other))
- {
- return;
- }
-
- this->~ErrorOr ();
- new (this) ErrorOr (std::move (Other));
- }
-
- pointer
- toPointer (pointer Val)
- {
- return Val;
- }
-
- const_pointer
- toPointer (const_pointer Val) const
- {
- return Val;
- }
-
- pointer
- toPointer (wrap *Val)
- {
- return &Val->get ();
- }
-
- const_pointer
- toPointer (const wrap *Val) const
- {
- return &Val->get ();
- }
-
- storage_type *
- getStorage ()
- {
- assert (!HasError && "Cannot get value when an error exists!");
- return &TStorage;
- }
-
- const storage_type *
- getStorage () const
- {
- assert (!HasError && "Cannot get value when an error exists!");
- return &TStorage;
- }
-
- std::error_code *
- getErrorStorage ()
- {
- assert (HasError && "Cannot get error when a value exists!");
- return &ErrorStorage;
- }
-
- const std::error_code *
- getErrorStorage () const
- {
- assert (HasError && "Cannot get error when a value exists!");
- return &ErrorStorage;
- }
-
- union
- {
- storage_type TStorage;
- std::error_code ErrorStorage;
- };
- bool HasError : 1;
-};
-
-template <class T, class E>
-std::enable_if_t<std::is_error_code_enum<E>::value ||
- std::is_error_condition_enum<E>::value,
- bool>
-operator== (const ErrorOr<T> &Err, E Code)
-{
- return Err.getError () == Code;
-}
-
-} // end namespace rcc
-
-#endif // RCC_SUPPORT_ERROROR_H
diff --git a/rcc/lib/CMakeLists.txt b/rcc/lib/CMakeLists.txt
deleted file mode 100644
index 6ab18c4..0000000
--- a/rcc/lib/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-add_rcc_library(RCCSupport
- STATIC
- Support/Errno.cc
- Support/MathExtras.cc
-)
diff --git a/ricochet/.clang-format b/ricochet/.clang-format
new file mode 100644
index 0000000..c49e66b
--- /dev/null
+++ b/ricochet/.clang-format
@@ -0,0 +1,23 @@
+---
+Language: Cpp
+BasedOnStyle: Mozilla
+ColumnLimit: 116
+SpacesInParentheses: true
+AlwaysBreakAfterReturnType: None
+AlwaysBreakAfterDefinitionReturnType: None
+SpaceBeforeCpp11BracedList: true
+BreakBeforeBinaryOperators: All
+Cpp11BracedListStyle: true
+AllowShortBlocksOnASingleLine: Always
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterClass: true
+ AfterControlStatement: Never
+ AfterFunction: true
+ AfterStruct: true
+ AfterEnum: true
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: false
+PackConstructorInitializers: NextLine
+...
diff --git a/ricochet/.clang-tidy b/ricochet/.clang-tidy
new file mode 100644
index 0000000..c1d3e9f
--- /dev/null
+++ b/ricochet/.clang-tidy
@@ -0,0 +1,9 @@
+Checks:
+ '*,-altera-*,-llvmlibc-*,-google-build-using-namespace,-modernize-use-trailing-return-type,-readability-implicit-bool-conversion,-*-non-private-member-variables-in-classes,-modernize-use-nodiscard,-*-magic-numbers,-readability-identifier-length,-concurrency-mt-unsafe,-llvm-header-guard,-fuchsia-default-arguments-calls,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-android-cloexec-accept,-cppcoreguidelines-pro-type-vararg,-hicpp-vararg,-hicpp-signed-bitwise,-misc-include-cleaner,-readability-redundant-member-init,-boost-use-ranges,-portability-template-virtual-member-function'
+
+WarningsAsErrors:
+ '*'
+
+CheckOptions:
+ hicpp-special-member-functions.AllowSoleDefaultDtor: true
+ cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true
diff --git a/ricochet/.gitignore b/ricochet/.gitignore
new file mode 100644
index 0000000..221a15f
--- /dev/null
+++ b/ricochet/.gitignore
@@ -0,0 +1,53 @@
+build/
+bin/
+lib/
+
+CMakeCache.txt
+CMakeFiles/
+CMakeScripts/
+cmake_install.cmake
+install_manifest.txt
+Makefile
+_CPack_Packages/
+CPackConfig.cmake
+CPackSourceConfig.cmake
+
+compile_commands.json
+.cache/
+
+*.o
+*.obj
+*.a
+*.lib
+*.so
+*.dylib
+*.dll
+*.d
+*.suo
+*.idb
+*.pdb
+
+Testing/
+*.gcov
+*.gcda
+*.gcno
+*.html
+
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+.idea/
+cmake-build-*/
+
+*.swp
+*.swo
+.netrwhist
+
+.DS_Store
+
+*~
+*.log
+*.tmp \ No newline at end of file
diff --git a/ricochet/CMakeLists.txt b/ricochet/CMakeLists.txt
new file mode 100644
index 0000000..c2150b8
--- /dev/null
+++ b/ricochet/CMakeLists.txt
@@ -0,0 +1,47 @@
+cmake_minimum_required(VERSION 3.24.2)
+
+project(
+ Ricochet
+ VERSION 0.1.0
+ DESCRIPTION "A minimalist CLI web browser for fast, distraction-free navigation."
+ LANGUAGES CXX
+)
+
+if(${PROJECT_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
+ message(FATAL_ERROR "Ricochet must be built outside its source directory, e.g. `cmake -B build`.")
+endif()
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
+
+foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES})
+ string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG)
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/bin)
+ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib)
+ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/lib)
+endforeach()
+
+include(etc/build_type.cmake)
+include(etc/cflags.cmake)
+include(etc/scanners.cmake)
+include(etc/tests.cmake)
+
+add_subdirectory(src)
+add_subdirectory(tests)
+
+install(TARGETS ricochet
+ RUNTIME DESTINATION bin
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib
+)
+
+if(NOT TARGET uninstall)
+ configure_file(
+ "${PROJECT_SOURCE_DIR}/etc/cmake_uninstall.cmake.in"
+ "${PROJECT_BINARY_DIR}/cmake_uninstall.cmake"
+ IMMEDIATE @ONLY)
+
+ add_custom_target(uninstall
+ COMMAND ${CMAKE_COMMAND} -P "${PROJECT_BINARY_DIR}/cmake_uninstall.cmake")
+endif() \ No newline at end of file
diff --git a/ricochet/COPYING b/ricochet/COPYING
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/ricochet/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/ricochet/README.md b/ricochet/README.md
new file mode 100644
index 0000000..a09bf4a
--- /dev/null
+++ b/ricochet/README.md
@@ -0,0 +1,163 @@
+# Ricochet
+> **A minimalist CLI web browser for fast, distraction-free navigation.**
+
+**Ricochet** is a terminal-based web browser designed for those who value speed, privacy, and the original spirit of the World Wide Web. By stripping away the "JavaScript Trap" and focus-stealing elements of modern web design, Ricochet delivers content in its purest form directly to your emulator.
+
+## Philosophy
+Modern browsing has become a minefield of non-free scripts, tracking pixels, and visual clutter. Ricochet ricochets off the noise:
+* **Freedom First:** No JavaScript support by design. We serve the user, not the scripts.
+* **Minimalist:** Focused entirely on text and structure.
+* **Distraction-Free:** No ads, no popups, no auto-playing media. Just the data you came for.
+
+## Instructions
+- Keyboard-Driven Navigation: Fully controllable via Vim-style keybindings (j/k for scrolling, H/L for history).
+
+- Link Hinting: Press f to highlight all links with two-letter codes. Jump instantly without using a mouse or typing long numbers.
+
+- Privacy First: Industrial-grade Cookie Domain Isolation ensures your session data is never leaked across sites.
+
+- Fast Search: Built-in "Find in Page" (n/N) with real-time ANSI highlighting.
+
+- Persistent Data: Bookmarks and Browsing History are automatically saved to ~/.ricochet/ for a seamless experience.
+
+- Highly Configurable: Customize your User-Agent and default Homepage via a simple config file.
+
+## Key Features
+- [x] **High-Performance C++ Core:** Native speed with minimal memory footprint.
+- [x] **Robust HTML5 Parser:** A custom, fault-tolerant DOM builder for the "wild" web.
+- [x] **TUI Navigation:** Intuitive, keyboard-driven interface (Vim-like bindings).
+- [x] **Privacy Centric:** No cookies by default, no telemetry, and zero JS execution.
+- [x] **Secure:** Native HTTPS support via modern SSL/TLS stacks.
+
+## Architecture
+Ricochet is built with a strictly modular approach:
+* **`Net`**: Handles TCP/TLS streams and HTTP/1.1 protocol state.
+* **`Parser`**: A state-machine driven HTML lexer and DOM tree builder.
+* **`Layout`**: Calculates box models and word-wrapping for terminal constraints.
+* **`TUI`**: An event-driven terminal UI handling viewport rendering and user input.
+
+## Usage
+```bash
+# Start with default homepage
+./build/bin/ricochet
+
+# Open a specific URL
+./build/bin/ricochet https://www.gnu.org
+
+# Use DuckDuckGo smart search
+./build/bin/ricochet "linux kernel news"
+```
+
+## Navigation
+| Key | Action |
+| :--- | :--- |
+| `j / k` | Scroll Down / Up |
+| `f` | Follow Link (Hint Mode) |
+| `n / N` | Find in Page / Next Match |
+| `H / L` | History Back / Forward |
+| `h` | View Browsing History |
+| `b / B` | Add Bookmark / View Bookmarks |
+| `/` | Open URL Prompt |
+| `i` | Form Input Mode |
+| `?` | Show Help Manual |
+| `q` | Quit |
+
+## Configuration
+Ricochet stores data in ~/.ricochet/. You can create config.txt to customize your experience:
+
+```plaintext
+homepage=https://v2ex.com
+user_agent=MyCustomBrowser/1.0
+```
+
+## Getting Started
+
+### Prerequisites
+To build Ricochet, ensure your environment meets the following requirements:
+* **Compiler**: A C++ compiler with full **C++23** support (e.g., **GCC 13+** or **Clang 15+**).
+* **Build System**: **CMake 3.24.2** or higher.
+* **Development Tools** (Optional but recommended):
+ * `clang-format` for automated code styling.
+ * `clang-tidy` for static analysis and linting.
+
+### Building from Source
+Ricochet enforces out-of-source builds to keep the project structure clean. By default, the project builds in Debug mode with Address and Undefined Behavior sanitizers enabled.
+
+#### 1. Clone the repository
+```bash
+git clone https://github.com/yourusername/ricochet.git
+cd ricochet
+```
+
+#### 2. Configure the project
+This will generate compile commands and set the build type to Release
+```bash
+cmake -B build -DCMAKE_BUILD_TYPE=Release
+```
+
+#### 3. Compile the project using all available cores
+```bash
+cmake --build build -j$(nproc)
+```
+
+### Install
+To install Ricochet to your system (defaulting to /usr/local/bin), run the following command with root privileges:
+
+```bash
+sudo cmake --install build
+```
+
+This will install the `ricochet` executable to the `bin` destination and required libraries to `lib`.
+
+### Build Parameters & Configuration
+
+You can customize the build by passing `-D<VAR>=<VALUE>` to the initial `cmake -B build` command.
+
+#### 1. Build Types (`CMAKE_BUILD_TYPE`)
+The project defaults to **Debug** mode if no type is specified.
+* **`Debug`**: Enables full sanitizers and debug symbols (Default).
+* **`Release`**: Optimizes for performance and reduces binary size.
+* **`RelWithDebInfo`**: Optimized build that retains debug symbols.
+
+#### 2. Security & Quality Flags
+These parameters are automatically managed by `etc/cflags.cmake`:
+* **Sanitizers:** Includes AddressSanitizer (ASan) and UndefinedBehaviorSanitizer (UBSan). Recovery is disabled for all errors (`-fno-sanitize-recover=all`).
+* **Warning Level:** Set to extremely strict levels, including `-Wall`, `-Wextra`, `-Wpedantic`, and `-Weffc++`.
+* **Warnings as Errors:** All compiler warnings are treated as errors (`-Werror`), ensuring no code is merged with unresolved issues.
+
+#### 3. Output Directories
+The build system organizes outputs into the following structure within your build folder:
+* **Executables:** `build/bin/`
+* **Static/Shared Libraries:** `build/lib/`
+* **Compile Commands:** `build/compile_commands.json` is generated for LSP (Language Server) support.
+
+---
+
+### Development & Maintenance Targets
+
+The project includes several custom targets for code maintenance:
+
+| Target | Command | Description |
+| :--- | :--- | :--- |
+| **Format** | `cmake --build build --target format` | Uses `clang-format` to enforce consistent coding style across all `.cc` and `.hh` files. |
+| **Tidy** | `cmake --build build --target tidy` | Runs `clang-tidy` on the core Ricochet source files for static analysis. |
+| **Tidy-All** | `cmake --build build --target tidy-all` | Runs `clang-tidy` on every file in the project, including tests. |
+| **Test** | `ctest --test-dir build` | Executes the test suite via CTest. |
+
+### Installation
+To install the compiled binaries to your system, use:
+```bash
+cmake --install build --prefix /usr/local
+```
+This installs the `ricochet` binary to `bin` and supporting libraries to `lib`.
+
+
+
+## Acknowledgments
+The low-level POSIX socket wrappers in `src/net/sys/` are adapted from the Minnow networking framework developed for Stanford University's CS144 course. These utilities are used gracefully to handle file descriptors and network interfaces.
+
+## License
+Ricochet is free software: you can redistribute it and/or modify it under the terms of the **GNU General Public License (GPL) v3**.
+
+---
+*Built for the terminal. Built for freedom.* \ No newline at end of file
diff --git a/ricochet/etc/build_type.cmake b/ricochet/etc/build_type.cmake
new file mode 100644
index 0000000..69d019f
--- /dev/null
+++ b/ricochet/etc/build_type.cmake
@@ -0,0 +1,11 @@
+set(default_build_type "Debug")
+
+if (NOT (CMAKE_BUILD_TYPE_SHADOW STREQUAL CMAKE_BUILD_TYPE))
+ if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ message (STATUS "Setting build type to '${default_build_type}'")
+ set (CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE)
+ endif ()
+ set (CMAKE_BUILD_TYPE_SHADOW ${CMAKE_BUILD_TYPE} CACHE STRING "used to detect changes in build type" FORCE)
+endif ()
+
+message (STATUS "Building in '${CMAKE_BUILD_TYPE}' mode.")
diff --git a/ricochet/etc/cflags.cmake b/ricochet/etc/cflags.cmake
new file mode 100644
index 0000000..cf60dd7
--- /dev/null
+++ b/ricochet/etc/cflags.cmake
@@ -0,0 +1,13 @@
+set (CMAKE_CXX_STANDARD 23)
+set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
+set (CMAKE_CXX_STANDARD_REQUIRED ON)
+set (CMAKE_CXX_EXTENSIONS OFF)
+
+set(SANITIZING_FLAGS "-fno-sanitize-recover=all -fsanitize=undefined -fsanitize=address")
+
+# ask for more warnings from the compiler
+set (CMAKE_BASE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wpedantic -Wextra -Weffc++ -Werror -Wshadow -Wpointer-arith -Wcast-qual -Wformat=2 -Wno-unqualified-std-cast-call -Wno-non-virtual-dtor")
+
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${SANITIZING_FLAGS}")
+set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native") \ No newline at end of file
diff --git a/ricochet/etc/cmake_uninstall.cmake.in b/ricochet/etc/cmake_uninstall.cmake.in
new file mode 100644
index 0000000..33ae281
--- /dev/null
+++ b/ricochet/etc/cmake_uninstall.cmake.in
@@ -0,0 +1,21 @@
+if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt")
+ message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt")
+endif()
+
+file(STRINGS "@CMAKE_BINARY_DIR@/install_manifest.txt" files)
+
+foreach(file ${files})
+ message(STATUS "Uninstalling $ENV{DESTDIR}${file}")
+ if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
+ execute_process(
+ COMMAND "@CMAKE_COMMAND@" "-E" "rm" "-f" "$ENV{DESTDIR}${file}"
+ OUTPUT_VARIABLE rm_out
+ RESULT_VARIABLE rm_retval
+ )
+ if(NOT "${rm_retval}" STREQUAL 0)
+ message(FATAL_ERROR "Problem uninstalling $ENV{DESTDIR}${file}")
+ endif()
+ else()
+ message(STATUS "File $ENV{DESTDIR}${file} does not exist.")
+ endif()
+endforeach() \ No newline at end of file
diff --git a/ricochet/etc/scanners.cmake b/ricochet/etc/scanners.cmake
new file mode 100644
index 0000000..2dd9da0
--- /dev/null
+++ b/ricochet/etc/scanners.cmake
@@ -0,0 +1,27 @@
+file (GLOB_RECURSE RICOCHET_CC_FILES ${CMAKE_SOURCE_DIR}/src/*.cc)
+
+file(GLOB_RECURSE ALL_SRC_FILES
+ ${CMAKE_SOURCE_DIR}/src/*.cc
+ ${CMAKE_SOURCE_DIR}/src/*.hh
+ ${CMAKE_SOURCE_DIR}/include/*.hh
+)
+
+add_custom_target (format "clang-format" -i ${ALL_SRC_FILES} COMMENT "Formatting source code...")
+
+foreach (tidy_target ${ALL_SRC_FILES})
+ get_filename_component (basename ${tidy_target} NAME)
+ get_filename_component (dirname ${tidy_target} DIRECTORY)
+ get_filename_component (basedir ${dirname} NAME)
+ set (tidy_target_name "${basedir}__${basename}")
+ set (tidy_command clang-tidy --quiet -header-filter=.* -p=${PROJECT_BINARY_DIR} ${tidy_target})
+ add_custom_target (tidy_${tidy_target_name} ${tidy_command})
+ list (APPEND ALL_TIDY_TARGETS tidy_${tidy_target_name})
+
+ if (${tidy_target} IN_LIST RICOCHET_CC_FILES)
+ list (APPEND RICOCHET_TIDY_TARGETS tidy_${tidy_target_name})
+ endif ()
+endforeach (tidy_target)
+
+add_custom_target (tidy DEPENDS ${RICOCHET_TIDY_TARGETS})
+
+add_custom_target (tidy-all DEPENDS ${ALL_TIDY_TARGETS})
diff --git a/ricochet/etc/tests.cmake b/ricochet/etc/tests.cmake
new file mode 100644
index 0000000..f32724e
--- /dev/null
+++ b/ricochet/etc/tests.cmake
@@ -0,0 +1,19 @@
+include(CTest)
+
+# list(APPEND CMAKE_CTEST_ARGUMENTS --output-on-failure --stop-on-failure --timeout 15 -E 'speed_test|optimization|webget')
+
+# set(compile_name "compile with bug-checkers")
+# add_test(NAME ${compile_name}
+# COMMAND "${CMAKE_COMMAND}" --build "${CMAKE_BINARY_DIR}" -t functionality_testing webget)
+
+# macro (ttest name)
+# add_test(NAME ${name} COMMAND "${name}_sanitized")
+# set_property(TEST ${name} PROPERTY FIXTURES_REQUIRED compile)
+# endmacro (ttest)
+
+# set_property(TEST ${compile_name} PROPERTY TIMEOUT 0)
+# set_tests_properties(${compile_name} PROPERTIES FIXTURES_SETUP compile)
+
+# add_test(NAME t_webget COMMAND "${PROJECT_SOURCE_DIR}/tests/webget_t.sh" "${PROJECT_BINARY_DIR}")
+# set_property(TEST t_webget PROPERTY FIXTURES_REQUIRED compile)
+
diff --git a/ricochet/include/.gitkeep b/ricochet/include/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ricochet/include/.gitkeep
diff --git a/ricochet/include/ricochet/core/browser.hh b/ricochet/include/ricochet/core/browser.hh
new file mode 100644
index 0000000..fabcba4
--- /dev/null
+++ b/ricochet/include/ricochet/core/browser.hh
@@ -0,0 +1,40 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::core {
+
+class Browser
+{
+public:
+ Browser() = default;
+ ~Browser() = default;
+
+ Browser( const Browser& ) = delete;
+ Browser& operator=( const Browser& ) = delete;
+ Browser( Browser&& ) = delete;
+ Browser& operator=( Browser&& ) = delete;
+
+ static std::string format_url( std::string input );
+ static std::string get_configured_homepage();
+};
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/form_handler.hh b/ricochet/include/ricochet/core/form_handler.hh
new file mode 100644
index 0000000..d95bd02
--- /dev/null
+++ b/ricochet/include/ricochet/core/form_handler.hh
@@ -0,0 +1,27 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+#include "ricochet/render/renderer.hh"
+#include <cstddef>
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+InputAction submit_form( const render::InputField& target_input,
+ const std::vector<render::InputField>& inputs,
+ const std::string& current_url,
+ std::vector<HttpRequest>& history,
+ std::size_t& history_idx );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/license.hh b/ricochet/include/ricochet/core/license.hh
new file mode 100644
index 0000000..32c04c6
--- /dev/null
+++ b/ricochet/include/ricochet/core/license.hh
@@ -0,0 +1,29 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+namespace ricochet::core {
+
+void print_license_notice();
+
+void show_warranty();
+
+void show_copying();
+
+} // namespace ricochet::core \ No newline at end of file
diff --git a/ricochet/include/ricochet/core/local_data.hh b/ricochet/include/ricochet/core/local_data.hh
new file mode 100644
index 0000000..ca07846
--- /dev/null
+++ b/ricochet/include/ricochet/core/local_data.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::core {
+
+std::string get_config_path( const std::string& filename );
+void save_bookmark( const std::string& title, const std::string& url );
+std::string generate_bookmarks_html();
+void append_to_history( const std::string& url, const std::string& title );
+std::string generate_history_html();
+std::string generate_help_html();
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/page_loader.hh b/ricochet/include/ricochet/core/page_loader.hh
new file mode 100644
index 0000000..11131e2
--- /dev/null
+++ b/ricochet/include/ricochet/core/page_loader.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+// #include "ricochet/net/http_client.hh"
+// #include "ricochet/parser/dom_tree.hh"
+// #include <string>
+
+namespace ricochet::core {
+
+// std::string extract_title( const parser::DomNode& root_node );
+// PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp );
+PageData load_page( const HttpRequest& req );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/page_processor.hh b/ricochet/include/ricochet/core/page_processor.hh
new file mode 100644
index 0000000..8f40d3a
--- /dev/null
+++ b/ricochet/include/ricochet/core/page_processor.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+#include "ricochet/net/http_client.hh"
+#include "ricochet/parser/dom_tree.hh"
+#include <string>
+
+namespace ricochet::core {
+
+std::string extract_title( const parser::DomNode& root_node );
+
+PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/types.hh b/ricochet/include/ricochet/core/types.hh
new file mode 100644
index 0000000..ef48016
--- /dev/null
+++ b/ricochet/include/ricochet/core/types.hh
@@ -0,0 +1,50 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/render/renderer.hh"
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+struct PageData
+{
+ std::vector<std::string> lines;
+ std::vector<std::string> links;
+ std::vector<render::InputField> inputs;
+ std::string title;
+};
+
+struct HttpRequest
+{
+ std::string url {};
+ std::string method = "GET";
+ std::string body {};
+};
+
+enum class InputAction : std::uint8_t
+{
+ None,
+ Quit,
+ Navigate
+};
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/utils.hh b/ricochet/include/ricochet/core/utils.hh
new file mode 100644
index 0000000..61d480b
--- /dev/null
+++ b/ricochet/include/ricochet/core/utils.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+std::vector<std::string> split_into_lines( const std::string& text );
+std::string normalize_url( const std::string& target, const std::string& current_url );
+std::string strip_ddg_tracker( std::string target );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/net/config.hh b/ricochet/include/ricochet/net/config.hh
new file mode 100644
index 0000000..fa73193
--- /dev/null
+++ b/ricochet/include/ricochet/net/config.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::net {
+
+std::string get_config_path( const std::string& filename );
+
+struct BrowserConfig
+{
+ std::string user_agent;
+ std::string homepage;
+};
+
+BrowserConfig& get_browser_config();
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/cookie_jar.hh b/ricochet/include/ricochet/net/cookie_jar.hh
new file mode 100644
index 0000000..5c5befb
--- /dev/null
+++ b/ricochet/include/ricochet/net/cookie_jar.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+namespace ricochet::net {
+
+using CookieJar = std::unordered_map<std::string, std::unordered_map<std::string, std::string>>;
+
+void load_cookies_from_file( CookieJar& jar );
+CookieJar& get_cookie_jar();
+void save_cookies_to_file( const CookieJar& jar );
+
+void update_cookies( const std::string& raw_response, const std::string& host );
+std::string build_cookie_header( const std::string& host );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/http_client.hh b/ricochet/include/ricochet/net/http_client.hh
new file mode 100644
index 0000000..f57cbcf
--- /dev/null
+++ b/ricochet/include/ricochet/net/http_client.hh
@@ -0,0 +1,63 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <expected>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+namespace ricochet::net {
+/**
+ * @brief Encapsulate HTTP response result
+ */
+
+struct HttpResponse
+{
+ int status_code { 0 };
+ std::unordered_map<std::string, std::string> headers {};
+ std::string body {};
+ std::string content_type {};
+};
+
+class HttpClient
+{
+public:
+ HttpClient() = default;
+ ~HttpClient() = default;
+
+ HttpClient( const HttpClient& ) = delete;
+ HttpClient& operator=( const HttpClient& ) = delete;
+ HttpClient( HttpClient&& ) = delete;
+ HttpClient& operator=( HttpClient&& ) = delete;
+
+ /**
+ * @brief Initiates an HTTP GET request (only supports http:// in MVP stage)
+ * @param url Target URL, for example "http://example.com"
+ * @return Returns HttpResponse on success, returns an error message string on failure
+ */
+ std::expected<HttpResponse, std::string> fetch( std::string_view url,
+ std::string_view method
+ = "GET", // NOLINT(bugprone-easily-swappable-parameters)
+ std::string_view body = "" ) const;
+
+private:
+};
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/include/ricochet/net/http_utils.hh b/ricochet/include/ricochet/net/http_utils.hh
new file mode 100644
index 0000000..3d96e79
--- /dev/null
+++ b/ricochet/include/ricochet/net/http_utils.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+std::string dechunk( std::string_view body );
+std::string remove_html_comments( std::string html );
+std::string remove_tag_blocks( std::string html, const std::string& tag_name );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/ssl_socket.hh b/ricochet/include/ricochet/net/ssl_socket.hh
new file mode 100644
index 0000000..36a92b6
--- /dev/null
+++ b/ricochet/include/ricochet/net/ssl_socket.hh
@@ -0,0 +1,93 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "sys/address.hh"
+#include "sys/socket.hh"
+#include <memory>
+#include <string>
+#include <string_view>
+
+extern "C" {
+using SSL_CTX = struct ssl_ctx_st;
+using SSL = struct ssl_st;
+}
+
+namespace ricochet::net {
+
+struct SslCtxDeleter
+{
+ void operator()( SSL_CTX* ptr ) const;
+};
+struct SslDeleter
+{
+ void operator()( SSL* ptr ) const;
+};
+
+using UniqueSslCtx = std::unique_ptr<SSL_CTX, SslCtxDeleter>;
+using UniqueSsl = std::unique_ptr<SSL, SslDeleter>;
+
+/**
+ * @brief Socket wrapper that supports TLS/SSL encrypted handshake
+ */
+class SSLSocket
+{
+public:
+ SSLSocket();
+ ~SSLSocket() = default;
+
+ SSLSocket( const SSLSocket& ) = delete;
+ SSLSocket& operator=( const SSLSocket& ) = delete;
+ SSLSocket( SSLSocket&& ) = delete;
+ SSLSocket& operator=( SSLSocket&& ) = delete;
+
+ /**
+ * @brief Connect to a remote host using TLS/SSL
+ * @param host The hostname to connect to
+ * @param port The port to connect to
+ * @return True if the connection was successful, false otherwise
+ */
+ void connect( const Address& address, const std::string& hostname );
+
+ /**
+ * @brief Send data over the TLS/SSL connection
+ * @param data The data to send
+ * @return The number of bytes sent, or -1 if an error occurred
+ */
+ std::size_t write( std::string_view data );
+
+ /**
+ * @brief Receive data from the TLS/SSL connection
+ * @param buffer The buffer to store the received data
+ */
+ void read( std::string& buffer );
+
+ /**
+ * @brief Check if the TLS/SSL connection is closed
+ * @return True if the connection is closed, false otherwise
+ */
+ bool eof() const;
+
+private:
+ TCPSocket underlying_socket_ {}; // TCP Socket
+ UniqueSslCtx ctx_ { nullptr }; // OpenSSL global context
+ UniqueSsl ssl_ { nullptr }; // Current connection's SSL state
+ bool eof_ { false }; // EOF flag
+};
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/include/ricochet/net/sys/address.hh b/ricochet/include/ricochet/net/sys/address.hh
new file mode 100644
index 0000000..39c9553
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/address.hh
@@ -0,0 +1,88 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <netdb.h>
+#include <string>
+#include <sys/socket.h>
+#include <utility>
+
+//! Wrapper around [IPv4 addresses](@ref man7::ip) and DNS operations.
+class Address
+{
+public:
+ //! \brief Wrapper around [sockaddr_storage](@ref man7::socket).
+ //! \details A `sockaddr_storage` is enough space to store any socket address (IPv4 or IPv6).
+ class Raw
+ {
+ public:
+ sockaddr_storage storage {}; //!< The wrapped struct itself.
+ // NOLINTBEGIN (*-explicit-*)
+ operator sockaddr*();
+ operator const sockaddr*() const;
+ // NOLINTEND (*-explicit-*)
+ };
+
+private:
+ socklen_t size_; //!< Size of the wrapped address.
+ Raw address_ {}; //!< A wrapped [sockaddr_storage](@ref man7::socket) containing the address.
+
+ //! Constructor from ip/host, service/port, and hints to the resolver.
+ Address( const std::string& node, const std::string& service, const addrinfo& hints );
+
+public:
+ //! Construct by resolving a hostname and servicename.
+ Address( const std::string& hostname, const std::string& service );
+
+ //! Construct from dotted-quad string ("18.243.0.1") and numeric port.
+ explicit Address( const std::string& ip, std::uint16_t port = 0 );
+
+ //! Construct from a [sockaddr *](@ref man7::socket).
+ Address( const sockaddr* addr, std::size_t size );
+
+ //! Default constructor
+ Address() : Address( nullptr, 0 ) {}
+
+ //! Equality comparison.
+ bool operator==( const Address& other ) const;
+ bool operator!=( const Address& other ) const { return not operator==( other ); }
+
+ //! \name Conversions
+ //!@{
+
+ //! Dotted-quad IP address string ("18.243.0.1") and numeric port.
+ std::pair<std::string, uint16_t> ip_port() const;
+ //! Dotted-quad IP address string ("18.243.0.1").
+ std::string ip() const { return ip_port().first; }
+ //! Numeric port (host byte order).
+ uint16_t port() const { return ip_port().second; }
+ //! Numeric IP address as an integer (i.e., in [host byte order](\ref man3::byteorder)).
+ uint32_t ipv4_numeric() const;
+ //! Create an Address from a 32-bit raw numeric IP address
+ static Address from_ipv4_numeric( uint32_t ip_address );
+ //! Human-readable string, e.g., "8.8.8.8:53".
+ std::string to_string() const;
+ //!@}
+
+ //! \name Low-level operations
+ //!@{
+
+ //! Size of the underlying address storage.
+ socklen_t size() const { return size_; }
+ //! Const pointer to the underlying socket address storage.
+ const sockaddr* raw() const { return static_cast<const sockaddr*>( address_ ); }
+ //! Safely convert to underlying sockaddr type
+ template<typename sockaddr_type>
+ const sockaddr_type* as() const;
+
+ //!@}
+};
diff --git a/ricochet/include/ricochet/net/sys/exception.hh b/ricochet/include/ricochet/net/sys/exception.hh
new file mode 100644
index 0000000..9587e5c
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/exception.hh
@@ -0,0 +1,80 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <cxxabi.h>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <system_error>
+
+class tagged_error : public std::system_error
+{
+private:
+ std::string attempt_and_error_;
+ int error_code_;
+
+public:
+ tagged_error( const std::error_category& category, const std::string_view s_attempt, const int error_code )
+ : system_error( error_code, category )
+ , attempt_and_error_( std::string( s_attempt ) + ": " + std::system_error::what() )
+ , error_code_( error_code )
+ {}
+
+ const char* what() const noexcept override { return attempt_and_error_.c_str(); }
+
+ int error_code() const { return error_code_; }
+};
+
+class unix_error : public tagged_error
+{
+public:
+ explicit unix_error( const std::string_view s_attempt, const int s_errno = errno )
+ : tagged_error( std::system_category(), s_attempt, s_errno )
+ {}
+};
+
+inline int CheckSystemCall( const std::string_view s_attempt, const int return_value )
+{
+ if ( return_value >= 0 ) {
+ return return_value;
+ }
+
+ throw unix_error { s_attempt };
+}
+
+template<typename T>
+inline T* notnull( const std::string_view context, T* const x )
+{
+ return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+template<typename T>
+inline std::unique_ptr<T> notnull( const std::string_view context, std::unique_ptr<T> x )
+{
+ return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+template<typename T>
+inline std::shared_ptr<T> notnull( const std::string_view context, std::shared_ptr<T>&& x )
+{
+ return x ? std::move( x ) : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+inline std::string demangle( const char* name )
+{
+ int status {};
+ const std::unique_ptr<char, decltype( &free )> res { abi::__cxa_demangle( name, nullptr, nullptr, &status ),
+ free };
+ if ( status ) {
+ throw std::runtime_error( "cxa_demangle" );
+ }
+ return res.get();
+}
diff --git a/ricochet/include/ricochet/net/sys/file_descriptor.hh b/ricochet/include/ricochet/net/sys/file_descriptor.hh
new file mode 100644
index 0000000..4af56cb
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/file_descriptor.hh
@@ -0,0 +1,141 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include "string_view_range.hh"
+
+#include <bits/types/struct_iovec.h>
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+// A reference-counted handle to a file descriptor
+class FileDescriptor
+{
+public:
+ // Construct from a file descriptor number returned by the kernel
+ explicit FileDescriptor( int fd );
+
+ // Read from the file descriptor into `buffer` (a single string) or `buffers` (a vector of strings)
+ void read( std::string& buffer );
+ void read( std::vector<std::string>& buffers );
+
+ // `write_all` writes a buffer completely.
+ void write_all( std::string_view buffer );
+
+ // `write` writes *from* a buffer or range of buffers and returns the number of bytes it actually wrote.
+ size_t write( std::string_view buffer );
+ size_t write( const StringViewRange auto&& buffers )
+ {
+ static thread_local std::vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( buffers, iovecs );
+ return write( iovecs, total_size );
+ }
+
+ // Close the underlying file descriptor
+ void close() { internal_fd_->close(); }
+
+ // Set blocking(true) or non-blocking(false) status on the file descriptor
+ void set_blocking( bool blocking );
+
+ // Copy a FileDescriptor explicitly, increasing the internal reference count
+ FileDescriptor duplicate() const;
+
+ // The destructor calls close() when no more references remain.
+ ~FileDescriptor() = default;
+
+ // Accessors
+ int fd_num() const { return internal_fd_->fd_; } // underlying descriptor number
+ bool eof() const { return internal_fd_->eof_; } // EOF flag state
+ bool closed() const { return internal_fd_->closed_; } // closed flag state
+ bool blocking() const { return not internal_fd_->non_blocking_; } // blocking state
+ unsigned int read_count() const { return internal_fd_->read_count_; } // number of reads
+ unsigned int write_count() const { return internal_fd_->write_count_; } // number of writes
+
+ // Copy/move constructor/assignment operators
+ // FileDescriptor can be moved, but cannot be copied implicitly (see duplicate())
+ FileDescriptor( const FileDescriptor& other ) = delete; // copy construction is forbidden
+ FileDescriptor& operator=( const FileDescriptor& other ) = delete; // copy assignment is forbidden
+ FileDescriptor( FileDescriptor&& other ) = default; // move construction is allowed
+ FileDescriptor& operator=( FileDescriptor&& other ) = default; // move assignment is allowed
+
+private:
+ // FDWrapper: A handle on a kernel file descriptor.
+ // FileDescriptor objects contain a std::shared_ptr to a FDWrapper.
+ class FDWrapper
+ {
+ public:
+ int fd_; // The file descriptor number returned by the kernel
+ bool eof_ = false; // Flag indicating whether FDWrapper::fd_ is at EOF
+ bool closed_ = false; // Flag indicating whether FDWrapper::fd_ has been closed
+ bool non_blocking_ = false; // Flag indicating whether FDWrapper::fd_ is non-blocking
+ unsigned read_count_ = 0; // The number of times FDWrapper::fd_ has been read
+ unsigned write_count_ = 0; // The numberof times FDWrapper::fd_ has been written
+
+ // Construct from a file descriptor number returned by the kernel
+ explicit FDWrapper( int fd );
+ // Closes the file descriptor upon destruction
+ ~FDWrapper();
+ // Calls [close(2)](\ref man2::close) on FDWrapper::fd_
+ void close();
+
+ size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const;
+ size_t CheckRead( std::string_view what, ssize_t return_value );
+
+ // An FDWrapper cannot be copied or moved
+ FDWrapper( const FDWrapper& other ) = delete;
+ FDWrapper& operator=( const FDWrapper& other ) = delete;
+ FDWrapper( FDWrapper&& other ) = delete;
+ FDWrapper& operator=( FDWrapper&& other ) = delete;
+ };
+
+ // A reference-counted handle to a shared FDWrapper
+ std::shared_ptr<FDWrapper> internal_fd_;
+
+ // Private constructor used to duplicate the FileDescriptor (increase the reference count)
+ explicit FileDescriptor( std::shared_ptr<FDWrapper> other_shared_ptr );
+
+ // Private write method that takes a vector of iovecs (an internal OS structure)
+ size_t write( const std::vector<iovec>& iovecs, size_t total_size );
+
+protected:
+ // size of buffer to allocate by read(), recv(), etc., when passed-in buffer is empty
+ static constexpr size_t kReadBufferSize = 16384;
+
+ void set_eof() { internal_fd_->eof_ = true; }
+ void register_read() { ++internal_fd_->read_count_; } // increment read count
+ void register_write() { ++internal_fd_->write_count_; } // increment write count
+
+ size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const;
+ size_t CheckRead( std::string_view what, ssize_t return_value );
+
+ // Convert a range of string_view-convertible objects to a vector of iovecs
+ static size_t to_iovecs( const StringViewRange auto& buffers, std::vector<iovec>& iovecs )
+ {
+ if ( buffers.empty() ) {
+ throw std::runtime_error( "to_iovecs called with empty buffer list" );
+ }
+ iovecs.clear();
+ iovecs.reserve( buffers.size() );
+ size_t total_size = 0;
+ for ( const auto& buf : buffers ) {
+ const std::string_view x { buf };
+ if ( x.empty() ) {
+ throw std::runtime_error( "to_iovecs called with empty buffer in buffer list" );
+ }
+ iovecs.push_back( { const_cast<char*>( x.data() ), x.size() } ); // NOLINT(*-const-cast)
+ total_size += x.size();
+ }
+ if ( total_size == 0 ) {
+ throw std::runtime_error( "to_iovecs called with zero-size buffer list" );
+ }
+ return total_size;
+ }
+};
diff --git a/ricochet/include/ricochet/net/sys/socket.hh b/ricochet/include/ricochet/net/sys/socket.hh
new file mode 100644
index 0000000..f84be26
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/socket.hh
@@ -0,0 +1,160 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include "address.hh"
+#include "file_descriptor.hh"
+
+#include <functional>
+#include <sys/socket.h>
+
+//! \brief Base class for network sockets (TCP, UDP, etc.)
+//! \details Socket is generally used via a subclass. See TCPSocket and UDPSocket for usage examples.
+class Socket : public FileDescriptor
+{
+public:
+ //! Bind a socket to a specified address with [bind(2)](\ref man2::bind), usually for listen/accept
+ void bind( const Address& address );
+
+ //! Bind a socket to a specified device
+ void bind_to_device( std::string_view device_name );
+
+ //! Connect a socket to a specified peer address with [connect(2)](\ref man2::connect)
+ void connect( const Address& address );
+
+ //! Shut down a socket via [shutdown(2)](\ref man2::shutdown)
+ void shutdown( int how );
+
+ //! Get local address of socket with [getsockname(2)](\ref man2::getsockname)
+ Address local_address() const;
+ //! Get peer address of socket with [getpeername(2)](\ref man2::getpeername)
+ Address peer_address() const;
+
+ //! Allow local address to be reused sooner via [SO_REUSEADDR](\ref man7::socket)
+ void set_reuseaddr();
+
+ //! Check for errors (will be seen on non-blocking sockets)
+ void throw_if_error() const;
+
+private:
+ //! Get the local or peer address the socket is connected to
+ Address get_address( const std::string& name_of_function,
+ const std::function<int( int, sockaddr*, socklen_t* )>& function ) const;
+
+protected:
+ //! Construct via [socket(2)](\ref man2::socket)
+ Socket( int domain, int type, int protocol = 0 );
+
+ //! Construct from a file descriptor.
+ Socket( FileDescriptor&& fd, int domain, int type, int protocol = 0 );
+
+ //! Wrapper around [getsockopt(2)](\ref man2::getsockopt)
+ template<typename option_type>
+ socklen_t getsockopt( int level, int option, option_type& option_value ) const;
+
+ //! Wrappers around [setsockopt(2)](\ref man2::setsockopt)
+ template<typename option_type>
+ void setsockopt( int level, int option, const option_type& option_value );
+
+ void setsockopt( int level, int option, std::string_view option_val );
+};
+
+class DatagramSocket : public Socket
+{
+public:
+ // Receive a datagram and the Address of its sender, into a buffer or collection of buffers.
+ void recv( Address& source_address, std::string& payload );
+ void recv( Address& source_address, std::vector<std::string>& payloads );
+
+ //! Send a datagram to specified Address (if destination is missing, sends to already-connected address)
+ void send( std::string_view payload, const std::optional<Address>& destination = {} );
+ void send( const StringViewRange auto&& payloads, const std::optional<Address>& destination = {} )
+ {
+ static thread_local std::vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( payloads, iovecs );
+ send( iovecs, total_size, destination );
+ }
+
+private:
+ void send( std::vector<iovec>& iovecs, size_t total_size, const std::optional<Address>& destination = {} );
+
+protected:
+ DatagramSocket( int domain, int type, int protocol = 0 ) : Socket( domain, type, protocol ) {}
+
+ //! Construct from a file descriptor.
+ DatagramSocket( FileDescriptor&& fd, int domain, int type, int protocol = 0 )
+ : Socket( std::move( fd ), domain, type, protocol )
+ {}
+};
+
+//! A wrapper around [UDP sockets](\ref man7::udp)
+class UDPSocket : public DatagramSocket
+{
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit UDPSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_INET, SOCK_DGRAM ) {}
+
+public:
+ //! Default: construct an unbound, unconnected UDP socket
+ UDPSocket() : DatagramSocket( AF_INET, SOCK_DGRAM ) {}
+};
+
+//! A wrapper around [TCP sockets](\ref man7::tcp)
+class TCPSocket : public Socket
+{
+private:
+ //! \brief Construct from FileDescriptor (used by accept())
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit TCPSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_INET, SOCK_STREAM, IPPROTO_TCP ) {}
+
+public:
+ //! Default: construct an unbound, unconnected TCP socket
+ TCPSocket() : Socket( AF_INET, SOCK_STREAM ) {}
+
+ //! Mark a socket as listening for incoming connections
+ void listen( int backlog = 16 );
+
+ //! Accept a new incoming connection
+ TCPSocket accept();
+};
+
+//! A wrapper around [packet sockets](\ref man7:packet)
+class PacketSocket : public DatagramSocket
+{
+public:
+ PacketSocket( const int type, const int protocol ) : DatagramSocket( AF_PACKET, type, protocol ) {}
+
+ void set_promiscuous();
+};
+
+//! A wrapper around [raw sockets](\ref man7:raw)
+class RawSocket : public DatagramSocket
+{
+public:
+ RawSocket() : DatagramSocket( AF_INET, SOCK_RAW, IPPROTO_RAW ) {}
+};
+
+//! A wrapper around [Unix-domain stream sockets](\ref man7::unix)
+class LocalStreamSocket : public Socket
+{
+public:
+ //! Construct from a file descriptor
+ explicit LocalStreamSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_UNIX, SOCK_STREAM ) {}
+};
+
+//! A wrapper around [Unix-domain datagram sockets](\ref man7::unix)
+class LocalDatagramSocket : public DatagramSocket
+{
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit LocalDatagramSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_UNIX, SOCK_DGRAM ) {}
+
+public:
+ //! Default: construct an unbound, unconnected socket
+ LocalDatagramSocket() : DatagramSocket( AF_UNIX, SOCK_DGRAM ) {}
+};
diff --git a/ricochet/include/ricochet/net/sys/string_view_range.hh b/ricochet/include/ricochet/net/sys/string_view_range.hh
new file mode 100644
index 0000000..84be394
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/string_view_range.hh
@@ -0,0 +1,18 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <ranges>
+#include <type_traits>
+
+// This concept represents any range (e.g. vector) whose elements can be converted to string_view.
+template<typename R>
+concept StringViewRange
+ = std::ranges::sized_range<R> && std::is_constructible_v<std::string_view, std::ranges::range_value_t<R>>;
diff --git a/ricochet/include/ricochet/net/url_parser.hh b/ricochet/include/ricochet/net/url_parser.hh
new file mode 100644
index 0000000..039e4dc
--- /dev/null
+++ b/ricochet/include/ricochet/net/url_parser.hh
@@ -0,0 +1,37 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <expected>
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+struct ParsedUrl
+{
+ std::string host {};
+ std::string path {};
+};
+
+std::expected<ParsedUrl, std::string> parse_url( std::string_view url );
+
+std::string extract_redirect_url( const std::string& raw_response, bool is_https, const std::string& host );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/parser/dom_tree.hh b/ricochet/include/ricochet/parser/dom_tree.hh
new file mode 100644
index 0000000..e3b3664
--- /dev/null
+++ b/ricochet/include/ricochet/parser/dom_tree.hh
@@ -0,0 +1,36 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ricochet::parser {
+
+/**
+ * @brief DOM Node. Pure value semantics, no pointers.
+ */
+struct DomNode
+{
+ std::string tag_name {}; // If empty, it's a pure text node
+ std::string text_content {}; // Only meaningful when tag_name is empty
+ std::vector<DomNode> children {}; // Recursive child nodes list
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/parser/html_lexer.hh b/ricochet/include/ricochet/parser/html_lexer.hh
new file mode 100644
index 0000000..d9ce23a
--- /dev/null
+++ b/ricochet/include/ricochet/parser/html_lexer.hh
@@ -0,0 +1,64 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <variant>
+#include <vector>
+
+namespace ricochet::parser {
+
+struct TextToken
+{
+ std::string content {};
+};
+
+struct TagOpenToken
+{
+ std::string name {};
+};
+
+struct TagCloseToken
+{
+ std::string name {};
+};
+
+using HtmlToken = std::variant<TextToken, TagOpenToken, TagCloseToken>;
+
+class HtmlLexer
+{
+public:
+ HtmlLexer() = default;
+ ~HtmlLexer() = default;
+
+ HtmlLexer( const HtmlLexer& ) = delete;
+ HtmlLexer& operator=( const HtmlLexer& ) = delete;
+ HtmlLexer( HtmlLexer&& ) = delete;
+ HtmlLexer& operator=( HtmlLexer&& ) = delete;
+
+ /**
+ * @brief Perform lexical analysis to convert HTML source code into a structured sequence of tokens
+ */
+ std::vector<HtmlToken> tokenize( std::string_view html ) const;
+
+private:
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/parser/tree_builder.hh b/ricochet/include/ricochet/parser/tree_builder.hh
new file mode 100644
index 0000000..2cb0748
--- /dev/null
+++ b/ricochet/include/ricochet/parser/tree_builder.hh
@@ -0,0 +1,45 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/parser/html_lexer.hh"
+
+#include <vector>
+
+namespace ricochet::parser {
+
+class TreeBuilder
+{
+public:
+ TreeBuilder() = default;
+ ~TreeBuilder() = default;
+
+ TreeBuilder( const TreeBuilder& ) = delete;
+ TreeBuilder& operator=( const TreeBuilder& ) = delete;
+ TreeBuilder( TreeBuilder&& ) = delete;
+ TreeBuilder& operator=( TreeBuilder&& ) = delete;
+
+ /**
+ * @brief Assemble a one-dimensional token sequence into a multi-dimensional DOM tree
+ */
+ DomNode build( const std::vector<HtmlToken>& tokens ) const;
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/render/input_renderer.hh b/ricochet/include/ricochet/render/input_renderer.hh
new file mode 100644
index 0000000..0114673
--- /dev/null
+++ b/ricochet/include/ricochet/render/input_renderer.hh
@@ -0,0 +1,34 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct FormContext;
+
+void render_input_node( const parser::DomNode& node,
+ RenderResult& result,
+ const FormContext& ctx,
+ std::string& output );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/internal.hh b/ricochet/include/ricochet/render/internal.hh
new file mode 100644
index 0000000..5e7cac0
--- /dev/null
+++ b/ricochet/include/ricochet/render/internal.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct FormContext
+{
+ std::string action;
+ std::string method;
+};
+
+void render_node( const parser::DomNode& node, RenderResult& result, FormContext ctx );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/renderer.hh b/ricochet/include/ricochet/render/renderer.hh
new file mode 100644
index 0000000..472afaa
--- /dev/null
+++ b/ricochet/include/ricochet/render/renderer.hh
@@ -0,0 +1,60 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct InputField
+{
+ std::string name;
+ std::string action;
+ std::string method;
+ std::string value {};
+ std::string type {};
+ bool checked { false };
+};
+struct RenderResult
+{
+ std::string text {};
+ std::vector<std::string> links {};
+ std::vector<InputField> inputs {};
+};
+
+class Renderer
+{
+public:
+ Renderer() = default;
+ ~Renderer() = default;
+
+ Renderer( const Renderer& ) = delete;
+ Renderer& operator=( const Renderer& ) = delete;
+ Renderer( Renderer&& ) = delete;
+ Renderer& operator=( Renderer&& ) = delete;
+
+ /**
+ * @brief Traverse the DOM tree and output the formatted result to the terminal using ANSI escape codes
+ * @param node The root node of the DOM tree
+ */
+ RenderResult render( const parser::DomNode& node ) const;
+};
+
+} // namespace ricochet::render \ No newline at end of file
diff --git a/ricochet/include/ricochet/render/table_renderer.hh b/ricochet/include/ricochet/render/table_renderer.hh
new file mode 100644
index 0000000..5efc42c
--- /dev/null
+++ b/ricochet/include/ricochet/render/table_renderer.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+
+namespace ricochet::render {
+
+struct FormContext;
+
+void render_table( const parser::DomNode& node, RenderResult& result, const FormContext& ctx );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/utils.hh b/ricochet/include/ricochet/render/utils.hh
new file mode 100644
index 0000000..38b3022
--- /dev/null
+++ b/ricochet/include/ricochet/render/utils.hh
@@ -0,0 +1,33 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace ricochet::render {
+
+std::string decode_entities( std::string text );
+std::string extract_attr( const std::string& tag_name, std::string_view attr_name );
+std::size_t get_visible_length( std::string_view str );
+
+bool is_ignored_tag( std::string_view tag );
+bool is_heading_tag( std::string_view tag );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/tui/document.hh b/ricochet/include/ricochet/tui/document.hh
new file mode 100644
index 0000000..bd76442
--- /dev/null
+++ b/ricochet/include/ricochet/tui/document.hh
@@ -0,0 +1,255 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/position.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ricochet::tui {
+
+struct TextStyle
+{
+ bool emphasis { false };
+ bool strong { false };
+ bool code { false };
+ bool subdued { false };
+ bool warning { false };
+};
+
+struct StyledRange
+{
+ ByteRange bytes {};
+ TextStyle style {};
+};
+
+// Leaf IDs and structural block IDs share one document-local namespace.
+// All text offsets address a Text leaf, not a concatenation of rendered lines.
+struct Text
+{
+ BlockId id {};
+ // LF is an explicit hard break; soft wrapping never changes this string.
+ std::string utf8 {};
+ std::vector<StyledRange> styles {};
+};
+
+struct ControlPlaceholder
+{ ControlId control {}; };
+
+using Inline = std::variant<Text, ControlPlaceholder>;
+
+struct Paragraph
+{ std::vector<Inline> content {}; };
+
+struct Heading
+{
+ unsigned int level { 1 };
+ std::vector<Inline> content {};
+};
+
+struct Preformatted
+{
+ Text content {};
+ std::optional<std::string> language {};
+};
+
+struct Block;
+
+struct ListItem
+{
+ std::optional<std::string> marker {};
+ std::vector<Block> content {};
+};
+
+enum class ListKind : std::uint8_t
+{
+ Unordered,
+ Ordered
+};
+
+struct List
+{
+ ListKind kind { ListKind::Unordered };
+ std::int64_t first_number { 1 };
+ std::vector<ListItem> items {};
+};
+
+struct TableCell
+{
+ // The producer resolves HTML spans to grid coordinates before delivery.
+ std::size_t column { 0 };
+ std::size_t column_span { 1 };
+ std::size_t row_span { 1 };
+ bool header { false };
+ std::vector<Block> content {};
+};
+
+struct TableRow
+{ std::vector<TableCell> cells {}; };
+
+struct Table
+{
+ std::size_t columns { 0 };
+ std::vector<TableRow> rows {};
+};
+
+struct Alternative
+{ Text description {}; };
+
+struct Separator
+{};
+
+struct Block
+{
+ // Structural identity supports empty blocks and per-block horizontal scroll.
+ BlockId id {};
+ std::variant<Paragraph, Heading, Preformatted, List, Table, ControlPlaceholder, Alternative, Separator>
+ content {};
+};
+
+struct Link
+{
+ LinkId id {};
+ std::string target {};
+ std::string description {};
+ // Multiple ranges can belong to one link, independently of text styling.
+ std::vector<TextRange> ranges {};
+};
+
+enum class TextControlKind : std::uint8_t
+{
+ SingleLine,
+ MultiLine,
+ Password
+};
+
+struct TextControl
+{
+ TextControlKind kind { TextControlKind::SingleLine };
+ std::string value {};
+ std::string default_value {};
+};
+
+struct Checkbox
+{
+ bool checked { false };
+ bool default_checked { false };
+};
+
+struct RadioButton
+{
+ RadioGroupId group {};
+ bool checked { false };
+ bool default_checked { false };
+};
+
+struct SelectOption
+{
+ OptionId id {};
+ std::string label {};
+ bool disabled { false };
+};
+
+enum class SelectKind : std::uint8_t
+{
+ Single,
+ Multiple
+};
+
+struct SelectControl
+{
+ SelectKind kind { SelectKind::Single };
+ std::vector<SelectOption> options {};
+ std::vector<OptionId> selected {};
+ std::vector<OptionId> default_selected {};
+};
+
+enum class ButtonKind : std::uint8_t
+{
+ Submit,
+ Reset
+};
+
+struct Button
+{ ButtonKind kind { ButtonKind::Submit }; };
+
+struct UnsupportedControl
+{ std::string description {}; };
+
+struct Control
+{
+ ControlId id {};
+ std::optional<FormId> form {};
+ std::string label {};
+ std::string hint {};
+ std::optional<std::string> error {};
+ bool disabled { false };
+ bool read_only { false };
+ std::variant<TextControl, Checkbox, RadioButton, SelectControl, Button, UnsupportedControl> content {};
+};
+
+struct Form
+{
+ FormId id {};
+ std::string label {};
+};
+
+struct Anchor
+{
+ std::string name {};
+ ContentPoint point {};
+};
+
+struct DocumentMetadata
+{
+ std::string title {};
+ std::string display_url {};
+ std::optional<std::string> language {};
+};
+
+// Producers build owned values. No DOM pointers, terminal codes or screen coordinates.
+struct DocumentData
+{
+ DocumentVersion version {};
+ DocumentMetadata metadata {};
+ std::vector<Block> blocks {};
+ std::vector<Link> links {};
+ std::vector<Control> controls {};
+ std::vector<Form> forms {};
+ std::vector<Anchor> anchors {};
+};
+
+class Document
+{
+public:
+ explicit Document( DocumentData data );
+
+ const DocumentData& data() const noexcept;
+
+private:
+ std::shared_ptr<const DocumentData> data_;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/identity.hh b/ricochet/include/ricochet/tui/identity.hh
new file mode 100644
index 0000000..624c6cc
--- /dev/null
+++ b/ricochet/include/ricochet/tui/identity.hh
@@ -0,0 +1,63 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <compare>
+#include <cstdint>
+
+namespace ricochet::tui {
+
+template<class Tag>
+struct Identity
+{
+ std::uint64_t value { 0 };
+
+ auto operator<=>( const Identity& ) const = default;
+};
+
+using TabId = Identity<struct TabTag>;
+using DocumentId = Identity<struct DocumentTag>;
+using Revision = Identity<struct RevisionTag>;
+using DraftRevision = Identity<struct DraftRevisionTag>;
+using BlockId = Identity<struct BlockTag>;
+using LinkId = Identity<struct LinkTag>;
+using ControlId = Identity<struct ControlTag>;
+using FormId = Identity<struct FormTag>;
+using OptionId = Identity<struct OptionTag>;
+using RadioGroupId = Identity<struct RadioGroupTag>;
+using TaskId = Identity<struct TaskTag>;
+using RequestId = Identity<struct RequestTag>;
+
+struct DocumentVersion
+{
+ DocumentId document {};
+ Revision revision {};
+
+ bool operator==( const DocumentVersion& ) const = default;
+};
+
+struct DocumentScope
+{
+ TabId tab {};
+ DocumentVersion version {};
+
+ bool operator==( const DocumentScope& ) const = default;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/input.hh b/ricochet/include/ricochet/tui/input.hh
new file mode 100644
index 0000000..df5b458
--- /dev/null
+++ b/ricochet/include/ricochet/tui/input.hh
@@ -0,0 +1,119 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+#include <string>
+#include <variant>
+
+namespace ricochet::tui {
+
+struct ScreenSize
+{
+ int rows { 0 };
+ int columns { 0 };
+ bool operator==( const ScreenSize& ) const = default;
+};
+
+struct CellPoint
+{
+ int row { 0 };
+ int column { 0 };
+};
+
+struct Modifiers
+{
+ bool shift { false };
+ bool control { false };
+ bool alt { false };
+ bool operator==( const Modifiers& ) const = default;
+};
+
+enum class KeyCode : std::uint8_t
+{
+ Character,
+ Escape,
+ Enter,
+ Tab,
+ Backspace,
+ Delete,
+ Insert,
+ Left,
+ Right,
+ Up,
+ Down,
+ Home,
+ End,
+ PageUp,
+ PageDown,
+ Function,
+ Unsupported
+};
+
+struct Key
+{
+ KeyCode code { KeyCode::Unsupported };
+ Modifiers modifiers {};
+ char32_t character { 0 };
+ unsigned int function { 0 };
+};
+
+struct TextInput
+{ char32_t character { 0 }; };
+
+struct Paste
+{ std::u32string text {}; };
+
+struct MouseButtons
+{
+ bool left { false };
+ bool middle { false };
+ bool right { false };
+};
+
+struct Mouse
+{
+ // Screen coordinates; the view router converts these to region-local coordinates.
+ CellPoint position {};
+ Modifiers modifiers {};
+ MouseButtons pressed {};
+ MouseButtons released {};
+ bool motion { false };
+ int wheel_steps { 0 }; // Positive scrolls down.
+};
+
+struct Resize
+{ ScreenSize size {}; };
+
+struct Tick
+{ std::chrono::steady_clock::time_point now {}; };
+
+enum class TerminalCloseReason : std::uint8_t
+{
+ Disconnected,
+ TerminationSignal
+};
+
+struct TerminalClosed
+{ TerminalCloseReason reason { TerminalCloseReason::Disconnected }; };
+
+using InputEvent = std::variant<Key, TextInput, Paste, Mouse, Resize, Tick, TerminalClosed>;
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/messages.hh b/ricochet/include/ricochet/tui/messages.hh
new file mode 100644
index 0000000..9d51fa3
--- /dev/null
+++ b/ricochet/include/ricochet/tui/messages.hh
@@ -0,0 +1,268 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/document.hh"
+#include "ricochet/tui/view.hh"
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ricochet::tui {
+
+struct TabInfo
+{
+ TabId id {};
+ std::string title {};
+ std::string display_url {};
+ std::optional<TaskId> loading_task {};
+};
+
+struct SyncTabs
+{
+ // Vector order is tab order. Request IDs acknowledge create/close actions.
+ std::vector<TabInfo> tabs {};
+ std::optional<TabId> active {};
+ std::vector<RequestId> completed_requests {};
+};
+
+struct NavigationStarted
+{
+ TabId tab {};
+ RequestId request {};
+ TaskId task {};
+ std::string display_target {};
+};
+
+struct CommitDocument
+{
+ TabId tab {};
+ RequestId navigation {};
+ Document document;
+ std::optional<ViewSnapshot> restore {};
+};
+
+struct TextCorrespondence
+{
+ // The producer identifies identical text in the two revisions, including every
+ // changed portion crossed by a selection. This is not an inferred text diff.
+ TextRange source {};
+ TextRange target {};
+};
+
+struct PositionMapping
+{
+ DocumentVersion source {};
+ DocumentVersion target {};
+ std::vector<TextCorrespondence> text {};
+};
+
+enum class ViewRetention : std::uint8_t
+{
+ Reset,
+ PreserveUnchanged
+};
+
+struct UpdateDocument
+{
+ TabId tab {};
+ RequestId navigation {};
+ DocumentVersion source {};
+ Document document;
+ ViewRetention retention { ViewRetention::PreserveUnchanged };
+ std::optional<PositionMapping> mapping {};
+};
+
+struct LocateView
+{
+ DocumentScope scope {};
+ std::variant<std::string, DocumentPoint> target {};
+};
+
+// Values are keyed by control identity, never by field name or action URL.
+// These business messages may carry passwords; ViewSnapshot deliberately does not.
+using ControlValue = std::variant<std::string, bool, std::vector<OptionId>>;
+
+struct ControlDraft
+{
+ ControlId control {};
+ ControlValue value {};
+};
+
+struct FieldError
+{
+ ControlId control {};
+ std::string message {};
+};
+
+struct FormFeedback
+{
+ DocumentScope scope {};
+ FormId form {};
+ RequestId request {};
+ DraftRevision draft_revision {};
+ std::vector<FieldError> errors {};
+ std::vector<ControlDraft> normalized_values {};
+};
+
+struct ActionFailed
+{
+ RequestId request {};
+ std::optional<TabId> tab {};
+ std::string summary {};
+ std::optional<std::string> details {};
+};
+
+struct ExitDecision
+{
+ RequestId request {};
+ bool approved { false };
+ std::optional<std::string> explanation {};
+};
+
+enum class CopyStatus : std::uint8_t
+{
+ Confirmed,
+ Dispatched,
+ Unavailable,
+ Failed
+};
+
+// The host's injected clipboard provider completes a CopySelection request once.
+// Dispatched must not be used as confirmation that a terminal accepted the text.
+struct CopyResult
+{
+ RequestId request {};
+ CopyStatus status { CopyStatus::Unavailable };
+ std::string summary {};
+};
+
+using HostUpdate = std::variant<SyncTabs,
+ NavigationStarted,
+ CommitDocument,
+ UpdateDocument,
+ LocateView,
+ FormFeedback,
+ CopyResult,
+ ActionFailed,
+ ExitDecision>;
+
+enum class OpenTarget : std::uint8_t
+{
+ CurrentTab,
+ NewTab
+};
+
+struct OpenLocation
+{
+ RequestId request {};
+ TabId tab {};
+ std::string input {};
+ OpenTarget target { OpenTarget::CurrentTab };
+};
+
+struct OpenLink
+{
+ RequestId request {};
+ DocumentScope source {};
+ LinkId link {};
+ std::string target {};
+ OpenTarget destination { OpenTarget::CurrentTab };
+};
+
+enum class NavigationCommand : std::uint8_t
+{
+ Back,
+ Forward,
+ Reload
+};
+
+struct Navigate
+{
+ RequestId request {};
+ TabId tab {};
+ std::optional<DocumentVersion> document {};
+ NavigationCommand command { NavigationCommand::Reload };
+};
+
+struct CreateTab
+{ RequestId request {}; };
+
+struct CloseTab
+{
+ RequestId request {};
+ TabId tab {};
+};
+
+enum class FormOperation : std::uint8_t
+{
+ Submit,
+ Reset
+};
+
+struct FormAction
+{
+ RequestId request {};
+ DocumentScope scope {};
+ FormId form {};
+ std::optional<ControlId> button {};
+ DraftRevision draft_revision {};
+ FormOperation operation { FormOperation::Submit };
+ std::vector<ControlDraft> values {};
+};
+
+struct ExportSelection
+{
+ RequestId request {};
+ DocumentScope source {};
+ // Already extracted from one view snapshot; passwords are masked.
+ std::string text {};
+};
+
+struct CopySelection
+{
+ RequestId request {};
+ DocumentScope source {};
+ DraftRevision draft_revision {};
+ // Frozen before dispatch; later cursor/selection changes cannot change this copy.
+ // Password content is already masked. Clipboard providers never receive drafts.
+ std::string text {};
+};
+
+struct RequestExit
+{
+ RequestId request {};
+ std::optional<TabId> active {};
+ std::vector<ViewSnapshot> views {};
+};
+
+using UserAction = std::variant<OpenLocation,
+ OpenLink,
+ Navigate,
+ CreateTab,
+ CloseTab,
+ FormAction,
+ ExportSelection,
+ CopySelection,
+ RequestExit>;
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/position.hh b/ricochet/include/ricochet/tui/position.hh
new file mode 100644
index 0000000..49b63f2
--- /dev/null
+++ b/ricochet/include/ricochet/tui/position.hh
@@ -0,0 +1,116 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/identity.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <variant>
+
+namespace ricochet::tui {
+
+// UTF-8 byte ranges are half-open. Text operations use grapheme boundaries.
+struct ByteRange
+{
+ std::size_t start { 0 };
+ std::size_t end { 0 };
+
+ bool operator==( const ByteRange& ) const = default;
+};
+
+struct TextPosition
+{
+ BlockId block {};
+ std::size_t byte_offset { 0 };
+
+ bool operator==( const TextPosition& ) const = default;
+};
+
+struct ControlTextPosition
+{
+ ControlId control {};
+ DraftRevision revision {};
+ // Offset in display text; password offsets refer only to its mask.
+ std::size_t byte_offset { 0 };
+
+ bool operator==( const ControlTextPosition& ) const = default;
+};
+
+enum class BoundarySide : std::uint8_t
+{
+ Before,
+ After
+};
+
+struct ObjectPosition
+{
+ std::variant<BlockId, ControlId> object {};
+ BoundarySide side { BoundarySide::Before };
+
+ bool operator==( const ObjectPosition& ) const = default;
+};
+
+struct DocumentStart
+{ bool operator==( const DocumentStart& ) const = default; };
+
+// Local to a document snapshot. Stored anchors cannot depend on mutable drafts.
+using ContentPoint = std::variant<DocumentStart, TextPosition, ObjectPosition>;
+using DocumentPoint = std::variant<DocumentStart, TextPosition, ControlTextPosition, ObjectPosition>;
+
+struct LogicalPosition
+{
+ DocumentScope scope {};
+ DocumentPoint point {};
+
+ bool operator==( const LogicalPosition& ) const = default;
+};
+
+// Endpoints follow reading order, never the numeric order of their IDs.
+struct DocumentRange
+{
+ DocumentPoint start {};
+ DocumentPoint end {};
+
+ bool operator==( const DocumentRange& ) const = default;
+};
+
+struct TextRange
+{
+ BlockId block {};
+ ByteRange bytes {};
+
+ bool operator==( const TextRange& ) const = default;
+};
+
+enum class WrapAffinity : std::uint8_t
+{
+ PreviousLine,
+ NextLine
+};
+
+struct DisplayPosition
+{
+ DocumentPoint point {};
+ WrapAffinity affinity { WrapAffinity::NextLine };
+
+ bool operator==( const DisplayPosition& ) const = default;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/session.hh b/ricochet/include/ricochet/tui/session.hh
new file mode 100644
index 0000000..94cae68
--- /dev/null
+++ b/ricochet/include/ricochet/tui/session.hh
@@ -0,0 +1,110 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/input.hh"
+#include "ricochet/tui/messages.hh"
+
+#include <chrono>
+#include <cstdio>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace ricochet::tui {
+
+namespace detail {
+class Mailbox;
+}
+
+struct TerminalOptions
+{
+ bool capture_mouse { true };
+ // Explicit host opt-in: terminfo does not establish bracketed-paste support.
+ bool bracketed_paste { false };
+ std::chrono::milliseconds escape_delay { 25 };
+};
+
+struct TerminalCapabilities
+{
+ bool colors { false };
+ bool mouse_buttons { false };
+ bool mouse_motion { false };
+ bool bracketed_paste { false };
+};
+
+enum class PostResult : std::uint8_t
+{
+ Queued,
+ Closed
+};
+
+// A sender can outlive the UI session. Posting is thread-safe; it never calls curses.
+class UpdateSender
+{
+public:
+ PostResult post_update( HostUpdate update ) const;
+
+private:
+ friend class Session;
+ explicit UpdateSender( std::shared_ptr<detail::Mailbox> mailbox );
+ std::shared_ptr<detail::Mailbox> mailbox_;
+};
+
+struct StepResult
+{
+ bool running { true };
+ // Incremental document updates and editing are subsequent milestones. Until then,
+ // unsupported updates are returned intact rather than silently acknowledged.
+ std::vector<HostUpdate> unhandled_updates {};
+ std::optional<TerminalClosed> terminal_closed {};
+ std::optional<std::string> error {};
+};
+
+class Session
+{
+public:
+ explicit Session( TerminalOptions options = {} );
+ // Streams must remain open until destruction. Construction, step, close and
+ // destruction run on the same UI thread; only one terminal session may exist.
+ Session( std::FILE& input, std::FILE& output, TerminalOptions options = {} );
+ ~Session();
+
+ Session( const Session& ) = delete;
+ Session& operator=( const Session& ) = delete;
+ Session( Session&& ) = delete;
+ Session& operator=( Session&& ) = delete;
+
+ UpdateSender sender() const;
+ PostResult post_update( HostUpdate update );
+ // max_wait bounds idle waiting. Decoding a partial terminal key sequence can
+ // additionally wait for escape_delay; processing batches is bounded separately.
+ StepResult step( std::chrono::milliseconds max_wait );
+ std::vector<UserAction> take_actions();
+ std::optional<ViewSnapshot> snapshot( TabId tab ) const;
+ TerminalCapabilities capabilities() const;
+ void close() noexcept;
+
+private:
+ class Impl;
+ std::unique_ptr<Impl> impl_;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/view.hh b/ricochet/include/ricochet/tui/view.hh
new file mode 100644
index 0000000..cc1d0fd
--- /dev/null
+++ b/ricochet/include/ricochet/tui/view.hh
@@ -0,0 +1,74 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/position.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace ricochet::tui {
+
+enum class SelectionDirection : std::uint8_t
+{
+ Forward,
+ Backward
+};
+
+struct SelectionSnapshot
+{
+ // The normalized half-open range also covers any controls between its endpoints.
+ DocumentRange range {};
+ SelectionDirection direction { SelectionDirection::Forward };
+ // Inclusive endpoint units retain the anchor through reversal and history restore.
+ DocumentRange anchor {};
+ DocumentRange active {};
+};
+
+struct HorizontalScroll
+{
+ BlockId block {};
+ std::size_t columns { 0 };
+};
+
+struct SearchSnapshot
+{
+ std::string query {};
+ std::optional<DocumentRange> current_match {};
+};
+
+// All positions and ranges belong to scope and draft_revision.
+// Editing sessions and control values (including passwords) are not exported here.
+struct ViewSnapshot
+{
+ DocumentScope scope {};
+ DraftRevision draft_revision {};
+ DisplayPosition cursor {};
+ DisplayPosition viewport_anchor {};
+ // Relative visual row for blank table space with no text at the exact top.
+ std::int64_t viewport_row_offset { 0 };
+ std::optional<SelectionSnapshot> selection {};
+ std::vector<HorizontalScroll> horizontal_scroll {};
+ SearchSnapshot search {};
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/scripts/add_license.py b/ricochet/scripts/add_license.py
new file mode 100644
index 0000000..d15c1fc
--- /dev/null
+++ b/ricochet/scripts/add_license.py
@@ -0,0 +1,57 @@
+import os
+from pathlib import Path
+
+# File extensions to process
+EXTENSIONS = {'.cpp', '.h', '.hpp', '.c', '.js', '.rs'}
+
+# License template
+LICENSE_TEMPLATE = """/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+"""
+
+# Identifier to check if license already exists
+IDENTIFIER = "Ricochet - A minimalist CLI web browser"
+
+def add_license_to_file(file_path):
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ if IDENTIFIER in content[:500]:
+ print(f"[-] Skip: {file_path} (License already exists)")
+ return
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(LICENSE_TEMPLATE + "\n" + content)
+ print(f"[+] Updated: {file_path}")
+
+ except Exception as e:
+ print(f"[!] Error: Failed to process {file_path} -> {e}")
+
+def main(target_dir):
+ path = Path(target_dir)
+ if not path.exists():
+ print(f"Error: Path {target_dir} does not exist")
+ return
+
+ for file in path.rglob('*'):
+ if file.suffix in EXTENSIONS:
+ add_license_to_file(file)
+
+if __name__ == "__main__":
+ project_root = input("Please enter the project root directory (press Enter to use the current directory): ").strip() or "."
+ main(project_root) \ No newline at end of file
diff --git a/ricochet/scripts/make-parallel.sh b/ricochet/scripts/make-parallel.sh
new file mode 100644
index 0000000..879af9a
--- /dev/null
+++ b/ricochet/scripts/make-parallel.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec make -j`nproc` "$@"
diff --git a/ricochet/src/CMakeLists.txt b/ricochet/src/CMakeLists.txt
new file mode 100644
index 0000000..fece630
--- /dev/null
+++ b/ricochet/src/CMakeLists.txt
@@ -0,0 +1,25 @@
+add_subdirectory(tui)
+
+file(GLOB_RECURSE CORE_SOURCES CONFIGURE_DEPENDS
+ "core/*.cc"
+ "net/*.cc"
+ "parser/*.cc"
+ "render/*.cc"
+)
+
+set(MAIN_SRC "${CMAKE_CURRENT_SOURCE_DIR}/main.cc")
+
+find_package(OpenSSL REQUIRED)
+message(STATUS "Found OpenSSL Version: ${OPENSSL_VERSION}")
+message(STATUS "OpenSSL Include dir: ${OPENSSL_INCLUDE_DIR}")
+
+add_library(ricochet_core STATIC ${CORE_SOURCES})
+target_include_directories(ricochet_core PUBLIC ${PROJECT_SOURCE_DIR}/include)
+target_link_libraries(ricochet_core
+ PUBLIC
+ OpenSSL::SSL
+ OpenSSL::Crypto
+)
+
+add_executable(ricochet ${MAIN_SRC})
+target_link_libraries(ricochet PRIVATE ricochet_core)
diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc
new file mode 100644
index 0000000..c413a0d
--- /dev/null
+++ b/ricochet/src/core/browser.cc
@@ -0,0 +1,66 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/browser.hh"
+
+#include <fstream>
+
+#include "ricochet/core/local_data.hh"
+
+namespace ricochet::core {
+
+std::string Browser::format_url( std::string input )
+{
+ if ( input.empty() ) {
+ return input;
+ }
+
+ if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) {
+ return input;
+ }
+
+ const bool is_domain = input.contains( '.' ) && !input.contains( ' ' );
+ if ( is_domain ) {
+ input.insert( 0, "https://" );
+ return input;
+ }
+
+ for ( char& ch : input ) {
+ if ( ch == ' ' ) {
+ ch = '+';
+ }
+ }
+ return "https://lite.duckduckgo.com/lite/?q=" + input;
+}
+
+std::string Browser::get_configured_homepage()
+{
+ std::ifstream file( get_config_path( "config.txt" ) );
+ if ( file.is_open() ) {
+ std::string line;
+ while ( std::getline( file, line ) ) {
+ const std::size_t eq = line.find( '=' );
+ if ( eq != std::string::npos && line.substr( 0, eq ) == "homepage" ) {
+ return line.substr( eq + 1 );
+ }
+ }
+ }
+ return "https://wikipedia.org";
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/core/form_handler.cc b/ricochet/src/core/form_handler.cc
new file mode 100644
index 0000000..6100a30
--- /dev/null
+++ b/ricochet/src/core/form_handler.cc
@@ -0,0 +1,64 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/form_handler.hh"
+#include "ricochet/core/utils.hh"
+
+namespace ricochet::core {
+
+InputAction submit_form( const render::InputField& target_input,
+ const std::vector<render::InputField>& inputs,
+ const std::string& current_url,
+ std::vector<HttpRequest>& history,
+ std::size_t& history_idx )
+{
+ std::string body;
+ for ( const auto& in : inputs ) {
+ if ( in.action == target_input.action && !in.name.empty() && in.type != "submit" && in.type != "button" ) {
+ if ( ( in.type == "checkbox" || in.type == "radio" ) && !in.checked ) {
+ continue;
+ }
+ std::string encoded = in.value;
+ for ( char& ch : encoded ) {
+ if ( ch == ' ' ) {
+ ch = '+';
+ }
+ }
+ if ( !body.empty() ) {
+ body += "&";
+ }
+ body += in.name + "=" + encoded;
+ }
+ }
+ const std::string action
+ = target_input.action.empty() ? current_url : normalize_url( target_input.action, current_url );
+ HttpRequest next_req { .url = action, .method = "GET", .body = "" };
+ if ( target_input.method == "post" || target_input.method == "POST" ) {
+ next_req.method = "POST";
+ next_req.body = body;
+ } else {
+ const char sep = action.contains( '?' ) ? '&' : '?';
+ next_req.url += sep + body;
+ }
+ history.resize( history_idx + 1 );
+ history.push_back( next_req );
+ history_idx++;
+ return InputAction::Navigate;
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/core/license.cc b/ricochet/src/core/license.cc
new file mode 100644
index 0000000..587a942
--- /dev/null
+++ b/ricochet/src/core/license.cc
@@ -0,0 +1,69 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/license.hh"
+#include <iostream>
+
+namespace ricochet::core {
+
+void print_license_notice()
+{
+ std::cout << "Ricochet Copyright (C) 2026 Your Name\n"
+ << "Ricochet comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.\n"
+ << "This is free software, and you are welcome to redistribute it\n"
+ << "under certain conditions; type 'show c' for details.\n\n";
+}
+
+void show_warranty()
+{
+ std::cout << R"(
+[ Ricochet - WARRANTY INFORMATION ]
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
+PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
+SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
+SERVICING, REPAIR OR CORRECTION.
+)" << '\n';
+}
+
+void show_copying()
+{
+ std::cout << R"(
+[ Ricochet - COPYING CONDITIONS ]
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+)" << '\n';
+}
+
+} // namespace ricochet::core \ No newline at end of file
diff --git a/ricochet/src/core/local_data.cc b/ricochet/src/core/local_data.cc
new file mode 100644
index 0000000..9115844
--- /dev/null
+++ b/ricochet/src/core/local_data.cc
@@ -0,0 +1,171 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/local_data.hh"
+#include <cstdlib>
+#include <fstream>
+#include <sys/stat.h>
+#include <vector>
+
+namespace ricochet::core {
+
+std::string get_config_path( const std::string& filename )
+{
+ const char* home = std::getenv( "HOME" ); // NOLINT(concurrency-mt-unsafe)
+ const std::string dir = ( home != nullptr ) ? std::string( home ) + "/.ricochet" : ".ricochet";
+ static_cast<void>( mkdir( dir.c_str(), 0755 ) );
+ return dir + "/" + filename;
+}
+
+void save_bookmark( const std::string& title, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& url )
+{
+ if ( url == "ricochet://bookmarks" ) {
+ return;
+ }
+ std::ofstream file( get_config_path( "bookmarks.txt" ), std::ios::app );
+ if ( !file.is_open() ) {
+ return;
+ }
+
+ std::string safe_title = title;
+ if ( safe_title.empty() ) {
+ safe_title = url;
+ }
+ for ( char& c : safe_title ) {
+ if ( c == '\n' || c == '\r' || c == '\t' ) {
+ c = ' ';
+ }
+ }
+ file << url << '\t' << safe_title << '\n';
+}
+
+std::string generate_bookmarks_html()
+{
+ std::ifstream file( get_config_path( "bookmarks.txt" ) );
+ std::string html = "<html><head><title>My Bookmarks</title></head><body><h1>Saved Bookmarks</h1><ul>";
+ if ( !file.is_open() ) {
+ html += "<li>No bookmarks found. Press 'b' on any page to add one!</li>";
+ } else {
+ std::string line;
+ while ( std::getline( file, line ) ) {
+ const std::size_t tab_pos = line.find( '\t' );
+ if ( tab_pos != std::string::npos ) {
+ const std::string url = line.substr( 0, tab_pos );
+ const std::string title = line.substr( tab_pos + 1 );
+ html += "<li><a href=\"";
+ html += url;
+ html += "\">[Link] ";
+ html += title;
+ html += "</a></li>";
+ }
+ }
+ }
+ html += "</ul></body></html>";
+ return html;
+}
+
+void append_to_history( const std::string& url, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& title )
+{
+ if ( url.starts_with( "ricochet://" ) ) {
+ return;
+ }
+ std::ofstream file( get_config_path( "history.txt" ), std::ios::app );
+ if ( !file.is_open() ) {
+ return;
+ }
+
+ std::string safe_title = title;
+ if ( safe_title.empty() ) {
+ safe_title = url;
+ }
+ for ( char& c : safe_title ) {
+ if ( c == '\n' || c == '\r' || c == '\t' ) {
+ c = ' ';
+ }
+ }
+ file << url << '\t' << safe_title << '\n';
+}
+
+std::string generate_history_html()
+{
+ std::ifstream file( get_config_path( "history.txt" ) );
+ std::string html = "<html><head><title>History</title></head><body><h1>Browsing History</h1><ul>";
+ if ( !file.is_open() ) {
+ html += "<li>No history found. Start browsing!</li>";
+ } else {
+ std::vector<std::string> lines;
+ std::string line;
+ while ( std::getline( file, line ) ) {
+ lines.push_back( std::move( line ) );
+ }
+
+ std::size_t count = 0;
+ for ( auto it = lines.rbegin(); it != lines.rend() && count < 100; ++it, ++count ) {
+ const std::size_t tab_pos = it->find( '\t' );
+ if ( tab_pos != std::string::npos ) {
+ const std::string url = it->substr( 0, tab_pos );
+ const std::string title = it->substr( tab_pos + 1 );
+ html += "<li><a href=\"";
+ html += url;
+ html += "\">[Link] ";
+ html += title;
+ html += "</a></li>";
+ }
+ }
+ }
+ html += "</ul></body></html>";
+ return html;
+}
+
+std::string generate_help_html()
+{
+ return R"(<html>
+ <head><title>Ricochet Help Manual</title></head>
+ <body>
+ <h1>Ricochet User Manual</h1>
+ <h2>1. Navigation & Browsing</h2>
+ <ul>
+ <li><b>j / k</b> : Scroll down / up</li>
+ <li><b>H / L</b> : History backward / forward</li>
+ <li><b>/</b> : Open prompt to enter a new URL</li>
+ </ul>
+ <h2>2. Page Interaction</h2>
+ <ul>
+ <li><b>f</b> : Toggle Link Hints (type the two letters to follow a link instantly)</li>
+ <li><b>n</b> : Find in page (search for text)</li>
+ <li><b>N</b> : Jump to the next search match</li>
+ <li><b>i</b> : Enter form input mode (fill textboxes, click buttons, toggle checkboxes)</li>
+ </ul>
+ <h2>3. Bookmarks & History</h2>
+ <ul>
+ <li><b>b</b> : Bookmark the current page</li>
+ <li><b>B</b> : View all saved bookmarks</li>
+ <li><b>h</b> : View browsing history</li>
+ </ul>
+ <h2>4. System</h2>
+ <ul>
+ <li><b>?</b> : Show this help manual</li>
+ <li><b>q</b> : Quit Ricochet</li>
+ </ul>
+ </body>
+ </html>)";
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/core/page_loader.cc b/ricochet/src/core/page_loader.cc
new file mode 100644
index 0000000..b9a5da5
--- /dev/null
+++ b/ricochet/src/core/page_loader.cc
@@ -0,0 +1,78 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/page_loader.hh"
+#include "ricochet/core/local_data.hh"
+#include "ricochet/core/page_processor.hh"
+#include "ricochet/core/utils.hh"
+#include "ricochet/parser/html_lexer.hh"
+#include "ricochet/parser/tree_builder.hh"
+#include "ricochet/render/renderer.hh"
+
+namespace ricochet::core {
+
+PageData load_page( const HttpRequest& req )
+{
+ std::string html_body;
+ if ( req.url == "ricochet://bookmarks" ) {
+ html_body = generate_bookmarks_html();
+ } else if ( req.url == "ricochet://history" ) {
+ html_body = generate_history_html();
+ } else if ( req.url == "ricochet://help" ) {
+ html_body = generate_help_html();
+ } else {
+ const net::HttpClient client;
+ auto response_result = client.fetch( req.url, req.method, req.body );
+ if ( !response_result.has_value() ) {
+ return { .lines
+ = { "\033[1;31m[!] Failed to load:\033[0m " + req.url, "", "Reason: " + response_result.error() },
+ .links = {},
+ .inputs = {},
+ .title = "Error" };
+ }
+
+ const auto& resp = response_result.value();
+ std::string c_type = resp.content_type;
+ for ( char& c : c_type ) {
+ if ( c >= 'A' && c <= 'Z' ) {
+ c = static_cast<char>( c + 32 );
+ }
+ }
+
+ const bool is_text
+ = c_type.starts_with( "text/" ) || c_type.contains( "xml" ) || c_type.contains( "json" ) || c_type.empty();
+
+ if ( !is_text ) {
+ return process_binary_download( req, resp );
+ }
+ html_body = response_result.value().body;
+ }
+
+ const parser::HtmlLexer lexer;
+ const parser::TreeBuilder builder;
+ const render::Renderer renderer;
+
+ const auto dom_root = builder.build( lexer.tokenize( html_body ) );
+ const render::RenderResult result = renderer.render( dom_root );
+ const std::string title = extract_title( dom_root );
+
+ return {
+ .lines = split_into_lines( result.text ), .links = result.links, .inputs = result.inputs, .title = title };
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/core/page_processor.cc b/ricochet/src/core/page_processor.cc
new file mode 100644
index 0000000..00728a9
--- /dev/null
+++ b/ricochet/src/core/page_processor.cc
@@ -0,0 +1,97 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/page_processor.hh"
+#include <fstream>
+#include <ranges>
+#include <vector>
+
+namespace ricochet::core {
+
+std::string extract_title( const parser::DomNode& root_node )
+{
+ std::vector<const parser::DomNode*> stack;
+ stack.push_back( &root_node );
+
+ while ( !stack.empty() ) {
+ const parser::DomNode* current = stack.back();
+ stack.pop_back();
+
+ std::string base_tag = current->tag_name;
+ const std::size_t space_pos = base_tag.find( ' ' );
+ if ( space_pos != std::string::npos ) {
+ base_tag = base_tag.substr( 0, space_pos );
+ }
+
+ for ( char& c : base_tag ) {
+ if ( c >= 'A' && c <= 'Z' ) {
+ c = static_cast<char>( c + 32 );
+ }
+ }
+
+ if ( base_tag == "title" ) {
+ std::string full_title;
+ for ( const auto& child : current->children ) {
+ if ( child.tag_name.empty() ) {
+ full_title += child.text_content;
+ }
+ }
+ if ( !full_title.empty() ) {
+ return full_title;
+ }
+ }
+
+ for ( const auto& child : std::ranges::reverse_view( current->children ) ) {
+ stack.push_back( &child );
+ }
+ }
+ return "";
+}
+
+PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp )
+{
+ std::string fname = "download.dat";
+ const std::size_t slash = req.url.rfind( '/' );
+ if ( slash != std::string::npos && slash + 1 < req.url.length() ) {
+ fname = req.url.substr( slash + 1 );
+ const std::size_t q_pos = fname.find( '?' );
+ if ( q_pos != std::string::npos ) {
+ fname = fname.substr( 0, q_pos );
+ }
+ }
+ if ( fname.empty() ) {
+ fname = "download.dat";
+ }
+
+ std::ofstream out( fname, std::ios::binary );
+ out.write( resp.body.data(), static_cast<std::streamsize>( resp.body.size() ) );
+ out.close();
+
+ return { .lines = { "\033[1;32m[ Download Complete ]\033[0m",
+ "",
+ "Saved File : " + fname,
+ "File Type : " + resp.content_type,
+ "File Size : " + std::to_string( resp.body.size() ) + " bytes",
+ "",
+ "Press 'H' to return to the previous page." },
+ .links = {},
+ .inputs = {},
+ .title = "Download Manager" };
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/core/utils.cc b/ricochet/src/core/utils.cc
new file mode 100644
index 0000000..a410c8a
--- /dev/null
+++ b/ricochet/src/core/utils.cc
@@ -0,0 +1,85 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/core/utils.hh"
+
+namespace ricochet::core {
+
+std::vector<std::string> split_into_lines( const std::string& text )
+{
+ std::vector<std::string> lines;
+ std::size_t start = 0;
+ while ( start < text.length() ) {
+ const std::size_t end = text.find( '\n', start );
+ if ( end == std::string::npos ) {
+ lines.push_back( text.substr( start ) );
+ break;
+ }
+ lines.push_back( text.substr( start, end - start ) );
+ start = end + 1;
+ }
+ return lines;
+}
+
+std::string normalize_url( const std::string& target, const std::string& current_url )
+{
+ if ( target.starts_with( "/" ) ) {
+ if ( target.starts_with( "//" ) ) {
+ return "https:" + target;
+ }
+ const std::size_t slash_pos = current_url.find( '/', 8 );
+ const std::string domain
+ = ( slash_pos == std::string::npos ) ? current_url : current_url.substr( 0, slash_pos );
+ return domain + target;
+ }
+ if ( !target.starts_with( "http" ) ) {
+ const std::size_t last_slash = current_url.rfind( '/' );
+ if ( last_slash > 7 ) {
+ return current_url.substr( 0, last_slash + 1 ) + target;
+ }
+ return current_url + "/" + target;
+ }
+ return target;
+}
+
+std::string strip_ddg_tracker( std::string target )
+{
+ const std::size_t uddg_pos = target.find( "uddg=" );
+ if ( uddg_pos != std::string::npos ) {
+ const std::size_t end_pos = target.find( '&', uddg_pos );
+ const std::string encoded = target.substr( uddg_pos + 5, end_pos - ( uddg_pos + 5 ) );
+ std::string decoded;
+ for ( std::size_t i = 0; i < encoded.length(); ++i ) {
+ if ( encoded[i] == '%' && i + 2 < encoded.length() ) {
+ const std::string hex = encoded.substr( i + 1, 2 );
+ try {
+ decoded += static_cast<char>( std::stoi( hex, nullptr, 16 ) );
+ } catch ( const std::exception& ) {
+ (void)0;
+ }
+ i += 2;
+ } else {
+ decoded += encoded[i];
+ }
+ }
+ return decoded;
+ }
+ return target;
+}
+
+} // namespace ricochet::core
diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc
new file mode 100644
index 0000000..d27ab55
--- /dev/null
+++ b/ricochet/src/main.cc
@@ -0,0 +1,73 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <iostream>
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include "ricochet/core/browser.hh"
+#include "ricochet/core/license.hh"
+
+namespace {
+
+void print_usage( std::string_view program_name )
+{
+ std::cout << "Usage: " << program_name << " [URL]\n"
+ << "Example: " << program_name << " https://wikipedia.org\n";
+}
+
+} // namespace
+
+int main( int argc, char* argv[] )
+{
+ const std::span<char*> args_view( argv, static_cast<size_t>( argc ) );
+ const std::vector<std::string_view> args( args_view.begin(), args_view.end() );
+
+ ricochet::core::print_license_notice();
+
+ if ( args.size() >= 2 && args[1] == "show" ) {
+ if ( args.size() >= 3 && args[2] == "w" ) {
+ ricochet::core::show_warranty();
+ return 0;
+ }
+ if ( args.size() >= 3 && args[2] == "c" ) {
+ ricochet::core::show_copying();
+ return 0;
+ }
+ std::cerr << "Error: Invalid argument for show command.\n";
+ print_usage( args[0] );
+ return 1;
+ }
+
+ std::string start_url;
+ if ( args.size() < 2 ) {
+ start_url = ricochet::core::Browser::get_configured_homepage();
+ } else {
+ start_url = std::string( args[1] );
+ }
+
+ start_url = ricochet::core::Browser::format_url( std::move( start_url ) );
+
+ try {
+ return ricochet::core::Browser::run( start_url );
+ } catch ( const std::exception& e ) {
+ std::cerr << "Critical Error: " << e.what() << "\n";
+ return 1;
+ }
+} \ No newline at end of file
diff --git a/ricochet/src/net/config.cc b/ricochet/src/net/config.cc
new file mode 100644
index 0000000..6f257df
--- /dev/null
+++ b/ricochet/src/net/config.cc
@@ -0,0 +1,64 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/config.hh"
+
+#include <cstdlib>
+#include <fstream>
+#include <sys/stat.h>
+
+namespace ricochet::net {
+
+std::string get_config_path( const std::string& filename )
+{
+ const char* home = std::getenv( "HOME" ); // NOLINT(concurrency-mt-unsafe)
+ const std::string dir = ( home != nullptr ) ? std::string( home ) + "/.ricochet" : ".ricochet";
+ static_cast<void>( mkdir( dir.c_str(), 0755 ) );
+ return dir + "/" + filename;
+}
+
+BrowserConfig& get_browser_config()
+{
+ static BrowserConfig config { .user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
+ .homepage = "https://wikipedia.org" };
+ static bool loaded = false;
+ if ( !loaded ) {
+ std::ifstream file( get_config_path( "config.txt" ) );
+ if ( file.is_open() ) {
+ std::string line;
+ while ( std::getline( file, line ) ) {
+ const std::size_t eq = line.find( '=' );
+ if ( eq == std::string::npos ) {
+ continue;
+ }
+ const std::string key = line.substr( 0, eq );
+ const std::string val = line.substr( eq + 1 );
+ if ( key == "user_agent" ) {
+ config.user_agent = val;
+ } else if ( key == "homepage" ) {
+ config.homepage = val;
+ }
+ }
+ }
+ loaded = true;
+ }
+ return config;
+}
+
+} // namespace ricochet::net
diff --git a/ricochet/src/net/cookie_jar.cc b/ricochet/src/net/cookie_jar.cc
new file mode 100644
index 0000000..1328511
--- /dev/null
+++ b/ricochet/src/net/cookie_jar.cc
@@ -0,0 +1,124 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/cookie_jar.hh"
+#include "ricochet/net/config.hh"
+
+#include <fstream>
+
+namespace ricochet::net {
+
+void load_cookies_from_file( CookieJar& jar )
+{
+ std::ifstream file( get_config_path( "cookies.txt" ) );
+ if ( !file.is_open() ) {
+ return;
+ }
+
+ std::string line;
+ while ( std::getline( file, line ) ) {
+ const std::size_t t1 = line.find( '\t' );
+ if ( t1 == std::string::npos ) {
+ continue;
+ }
+ const std::size_t t2 = line.find( '\t', t1 + 1 );
+ if ( t2 != std::string::npos ) {
+ jar[line.substr( 0, t1 )][line.substr( t1 + 1, t2 - t1 - 1 )] = line.substr( t2 + 1 );
+ }
+ }
+}
+
+CookieJar& get_cookie_jar()
+{
+ static CookieJar jar;
+ static bool initialized = false;
+ if ( !initialized ) {
+ load_cookies_from_file( jar );
+ initialized = true;
+ }
+ return jar;
+}
+
+void save_cookies_to_file( const CookieJar& jar )
+{
+ std::ofstream file( get_config_path( "cookies.txt" ), std::ios::trunc );
+ if ( !file.is_open() ) {
+ return;
+ }
+
+ for ( const auto& [domain, cookies] : jar ) {
+ for ( const auto& [k, v] : cookies ) {
+ file << domain << '\t' << k << '\t' << v << '\n';
+ }
+ }
+}
+
+void update_cookies( const std::string& raw_response, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& host )
+{
+ std::size_t pos = 0;
+ bool updated = false;
+ while ( ( pos = raw_response.find( "Set-Cookie: ", pos ) ) != std::string::npos ) {
+ pos += 12;
+ const std::size_t end_pos = raw_response.find( '\r', pos );
+ if ( end_pos == std::string::npos ) {
+ break;
+ }
+
+ const std::string cookie_line = raw_response.substr( pos, end_pos - pos );
+ const std::size_t semi_pos = cookie_line.find( ';' );
+ const std::string cookie_pair
+ = ( semi_pos == std::string::npos ) ? cookie_line : cookie_line.substr( 0, semi_pos );
+
+ const std::size_t eq_pos = cookie_pair.find( '=' );
+ if ( eq_pos != std::string::npos ) {
+ const std::string key = cookie_pair.substr( 0, eq_pos );
+ const std::string val = cookie_pair.substr( eq_pos + 1 );
+ get_cookie_jar()[host][key] = val;
+ updated = true;
+ }
+ }
+ if ( updated ) {
+ save_cookies_to_file( get_cookie_jar() );
+ }
+}
+
+std::string build_cookie_header( const std::string& host )
+{
+ const auto& jar = get_cookie_jar();
+ auto it = jar.find( host );
+ if ( it == jar.end() || it->second.empty() ) {
+ return "";
+ }
+
+ std::string header = "Cookie: ";
+ bool first = true;
+ for ( const auto& [k, v] : it->second ) {
+ if ( !first ) {
+ header += "; ";
+ }
+ header += k;
+ header += "=";
+ header += v;
+ first = false;
+ }
+ header += "\r\n";
+ return header;
+}
+
+} // namespace ricochet::net
diff --git a/ricochet/src/net/http_client.cc b/ricochet/src/net/http_client.cc
new file mode 100644
index 0000000..4a10c02
--- /dev/null
+++ b/ricochet/src/net/http_client.cc
@@ -0,0 +1,212 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/http_client.hh"
+
+#include "ricochet/net/config.hh"
+#include "ricochet/net/cookie_jar.hh"
+#include "ricochet/net/http_utils.hh"
+#include "ricochet/net/ssl_socket.hh"
+#include "ricochet/net/sys/address.hh"
+#include "ricochet/net/sys/socket.hh"
+#include "ricochet/net/url_parser.hh"
+
+#include <cstddef>
+#include <format>
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+namespace {
+
+struct FetchResult
+{
+ std::string raw_response {};
+ std::string error {};
+};
+
+FetchResult do_network_request( const std::string& host,
+ const std::string& service, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& path, // NOLINT(bugprone-easily-swappable-parameters)
+ bool is_https,
+ const std::string& method, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& body,
+ const std::string& cookie_header ) // NOLINT(bugprone-easily-swappable-parameters)
+{
+ FetchResult res;
+ try {
+ const Address server_addr( host, service );
+ std::string request = std::format( "{} {} HTTP/1.1\r\n"
+ "Host: {}\r\n"
+ "User-Agent: {}\r\n"
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
+ "Accept-Language: en-US,en;q=0.9\r\n"
+ "DNT: 1\r\n"
+ "{}"
+ "Connection: close\r\n",
+ method,
+ path,
+ host,
+ get_browser_config().user_agent,
+ cookie_header );
+ if ( !body.empty() ) {
+ request += std::format( "Content-Length: {}\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n",
+ body.length() );
+ }
+ request += "\r\n" + body;
+
+ if ( is_https ) {
+ SSLSocket ssl_socket;
+ ssl_socket.connect( server_addr, host );
+ for ( size_t bw = 0; bw < request.size(); ) {
+ bw += ssl_socket.write( request.substr( bw ) );
+ }
+ while ( !ssl_socket.eof() ) {
+ std::string buffer;
+ ssl_socket.read( buffer );
+ res.raw_response += buffer;
+ }
+ } else {
+ TCPSocket tcp_socket;
+ tcp_socket.connect( server_addr );
+ for ( size_t bw = 0; bw < request.size(); ) {
+ bw += tcp_socket.write( request.substr( bw ) );
+ }
+ while ( !tcp_socket.eof() ) {
+ std::string buffer;
+ tcp_socket.read( buffer );
+ res.raw_response += buffer;
+ }
+ }
+ } catch ( const std::exception& e ) {
+ res.error = std::string( "Network Error: " ) + e.what();
+ }
+ return res;
+}
+
+void parse_response_headers( const std::string& raw_response, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& host, // NOLINT(bugprone-easily-swappable-parameters)
+ HttpResponse& response,
+ std::string& headers )
+{
+ const auto header_end = raw_response.find( "\r\n\r\n" );
+ headers = ( header_end != std::string::npos ) ? raw_response.substr( 0, header_end ) : raw_response;
+ update_cookies( headers, host );
+
+ if ( header_end != std::string::npos ) {
+ response.body = raw_response.substr( header_end + 4 );
+ std::size_t ct_pos = headers.find( "Content-Type: " );
+ if ( ct_pos == std::string::npos ) {
+ ct_pos = headers.find( "content-type: " );
+ }
+ if ( ct_pos != std::string::npos ) {
+ ct_pos += 14;
+ const std::size_t end_pos = headers.find( '\r', ct_pos );
+ if ( end_pos != std::string::npos ) {
+ response.content_type = headers.substr( ct_pos, end_pos - ct_pos );
+ }
+ }
+ if ( raw_response.size() >= 12 ) {
+ try {
+ response.status_code = std::stoi( raw_response.substr( 9, 3 ) );
+ } catch ( ... ) {
+ response.status_code = 0;
+ }
+ }
+ } else {
+ response.body = raw_response;
+ response.status_code = 0;
+ }
+}
+
+void sanitize_response_body( HttpResponse& response, const std::string& raw_response )
+{
+ if ( raw_response.contains( "Transfer-Encoding: chunked" ) ) {
+ response.body = dechunk( response.body );
+ }
+ std::string ct_lower = response.content_type;
+ for ( char& c : ct_lower ) {
+ if ( c >= 'A' && c <= 'Z' ) {
+ c = static_cast<char>( c + 32 );
+ }
+ }
+
+ if ( ct_lower.starts_with( "text/" ) || ct_lower.contains( "xml" ) ) {
+ response.body = remove_html_comments( response.body );
+ response.body = remove_tag_blocks( response.body, "script" );
+ response.body = remove_tag_blocks( response.body, "style" );
+ }
+}
+
+} // namespace
+
+std::expected<HttpResponse, std::string>
+HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static)
+ std::string_view url, // NOLINT(bugprone-easily-swappable-parameters)
+ std::string_view method, // NOLINT(bugprone-easily-swappable-parameters)
+ std::string_view body ) const // NOLINT(bugprone-easily-swappable-parameters)
+{
+ std::string current_url( url );
+ std::string current_method( method );
+ std::string current_body( body );
+
+ for ( int redirect_count = 0; redirect_count < 5; ++redirect_count ) {
+ const auto parsed_url = parse_url( current_url );
+ if ( !parsed_url.has_value() ) {
+ return std::unexpected( parsed_url.error() );
+ }
+
+ const auto [host, path] = parsed_url.value();
+ const bool is_https = current_url.starts_with( "https://" );
+ const std::string service = is_https ? "https" : "http";
+
+ const std::string cookie_header = build_cookie_header( host );
+
+ const FetchResult net_res
+ = do_network_request( host, service, path, is_https, current_method, current_body, cookie_header );
+ if ( !net_res.error.empty() ) {
+ return std::unexpected( net_res.error );
+ }
+ const std::string& raw_response = net_res.raw_response;
+
+ HttpResponse response;
+ std::string headers;
+ parse_response_headers( raw_response, host, response, headers );
+
+ if ( response.status_code >= 300 && response.status_code < 400 ) {
+ const std::string next_url = extract_redirect_url( headers, is_https, host );
+ if ( !next_url.empty() ) {
+ current_url = next_url;
+ if ( response.status_code == 301 || response.status_code == 302 || response.status_code == 303 ) {
+ current_method = "GET";
+ current_body.clear();
+ }
+ continue;
+ }
+ }
+
+ sanitize_response_body( response, raw_response );
+ return response;
+ }
+
+ return std::unexpected( "Too many redirects" );
+}
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/src/net/http_utils.cc b/ricochet/src/net/http_utils.cc
new file mode 100644
index 0000000..98f6dae
--- /dev/null
+++ b/ricochet/src/net/http_utils.cc
@@ -0,0 +1,93 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/http_utils.hh"
+
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+std::string dechunk( std::string_view body )
+{
+ std::string result;
+ std::size_t pos = 0;
+ while ( pos < body.size() ) {
+ auto line_end = body.find( "\r\n", pos );
+ if ( line_end == std::string_view::npos ) {
+ break;
+ }
+
+ const std::string len_str( body.substr( pos, line_end - pos ) );
+ unsigned int chunk_size = 0;
+ try {
+ chunk_size = std::stoul( len_str, nullptr, 16 );
+ } catch ( ... ) {
+ break;
+ }
+
+ if ( chunk_size == 0 ) {
+ break;
+ }
+
+ pos = line_end + 2;
+ if ( pos + chunk_size > body.size() ) {
+ break;
+ }
+ result.append( body.substr( pos, chunk_size ) );
+
+ pos += chunk_size + 2;
+ }
+ return result;
+}
+
+std::string remove_html_comments( std::string html )
+{
+ std::size_t start_pos = 0;
+ while ( ( start_pos = html.find( "<!--", start_pos ) ) != std::string::npos ) {
+ auto end_pos = html.find( "-->", start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + 3 );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
+std::string remove_tag_blocks( std::string html, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& tag_name )
+{
+ const std::string open_tag = "<" + tag_name;
+ const std::string close_tag = "</" + tag_name + ">";
+ std::size_t start_pos = 0;
+
+ while ( ( start_pos = html.find( open_tag, start_pos ) ) != std::string::npos ) {
+ const std::size_t end_pos = html.find( close_tag, start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + close_tag.length() );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
+} // namespace ricochet::net
diff --git a/ricochet/src/net/ssl_socket.cc b/ricochet/src/net/ssl_socket.cc
new file mode 100644
index 0000000..a31b564
--- /dev/null
+++ b/ricochet/src/net/ssl_socket.cc
@@ -0,0 +1,118 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/ssl_socket.hh"
+#include <iostream>
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#include <stdexcept>
+
+namespace ricochet::net {
+
+// --- RAII Deleters ---
+
+void SslCtxDeleter::operator()( SSL_CTX* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_CTX_free( ptr );
+ }
+}
+
+void SslDeleter::operator()( SSL* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_free( ptr );
+ }
+}
+
+// SSLSocket Implementation
+
+SSLSocket::SSLSocket()
+{
+ const SSL_METHOD* method = TLS_client_method();
+ ctx_.reset( SSL_CTX_new( method ) );
+ if ( !ctx_ ) {
+ throw std::runtime_error( "Failed to create SSL_CTX" );
+ }
+
+ if ( SSL_CTX_set_default_verify_paths( ctx_.get() ) != 1 ) {
+ throw std::runtime_error( "Failed to load default CA certificates" );
+ }
+}
+
+void SSLSocket::connect( const Address& address, const std::string& hostname )
+{
+ // TCP Connect
+ underlying_socket_.connect( address );
+
+ ssl_.reset( SSL_new( ctx_.get() ) );
+ if ( !ssl_ ) {
+ throw std::runtime_error( "Failed to create SSL object" );
+ }
+
+ // Setup SNI (Server Name Indication)
+ if ( SSL_set_tlsext_host_name( ssl_.get(), hostname.c_str() ) != 1 ) {
+ // Some old servers do not support SNI, here we can ignore the error or throw an exception, depending on the
+ // requirements
+ std::cerr << "Warning: Failed to set SNI" << '\n';
+ }
+
+ SSL_set_fd( ssl_.get(), underlying_socket_.fd_num() );
+
+ if ( SSL_connect( ssl_.get() ) != 1 ) {
+ throw std::runtime_error( "TLS Handshake failed!" );
+ }
+}
+
+std::size_t SSLSocket::write( std::string_view data )
+{
+ if ( !ssl_ ) {
+ return 0;
+ }
+
+ const int bytes_written = SSL_write( ssl_.get(), data.data(), static_cast<int>( data.size() ) );
+ if ( bytes_written <= 0 ) {
+ throw std::runtime_error( "SSL write failed" );
+ }
+ return static_cast<std::size_t>( bytes_written );
+}
+
+void SSLSocket::read( std::string& buffer )
+{
+ if ( !ssl_ ) {
+ return;
+ }
+
+ std::array<char, 4096> buf {};
+ // Read decrypted data from the encrypted channel
+ const int bytes_read = SSL_read( ssl_.get(), buf.data(), static_cast<int>( buf.size() ) );
+
+ if ( bytes_read > 0 ) {
+ buffer.append( buf.data(), static_cast<std::size_t>( bytes_read ) );
+ } else {
+ // Read 0 or negative, representing the server closed the connection (EOF)
+ eof_ = true;
+ }
+}
+
+bool SSLSocket::eof() const
+{
+ return eof_;
+}
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/src/net/sys/address.cc b/ricochet/src/net/sys/address.cc
new file mode 100644
index 0000000..48f22ac
--- /dev/null
+++ b/ricochet/src/net/sys/address.cc
@@ -0,0 +1,207 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/address.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <array>
+#include <cstring>
+#include <linux/if_packet.h>
+#include <memory>
+#include <netdb.h>
+#include <stdexcept>
+#include <sys/socket.h>
+#include <system_error>
+
+using namespace std;
+
+//! Converts Raw to `sockaddr *`.
+Address::Raw::operator sockaddr*()
+{
+ return reinterpret_cast<sockaddr*>( &storage ); // NOLINT(*-reinterpret-cast)
+}
+
+//! Converts Raw to `const sockaddr *`.
+Address::Raw::operator const sockaddr*() const
+{
+ return reinterpret_cast<const sockaddr*>( &storage ); // NOLINT(*-reinterpret-cast)
+}
+
+//! \param[in] addr points to a raw socket address
+//! \param[in] size is `addr`'s length
+Address::Address( const sockaddr* addr, const size_t size ) : size_( size )
+{
+ // make sure proposed sockaddr can fit
+ if ( size > sizeof( address_.storage ) ) {
+ throw runtime_error( "invalid sockaddr size" );
+ }
+
+ memcpy( &address_.storage, addr, size );
+}
+
+//! Error category for getaddrinfo and getnameinfo failures.
+class gai_error_category : public error_category
+{
+public:
+ //! The name of the wrapped error
+ const char* name() const noexcept override { return "gai_error_category"; }
+ //! \brief An error message
+ //! \param[in] return_value the error return value from [getaddrinfo(3)](\ref man3::getaddrinfo)
+ //! or [getnameinfo(3)](\ref man3::getnameinfo)
+ string message( const int return_value ) const noexcept override { return gai_strerror( return_value ); }
+};
+
+//! \param[in] node is the hostname or dotted-quad address
+//! \param[in] service is the service name or numeric string
+//! \param[in] hints are criteria for resolving the supplied name
+Address::Address( const string& node, const string& service, const addrinfo& hints ) : size_()
+{
+ // prepare for the answer
+ addrinfo* resolved_address = nullptr;
+
+ // look up the name or names
+ const int gai_ret = getaddrinfo( node.c_str(), service.c_str(), &hints, &resolved_address );
+ if ( gai_ret != 0 ) {
+ throw tagged_error( gai_error_category(), "getaddrinfo(" + node + ", " + service + ")", gai_ret );
+ }
+
+ // if success, should always have at least one entry
+ if ( resolved_address == nullptr ) {
+ throw runtime_error( "getaddrinfo returned successfully but with no results" );
+ }
+
+ // put resolved_address in a wrapper so it will get freed if we have to throw an exception
+ auto addrinfo_deleter = []( addrinfo* const x ) { freeaddrinfo( x ); };
+ unique_ptr<addrinfo, decltype( addrinfo_deleter )> wrapped_address( resolved_address, move( addrinfo_deleter ) );
+
+ // assign to our private members (making sure size fits)
+ *this = Address( wrapped_address->ai_addr, wrapped_address->ai_addrlen );
+}
+
+//! \brief Build a `struct addrinfo` containing hints for [getaddrinfo(3)](\ref man3::getaddrinfo)
+//! \param[in] ai_flags is the value of the `ai_flags` field in the [struct addrinfo](\ref man3::getaddrinfo)
+//! \param[in] ai_family is the value of the `ai_family` field in the [struct addrinfo](\ref
+//! man3::getaddrinfo)
+namespace {
+inline addrinfo make_hints( int ai_flags, int ai_family ) // NOLINT(*-swappable-parameters)
+{
+ addrinfo hints {}; // value initialized to all zeros
+ hints.ai_flags = ai_flags;
+ hints.ai_family = ai_family;
+ return hints;
+}
+} // namespace
+
+//! \param[in] hostname to resolve
+//! \param[in] service name (from `/etc/services`, e.g., "http" is port 80)
+Address::Address( const string& hostname, const string& service )
+ : Address( hostname, service, make_hints( AI_ALL, AF_INET ) )
+{}
+
+//! \param[in] ip address as a dotted quad ("1.1.1.1")
+//! \param[in] port number
+Address::Address( const string& ip, const uint16_t port )
+ // tell getaddrinfo that we don't want to resolve anything
+ : Address( ip, ::to_string( port ), make_hints( AI_NUMERICHOST | AI_NUMERICSERV, AF_INET ) )
+{}
+
+// accessors
+pair<string, uint16_t> Address::ip_port() const
+{
+ if ( address_.storage.ss_family != AF_INET and address_.storage.ss_family != AF_INET6 ) {
+ throw runtime_error( "Address::ip_port() called on non-Internet address" );
+ }
+
+ array<char, NI_MAXHOST> ip {};
+ array<char, NI_MAXSERV> port {};
+
+ const int gni_ret = getnameinfo( static_cast<const sockaddr*>( address_ ),
+ size_,
+ ip.data(),
+ ip.size(),
+ port.data(),
+ port.size(),
+ NI_NUMERICHOST | NI_NUMERICSERV );
+ if ( gni_ret != 0 ) {
+ throw tagged_error( gai_error_category(), "getnameinfo", gni_ret );
+ }
+
+ return { ip.data(), stoi( port.data() ) };
+}
+
+string Address::to_string() const
+{
+ if ( address_.storage.ss_family == AF_INET or address_.storage.ss_family == AF_INET6 ) {
+ const auto ip_and_port = ip_port();
+ return ip_and_port.first + ":" + ::to_string( ip_and_port.second );
+ }
+
+ return "(non-Internet address)";
+}
+
+uint32_t Address::ipv4_numeric() const
+{
+ if ( address_.storage.ss_family != AF_INET or size_ != sizeof( sockaddr_in ) ) {
+ throw runtime_error( "ipv4_numeric called on non-IPV4 address" );
+ }
+
+ sockaddr_in ipv4_addr {};
+ memcpy( &ipv4_addr, &address_.storage, size_ );
+
+ return be32toh( ipv4_addr.sin_addr.s_addr );
+}
+
+Address Address::from_ipv4_numeric( const uint32_t ip_address )
+{
+ sockaddr_in ipv4_addr {};
+ ipv4_addr.sin_family = AF_INET;
+ ipv4_addr.sin_addr.s_addr = htobe32( ip_address );
+
+ return { reinterpret_cast<sockaddr*>( &ipv4_addr ), sizeof( ipv4_addr ) }; // NOLINT(*-reinterpret-cast)
+}
+
+// equality
+bool Address::operator==( const Address& other ) const
+{
+ if ( size_ != other.size_ ) {
+ return false;
+ }
+
+ return 0 == memcmp( &address_, &other.address_, size_ );
+}
+
+// address families that correspond to each sockaddr type
+template<typename sockaddr_type>
+constexpr int sockaddr_family = -1;
+
+template<>
+constexpr int sockaddr_family<sockaddr_in> = AF_INET;
+
+template<>
+constexpr int sockaddr_family<sockaddr_in6> = AF_INET6;
+
+template<>
+constexpr int sockaddr_family<sockaddr_ll> = AF_PACKET;
+
+// safely cast the address to its underlying sockaddr type
+template<typename sockaddr_type>
+const sockaddr_type* Address::as() const
+{
+ const sockaddr* raw { address_ };
+ if ( sizeof( sockaddr_type ) < size() or raw->sa_family != sockaddr_family<sockaddr_type> ) {
+ throw runtime_error( "Address::as() conversion failure" );
+ }
+
+ return reinterpret_cast<const sockaddr_type*>( raw ); // NOLINT(*-reinterpret-cast)
+}
+
+template const sockaddr_in* Address::as<sockaddr_in>() const;
+template const sockaddr_in6* Address::as<sockaddr_in6>() const;
+template const sockaddr_ll* Address::as<sockaddr_ll>() const;
diff --git a/ricochet/src/net/sys/file_descriptor.cc b/ricochet/src/net/sys/file_descriptor.cc
new file mode 100644
index 0000000..b765308
--- /dev/null
+++ b/ricochet/src/net/sys/file_descriptor.cc
@@ -0,0 +1,213 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/file_descriptor.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <fcntl.h>
+#include <iostream>
+#include <stdexcept>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+using namespace std;
+
+size_t FileDescriptor::FDWrapper::CheckFDSystemCall( string_view what, ssize_t return_value ) const
+{
+ if ( return_value >= 0 ) {
+ return return_value;
+ }
+
+ if ( non_blocking_ and ( errno == EAGAIN or errno == EWOULDBLOCK or errno == EINPROGRESS ) ) {
+ return 0;
+ }
+
+ throw unix_error { what };
+}
+
+size_t FileDescriptor::FDWrapper::CheckRead( string_view what, ssize_t return_value )
+{
+ if ( return_value == 0 ) {
+ eof_ = true;
+ }
+
+ return CheckFDSystemCall( what, return_value );
+}
+
+size_t FileDescriptor::CheckFDSystemCall( string_view what, ssize_t return_value ) const
+{
+ if ( not internal_fd_ ) {
+ throw runtime_error( "internal error: missing internal_fd_" );
+ }
+ return internal_fd_->CheckFDSystemCall( what, return_value );
+}
+
+size_t FileDescriptor::CheckRead( string_view what, ssize_t return_value )
+{
+ if ( not internal_fd_ ) {
+ throw runtime_error( "internal error: missing internal_fd_" );
+ }
+ return internal_fd_->CheckRead( what, return_value );
+}
+
+// fd is the file descriptor number returned by [open(2)](\ref man2::open) or similar
+FileDescriptor::FDWrapper::FDWrapper( int fd ) : fd_( fd )
+{
+ if ( fd < 0 ) {
+ throw runtime_error( "invalid fd number:" + to_string( fd ) );
+ }
+
+ const int flags = CheckSystemCall( "fcntl", fcntl( fd, F_GETFL ) ); // NOLINT(*-vararg)
+ non_blocking_ = flags & O_NONBLOCK; // NOLINT(*-bitwise)
+}
+
+void FileDescriptor::FDWrapper::close()
+{
+ CheckSystemCall( "close", ::close( fd_ ) );
+ eof_ = closed_ = true;
+}
+
+FileDescriptor::FDWrapper::~FDWrapper()
+{
+ try {
+ if ( closed_ ) {
+ return;
+ }
+ close();
+ } catch ( const exception& e ) {
+ // don't throw an exception from the destructor
+ cerr << "Exception destructing FDWrapper: " << e.what() << "\n";
+ }
+}
+
+// fd is the file descriptor number returned by [open(2)](\ref man2::open) or similar
+FileDescriptor::FileDescriptor( int fd ) : internal_fd_( make_shared<FDWrapper>( fd ) ) {}
+
+// Private constructor used by duplicate()
+FileDescriptor::FileDescriptor( shared_ptr<FDWrapper> other_shared_ptr ) : internal_fd_( move( other_shared_ptr ) )
+{}
+
+// returns a copy of this FileDescriptor
+FileDescriptor FileDescriptor::duplicate() const
+{
+ return FileDescriptor { internal_fd_ };
+}
+
+// Read into a single buffer of given length (which if empty will be resized to a reasonable value before reading).
+// Afer the read, the buffer will be resized to match whatever was read.
+void FileDescriptor::read( string& buffer )
+{
+ if ( buffer.empty() ) {
+ buffer.resize( kReadBufferSize );
+ }
+
+ const size_t bytes_read = CheckRead( "read", ::read( fd_num(), buffer.data(), buffer.size() ) );
+ register_read();
+
+ if ( bytes_read > buffer.size() ) {
+ throw runtime_error( "read() read more than requested" );
+ }
+
+ buffer.resize( bytes_read );
+}
+
+// Read into a vector of buffers (if all empty, the last one will be resized to a reasonable value).
+// After the read, the buffers will be resized to match whatever was read.
+void FileDescriptor::read( vector<string>& buffers )
+{
+ if ( buffers.empty() ) {
+ throw runtime_error( "FileDescriptor::read called with no buffers" );
+ }
+
+ if ( buffers.back().empty() ) {
+ buffers.back().resize( kReadBufferSize );
+ }
+
+ static thread_local vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( buffers, iovecs );
+
+ const size_t bytes_read
+ = CheckRead( "readv", readv( fd_num(), iovecs.data(), static_cast<int>( iovecs.size() ) ) );
+ register_read();
+
+ if ( bytes_read > total_size ) {
+ throw runtime_error( "read() read more than requested" );
+ }
+
+ size_t remaining_size = bytes_read;
+ for ( auto& buf : buffers ) {
+ if ( remaining_size >= buf.size() ) {
+ remaining_size -= buf.size();
+ } else {
+ buf.resize( remaining_size );
+ remaining_size = 0;
+ }
+ }
+}
+
+void FileDescriptor::write_all( string_view buffer )
+{
+ if ( not blocking() ) {
+ throw runtime_error( "write_all requires a blocking file descriptor" );
+ // otherwise, this could call write on a non-blocking fd that isn't "ready" for writing
+ }
+
+ while ( not buffer.empty() ) {
+ buffer.remove_prefix( write( buffer ) );
+ }
+}
+
+size_t FileDescriptor::write( string_view buffer )
+{
+ const size_t bytes_written = CheckFDSystemCall( "write", ::write( fd_num(), buffer.data(), buffer.size() ) );
+ register_write();
+
+ if ( bytes_written == 0 and not buffer.empty() ) {
+ throw runtime_error( "write returned 0 given non-empty input buffer" );
+ }
+
+ if ( bytes_written > buffer.size() ) {
+ throw runtime_error( "write wrote more than length of input buffer" );
+ }
+
+ return bytes_written;
+}
+
+size_t FileDescriptor::write( const std::vector<iovec>& iovecs, size_t total_size )
+{
+ const size_t bytes_written
+ = CheckFDSystemCall( "writev", ::writev( fd_num(), iovecs.data(), static_cast<int>( iovecs.size() ) ) );
+ register_write();
+
+ if ( bytes_written == 0 and total_size != 0 ) {
+ throw runtime_error( "writev returned 0 given non-empty input buffer" );
+ }
+
+ if ( bytes_written > total_size ) {
+ throw runtime_error( "writev wrote more than length of input buffer" );
+ }
+
+ return bytes_written;
+}
+
+void FileDescriptor::set_blocking( bool blocking )
+{
+ int flags = CheckSystemCall( "fcntl", fcntl( fd_num(), F_GETFL ) ); // NOLINT(*-vararg)
+ if ( blocking ) {
+ flags ^= ( flags & O_NONBLOCK ); // NOLINT(*-bitwise)
+ } else {
+ flags |= O_NONBLOCK; // NOLINT(*-bitwise)
+ }
+
+ CheckSystemCall( "fcntl", fcntl( fd_num(), F_SETFL, flags ) ); // NOLINT(*-vararg)
+
+ internal_fd_->non_blocking_ = not blocking;
+}
diff --git a/ricochet/src/net/sys/socket.cc b/ricochet/src/net/sys/socket.cc
new file mode 100644
index 0000000..701704f
--- /dev/null
+++ b/ricochet/src/net/sys/socket.cc
@@ -0,0 +1,286 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#include "ricochet/net/sys/socket.hh"
+
+#include "ricochet/net/sys/exception.hh"
+
+#include <linux/if_packet.h>
+#include <stdexcept>
+#include <unistd.h>
+
+using namespace std;
+
+// default constructor for socket of (subclassed) domain and type
+//! \param[in] domain is as described in [socket(7)](\ref man7::socket), probably `AF_INET` or `AF_UNIX`
+//! \param[in] type is as described in [socket(7)](\ref man7::socket)
+Socket::Socket( const int domain, const int type, const int protocol )
+ : FileDescriptor( CheckSystemCall( "socket", socket( domain, type, protocol ) ) )
+{}
+
+// construct from file descriptor
+//! \param[in] fd is the FileDescriptor from which to construct
+//! \param[in] domain is `fd`'s domain; throws std::runtime_error if wrong value is supplied
+//! \param[in] type is `fd`'s type; throws std::runtime_error if wrong value is supplied
+//! \param[in] protocol is `fd`'s protocol; throws std::runtime_error if wrong value is supplied
+Socket::Socket( FileDescriptor&& fd, int domain, int type, int protocol ) // NOLINT(*-swappable-parameters)
+ : FileDescriptor( move( fd ) )
+{
+ int actual_value {};
+ socklen_t len {};
+
+ // verify domain
+ len = getsockopt( SOL_SOCKET, SO_DOMAIN, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != domain ) ) {
+ throw runtime_error( "socket domain mismatch" );
+ }
+
+ // verify type
+ len = getsockopt( SOL_SOCKET, SO_TYPE, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != type ) ) {
+ throw runtime_error( "socket type mismatch" );
+ }
+
+ // verify protocol
+ len = getsockopt( SOL_SOCKET, SO_PROTOCOL, actual_value );
+ if ( ( len != sizeof( actual_value ) ) or ( actual_value != protocol ) ) {
+ throw runtime_error( "socket protocol mismatch" );
+ }
+}
+
+// get the local or peer address the socket is connected to
+//! \param[in] name_of_function is the function to call (string passed to CheckSystemCall())
+//! \param[in] function is a pointer to the function
+//! \returns the requested Address
+Address Socket::get_address( const string& name_of_function,
+ const function<int( int, sockaddr*, socklen_t* )>& function ) const
+{
+ Address::Raw address;
+ socklen_t size = sizeof( address );
+
+ CheckSystemCall( name_of_function, function( fd_num(), address, &size ) );
+
+ return Address { address, size };
+}
+
+//! \returns the local Address of the socket
+Address Socket::local_address() const
+{
+ return get_address( "getsockname", getsockname );
+}
+
+//! \returns the socket's peer's Address
+Address Socket::peer_address() const
+{
+ return get_address( "getpeername", getpeername );
+}
+
+// bind socket to a specified local address (usually to listen/accept)
+//! \param[in] address is a local Address to bind
+void Socket::bind( const Address& address )
+{
+ CheckSystemCall( "bind", ::bind( fd_num(), address.raw(), address.size() ) );
+}
+
+void Socket::bind_to_device( const string_view device_name )
+{
+ setsockopt( SOL_SOCKET, SO_BINDTODEVICE, device_name );
+}
+
+// connect socket to a specified peer address
+//! \param[in] address is the peer's Address
+void Socket::connect( const Address& address )
+{
+ CheckFDSystemCall( "connect", ::connect( fd_num(), address.raw(), address.size() ) );
+}
+
+// shut down a socket in the specified way
+//! \param[in] how can be `SHUT_RD`, `SHUT_WR`, or `SHUT_RDWR`; see [shutdown(2)](\ref man2::shutdown)
+void Socket::shutdown( const int how )
+{
+ CheckSystemCall( "shutdown", ::shutdown( fd_num(), how ) );
+ switch ( how ) {
+ case SHUT_RD:
+ register_read();
+ break;
+ case SHUT_WR:
+ register_write();
+ break;
+ case SHUT_RDWR:
+ register_read();
+ register_write();
+ break;
+ default:
+ throw runtime_error( "Socket::shutdown() called with invalid `how`" );
+ }
+}
+
+void DatagramSocket::recv( Address& source_address, string& payload )
+{
+ if ( payload.empty() ) {
+ payload.resize( kReadBufferSize );
+ }
+
+ Address::Raw raw_source_address;
+ socklen_t namelen = sizeof( raw_source_address );
+ const size_t recv_len = CheckFDSystemCall(
+ "recvfrom", ::recvfrom( fd_num(), payload.data(), payload.size(), MSG_TRUNC, raw_source_address, &namelen ) );
+ register_read();
+
+ if ( recv_len > payload.size() ) {
+ throw runtime_error( "recvfrom (oversized datagram of length " + to_string( recv_len ) + ")" );
+ }
+ if ( namelen <= 0 || namelen > sizeof( raw_source_address ) ) {
+ throw runtime_error( "recvfrom gave invalid namelen" );
+ }
+
+ source_address = { raw_source_address, namelen };
+}
+
+void DatagramSocket::recv( Address& source_address, vector<string>& payloads )
+{
+ if ( payloads.empty() ) {
+ throw runtime_error( "DatagramSocket::recv called with no payload buffers" );
+ }
+
+ if ( payloads.back().empty() ) {
+ payloads.back().resize( kReadBufferSize );
+ }
+
+ // set up iovecs
+ static thread_local vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( payloads, iovecs );
+
+ // set up msghdr
+ Address::Raw raw_source_address;
+ msghdr message { .msg_name = &raw_source_address,
+ .msg_namelen = sizeof( raw_source_address ),
+ .msg_iov = iovecs.data(),
+ .msg_iovlen = iovecs.size(),
+ .msg_control = nullptr,
+ .msg_controllen {},
+ .msg_flags {} };
+ const size_t recv_len = CheckFDSystemCall( "recvmsg", ::recvmsg( fd_num(), &message, MSG_TRUNC ) );
+ register_read();
+ if ( recv_len > total_size ) {
+ throw runtime_error( "recvmsg (oversized datagram of length " + to_string( recv_len ) + ")" );
+ }
+ if ( message.msg_flags & MSG_TRUNC ) {
+ throw runtime_error( "recvmsg (oversized datagram indicated only by MSG_TRUNC)" );
+ }
+ if ( message.msg_namelen <= 0 || message.msg_namelen > sizeof( raw_source_address ) ) {
+ throw runtime_error( "recvmsg gave invalid namelen" );
+ }
+ source_address = { raw_source_address, message.msg_namelen };
+
+ size_t remaining_size = recv_len;
+ for ( auto& buf : payloads ) {
+ if ( remaining_size >= buf.size() ) {
+ remaining_size -= buf.size();
+ } else {
+ buf.resize( remaining_size );
+ remaining_size = 0;
+ }
+ }
+}
+
+void DatagramSocket::send( string_view payload, const optional<Address>& destination )
+{
+ const size_t bytes_sent = CheckFDSystemCall( "sendto",
+ ::sendto( fd_num(),
+ payload.data(),
+ payload.size(),
+ 0,
+ destination.has_value() ? destination->raw() : nullptr,
+ destination.has_value() ? destination->size() : 0 ) );
+ register_write();
+ if ( bytes_sent != payload.size() ) {
+ throw runtime_error( "sendto sent some length other than that of payload" );
+ }
+}
+
+void DatagramSocket::send( vector<iovec>& iovecs, size_t total_size, const optional<Address>& destination )
+{
+ const msghdr message { .msg_name = destination.has_value() // NOLINTNEXTLINE(*-const-cast)
+ ? static_cast<void*>( const_cast<sockaddr*>( destination->raw() ) )
+ : nullptr,
+ .msg_namelen = destination.has_value() ? destination->size() : 0,
+ .msg_iov = iovecs.data(),
+ .msg_iovlen = iovecs.size(),
+ .msg_control = nullptr,
+ .msg_controllen {},
+ .msg_flags {} };
+ const size_t bytes_sent = CheckFDSystemCall( "sendmsg", ::sendmsg( fd_num(), &message, 0 ) );
+ register_write();
+ if ( bytes_sent != total_size ) {
+ throw runtime_error( "sendmsg sent some length other than that of payload" );
+ }
+}
+
+// mark the socket as listening for incoming connections
+//! \param[in] backlog is the number of waiting connections to queue (see [listen(2)](\ref man2::listen))
+void TCPSocket::listen( const int backlog )
+{
+ CheckSystemCall( "listen", ::listen( fd_num(), backlog ) );
+}
+
+// accept a new incoming connection
+//! \returns a new TCPSocket connected to the peer.
+//! \note This function blocks until a new connection is available
+TCPSocket TCPSocket::accept()
+{
+ register_read();
+ return TCPSocket( FileDescriptor( CheckSystemCall( "accept", ::accept( fd_num(), nullptr, nullptr ) ) ) );
+}
+
+// get socket option
+template<typename option_type>
+socklen_t Socket::getsockopt( const int level, const int option, option_type& option_value ) const
+{
+ socklen_t optlen = sizeof( option_value );
+ CheckSystemCall( "getsockopt", ::getsockopt( fd_num(), level, option, &option_value, &optlen ) );
+ return optlen;
+}
+
+// set socket option
+//! \param[in] level The protocol level at which the argument resides
+//! \param[in] option A single option to set
+//! \param[in] option_value The value to set
+//! \details See [setsockopt(2)](\ref man2::setsockopt) for details.
+template<typename option_type>
+void Socket::setsockopt( const int level, const int option, const option_type& option_value )
+{
+ CheckSystemCall( "setsockopt", ::setsockopt( fd_num(), level, option, &option_value, sizeof( option_value ) ) );
+}
+
+// setsockopt with size only known at runtime
+void Socket::setsockopt( const int level, const int option, const string_view option_val )
+{
+ CheckSystemCall( "setsockopt", ::setsockopt( fd_num(), level, option, option_val.data(), option_val.size() ) );
+}
+
+// allow local address to be reused sooner, at the cost of some robustness
+//! \note Using `SO_REUSEADDR` may reduce the robustness of your application
+void Socket::set_reuseaddr()
+{
+ setsockopt( SOL_SOCKET, SO_REUSEADDR, int { true } );
+}
+
+void Socket::throw_if_error() const
+{
+ int socket_error = 0;
+ const socklen_t len = getsockopt( SOL_SOCKET, SO_ERROR, socket_error );
+ if ( len != sizeof( socket_error ) ) {
+ throw runtime_error( "unexpected length from getsockopt: " + to_string( len ) );
+ }
+
+ if ( socket_error ) {
+ throw unix_error( "socket error", socket_error );
+ }
+}
diff --git a/ricochet/src/net/url_parser.cc b/ricochet/src/net/url_parser.cc
new file mode 100644
index 0000000..95e86ef
--- /dev/null
+++ b/ricochet/src/net/url_parser.cc
@@ -0,0 +1,87 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/net/url_parser.hh"
+
+#include <cctype>
+
+namespace ricochet::net {
+
+std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
+{
+ std::string_view prefix = "http://";
+ if ( url.starts_with( "https://" ) ) {
+ prefix = "https://";
+ } else if ( url.starts_with( "http://" ) ) {
+ prefix = "http://";
+ } else {
+ return std::unexpected( "Only 'http://' or 'https://' URLs are supported." );
+ }
+
+ url.remove_prefix( prefix.length() );
+
+ const auto sep_pos = url.find_first_of( "/?#" );
+
+ ParsedUrl result;
+ if ( sep_pos == std::string_view::npos ) {
+ result.host = std::string( url );
+ result.path = "/";
+ } else {
+ result.host = std::string( url.substr( 0, sep_pos ) );
+ std::string_view path_view = url.substr( sep_pos );
+ const auto hash_pos = path_view.find( '#' );
+ if ( hash_pos != std::string_view::npos ) {
+ path_view = path_view.substr( 0, hash_pos );
+ }
+ result.path = std::string( path_view );
+ if ( result.path.empty() || result.path[0] != '/' ) {
+ result.path.insert( 0, "/" );
+ }
+ }
+ return result;
+}
+
+std::string extract_redirect_url( const std::string& raw_response, bool is_https, const std::string& host )
+{
+ auto loc_pos = raw_response.find( "Location: " );
+ if ( loc_pos == std::string::npos ) {
+ loc_pos = raw_response.find( "location: " );
+ }
+ if ( loc_pos != std::string::npos ) {
+ loc_pos += 10;
+ auto end_pos = raw_response.find( '\r', loc_pos );
+ if ( end_pos == std::string::npos ) {
+ end_pos = raw_response.find( '\n', loc_pos );
+ }
+ if ( end_pos != std::string::npos ) {
+ std::string next_url = raw_response.substr( loc_pos, end_pos - loc_pos );
+ while ( !next_url.empty() && std::isspace( static_cast<unsigned char>( next_url.front() ) ) ) {
+ next_url.erase( 0, 1 );
+ }
+ if ( next_url.starts_with( "//" ) ) {
+ next_url = ( is_https ? "https:" : "http:" ) + next_url;
+ } else if ( next_url.starts_with( "/" ) ) {
+ next_url = ( is_https ? "https://" : "http://" ) + host + next_url;
+ }
+ return next_url;
+ }
+ }
+ return "";
+}
+
+} // namespace ricochet::net
diff --git a/ricochet/src/parser/html_lexer.cc b/ricochet/src/parser/html_lexer.cc
new file mode 100644
index 0000000..4d70288
--- /dev/null
+++ b/ricochet/src/parser/html_lexer.cc
@@ -0,0 +1,119 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/parser/html_lexer.hh"
+
+#include <cstdint>
+#include <string_view>
+
+namespace ricochet::parser {
+
+namespace {
+enum class LexerState : std::uint8_t
+{
+ Data, // Reading plain text
+ TagOpen, // Just encountered '<'
+ TagName, // Reading the name of an opening tag (e.g., "div")
+ ClosingTagName, // Reading the name of a closing tag (e.g., "/div")
+ SwallowAttributes // Ignore attributes mode (ignore until encountering '>')
+};
+
+void emit_text( std::string& buffer, std::vector<HtmlToken>& tokens )
+{
+ if ( buffer.empty() ) {
+ return;
+ }
+
+ bool all_whitespace = true;
+ for ( const char c : buffer ) {
+ if ( !std::isspace( static_cast<unsigned char>( c ) ) ) {
+ all_whitespace = false;
+ break;
+ }
+ }
+
+ if ( !all_whitespace ) {
+ tokens.emplace_back( TextToken { buffer } );
+ }
+ buffer.clear();
+}
+
+} // namespace
+
+std::vector<HtmlToken> HtmlLexer::tokenize( // NOLINT(readability-convert-member-functions-to-static)
+ std::string_view html ) const
+{
+ std::vector<HtmlToken> tokens;
+
+ LexerState state = LexerState::Data;
+ std::string buffer {};
+
+ for ( const char c : html ) {
+ switch ( state ) {
+ case LexerState::Data:
+ if ( c == '<' ) {
+ emit_text( buffer, tokens );
+ state = LexerState::TagOpen;
+ } else {
+ buffer += c;
+ }
+ break;
+ case LexerState::TagOpen:
+ if ( c == '/' ) {
+ state = LexerState::ClosingTagName;
+ } else if ( std::isalpha( static_cast<unsigned char>( c ) ) ) {
+ buffer.push_back( c );
+ state = LexerState::TagName;
+ } else {
+ buffer.push_back( '<' );
+ buffer.push_back( c );
+ state = LexerState::Data;
+ }
+ break;
+ case LexerState::TagName:
+ if ( c == '>' ) {
+ tokens.emplace_back( TagOpenToken { buffer } );
+ buffer.clear();
+ state = LexerState::Data;
+ } else {
+ buffer += c;
+ }
+ break;
+ case LexerState::ClosingTagName:
+ if ( c == '>' ) {
+ tokens.emplace_back( TagCloseToken { buffer } );
+ buffer.clear();
+ state = LexerState::Data;
+ } else {
+ buffer += static_cast<char>( std::tolower( static_cast<unsigned char>( c ) ) );
+ }
+ break;
+ case LexerState::SwallowAttributes:
+ if ( c == '>' ) {
+ state = LexerState::Data;
+ }
+ break;
+ }
+ }
+
+ emit_text( buffer, tokens );
+
+ return tokens;
+}
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/src/parser/tree_builder.cc b/ricochet/src/parser/tree_builder.cc
new file mode 100644
index 0000000..64f841b
--- /dev/null
+++ b/ricochet/src/parser/tree_builder.cc
@@ -0,0 +1,85 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/parser/tree_builder.hh"
+#include "ricochet/parser/dom_tree.hh"
+#include <utility>
+#include <vector>
+
+namespace ricochet::parser {
+
+namespace {
+struct TokenVisitor
+{
+ std::vector<DomNode>* stack;
+
+ void operator()( const TextToken& token ) const
+ {
+ stack->back().children.emplace_back(
+ DomNode { .tag_name = "", .text_content = token.content, .children = {} } );
+ }
+
+ void operator()( const TagOpenToken& token ) const
+ {
+ const std::size_t space_pos = token.name.find( ' ' );
+ const std::string base_name
+ = ( space_pos == std::string::npos ) ? token.name : token.name.substr( 0, space_pos );
+
+ DomNode new_node { .tag_name = token.name, .text_content = "", .children = {} };
+
+ if ( base_name == "meta" || base_name == "link" || base_name == "img" || base_name == "br" || base_name == "hr"
+ || base_name == "input" || base_name == "base" || base_name == "col" ) {
+
+ stack->back().children.push_back( std::move( new_node ) );
+ } else {
+ stack->push_back( std::move( new_node ) );
+ }
+ }
+
+ void operator()( const TagCloseToken& /*token*/ ) const
+ {
+ if ( stack->size() > 1 ) {
+ DomNode completed_node = std::move( stack->back() );
+ stack->pop_back();
+ stack->back().children.push_back( std::move( completed_node ) );
+ }
+ }
+};
+} // namespace
+
+DomNode TreeBuilder::build( // NOLINT(readability-convert-member-functions-to-static)
+ const std::vector<HtmlToken>& tokens ) const
+{
+ std::vector<DomNode> stack;
+ stack.emplace_back( DomNode { .tag_name = "Document", .text_content = "", .children = {} } );
+
+ TokenVisitor visitor { &stack };
+ for ( const auto& token : tokens ) {
+ std::visit( visitor, token );
+ }
+
+ while ( stack.size() > 1 ) {
+ DomNode completed_node = std::move( stack.back() );
+ stack.pop_back();
+ stack.back().children.push_back( std::move( completed_node ) );
+ }
+
+ return std::move( stack.front() );
+}
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/src/render/input_renderer.cc b/ricochet/src/render/input_renderer.cc
new file mode 100644
index 0000000..076fa2a
--- /dev/null
+++ b/ricochet/src/render/input_renderer.cc
@@ -0,0 +1,82 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/render/input_renderer.hh"
+#include "ricochet/render/internal.hh"
+#include "ricochet/render/utils.hh"
+
+namespace ricochet::render {
+
+namespace {
+
+std::string build_input_hint( const std::string& tag_name, bool checked )
+{
+ std::string placeholder = extract_attr( tag_name, "placeholder" );
+ const std::string type = extract_attr( tag_name, "type" );
+ std::string val = extract_attr( tag_name, "value" );
+
+ if ( !placeholder.empty() ) {
+ return placeholder;
+ }
+ if ( type == "submit" || type == "button" ) {
+ return val.empty() ? "Submit" : val;
+ }
+ if ( type == "checkbox" ) {
+ return checked ? "[X] " + val : "[ ] " + val;
+ }
+ if ( type == "radio" ) {
+ return checked ? "(X) " + val : "( ) " + val;
+ }
+ if ( type == "hidden" ) {
+ return "hidden";
+ }
+ if ( !val.empty() ) {
+ return val;
+ }
+ return "text";
+}
+
+} // namespace
+
+void render_input_node( const parser::DomNode& node,
+ RenderResult& result,
+ const FormContext& ctx,
+ std::string& output )
+{
+ std::string type = extract_attr( node.tag_name, "type" );
+ const std::size_t space_pos = node.tag_name.find( ' ' );
+ const std::string base_tag
+ = ( space_pos == std::string::npos ) ? node.tag_name : node.tag_name.substr( 0, space_pos );
+ if ( base_tag == "button" && type.empty() ) {
+ type = "submit";
+ }
+ const std::string default_val = extract_attr( node.tag_name, "value" );
+ const bool is_checked = node.tag_name.contains( "checked" );
+ result.inputs.push_back(
+ { extract_attr( node.tag_name, "name" ), ctx.action, ctx.method, default_val, type, is_checked } );
+
+ std::string hint = build_input_hint( node.tag_name, is_checked );
+ if ( base_tag == "button" ) {
+ hint = "Submit";
+ }
+ if ( hint != "hidden" ) {
+ output += " \033[7;33m[I" + std::to_string( result.inputs.size() ) + ":" + hint + "]\033[0m ";
+ }
+}
+
+} // namespace ricochet::render
diff --git a/ricochet/src/render/renderer.cc b/ricochet/src/render/renderer.cc
new file mode 100644
index 0000000..7fb665f
--- /dev/null
+++ b/ricochet/src/render/renderer.cc
@@ -0,0 +1,123 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/render/renderer.hh"
+#include "ricochet/render/input_renderer.hh"
+#include "ricochet/render/internal.hh"
+#include "ricochet/render/table_renderer.hh"
+#include "ricochet/render/utils.hh"
+
+namespace ricochet::render {
+
+namespace {
+
+void render_link_end( const parser::DomNode& node, RenderResult& result )
+{
+ result.text += "\033[0m";
+ const std::string href = extract_attr( node.tag_name, "href" );
+ if ( !href.empty() && !href.starts_with( "javascript:" ) && !href.starts_with( "#" ) ) {
+ result.links.push_back( decode_entities( href ) );
+ result.text += "\033[8m{L:" + std::to_string( result.links.size() ) + "}\033[0m";
+ }
+}
+
+void render_img( const parser::DomNode& node, RenderResult& result )
+{
+ std::size_t alt_pos = node.tag_name.find( "alt=\"" );
+ if ( alt_pos != std::string::npos ) {
+ alt_pos += 5;
+ const std::size_t alt_end = node.tag_name.find( '"', alt_pos );
+ result.text += " \033[90m[Image: " + node.tag_name.substr( alt_pos, alt_end - alt_pos ) + "]\033[0m ";
+ } else {
+ result.text += " \033[90m[IMAGE]\033[0m ";
+ }
+}
+
+} // namespace
+
+void render_node( const parser::DomNode& node, RenderResult& result, FormContext ctx ) // NOLINT(misc-no-recursion)
+{
+ std::string& output = result.text;
+ const std::size_t space_pos = node.tag_name.find( ' ' );
+ const std::string base_tag
+ = ( space_pos == std::string::npos ) ? node.tag_name : node.tag_name.substr( 0, space_pos );
+
+ if ( is_ignored_tag( base_tag ) ) {
+ return;
+ }
+ if ( base_tag.empty() ) {
+ output += decode_entities( node.text_content );
+ return;
+ }
+
+ if ( base_tag == "table" ) {
+ render_table( node, result, ctx );
+ return;
+ }
+
+ if ( base_tag == "form" ) {
+ ctx.action = extract_attr( node.tag_name, "action" );
+ ctx.method = extract_attr( node.tag_name, "method" );
+ }
+
+ const bool is_header = is_heading_tag( base_tag );
+ const bool is_link = ( base_tag == "a" );
+ const bool is_list_item = ( base_tag == "li" );
+ const bool is_input = ( base_tag == "input" || base_tag == "textarea" || base_tag == "button" );
+
+ if ( is_header ) {
+ output += "\n\n\033[1;31m";
+ } else if ( is_link ) {
+ output += "\033[4;34m";
+ } else if ( is_list_item ) {
+ output += "\n • ";
+ } else if ( is_input ) {
+ render_input_node( node, result, ctx, output );
+ } else if ( base_tag == "p" || base_tag == "div" || is_header ) {
+ if ( !output.empty() && output.back() != '\n' ) {
+ output += "\n";
+ }
+ }
+
+ for ( const auto& child : node.children ) {
+ render_node( child, result, ctx );
+ }
+
+ if ( is_header ) {
+ output += "\033[0m";
+ } else if ( is_link ) {
+ render_link_end( node, result );
+ }
+
+ if ( base_tag == "h1" || base_tag == "p" || base_tag == "div" ) {
+ output += "\n";
+ }
+ if ( base_tag == "img" ) {
+ render_img( node, result );
+ }
+}
+
+RenderResult Renderer::render( // NOLINT(readability-convert-member-functions-to-static)
+ const parser::DomNode& node ) const
+{
+ RenderResult result;
+ render_node( node, result, FormContext {} );
+ return result;
+}
+
+} // namespace ricochet::render \ No newline at end of file
diff --git a/ricochet/src/render/table_renderer.cc b/ricochet/src/render/table_renderer.cc
new file mode 100644
index 0000000..37e6bfb
--- /dev/null
+++ b/ricochet/src/render/table_renderer.cc
@@ -0,0 +1,147 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/render/table_renderer.hh"
+#include "ricochet/render/internal.hh"
+#include "ricochet/render/utils.hh"
+#include <algorithm>
+
+namespace ricochet::render {
+
+namespace {
+
+std::size_t get_node_text_len( const parser::DomNode& node ) // NOLINT(misc-no-recursion)
+{
+ std::size_t len = get_visible_length( decode_entities( node.text_content ) );
+ for ( const auto& child : node.children ) {
+ len += get_node_text_len( child );
+ }
+ return len;
+}
+
+void process_table_row_widths( const parser::DomNode& row, std::vector<std::size_t>& widths )
+{
+ std::size_t col_idx = 0;
+ for ( const auto& cell : row.children ) {
+ const std::size_t space_pos = cell.tag_name.find( ' ' );
+ const std::string tag
+ = ( space_pos == std::string::npos ) ? cell.tag_name : cell.tag_name.substr( 0, space_pos );
+ if ( tag == "td" || tag == "th" ) {
+ const std::size_t len = get_node_text_len( cell ) + 2;
+ if ( col_idx >= widths.size() ) {
+ widths.push_back( len );
+ } else if ( len > widths[col_idx] ) {
+ widths[col_idx] = len;
+ }
+ col_idx++;
+ }
+ }
+}
+
+std::vector<std::size_t> calc_table_widths( const parser::DomNode& table_node )
+{
+ std::vector<std::size_t> widths;
+ for ( const auto& child : table_node.children ) {
+ const std::size_t space_pos = child.tag_name.find( ' ' );
+ const std::string tag
+ = ( space_pos == std::string::npos ) ? child.tag_name : child.tag_name.substr( 0, space_pos );
+ if ( tag == "tr" ) {
+ process_table_row_widths( child, widths );
+ } else if ( tag == "tbody" || tag == "thead" ) {
+ for ( const auto& row : child.children ) {
+ if ( row.tag_name.starts_with( "tr" ) ) {
+ process_table_row_widths( row, widths );
+ }
+ }
+ }
+ }
+ for ( auto& w : widths ) {
+ w = std::min<std::size_t>( w, 30 );
+ }
+ return widths;
+}
+
+void render_table_row( const parser::DomNode& row,
+ RenderResult& result,
+ const FormContext& ctx,
+ const std::vector<std::size_t>& widths )
+{
+ std::string& output = result.text;
+ output += "\033[90m|\033[0m ";
+ std::size_t col_idx = 0;
+ for ( const auto& cell : row.children ) {
+ const std::size_t cell_space = cell.tag_name.find( ' ' );
+ const std::string c_tag
+ = ( cell_space == std::string::npos ) ? cell.tag_name : cell.tag_name.substr( 0, cell_space );
+ if ( c_tag != "td" && c_tag != "th" ) {
+ continue;
+ }
+
+ const std::size_t text_start = output.size();
+ if ( c_tag == "th" ) {
+ output += "\033[1;36m";
+ }
+
+ for ( const auto& cell_child : cell.children ) {
+ render_node( cell_child, result, ctx );
+ }
+
+ if ( c_tag == "th" ) {
+ output += "\033[0m";
+ }
+
+ const std::string added = output.substr( text_start );
+ const std::size_t vis_len = get_visible_length( added );
+ const std::size_t tw = ( col_idx < widths.size() ) ? widths[col_idx] : 10;
+
+ if ( vis_len < tw ) {
+ output.append( tw - vis_len, ' ' );
+ }
+ output += " \033[90m|\033[0m ";
+ col_idx++;
+ }
+ output += "\n";
+}
+
+} // namespace
+
+void render_table( const parser::DomNode& node, RenderResult& result, const FormContext& ctx )
+{
+ std::string& output = result.text;
+ const std::vector<std::size_t> widths = calc_table_widths( node );
+
+ output += "\n\033[90m+" + std::string( 78, '-' ) + "+\033[0m\n";
+
+ for ( const auto& child : node.children ) {
+ const std::size_t child_space = child.tag_name.find( ' ' );
+ const std::string child_tag
+ = ( child_space == std::string::npos ) ? child.tag_name : child.tag_name.substr( 0, child_space );
+ if ( child_tag == "tr" ) {
+ render_table_row( child, result, ctx, widths );
+ } else if ( child_tag == "tbody" || child_tag == "thead" ) {
+ for ( const auto& row : child.children ) {
+ if ( row.tag_name.starts_with( "tr" ) ) {
+ render_table_row( row, result, ctx, widths );
+ }
+ }
+ }
+ }
+ output += "\033[90m+" + std::string( 78, '-' ) + "+\033[0m\n";
+}
+
+} // namespace ricochet::render
diff --git a/ricochet/src/render/utils.cc b/ricochet/src/render/utils.cc
new file mode 100644
index 0000000..9c6610a
--- /dev/null
+++ b/ricochet/src/render/utils.cc
@@ -0,0 +1,135 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/render/utils.hh"
+
+#include <array>
+#include <cstdint>
+#include <stdexcept>
+
+namespace ricochet::render {
+
+std::string decode_entities( std::string text )
+{
+ constexpr std::array<std::pair<std::string_view, std::string_view>, 25> entities
+ = { { { "&nbsp;", " " }, { "&lt;", "<" }, { "&gt;", ">" }, { "&amp;", "&" }, { "&quot;", "\"" },
+ { "&apos;", "'" }, { "&copy;", "©" }, { "&reg;", "®" }, { "&trade;", "™" }, { "&euro;", "€" },
+ { "&pound;", "£" }, { "&yen;", "¥" }, { "&cent;", "¢" }, { "&sect;", "§" }, { "&para;", "¶" },
+ { "&hellip;", "…" }, { "&dagger;", "†" }, { "&Dagger;", "‡" }, { "&bull;", "•" }, { "&prime;", "′" },
+ { "&Prime;", "″" }, { "&lsaquo;", "‹" }, { "&rsaquo;", "›" }, { "&oline;", "‾" }, { "&frasl;", "⁄" } } };
+
+ for ( const auto& [entity, replacement] : entities ) {
+ std::size_t pos = 0;
+ while ( ( pos = text.find( entity, pos ) ) != std::string::npos ) {
+ text.replace( pos, entity.length(), replacement );
+ pos += replacement.length();
+ }
+ }
+
+ std::size_t pos = 0;
+ while ( ( pos = text.find( "&#", pos ) ) != std::string::npos ) {
+ const std::size_t end = text.find( ';', pos );
+ if ( end != std::string::npos && end - pos < 10 ) {
+ const std::string code = text.substr( pos + 2, end - pos - 2 );
+ try {
+ std::uint32_t cp = 0;
+ if ( !code.empty() && ( code[0] == 'x' || code[0] == 'X' ) ) {
+ cp = std::stoul( code.substr( 1 ), nullptr, 16 );
+ } else {
+ cp = std::stoul( code );
+ }
+ if ( cp < 128 ) {
+ text.replace( pos, end - pos + 1, 1, static_cast<char>( cp ) );
+ continue;
+ }
+ } catch ( const std::invalid_argument& ) {
+ (void)0;
+ } catch ( const std::out_of_range& ) {
+ (void)0;
+ }
+ }
+ pos++;
+ }
+ return text;
+}
+
+std::string extract_attr( const std::string& tag_name, std::string_view attr_name )
+{
+ std::string match = " " + std::string( attr_name ) + "=";
+ std::size_t pos = tag_name.find( match );
+
+ if ( pos == std::string::npos ) {
+ match = std::string( attr_name ) + "=";
+ if ( !tag_name.starts_with( match ) ) {
+ return "";
+ }
+ pos = 0;
+ } else {
+ pos += 1;
+ }
+
+ pos += attr_name.length() + 1;
+ if ( pos >= tag_name.length() ) {
+ return "";
+ }
+
+ const char quote = tag_name[pos];
+ if ( quote == '"' || quote == '\'' ) {
+ const std::size_t end = tag_name.find( quote, pos + 1 );
+ if ( end != std::string::npos ) {
+ return tag_name.substr( pos + 1, end - pos - 1 );
+ }
+ return "";
+ }
+
+ const std::size_t end = tag_name.find_first_of( " >/", pos );
+ return tag_name.substr( pos, ( end == std::string::npos ) ? std::string::npos : end - pos );
+}
+
+std::size_t get_visible_length( std::string_view str )
+{
+ std::size_t len = 0;
+ bool in_ansi = false;
+ for ( std::size_t i = 0; i < str.length(); ++i ) {
+ const auto c = static_cast<unsigned char>( str[i] );
+ if ( c == 0x1B ) {
+ in_ansi = true;
+ } else if ( in_ansi && c == 'm' ) {
+ in_ansi = false;
+ } else if ( !in_ansi ) {
+ if ( ( c & 0x80 ) == 0 || ( c & 0xE0 ) == 0xC0 ) {
+ len += 1;
+ } else if ( ( c & 0xF0 ) == 0xE0 || ( c & 0xF8 ) == 0xF0 ) {
+ len += 2;
+ }
+ }
+ }
+ return len;
+}
+
+bool is_ignored_tag( std::string_view tag )
+{
+ return tag == "head" || tag == "style" || tag == "script" || tag == "meta" || tag == "title" || tag == "link";
+}
+
+bool is_heading_tag( std::string_view tag )
+{
+ return tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4";
+}
+
+} // namespace ricochet::render
diff --git a/ricochet/src/tui/CMakeLists.txt b/ricochet/src/tui/CMakeLists.txt
new file mode 100644
index 0000000..27820e8
--- /dev/null
+++ b/ricochet/src/tui/CMakeLists.txt
@@ -0,0 +1,10 @@
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(NCURSESW REQUIRED IMPORTED_TARGET ncursesw)
+find_package(Threads REQUIRED)
+# ICU 76.1 supplies UTF-8 grapheme/line boundaries and Unicode properties.
+pkg_check_modules(ICU REQUIRED IMPORTED_TARGET icu-uc>=76.1 icu-i18n>=76.1)
+
+add_library(ricochet_tui STATIC editor.cc document_text.cc document.cc document_view.cc mailbox.cc screen.cc session.cc text.cc layout.cc)
+target_include_directories(ricochet_tui PUBLIC ${PROJECT_SOURCE_DIR}/include)
+target_compile_features(ricochet_tui PUBLIC cxx_std_23)
+target_link_libraries(ricochet_tui PRIVATE PkgConfig::NCURSESW PkgConfig::ICU Threads::Threads)
diff --git a/ricochet/src/tui/document.cc b/ricochet/src/tui/document.cc
new file mode 100644
index 0000000..335c6d6
--- /dev/null
+++ b/ricochet/src/tui/document.cc
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/tui/document.hh"
+
+#include <utility>
+
+namespace ricochet::tui {
+
+Document::Document( DocumentData data ) : data_( std::make_shared<const DocumentData>( std::move( data ) ) ) {}
+
+const DocumentData& Document::data() const noexcept
+{ return *data_; }
+
+} // namespace ricochet::tui
diff --git a/ricochet/src/tui/document_text.cc b/ricochet/src/tui/document_text.cc
new file mode 100644
index 0000000..dd0c64d
--- /dev/null
+++ b/ricochet/src/tui/document_text.cc
@@ -0,0 +1,231 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "document_text.hh"
+
+#include <algorithm>
+#include <limits>
+#include <type_traits>
+
+namespace ricochet::tui::detail {
+
+std::string control_display_text( const Control& control, const TextService& text )
+{
+ return std::visit(
+ [&control, &text]( const auto& value ) -> std::string {
+ using Value = std::decay_t<decltype( value )>;
+ if constexpr ( std::is_same_v<Value, TextControl> ) {
+ if ( value.kind == TextControlKind::Password ) {
+ return std::string( text.analyze( value.value, true, false ).graphemes.size(), '*' );
+ }
+ return value.value;
+ } else if constexpr ( std::is_same_v<Value, Checkbox> || std::is_same_v<Value, RadioButton>
+ || std::is_same_v<Value, Button> ) {
+ return control.label;
+ } else if constexpr ( std::is_same_v<Value, SelectControl> ) {
+ std::string result;
+ for ( const auto& option : value.options ) {
+ if ( std::ranges::find( value.selected, option.id ) != value.selected.end() ) {
+ if ( !result.empty() ) {
+ result += ", ";
+ }
+ result += option.label;
+ }
+ }
+ return result;
+ } else {
+ return value.description;
+ }
+ },
+ control.content );
+}
+
+DocumentText::DocumentText( const Document& document, const TextService& text ) : service_( text )
+{
+ for ( const auto& control : document.data().controls ) {
+ definitions_.emplace( control.id, &control );
+ }
+ append_blocks( document.data().blocks, 0 );
+}
+
+void DocumentText::append_text( const Text& text )
+{
+ const auto start = text_.size();
+ text_ += text.utf8;
+ blocks_.emplace( text.id, ByteRange { start, text_.size() } );
+}
+
+void DocumentText::append_control( ControlId id )
+{
+ const auto& control = *definitions_.at( id );
+ const auto start = text_.size();
+ // State is semantic content when selection crosses the object's opening boundary.
+ if ( const auto* checkbox = std::get_if<Checkbox>( &control.content ) ) {
+ text_ += checkbox->checked ? "checked " : "unchecked ";
+ } else if ( const auto* radio = std::get_if<RadioButton>( &control.content ) ) {
+ text_ += radio->checked ? "selected " : "unselected ";
+ }
+ const auto display = text_.size();
+ text_ += control_display_text( control, service_ );
+ controls_.emplace( id, ControlSpan { { start, text_.size() }, { display, text_.size() } } );
+}
+
+void DocumentText::append_inline( std::span<const Inline> content )
+{
+ for ( const auto& item : content ) {
+ std::visit(
+ [this]( const auto& value ) {
+ using Value = std::decay_t<decltype( value )>;
+ if constexpr ( std::is_same_v<Value, Text> ) {
+ append_text( value );
+ } else {
+ append_control( value.control );
+ }
+ },
+ item );
+ }
+}
+
+void DocumentText::append_blocks( std::span<const Block> blocks, std::size_t depth )
+{
+ for ( std::size_t index = 0; index < blocks.size(); ++index ) {
+ if ( index != 0 ) {
+ text_ += "\n\n";
+ }
+ append_block( blocks[index], depth );
+ }
+}
+
+void DocumentText::append_block( const Block& block, std::size_t depth )
+{
+ const auto start = text_.size();
+ std::visit(
+ [this, depth]( const auto& value ) {
+ using Value = std::decay_t<decltype( value )>;
+ if constexpr ( std::is_same_v<Value, Paragraph> || std::is_same_v<Value, Heading> ) {
+ append_inline( value.content );
+ } else if constexpr ( std::is_same_v<Value, Preformatted> ) {
+ append_text( value.content );
+ } else if constexpr ( std::is_same_v<Value, Alternative> ) {
+ append_text( value.description );
+ } else if constexpr ( std::is_same_v<Value, ControlPlaceholder> ) {
+ append_control( value.control );
+ } else if constexpr ( std::is_same_v<Value, List> ) {
+ auto number = value.first_number;
+ for ( std::size_t index = 0; index < value.items.size(); ++index ) {
+ if ( index != 0 ) {
+ text_ += '\n';
+ }
+ const auto& item = value.items[index];
+ const auto prefix = text_.size();
+ text_.append( depth * 2, ' ' );
+ text_ += item.marker.value_or( value.kind == ListKind::Ordered ? std::to_string( number ) + "." : "-" );
+ text_ += ' ';
+ prefixes_.push_back( { prefix, text_.size() } );
+ append_blocks( item.content, depth + 1 );
+ // Match layout's saturating counter at the model's largest list number.
+ if ( number < std::numeric_limits<std::int64_t>::max() ) {
+ ++number;
+ }
+ }
+ } else if constexpr ( std::is_same_v<Value, Table> ) {
+ for ( std::size_t row = 0; row < value.rows.size(); ++row ) {
+ if ( row != 0 ) {
+ text_ += '\n';
+ }
+ const auto& cells = value.rows[row].cells;
+ for ( std::size_t cell = 0; cell < cells.size(); ++cell ) {
+ if ( cell != 0 ) {
+ text_ += '\t';
+ }
+ append_blocks( cells[cell].content, depth );
+ }
+ }
+ }
+ // Separators carry positions but no copied decoration.
+ },
+ block.content );
+ blocks_.emplace( block.id, ByteRange { start, text_.size() } );
+}
+
+std::size_t DocumentText::offset( const DocumentPoint& point ) const
+{
+ return std::visit(
+ [this]( const auto& value ) -> std::size_t {
+ using Value = std::decay_t<decltype( value )>;
+ if constexpr ( std::is_same_v<Value, TextPosition> ) {
+ return blocks_.at( value.block ).start + value.byte_offset;
+ } else if constexpr ( std::is_same_v<Value, ControlTextPosition> ) {
+ return controls_.at( value.control ).display.start + value.byte_offset;
+ } else if constexpr ( std::is_same_v<Value, ObjectPosition> ) {
+ const auto span = std::visit(
+ [this]( const auto& id ) -> ByteRange {
+ using Id = std::decay_t<decltype( id )>;
+ if constexpr ( std::is_same_v<Id, BlockId> ) {
+ return blocks_.at( id );
+ } else {
+ return controls_.at( id ).object;
+ }
+ },
+ value.object );
+ return value.side == BoundarySide::Before ? span.start : span.end;
+ } else {
+ return 0;
+ }
+ },
+ point );
+}
+
+ByteRange DocumentText::offsets( const DocumentRange& range ) const
+{ return { offset( range.start ), offset( range.end ) }; }
+
+SelectionSnapshot DocumentText::select( const DocumentRange& anchor, const DocumentRange& active ) const
+{
+ const bool backward = offset( active.start ) < offset( anchor.start );
+ // A zero-width boundary can coincide with a nonempty endpoint unit. Keep the
+ // whole anchored unit even when the active endpoint comes back to that boundary.
+ return { .range = { backward ? active.start : anchor.start,
+ offset( active.end ) > offset( anchor.end ) ? active.end : anchor.end },
+ .direction = backward ? SelectionDirection::Backward : SelectionDirection::Forward,
+ .anchor = anchor,
+ .active = active };
+}
+
+std::string DocumentText::extract( const DocumentRange& range ) const
+{
+ auto bytes = offsets( range );
+ if ( bytes.start == bytes.end ) {
+ return {};
+ }
+ // Include a semantic marker only when selection includes the item's start.
+ // Inner prefixes expand first, including a surrounding item with a nested list.
+ for ( auto prefix = prefixes_.rbegin(); prefix != prefixes_.rend(); ++prefix ) {
+ if ( prefix->end == bytes.start ) {
+ bytes.start = prefix->start;
+ }
+ }
+ auto result = text_.substr( bytes.start, bytes.end - bytes.start );
+ // CRLF is one hard-break grapheme; copied hard breaks use one LF.
+ for ( auto position = result.find( "\r\n" ); position != std::string::npos;
+ position = result.find( "\r\n", position + 1 ) ) {
+ result.erase( position, 1 );
+ }
+ return result;
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/document_text.hh b/ricochet/src/tui/document_text.hh
new file mode 100644
index 0000000..d490c9d
--- /dev/null
+++ b/ricochet/src/tui/document_text.hh
@@ -0,0 +1,66 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/document.hh"
+#include "ricochet/tui/view.hh"
+#include "text.hh"
+
+#include <map>
+#include <span>
+#include <string>
+
+namespace ricochet::tui::detail {
+
+// Shared by layout and extraction: password positions address only this mask.
+std::string control_display_text( const Control& control, const TextService& text );
+
+// Logical reading-order text, independent of wrapping, clipping and decorations.
+// Owned together with one immutable document and its revision-zero control values.
+class DocumentText
+{
+public:
+ DocumentText( const Document& document, const TextService& text );
+ ByteRange offsets( const DocumentRange& range ) const;
+ SelectionSnapshot select( const DocumentRange& anchor, const DocumentRange& active ) const;
+ std::string extract( const DocumentRange& range ) const;
+
+private:
+ struct ControlSpan
+ {
+ ByteRange object {};
+ ByteRange display {};
+ };
+ std::size_t offset( const DocumentPoint& point ) const;
+ void append_blocks( std::span<const Block> blocks, std::size_t depth );
+ void append_block( const Block& block, std::size_t depth );
+ void append_inline( std::span<const Inline> content );
+ void append_text( const Text& text );
+ void append_control( ControlId id );
+
+ std::string text_ {};
+ std::map<BlockId, ByteRange> blocks_ {};
+ std::map<ControlId, ControlSpan> controls_ {};
+ std::map<ControlId, const Control*> definitions_ {};
+ // Prefix ends coincide with item starts; selecting that start includes its marker.
+ std::vector<ByteRange> prefixes_ {};
+ const TextService& service_;
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/document_view.cc b/ricochet/src/tui/document_view.cc
new file mode 100644
index 0000000..13a5d5f
--- /dev/null
+++ b/ricochet/src/tui/document_view.cc
@@ -0,0 +1,399 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "document_view.hh"
+
+#include <algorithm>
+#include <utility>
+
+namespace ricochet::tui::detail {
+namespace {
+
+ScreenSize body_size( ScreenSize viewport )
+{ return { std::max( 1, viewport.rows ), std::max( 1, viewport.columns ) }; }
+
+std::size_t shifted_row( std::size_t row, std::int64_t distance, std::size_t last )
+{
+ // Clamp the displacement before addition; restored relative offsets can be signed.
+ const auto origin = static_cast<std::int64_t>( row );
+ return static_cast<std::size_t>( origin
+ + std::clamp( distance, -origin, static_cast<std::int64_t>( last ) - origin ) );
+}
+
+} // namespace
+
+DocumentView::DocumentView( Document document, ViewSnapshot snapshot, const TextService& text, ScreenSize viewport )
+ : document_( std::move( document ) )
+ , snapshot_( std::move( snapshot ) )
+ , text_( text )
+ , content_( document_, text_ )
+ , viewport_( body_size( viewport ) )
+ , layout_( document_, text_, viewport_.columns, snapshot_.horizontal_scroll )
+{
+ // A history snapshot can name a removed boundary. The new document starts at
+ // its first reading unit; it never manufactures a byte offset into that text.
+ if ( std::holds_alternative<DocumentStart>( snapshot_.cursor.point ) || !layout_.locate( snapshot_.cursor ) ) {
+ snapshot_.cursor = layout_.first();
+ }
+ desired_column_ = layout_.locate( snapshot_.cursor ).value().column;
+ restore_viewport();
+ if ( snapshot_.selection ) {
+ const auto& selection = *snapshot_.selection;
+ // History endpoints must still be layout boundaries in this exact document/draft.
+ if ( layout_.locate( { selection.anchor.start } ) && layout_.locate( { selection.anchor.end } )
+ && layout_.locate( { selection.active.start } ) && layout_.locate( { selection.active.end } ) ) {
+ snapshot_.selection = content_.select( selection.anchor, selection.active );
+ } else {
+ snapshot_.selection.reset();
+ }
+ }
+}
+
+const Document& DocumentView::document() const
+{ return document_; }
+const ViewSnapshot& DocumentView::snapshot() const
+{ return snapshot_; }
+
+std::size_t DocumentView::last_top_row() const
+{ return layout_.rows() - std::min( layout_.rows(), static_cast<std::size_t>( viewport_.rows ) ); }
+
+void DocumentView::set_top_row( std::size_t row )
+{
+ top_row_ = std::min( row, last_top_row() );
+ snapshot_.viewport_anchor = layout_.on_row( { top_row_, 0 } );
+ const auto anchor = layout_.locate( snapshot_.viewport_anchor ).value();
+ snapshot_.viewport_row_offset = static_cast<std::int64_t>( top_row_ ) - static_cast<std::int64_t>( anchor.row );
+}
+
+void DocumentView::restore_viewport()
+{
+ auto anchor = layout_.locate( snapshot_.viewport_anchor );
+ if ( !anchor ) {
+ snapshot_.viewport_anchor = layout_.first();
+ snapshot_.viewport_row_offset = 0;
+ anchor = layout_.locate( snapshot_.viewport_anchor );
+ }
+ set_top_row( shifted_row( anchor->row, snapshot_.viewport_row_offset, last_top_row() ) );
+}
+
+void DocumentView::resize( ScreenSize viewport )
+{
+ const auto next = body_size( viewport );
+ if ( next == viewport_ ) {
+ return;
+ }
+ stop_drag();
+ const bool reflow = next.columns != viewport_.columns;
+ viewport_ = next;
+ if ( reflow ) {
+ layout_ = DocumentLayout( document_, text_, viewport_.columns, snapshot_.horizontal_scroll );
+ desired_column_ = layout_.locate( snapshot_.cursor ).value().column;
+ }
+ restore_viewport();
+ // A wheel/pan action deliberately allows the cursor to stay outside the view.
+ if ( follow_cursor_ ) {
+ reveal_cursor();
+ }
+}
+
+void DocumentView::apply_scrolls( std::span<const HorizontalScroll> changes )
+{
+ if ( changes.empty() ) {
+ return;
+ }
+ for ( const auto& change : changes ) {
+ const auto found = std::ranges::find( snapshot_.horizontal_scroll, change.block, &HorizontalScroll::block );
+ if ( found == snapshot_.horizontal_scroll.end() ) {
+ snapshot_.horizontal_scroll.push_back( change );
+ } else {
+ *found = change;
+ }
+ }
+ layout_ = DocumentLayout( document_, text_, viewport_.columns, snapshot_.horizontal_scroll );
+ restore_viewport();
+}
+
+void DocumentView::reveal_cursor()
+{
+ apply_scrolls( layout_.reveal( snapshot_.cursor ) );
+ const auto point = layout_.locate( snapshot_.cursor ).value();
+ if ( point.row < top_row_ ) {
+ set_top_row( point.row );
+ } else if ( point.row - top_row_ >= static_cast<std::size_t>( viewport_.rows ) ) {
+ set_top_row( point.row - static_cast<std::size_t>( viewport_.rows - 1 ) );
+ }
+}
+
+void DocumentView::finish_move( bool preserve_column )
+{
+ follow_cursor_ = true;
+ extend_selection();
+ reveal_cursor();
+ if ( !preserve_column ) {
+ desired_column_ = layout_.locate( snapshot_.cursor ).value().column;
+ }
+}
+
+void DocumentView::move_rows( std::int64_t distance, bool page )
+{
+ const auto origin = layout_.locate( snapshot_.cursor ).value();
+ const auto row = shifted_row( origin.row, distance, layout_.rows() - 1 );
+ snapshot_.cursor = layout_.on_row( { row, desired_column_ } );
+ if ( page ) {
+ // Shift both by the same amount before revealing, retaining screen-relative
+ // cursor position wherever document boundaries permit it.
+ set_top_row( shifted_row( top_row_, distance, last_top_row() ) );
+ }
+ finish_move( true );
+}
+
+void DocumentView::move( ViewMotion motion )
+{
+ stop_drag();
+ switch ( motion ) {
+ case ViewMotion::Left:
+ snapshot_.cursor = layout_.adjacent( snapshot_.cursor, -1 );
+ break;
+ case ViewMotion::Right:
+ snapshot_.cursor = layout_.adjacent( snapshot_.cursor, 1 );
+ break;
+ case ViewMotion::NextWord:
+ snapshot_.cursor = layout_.word( snapshot_.cursor, WordMotion::Next );
+ break;
+ case ViewMotion::PreviousWord:
+ snapshot_.cursor = layout_.word( snapshot_.cursor, WordMotion::Previous );
+ break;
+ case ViewMotion::WordEnd:
+ snapshot_.cursor = layout_.word( snapshot_.cursor, WordMotion::End );
+ break;
+ case ViewMotion::LineStart:
+ snapshot_.cursor = layout_.line_edge( snapshot_.cursor, false );
+ break;
+ case ViewMotion::LineEnd:
+ snapshot_.cursor = layout_.line_edge( snapshot_.cursor, true );
+ break;
+ case ViewMotion::DocumentStart:
+ snapshot_.cursor = layout_.first();
+ break;
+ case ViewMotion::DocumentEnd:
+ snapshot_.cursor = layout_.last();
+ break;
+ case ViewMotion::Up:
+ move_rows( -1, false );
+ return;
+ case ViewMotion::Down:
+ move_rows( 1, false );
+ return;
+ case ViewMotion::PageUp:
+ move_rows( -std::max( 1, viewport_.rows - 1 ), true );
+ return;
+ case ViewMotion::PageDown:
+ move_rows( std::max( 1, viewport_.rows - 1 ), true );
+ return;
+ case ViewMotion::HalfPageUp:
+ move_rows( -std::max( 1, viewport_.rows / 2 ), true );
+ return;
+ case ViewMotion::HalfPageDown:
+ move_rows( std::max( 1, viewport_.rows / 2 ), true );
+ return;
+ case ViewMotion::PanLeft:
+ case ViewMotion::PanRight: {
+ const auto changes = layout_.pan( snapshot_.cursor, motion == ViewMotion::PanLeft ? -1 : 1 );
+ if ( changes.empty() ) {
+ return;
+ }
+ const auto old_column = layout_.locate( snapshot_.cursor ).value().column;
+ apply_scrolls( changes );
+ // Panning changes screen coordinates, not the desired logical column for
+ // the next vertical move (including one remembered across a short line).
+ desired_column_ += layout_.locate( snapshot_.cursor ).value().column - old_column;
+ follow_cursor_ = false;
+ return;
+ }
+ }
+ finish_move( false );
+}
+
+void DocumentView::begin_selection()
+{
+ stop_drag();
+ const auto unit = layout_.unit( snapshot_.cursor );
+ snapshot_.selection = content_.select( unit, unit );
+}
+
+void DocumentView::cancel_selection()
+{
+ stop_drag();
+ snapshot_.selection.reset();
+}
+
+void DocumentView::extend_selection()
+{
+ if ( snapshot_.selection ) {
+ snapshot_.selection = content_.select( snapshot_.selection->anchor, layout_.unit( snapshot_.cursor ) );
+ }
+}
+
+std::optional<std::string> DocumentView::selected_text() const
+{
+ if ( !snapshot_.selection ) {
+ return std::nullopt;
+ }
+ return content_.extract( snapshot_.selection->range );
+}
+
+void DocumentView::press( CellPoint point, std::chrono::steady_clock::time_point now )
+{
+ const auto hit = layout_.hit( { top_row_ + static_cast<std::size_t>( point.row ), point.column } );
+ // A completely clipped wide glyph has no visible cell to hit.
+ if ( !hit ) {
+ return;
+ }
+ snapshot_.cursor = hit->position;
+ cancel_selection();
+ desired_column_ = layout_.locate( snapshot_.cursor ).value().column;
+ follow_cursor_ = false;
+ drag_ = Drag { point, point, layout_.unit( snapshot_.cursor ), false, now + std::chrono::milliseconds( 100 ) };
+}
+
+void DocumentView::drag_to_pointer()
+{
+ const auto row = std::clamp( drag_->pointer.row, 0, viewport_.rows - 1 );
+ const auto column = std::clamp( drag_->pointer.column, 0, viewport_.columns - 1 );
+ const auto hit = layout_.hit( { top_row_ + static_cast<std::size_t>( row ), column } );
+ if ( !hit ) {
+ return;
+ }
+ snapshot_.cursor = hit->position;
+ snapshot_.selection = content_.select( drag_->anchor, layout_.unit( snapshot_.cursor ) );
+ desired_column_ = layout_.locate( snapshot_.cursor ).value().column;
+ follow_cursor_ = false;
+}
+
+void DocumentView::drag( CellPoint point )
+{
+ drag_->pointer = point;
+ // One cell is the terminal's movement threshold; wide halves still share a unit.
+ drag_->selecting = drag_->selecting || point.row != drag_->press.row || point.column != drag_->press.column;
+ if ( drag_->selecting ) {
+ drag_to_pointer();
+ }
+}
+
+void DocumentView::release( CellPoint point )
+{
+ drag( point );
+ stop_drag();
+}
+
+void DocumentView::stop_drag()
+{ drag_.reset(); }
+
+bool DocumentView::dragging() const
+{ return drag_.has_value(); }
+
+bool DocumentView::tick( std::chrono::steady_clock::time_point now )
+{
+ if ( !drag_ || !drag_->selecting || now < drag_->next_scroll ) {
+ return false;
+ }
+ drag_->next_scroll = now + std::chrono::milliseconds( 100 );
+ // With a one-row body both edges coincide; leaving it above/below disambiguates.
+ const int direction = drag_->pointer.row < 0 ? -1
+ : drag_->pointer.row >= viewport_.rows ? 1
+ : viewport_.rows == 1 ? 0
+ : drag_->pointer.row == 0 ? -1
+ : drag_->pointer.row == viewport_.rows - 1 ? 1
+ : 0;
+ const auto next = shifted_row( top_row_, direction, last_top_row() );
+ if ( next == top_row_ ) {
+ return false;
+ }
+ set_top_row( next );
+ drag_to_pointer();
+ return true;
+}
+
+void DocumentView::wheel( int steps )
+{
+ set_top_row( shifted_row( top_row_, static_cast<std::int64_t>( steps ) * 3, last_top_row() ) );
+ follow_cursor_ = false;
+ if ( drag_ && drag_->selecting ) {
+ drag_to_pointer();
+ }
+}
+
+bool DocumentView::locate( const DocumentPoint& point )
+{
+ const DisplayPosition requested { point, WrapAffinity::NextLine };
+ const auto location = layout_.locate( requested );
+ // Host jumps can refer to absent anchors, removed leaves or non-grapheme offsets.
+ if ( !location ) {
+ return false;
+ }
+ snapshot_.cursor = requested;
+ if ( std::holds_alternative<DocumentStart>( point ) ) {
+ snapshot_.cursor = layout_.first();
+ } else if ( const auto* object = std::get_if<ObjectPosition>( &point );
+ object && std::holds_alternative<BlockId>( object->object ) ) {
+ snapshot_.cursor = layout_.on_row( *location );
+ }
+ cancel_selection();
+ finish_move( false );
+ return true;
+}
+
+std::optional<HitTarget> DocumentView::focused_target() const
+{ return layout_.target( snapshot_.cursor ); }
+
+FocusResult DocumentView::focus( bool backward )
+{
+ const auto next = layout_.focus( snapshot_.cursor, backward );
+ if ( !next ) {
+ return FocusResult::Empty;
+ }
+ stop_drag();
+ snapshot_.cursor = next->position;
+ finish_move( false );
+ return next->wrapped ? FocusResult::Wrapped : FocusResult::Moved;
+}
+
+const Link* DocumentView::focused_link() const
+{
+ const auto target = focused_target();
+ if ( !target || !std::holds_alternative<LinkId>( *target ) ) {
+ return nullptr;
+ }
+ // Layout link targets originate from this same immutable document.
+ return &*std::ranges::find( document_.data().links, std::get<LinkId>( *target ), &Link::id );
+}
+
+void DocumentView::draw( Frame& frame, CellPoint origin ) const
+{
+ const auto selection = snapshot_.selection ? std::optional( snapshot_.selection->range ) : std::nullopt;
+ layout_.append_frame(
+ frame, top_row_, origin, viewport_, content_, selection, snapshot_.cursor, focused_target() );
+ const auto cursor = layout_.cursor_cell( snapshot_.cursor );
+ if ( cursor && cursor->row >= top_row_ && cursor->row - top_row_ < static_cast<std::size_t>( viewport_.rows )
+ && cursor->column >= 0 && cursor->column < viewport_.columns ) {
+ frame.cursor = CellPoint { origin.row + static_cast<int>( cursor->row - top_row_ ),
+ origin.column + static_cast<int>( cursor->column ) };
+ }
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/document_view.hh b/ricochet/src/tui/document_view.hh
new file mode 100644
index 0000000..5f5e331
--- /dev/null
+++ b/ricochet/src/tui/document_view.hh
@@ -0,0 +1,117 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "document_text.hh"
+#include "layout.hh"
+
+#include <chrono>
+
+namespace ricochet::tui::detail {
+
+enum class ViewMotion : std::uint8_t
+{
+ Left,
+ Right,
+ Up,
+ Down,
+ NextWord,
+ PreviousWord,
+ WordEnd,
+ LineStart,
+ LineEnd,
+ DocumentStart,
+ DocumentEnd,
+ PageUp,
+ PageDown,
+ HalfPageUp,
+ HalfPageDown,
+ PanLeft,
+ PanRight
+};
+
+enum class FocusResult : std::uint8_t
+{
+ Empty,
+ Moved,
+ Wrapped
+};
+
+// One tab's reading state. It owns the logical snapshot; screen rows are caches.
+// Keyboard, mouse and host navigation all use the same layout/source mappings.
+class DocumentView
+{
+public:
+ DocumentView( Document document, ViewSnapshot snapshot, const TextService& text, ScreenSize viewport );
+
+ const Document& document() const;
+ const ViewSnapshot& snapshot() const;
+ void resize( ScreenSize viewport );
+ void move( ViewMotion motion );
+ void press( CellPoint point, std::chrono::steady_clock::time_point now );
+ // The session routes these only while dragging() owns mouse capture.
+ void drag( CellPoint point );
+ void release( CellPoint point );
+ void stop_drag();
+ bool dragging() const;
+ bool tick( std::chrono::steady_clock::time_point now );
+ void begin_selection();
+ void cancel_selection();
+ std::optional<std::string> selected_text() const;
+ void wheel( int steps );
+ bool locate( const DocumentPoint& point );
+ std::optional<HitTarget> focused_target() const;
+ // Browse/field-editor routing owns mode eligibility; this follows the shared order.
+ FocusResult focus( bool backward );
+ const Link* focused_link() const;
+ void draw( Frame& frame, CellPoint origin ) const;
+
+private:
+ std::size_t last_top_row() const;
+ void set_top_row( std::size_t row );
+ void restore_viewport();
+ void apply_scrolls( std::span<const HorizontalScroll> changes );
+ void reveal_cursor();
+ void finish_move( bool preserve_column );
+ void move_rows( std::int64_t distance, bool page );
+ void extend_selection();
+ void drag_to_pointer();
+
+ struct Drag
+ {
+ CellPoint press {};
+ CellPoint pointer {};
+ DocumentRange anchor {};
+ bool selecting { false };
+ std::chrono::steady_clock::time_point next_scroll {};
+ };
+
+ Document document_;
+ ViewSnapshot snapshot_;
+ const TextService& text_;
+ DocumentText content_;
+ ScreenSize viewport_;
+ DocumentLayout layout_;
+ std::size_t top_row_ { 0 };
+ std::int64_t desired_column_ { 0 };
+ bool follow_cursor_ { false };
+ std::optional<Drag> drag_ {};
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/editor.cc b/ricochet/src/tui/editor.cc
new file mode 100644
index 0000000..aa0545b
--- /dev/null
+++ b/ricochet/src/tui/editor.cc
@@ -0,0 +1,629 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "editor.hh"
+
+#include <algorithm>
+#include <limits>
+#include <utility>
+
+namespace ricochet::tui::detail {
+namespace {
+
+ScreenSize editor_size( ScreenSize viewport )
+{ return { std::max( 1, viewport.rows ), std::max( 1, viewport.columns ) }; }
+
+} // namespace
+
+TextEditor::TextEditor( std::string value, EditorOptions options, const TextService& text, ScreenSize viewport )
+ : options_( options ), text_( text ), viewport_( editor_size( viewport ) )
+{
+ state_.value = std::move( value );
+ state_.cursor = state_.value.size();
+ analyze();
+ desired_column_ = carets_.back().column;
+ reveal();
+}
+
+const std::string& TextEditor::value() const
+{ return state_.value; }
+std::uint64_t TextEditor::revision() const
+{ return revision_; }
+std::size_t TextEditor::cursor() const
+{ return state_.cursor; }
+
+ByteRange TextEditor::selection() const
+{
+ const auto anchor = state_.anchor.value_or( state_.cursor );
+ return { std::min( anchor, state_.cursor ), std::max( anchor, state_.cursor ) };
+}
+
+std::optional<std::string> TextEditor::copy_selection()
+{
+ killing_ = false;
+ prefix_.reset();
+ const auto range = selection();
+ if ( options_.kind == EditorKind::Password || range.start == range.end ) {
+ return std::nullopt;
+ }
+ return state_.value.substr( range.start, range.end - range.start );
+}
+
+std::optional<std::chrono::steady_clock::time_point> TextEditor::prefix_deadline() const
+{ return prefix_; }
+
+std::string TextEditor::normalize( std::string_view value ) const
+{
+ std::string result;
+ result.reserve( value.size() );
+ for ( std::size_t index = 0; index < value.size(); ++index ) {
+ const auto character = value[index];
+ if ( character == '\r' ) {
+ if ( index + 1 < value.size() && value[index + 1] == '\n' ) {
+ ++index;
+ }
+ result += options_.kind == EditorKind::MultiLine ? '\n' : ' ';
+ } else if ( character == '\n' && options_.kind != EditorKind::MultiLine ) {
+ result += ' ';
+ } else {
+ result += character;
+ }
+ }
+ return result;
+}
+
+std::size_t TextEditor::unit_index() const
+{
+ return static_cast<std::size_t>(
+ std::ranges::lower_bound(
+ analysis_.graphemes, state_.cursor, {}, []( const auto& glyph ) { return glyph.bytes.start; } )
+ - analysis_.graphemes.begin() );
+}
+
+std::size_t TextEditor::boundary( std::size_t index ) const
+{ return index == analysis_.graphemes.size() ? state_.value.size() : analysis_.graphemes[index].bytes.start; }
+
+std::size_t TextEditor::line_start() const
+{
+ const auto newline
+ = std::find_if( analysis_.graphemes.rbegin(), analysis_.graphemes.rend(), [this]( const auto& unit ) {
+ return unit.kind == TextUnitKind::LineBreak && unit.bytes.end <= state_.cursor;
+ } );
+ return newline == analysis_.graphemes.rend() ? 0 : newline->bytes.end;
+}
+
+std::size_t TextEditor::line_end() const
+{
+ const auto newline = std::ranges::find_if( analysis_.graphemes, [this]( const auto& unit ) {
+ return unit.kind == TextUnitKind::LineBreak && unit.bytes.start >= state_.cursor;
+ } );
+ return newline == analysis_.graphemes.end() ? state_.value.size() : newline->bytes.start;
+}
+
+void TextEditor::analyze()
+{
+ analysis_ = text_.analyze( state_.value, options_.kind == EditorKind::MultiLine, true );
+ layout();
+}
+
+void TextEditor::layout()
+{
+ glyphs_.clear();
+ carets_.assign( analysis_.graphemes.size() + 1, {} );
+ Point point;
+ for ( std::size_t index = 0; index < analysis_.graphemes.size(); ++index ) {
+ const auto& unit = analysis_.graphemes[index];
+ const bool password = options_.kind == EditorKind::Password;
+ if ( unit.kind == TextUnitKind::LineBreak && !password ) {
+ // A hard break after a full row stays on that row. Reuse the preceding
+ // complete glyph's cell for its insertion cursor instead of wrapping twice.
+ carets_[index] = point.column == viewport_.columns ? glyphs_.back().point : point;
+ glyphs_.push_back( { point, 0 } );
+ ++point.row;
+ point.column = 0;
+ } else {
+ const auto width_at = [&]( std::int64_t column ) {
+ const auto width = password ? 1
+ : unit.kind == TextUnitKind::Tab ? 8 - static_cast<int>( column % 8 )
+ : unit.columns;
+ return options_.kind == EditorKind::MultiLine ? std::min( width, viewport_.columns ) : width;
+ };
+ int width = width_at( point.column );
+ if ( options_.kind == EditorKind::MultiLine && point.column > 0
+ && point.column + width > viewport_.columns ) {
+ ++point.row;
+ point.column = 0;
+ width = width_at( 0 );
+ }
+ carets_[index] = point;
+ glyphs_.push_back( { point, width } );
+ point.column += width;
+ }
+ carets_[index + 1] = point;
+ }
+ // A full final soft row needs a real insertion cell after its final grapheme.
+ if ( options_.kind == EditorKind::MultiLine && point.column == viewport_.columns ) {
+ carets_.back() = { point.row + 1, 0 };
+ }
+}
+
+void TextEditor::reveal()
+{
+ const auto point = carets_[unit_index()];
+ const auto height = static_cast<std::size_t>( viewport_.rows );
+ const auto rows = carets_.back().row + 1;
+ top_row_ = std::min( top_row_, rows - std::min( rows, height ) );
+ if ( point.row < top_row_ ) {
+ top_row_ = point.row;
+ } else if ( point.row - top_row_ >= height ) {
+ top_row_ = point.row - height + 1;
+ }
+ if ( options_.kind == EditorKind::MultiLine ) {
+ left_column_ = 0;
+ } else {
+ left_column_ = std::min( left_column_, point.column );
+ left_column_ = std::max( left_column_, point.column - viewport_.columns + 1 );
+ }
+}
+
+void TextEditor::resize( ScreenSize viewport )
+{
+ const auto next = editor_size( viewport );
+ if ( next == viewport_ ) {
+ return;
+ }
+ viewport_ = next;
+ layout();
+ desired_column_ = carets_[unit_index()].column;
+ reveal();
+}
+
+std::size_t TextEditor::nearest( Point point ) const
+{
+ std::size_t chosen = 0;
+ auto distance = std::pair { std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::int64_t>::max() };
+ for ( std::size_t index = 0; index < carets_.size(); ++index ) {
+ const auto& candidate = carets_[index];
+ const auto delta = std::pair {
+ candidate.row > point.row ? candidate.row - point.row : point.row - candidate.row,
+ candidate.column > point.column ? candidate.column - point.column : point.column - candidate.column };
+ if ( delta < distance ) {
+ chosen = index;
+ distance = delta;
+ }
+ }
+ return boundary( chosen );
+}
+
+void TextEditor::move( std::size_t destination, bool extend, bool preserve_column )
+{
+ if ( extend || state_.selecting ) {
+ state_.anchor = state_.anchor.value_or( state_.cursor );
+ } else {
+ state_.anchor.reset();
+ }
+ state_.cursor = destination;
+ if ( !preserve_column ) {
+ desired_column_ = carets_[unit_index()].column;
+ }
+ reveal();
+}
+
+void TextEditor::replace( ByteRange range, std::string_view replacement )
+{
+ auto next = state_.value;
+ next.replace( range.start, range.end - range.start, replacement );
+ if ( next != state_.value ) {
+ past_.push_back( state_ );
+ future_.clear();
+ ++revision_;
+ }
+ state_.value = std::move( next );
+ state_.cursor = range.start + replacement.size();
+ state_.anchor.reset();
+ state_.selecting = false;
+ analyze();
+ // Insertion/deletion can join surrounding graphemes. Snap to the next boundary
+ // in the new text, never retain an offset inside a new combining/ZWJ cluster.
+ state_.cursor = boundary( unit_index() );
+ desired_column_ = carets_[unit_index()].column;
+ reveal();
+}
+
+void TextEditor::insert( std::u32string_view input )
+{
+ killing_ = false;
+ prefix_.reset();
+ if ( input.empty() ) {
+ return; // An empty paste must not delete an existing selection.
+ }
+ const auto value = normalize( text_.encode( input ) );
+ replace( selection(), value );
+}
+
+void TextEditor::replace_value( std::string value )
+{
+ killing_ = false;
+ prefix_.reset();
+ replace( { 0, state_.value.size() }, value );
+}
+
+void TextEditor::kill( ByteRange range )
+{
+ if ( range.start == range.end ) {
+ return; // Empty kills preserve both the buffer and the current kill chain.
+ }
+ auto removed = state_.value.substr( range.start, range.end - range.start );
+ if ( !killing_ ) {
+ killed_ = std::move( removed );
+ } else if ( range.end <= state_.cursor ) {
+ killed_.insert( 0, removed );
+ } else {
+ killed_ += removed;
+ }
+ replace( range, {} );
+ killing_ = true;
+}
+
+void TextEditor::undo( bool redo )
+{
+ auto& source = redo ? future_ : past_;
+ auto& destination = redo ? past_ : future_;
+ if ( source.empty() ) {
+ return;
+ }
+ destination.push_back( std::move( state_ ) );
+ state_ = std::move( source.back() );
+ source.pop_back();
+ ++revision_;
+ analyze();
+ desired_column_ = carets_[unit_index()].column;
+ reveal();
+}
+
+void TextEditor::execute( EditorCommand command, bool extend )
+{
+ prefix_.reset();
+ const bool killing = command == EditorCommand::KillToStart || command == EditorCommand::KillToEnd
+ || command == EditorCommand::KillWord;
+ killing_ = killing_ && killing;
+ const auto range = selection();
+ auto index = unit_index();
+ switch ( command ) {
+ case EditorCommand::Left:
+ move( !extend && !state_.selecting && range.start != range.end ? range.start
+ : boundary( index == 0 ? 0 : index - 1 ),
+ extend,
+ false );
+ break;
+ case EditorCommand::Right:
+ move( !extend && !state_.selecting && range.start != range.end
+ ? range.end
+ : boundary( std::min( index + 1, analysis_.graphemes.size() ) ),
+ extend,
+ false );
+ break;
+ case EditorCommand::Up:
+ case EditorCommand::Down: {
+ if ( options_.kind != EditorKind::MultiLine ) {
+ break;
+ }
+ auto row = carets_[index].row;
+ row = command == EditorCommand::Up ? ( row == 0 ? 0 : row - 1 ) : std::min( row + 1, carets_.back().row );
+ move( nearest( { row, desired_column_ } ), extend, true );
+ break;
+ }
+ case EditorCommand::LineStart:
+ move( line_start(), extend, false );
+ break;
+ case EditorCommand::LineEnd:
+ move( line_end(), extend, false );
+ break;
+ case EditorCommand::PreviousWord: {
+ const auto word = std::find_if( analysis_.words.rbegin(), analysis_.words.rend(), [this]( const auto& item ) {
+ return item.start < state_.cursor;
+ } );
+ move( word == analysis_.words.rend() ? 0 : word->start, extend, false );
+ break;
+ }
+ case EditorCommand::NextWord: {
+ const auto word
+ = std::ranges::find_if( analysis_.words, [this]( const auto& item ) { return item.end > state_.cursor; } );
+ move( word == analysis_.words.end() ? state_.value.size() : word->end, extend, false );
+ break;
+ }
+ case EditorCommand::Backspace:
+ replace( range.start != range.end ? range
+ : ByteRange { boundary( index == 0 ? 0 : index - 1 ), state_.cursor },
+ {} );
+ break;
+ case EditorCommand::Delete:
+ replace( range.start != range.end
+ ? range
+ : ByteRange { state_.cursor, boundary( std::min( index + 1, analysis_.graphemes.size() ) ) },
+ {} );
+ break;
+ case EditorCommand::KillToStart:
+ kill( range.start != range.end ? range : ByteRange { line_start(), state_.cursor } );
+ break;
+ case EditorCommand::KillToEnd:
+ kill( range.start != range.end ? range : ByteRange { state_.cursor, line_end() } );
+ break;
+ case EditorCommand::KillWord:
+ while ( index > 0 && analysis_.graphemes[index - 1].word == WordKind::Space ) {
+ --index;
+ }
+ while ( index > 0 && analysis_.graphemes[index - 1].word != WordKind::Space ) {
+ --index;
+ }
+ kill( range.start != range.end ? range : ByteRange { boundary( index ), state_.cursor } );
+ break;
+ case EditorCommand::Yank:
+ if ( !killed_.empty() ) {
+ replace( range, killed_ );
+ }
+ break;
+ case EditorCommand::BeginSelection:
+ state_.anchor = state_.cursor;
+ state_.selecting = true;
+ break;
+ case EditorCommand::SelectAll:
+ state_.anchor = 0;
+ state_.cursor = state_.value.size();
+ state_.selecting = false;
+ desired_column_ = carets_.back().column;
+ reveal();
+ break;
+ case EditorCommand::CancelSelection:
+ state_.anchor.reset();
+ state_.selecting = false;
+ break;
+ case EditorCommand::Clear:
+ replace( { 0, state_.value.size() }, {} );
+ break;
+ case EditorCommand::Newline:
+ if ( options_.kind == EditorKind::MultiLine ) {
+ replace( range, "\n" );
+ }
+ break;
+ case EditorCommand::Undo:
+ undo( false );
+ break;
+ case EditorCommand::Redo:
+ undo( true );
+ break;
+ }
+}
+
+void TextEditor::place( CellPoint point, bool extend )
+{
+ prefix_.reset();
+ killing_ = false;
+ // The owner routes body clicks/dragging; outside drag coordinates clamp to this viewport.
+ const Point location { top_row_ + static_cast<std::size_t>( std::clamp( point.row, 0, viewport_.rows - 1 ) ),
+ left_column_ + std::clamp( point.column, 0, viewport_.columns - 1 ) };
+ state_.selecting = false;
+ move( nearest( location ), extend, false );
+}
+
+EditorIntent TextEditor::key( const Key& input, std::chrono::steady_clock::time_point now )
+{
+ if ( prefix_ ) {
+ prefix_.reset();
+ if ( input.code == KeyCode::Character && input.modifiers == Modifiers { .control = true }
+ && input.character == U'u' ) {
+ execute( EditorCommand::Undo );
+ }
+ return EditorIntent::Consumed; // Mismatches never execute a second command.
+ }
+ const bool kill_key = input.code == KeyCode::Character && input.modifiers == Modifiers { .control = true }
+ && ( input.character == U'u' || input.character == U'k' || input.character == U'w' );
+ killing_ = killing_ && kill_key;
+ if ( input.code == KeyCode::Character && input.modifiers == Modifiers { .control = true } ) {
+ switch ( input.character ) {
+ case U'b':
+ execute( EditorCommand::Left );
+ break;
+ case U'f':
+ execute( EditorCommand::Right );
+ break;
+ case U'p':
+ case U'n':
+ if ( options_.kind == EditorKind::SingleLine && options_.history ) {
+ return input.character == U'p' ? EditorIntent::HistoryPrevious : EditorIntent::HistoryNext;
+ }
+ execute( input.character == U'p' ? EditorCommand::Up : EditorCommand::Down );
+ break;
+ case U'a':
+ execute( EditorCommand::LineStart );
+ break;
+ case U'e':
+ execute( EditorCommand::LineEnd );
+ break;
+ case U'd':
+ execute( EditorCommand::Delete );
+ break;
+ case U'u':
+ execute( EditorCommand::KillToStart );
+ break;
+ case U'k':
+ execute( EditorCommand::KillToEnd );
+ break;
+ case U'w':
+ execute( EditorCommand::KillWord );
+ break;
+ case U'y':
+ execute( EditorCommand::Yank );
+ break;
+ case U'_':
+ execute( EditorCommand::Undo );
+ break;
+ case U'x':
+ prefix_ = now + std::chrono::seconds( 1 );
+ break;
+ case U'l':
+ return EditorIntent::Redraw;
+ case U'c':
+ return EditorIntent::Cancel;
+ default:
+ return EditorIntent::Unhandled;
+ }
+ return EditorIntent::Consumed;
+ }
+ if ( input.code == KeyCode::Character && input.modifiers == Modifiers { .alt = true } ) {
+ switch ( input.character ) {
+ case U'b':
+ execute( EditorCommand::PreviousWord );
+ return EditorIntent::Consumed;
+ case U'f':
+ execute( EditorCommand::NextWord );
+ return EditorIntent::Consumed;
+ default:
+ return EditorIntent::Unhandled;
+ }
+ }
+ if ( input.modifiers != Modifiers {} && input.modifiers != Modifiers { .shift = true } ) {
+ return EditorIntent::Unhandled;
+ }
+ switch ( input.code ) {
+ case KeyCode::Left:
+ execute( EditorCommand::Left, input.modifiers.shift );
+ break;
+ case KeyCode::Right:
+ execute( EditorCommand::Right, input.modifiers.shift );
+ break;
+ case KeyCode::Up:
+ case KeyCode::Down:
+ if ( options_.kind == EditorKind::SingleLine && options_.history && !input.modifiers.shift ) {
+ return input.code == KeyCode::Up ? EditorIntent::HistoryPrevious : EditorIntent::HistoryNext;
+ }
+ execute( input.code == KeyCode::Up ? EditorCommand::Up : EditorCommand::Down, input.modifiers.shift );
+ break;
+ case KeyCode::Home:
+ execute( EditorCommand::LineStart, input.modifiers.shift );
+ break;
+ case KeyCode::End:
+ execute( EditorCommand::LineEnd, input.modifiers.shift );
+ break;
+ case KeyCode::Tab:
+ return input.modifiers.shift ? EditorIntent::BackTab : EditorIntent::Tab;
+ default:
+ if ( input.modifiers.shift ) {
+ return EditorIntent::Unhandled;
+ }
+ switch ( input.code ) {
+ case KeyCode::Backspace:
+ execute( EditorCommand::Backspace );
+ break;
+ case KeyCode::Delete:
+ execute( EditorCommand::Delete );
+ break;
+ case KeyCode::Enter:
+ return EditorIntent::Enter;
+ case KeyCode::Escape:
+ return EditorIntent::Escape;
+ case KeyCode::Function:
+ return input.function == 10 ? EditorIntent::Menu : EditorIntent::Unhandled;
+ default:
+ return EditorIntent::Unhandled;
+ }
+ break;
+ }
+ return EditorIntent::Consumed;
+}
+
+EditorIntent TextEditor::handle( const InputEvent& event, std::chrono::steady_clock::time_point now )
+{
+ if ( const auto* tick = std::get_if<Tick>( &event ) ) {
+ now = tick->now;
+ }
+ if ( prefix_ && now >= *prefix_ ) {
+ prefix_.reset();
+ }
+ if ( const auto* input = std::get_if<Key>( &event ) ) {
+ return key( *input, now );
+ }
+ if ( const auto* input = std::get_if<TextInput>( &event ) ) {
+ killing_ = false;
+ if ( prefix_ ) {
+ prefix_.reset();
+ } else {
+ insert( std::u32string_view( &input->character, 1 ) );
+ }
+ return EditorIntent::Consumed;
+ }
+ if ( const auto* paste = std::get_if<Paste>( &event ) ) {
+ insert( paste->text );
+ return EditorIntent::Consumed;
+ }
+ if ( std::holds_alternative<Tick>( event ) ) {
+ return EditorIntent::Consumed;
+ }
+ return EditorIntent::Unhandled;
+}
+
+void TextEditor::draw( Frame& frame, CellPoint origin ) const
+{
+ // Fields can overlay an already painted document. Clear their entire region so
+ // shortening the value cannot expose the old field text beneath the editor.
+ for ( int row = 0; row < viewport_.rows; ++row ) {
+ for ( int column = 0; column < viewport_.columns; ++column ) {
+ frame.glyphs.push_back( { .position = { origin.row + row, origin.column + column }, .text = L" " } );
+ }
+ }
+ const auto selected = selection();
+ const auto cursor_point = carets_[unit_index()];
+ for ( std::size_t index = 0; index < glyphs_.size(); ++index ) {
+ const auto& glyph = glyphs_[index];
+ if ( glyph.point.row < top_row_ || glyph.point.row - top_row_ >= static_cast<std::size_t>( viewport_.rows ) ) {
+ continue;
+ }
+ const auto& unit = analysis_.graphemes[index];
+ const bool password = options_.kind == EditorKind::Password;
+ const bool selected_unit = unit.bytes.start < selected.end && selected.start < unit.bytes.end;
+ const int row = origin.row + static_cast<int>( glyph.point.row - top_row_ );
+ const auto column = glyph.point.column - left_column_;
+ const bool at_cursor = cursor_point.row == glyph.point.row && cursor_point.column == glyph.point.column;
+ if ( unit.kind == TextUnitKind::Tab && !password ) {
+ for ( auto cell = std::max<std::int64_t>( 0, column );
+ cell < std::min<std::int64_t>( viewport_.columns, column + glyph.columns );
+ ++cell ) {
+ frame.glyphs.push_back( { .position = { row, origin.column + static_cast<int>( cell ) },
+ .text = L" ",
+ .selected = selected_unit,
+ .cursor = at_cursor && cell == column } );
+ }
+ } else {
+ const bool newline = unit.kind == TextUnitKind::LineBreak;
+ const int width = newline ? 1 : glyph.columns;
+ if ( column < 0 || column + width > viewport_.columns || ( newline && !selected_unit ) ) {
+ continue;
+ }
+ const auto display = password ? L"*" : newline ? L" " : glyph.columns < unit.columns ? L"?" : unit.display;
+ frame.glyphs.push_back( { .position = { row, origin.column + static_cast<int>( column ) },
+ .width = width,
+ .text = display,
+ .selected = selected_unit,
+ .cursor = at_cursor } );
+ }
+ }
+ frame.cursor = CellPoint { origin.row + static_cast<int>( cursor_point.row - top_row_ ),
+ origin.column + static_cast<int>( cursor_point.column - left_column_ ) };
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/editor.hh b/ricochet/src/tui/editor.hh
new file mode 100644
index 0000000..457b0ca
--- /dev/null
+++ b/ricochet/src/tui/editor.hh
@@ -0,0 +1,169 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "paint.hh"
+#include "text.hh"
+
+#include <chrono>
+#include <optional>
+
+namespace ricochet::tui::detail {
+
+enum class EditorKind : std::uint8_t
+{
+ SingleLine,
+ MultiLine,
+ Password
+};
+
+struct EditorOptions
+{
+ EditorKind kind { EditorKind::SingleLine };
+ // The owner supplies history; password and multiline editors never request it.
+ bool history { false };
+};
+
+enum class EditorCommand : std::uint8_t
+{
+ Left,
+ Right,
+ Up,
+ Down,
+ LineStart,
+ LineEnd,
+ PreviousWord,
+ NextWord,
+ Backspace,
+ Delete,
+ KillToStart,
+ KillToEnd,
+ KillWord,
+ Yank,
+ BeginSelection,
+ SelectAll,
+ CancelSelection,
+ Clear,
+ Newline,
+ Undo,
+ Redo
+};
+
+// Enter/Tab/Escape are context decisions, never implicit submit/finish operations.
+// The owner may translate Enter into Newline for a multiline field.
+// Unhandled keys stay with the owning editor context, never fall through to Browse.
+enum class EditorIntent : std::uint8_t
+{
+ Consumed,
+ Unhandled,
+ Enter,
+ Tab,
+ BackTab,
+ Escape,
+ Cancel,
+ Menu,
+ Redraw,
+ HistoryPrevious,
+ HistoryNext
+};
+
+// One editing session. Owners recreate it when changing fields; its text, undo
+// history and kill buffer are never shared with another editor or a view snapshot.
+class TextEditor
+{
+public:
+ TextEditor( std::string value, EditorOptions options, const TextService& text, ScreenSize viewport );
+ TextEditor( const TextEditor& ) = delete;
+ TextEditor& operator=( const TextEditor& ) = delete;
+ TextEditor( TextEditor&& ) = delete;
+ TextEditor& operator=( TextEditor&& ) = delete;
+ ~TextEditor() = default;
+
+ // Business value for the owning field; rendering and copying use the methods below.
+ const std::string& value() const;
+ std::uint64_t revision() const;
+ std::size_t cursor() const;
+ ByteRange selection() const;
+ std::optional<std::string> copy_selection();
+ std::optional<std::chrono::steady_clock::time_point> prefix_deadline() const;
+
+ void resize( ScreenSize viewport );
+ void insert( std::u32string_view input );
+ // Host history replacement is one undoable edit, not a new editing session.
+ void replace_value( std::string value );
+ void execute( EditorCommand command, bool extend = false );
+ EditorIntent handle( const InputEvent& event,
+ std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now() );
+ // Region-local mouse positioning, with extend=true while selecting/dragging.
+ void place( CellPoint point, bool extend );
+ void draw( Frame& frame, CellPoint origin ) const;
+
+private:
+ struct State
+ {
+ std::string value {};
+ std::size_t cursor { 0 };
+ std::optional<std::size_t> anchor {};
+ bool selecting { false };
+ };
+ struct Point
+ {
+ std::size_t row { 0 };
+ std::int64_t column { 0 };
+ };
+ struct Glyph
+ {
+ Point point {};
+ int columns { 0 };
+ };
+
+ std::string normalize( std::string_view value ) const;
+ std::size_t unit_index() const;
+ std::size_t boundary( std::size_t index ) const;
+ std::size_t line_start() const;
+ std::size_t line_end() const;
+ std::size_t nearest( Point point ) const;
+ void analyze();
+ void layout();
+ void reveal();
+ void move( std::size_t destination, bool extend, bool preserve_column );
+ void replace( ByteRange range, std::string_view replacement );
+ void kill( ByteRange range );
+ void undo( bool redo );
+ EditorIntent key( const Key& key, std::chrono::steady_clock::time_point now );
+
+ EditorOptions options_;
+ const TextService& text_;
+ ScreenSize viewport_;
+ State state_ {};
+ TextAnalysis analysis_ {};
+ std::vector<Glyph> glyphs_ {};
+ std::vector<Point> carets_ {};
+ std::size_t top_row_ { 0 };
+ std::int64_t left_column_ { 0 };
+ std::int64_t desired_column_ { 0 };
+ std::vector<State> past_ {};
+ std::vector<State> future_ {};
+ std::string killed_ {};
+ bool killing_ { false };
+ std::uint64_t revision_ { 0 };
+ std::optional<std::chrono::steady_clock::time_point> prefix_ {};
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/layout.cc b/ricochet/src/tui/layout.cc
new file mode 100644
index 0000000..46bd626
--- /dev/null
+++ b/ricochet/src/tui/layout.cc
@@ -0,0 +1,939 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "layout.hh"
+#include "document_text.hh"
+
+#include <algorithm>
+#include <limits>
+#include <map>
+#include <numeric>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+namespace ricochet::tui::detail {
+namespace {
+
+constexpr int tab_columns = 8;
+constexpr std::int64_t cell_gap = 3;
+constexpr std::int64_t minimum_cell_columns = 4;
+
+TextStyle overlay( TextStyle base, TextStyle extra )
+{
+ return { base.emphasis || extra.emphasis,
+ base.strong || extra.strong,
+ base.code || extra.code,
+ base.subdued || extra.subdued,
+ base.warning || extra.warning };
+}
+
+struct Atom
+{
+ Grapheme glyph {};
+ DocumentRange source {};
+ TextStyle style {};
+ std::optional<HitTarget> target {};
+ bool decoration { false };
+};
+
+struct TextSpan
+{
+ const Text* leaf;
+ ByteRange bytes;
+};
+
+DocumentPoint text_point( std::span<const TextSpan> spans, std::size_t offset, bool end )
+{
+ for ( const auto& span : spans ) {
+ // At an inline seam an end belongs to the preceding leaf, a start to the next.
+ if ( offset < span.bytes.end || ( end && offset == span.bytes.end ) ) {
+ return TextPosition { span.leaf->id, offset - span.bytes.start };
+ }
+ }
+ const auto& last = spans.back();
+ return TextPosition { last.leaf->id, last.leaf->utf8.size() };
+}
+
+LayoutGlyph anchor( DocumentPoint point, LayoutPoint location, std::int64_t width )
+{ return { .position = location, .source = { point, point }, .clip_right = width }; }
+
+void append_box( LayoutData& destination, LayoutData source, std::size_t row, std::int64_t column )
+{
+ destination.rows = std::max( destination.rows, row + source.rows );
+ destination.extent = std::max( destination.extent, column + source.extent );
+ for ( auto& glyph : source.glyphs ) {
+ glyph.position.row += row;
+ glyph.position.column += column;
+ glyph.line_start += column;
+ for ( auto& window : glyph.scrolls ) {
+ window.left += column;
+ }
+ glyph.clip_left += column;
+ glyph.clip_right += column;
+ destination.glyphs.push_back( std::move( glyph ) );
+ }
+}
+
+class LayoutBuilder
+{
+public:
+ LayoutBuilder( const DocumentData& document, const TextService& text, std::span<const HorizontalScroll> scrolls )
+ : document_( document ), text_( text ), scrolls_( scrolls )
+ {
+ for ( const auto& control : document.controls ) {
+ controls_.emplace( control.id, &control );
+ }
+ }
+
+ LayoutData blocks( std::span<const Block> content, std::int64_t width, TextStyle style = {} ) const
+ {
+ LayoutData result;
+ std::size_t row = 0;
+ for ( const auto& block : content ) {
+ auto box = block_box( block, width, style );
+ const auto height = box.rows;
+ append_box( result, std::move( box ), row, 0 );
+ row += height;
+ }
+ return result;
+ }
+
+private:
+ void text_run( std::vector<Atom>& atoms,
+ std::span<const TextSpan> spans,
+ const std::string& utf8,
+ bool tabs,
+ TextStyle base ) const
+ {
+ const auto analysis = text_.analyze( utf8, true, tabs );
+ for ( const auto& glyph : analysis.graphemes ) {
+ Atom atom { .glyph = glyph,
+ .source
+ = { text_point( spans, glyph.bytes.start, false ), text_point( spans, glyph.bytes.end, true ) },
+ .style = base };
+ for ( const auto& span : spans ) {
+ // Only source portions intersecting this grapheme can contribute style/link.
+ if ( span.bytes.start >= glyph.bytes.end || span.bytes.end <= glyph.bytes.start ) {
+ continue;
+ }
+ const ByteRange local { std::max( span.bytes.start, glyph.bytes.start ) - span.bytes.start,
+ std::min( span.bytes.end, glyph.bytes.end ) - span.bytes.start };
+ for ( const auto& styled : span.leaf->styles ) {
+ if ( styled.bytes.start < local.end && local.start < styled.bytes.end ) {
+ atom.style = overlay( atom.style, styled.style );
+ }
+ }
+ for ( const auto& link : document_.links ) {
+ for ( const auto& range : link.ranges ) {
+ if ( range.block == span.leaf->id && range.bytes.start < local.end && local.start < range.bytes.end ) {
+ const HitTarget target = link.id;
+ // One grapheme cannot activate two links, including across leaf seams.
+ if ( atom.target && atom.target != target ) {
+ throw std::invalid_argument( "Overlapping links address the same grapheme" );
+ }
+ atom.target = target;
+ atom.style.emphasis = true;
+ }
+ }
+ }
+ }
+ atoms.push_back( std::move( atom ) );
+ }
+ // Empty text still has a real byte boundary; no fabricated display character.
+ if ( utf8.empty() ) {
+ const auto point = text_point( spans, 0, false );
+ atoms.push_back( { .glyph = { .columns = 0 }, .source = { point, point }, .style = base } );
+ }
+ }
+
+ void control_atoms( std::vector<Atom>& atoms, ControlId id, TextStyle base ) const
+ {
+ // A missing ID is a malformed document reference; map::at reports it directly.
+ const auto& control = *controls_.at( id );
+ const auto* input = std::get_if<TextControl>( &control.content );
+ const bool multiline = input && input->kind == TextControlKind::MultiLine;
+ const auto before = ObjectPosition { id, BoundarySide::Before };
+ const auto after = ObjectPosition { id, BoundarySide::After };
+ const auto style = overlay( base, { .subdued = control.disabled, .warning = control.error.has_value() } );
+ std::wstring_view opening = L"[";
+ std::wstring_view closing = L"]";
+ if ( const auto* checkbox = std::get_if<Checkbox>( &control.content ) ) {
+ opening = checkbox->checked ? L"[x] " : L"[ ] ";
+ closing = L"";
+ } else if ( const auto* radio = std::get_if<RadioButton>( &control.content ) ) {
+ opening = radio->checked ? L"(*) " : L"( ) ";
+ closing = L"";
+ }
+ for ( const auto character : opening ) {
+ atoms.push_back( { .glyph = { .display = std::wstring( 1, character ) },
+ .source = { before, before },
+ .style = style,
+ .target = id } );
+ }
+ const auto display = control_display_text( control, text_ );
+ const auto analysis = text_.analyze( display, multiline, false );
+ for ( const auto& glyph : analysis.graphemes ) {
+ atoms.push_back( { .glyph = glyph,
+ .source = { ControlTextPosition { id, {}, glyph.bytes.start },
+ ControlTextPosition { id, {}, glyph.bytes.end } },
+ .style = style,
+ .target = id } );
+ }
+ if ( display.empty() ) {
+ const auto point = ControlTextPosition { id, {}, 0 };
+ atoms.push_back( { .glyph = { .display = L" " }, .source = { point, point }, .style = style, .target = id } );
+ }
+ for ( const auto character : closing ) {
+ atoms.push_back( { .glyph = { .display = std::wstring( 1, character ), .break_after = true },
+ .source = { after, after },
+ .style = style,
+ .target = id } );
+ }
+ // Checkbox/radio controls have no closing bracket but still have an after boundary.
+ atoms.push_back( { .glyph = { .columns = 0, .break_after = true },
+ .source = { after, after },
+ .style = style,
+ .target = id } );
+ }
+
+ std::vector<Atom> inline_atoms( std::span<const Inline> content, TextStyle style ) const
+ {
+ std::vector<Atom> result;
+ std::vector<TextSpan> spans;
+ std::string utf8;
+ const auto flush = [&] {
+ if ( !spans.empty() ) {
+ text_run( result, spans, utf8, false, style );
+ spans.clear();
+ utf8.clear();
+ }
+ };
+ for ( const auto& item : content ) {
+ if ( const auto* text = std::get_if<Text>( &item ) ) {
+ const auto start = utf8.size();
+ utf8 += text->utf8;
+ spans.push_back( { text, { start, utf8.size() } } );
+ } else {
+ flush();
+ control_atoms( result, std::get<ControlPlaceholder>( item ).control, style );
+ }
+ }
+ flush();
+ return result;
+ }
+
+ LayoutData flow( std::span<const Atom> atoms, std::int64_t width, bool wrap ) const
+ {
+ LayoutData result;
+ std::size_t start = 0;
+ std::size_t row = 0;
+ while ( start < atoms.size() ) {
+ std::size_t end = start;
+ std::size_t opportunity = start;
+ std::int64_t column = 0;
+ while ( end < atoms.size() && atoms[end].glyph.kind != TextUnitKind::LineBreak ) {
+ const auto& glyph = atoms[end].glyph;
+ const int cells = glyph.kind == TextUnitKind::Tab ? tab_columns - column % tab_columns : glyph.columns;
+ if ( wrap && column + cells > width && end > start ) {
+ break;
+ }
+ column += cells;
+ ++end;
+ if ( glyph.break_after ) {
+ opportunity = end;
+ }
+ }
+ // Prefer a Unicode line boundary; an overlong word breaks at a grapheme.
+ if ( end < atoms.size() && atoms[end].glyph.kind != TextUnitKind::LineBreak && opportunity > start ) {
+ end = opportunity;
+ }
+ column = 0;
+ for ( auto index = start; index < end; ++index ) {
+ const auto& atom = atoms[index];
+ const bool tab = atom.glyph.kind == TextUnitKind::Tab;
+ const int cells = tab ? tab_columns - column % tab_columns : atom.glyph.columns;
+ result.glyphs.push_back( { .position = { row, column },
+ .columns = cells,
+ .text = atom.glyph.display,
+ .style = atom.style,
+ .source = atom.source,
+ .target = atom.target,
+ .clip_right = wrap ? width : std::numeric_limits<std::int64_t>::max(),
+ .decoration = atom.decoration,
+ .fill = tab,
+ .word_start = atom.glyph.word_start,
+ .word_end = atom.glyph.word_end } );
+ column += cells;
+ }
+ result.extent = std::max( result.extent, column );
+ if ( end < atoms.size() && atoms[end].glyph.kind == TextUnitKind::LineBreak ) {
+ const auto& atom = atoms[end++];
+ result.glyphs.push_back( { .position = { row, column },
+ .source = atom.source,
+ .target = atom.target,
+ .clip_right = width,
+ .line_break = true } );
+ // The endpoint of a hard LF belongs to the next line, including a
+ // trailing empty line; it must also be reachable by hit testing.
+ auto boundary = anchor( atom.source.end, { row + 1, 0 }, width );
+ boundary.target = atom.target;
+ result.glyphs.push_back( std::move( boundary ) );
+ result.rows = row + 2;
+ } else {
+ result.rows = row + 1;
+ }
+ start = end;
+ ++row;
+ }
+ return result;
+ }
+
+ void horizontal( LayoutData& box, BlockId block, std::int64_t width ) const
+ {
+ const auto scroll = std::ranges::find( scrolls_, block, &HorizontalScroll::block );
+ const auto requested = scroll == scrolls_.end() ? 0 : scroll->columns;
+ const auto maximum = std::max<std::int64_t>( 0, box.extent - width );
+ const auto shift = static_cast<std::int64_t>( std::min<std::uint64_t>( requested, maximum ) );
+ for ( auto& glyph : box.glyphs ) {
+ for ( auto& window : glyph.scrolls ) {
+ window.left -= shift;
+ }
+ glyph.scrolls.push_back( { block, 0, width, shift, maximum } );
+ glyph.position.column -= shift;
+ glyph.line_start -= shift;
+ glyph.clip_left = std::max<std::int64_t>( 0, glyph.clip_left - shift );
+ glyph.clip_right = std::min( width, glyph.clip_right - shift );
+ }
+ }
+
+ LayoutData list_box( const List& list, std::int64_t width, TextStyle style ) const
+ {
+ LayoutData result;
+ std::size_t row = 0;
+ auto number = list.first_number;
+ for ( const auto& item : list.items ) {
+ const auto marker
+ = item.marker.value_or( list.kind == ListKind::Ordered ? std::to_string( number ) + "." : "-" );
+ const auto analysis = text_.analyze( marker, false, false );
+ const auto marker_width
+ = std::accumulate( analysis.graphemes.begin(),
+ analysis.graphemes.end(),
+ std::int64_t { 0 },
+ []( auto total, const auto& glyph ) { return total + glyph.columns; } );
+ // Deep nesting must leave at least one body cell; clipped markers stay decoration.
+ const auto indent = std::min( marker_width + 1, width - 1 );
+ auto body = blocks( item.content, width - indent, style );
+ const auto height = body.rows;
+ append_box( result, std::move( body ), row, indent );
+ std::int64_t column = 0;
+ for ( const auto& glyph : analysis.graphemes ) {
+ result.glyphs.push_back( { .position = { row, column },
+ .columns = glyph.columns,
+ .text = glyph.display,
+ .style = style,
+ .clip_right = indent,
+ .decoration = true } );
+ column += glyph.columns;
+ }
+ row += height;
+ // Ordered counters can start at the model's largest int64 value.
+ if ( number < std::numeric_limits<std::int64_t>::max() ) {
+ ++number;
+ }
+ }
+ return result;
+ }
+
+ LayoutData table_box( const Table& table, BlockId id, std::int64_t width, TextStyle style ) const
+ {
+ if ( table.columns == 0 ) {
+ return {};
+ }
+ std::vector<std::int64_t> widths( table.columns, 1 );
+ std::vector<std::int64_t> desired( table.columns, 1 );
+ for ( std::size_t row = 0; row < table.rows.size(); ++row ) {
+ for ( const auto& cell : table.rows[row].cells ) {
+ // Spans index the supplied grid. Reject zero/out-of-grid spans before indexing.
+ if ( cell.column >= table.columns || cell.column_span == 0 || cell.row_span == 0
+ || cell.column_span > table.columns - cell.column || cell.row_span > table.rows.size() - row ) {
+ throw std::invalid_argument( "Table cell span lies outside its resolved grid" );
+ }
+ const auto measure = blocks( cell.content, width, style );
+ const auto span = static_cast<std::int64_t>( cell.column_span );
+ const auto preferred = std::max<std::int64_t>( 1, ( measure.extent + span - 1 ) / span );
+ for ( auto column = cell.column; column < cell.column + cell.column_span; ++column ) {
+ desired[column] = std::max( desired[column], preferred );
+ widths[column] = std::max( widths[column], std::min( preferred, minimum_cell_columns ) );
+ }
+ }
+ }
+ const auto gaps = cell_gap * static_cast<std::int64_t>( table.columns - 1 );
+ auto remaining = width - gaps - std::accumulate( widths.begin(), widths.end(), std::int64_t { 0 } );
+ // Grow content-bearing columns toward their preferred width, within the viewport.
+ while ( remaining > 0 ) {
+ bool grew = false;
+ for ( std::size_t column = 0; column < widths.size() && remaining > 0; ++column ) {
+ if ( widths[column] < desired[column] ) {
+ ++widths[column];
+ --remaining;
+ grew = true;
+ }
+ }
+ if ( !grew ) {
+ break;
+ }
+ }
+ struct CellBox
+ {
+ const TableCell* cell;
+ std::size_t row;
+ LayoutData box;
+ };
+ std::vector<CellBox> cells;
+ std::vector<std::size_t> heights( table.rows.size(), 1 );
+ for ( std::size_t row = 0; row < table.rows.size(); ++row ) {
+ for ( const auto& cell : table.rows[row].cells ) {
+ const auto first = widths.begin() + static_cast<std::ptrdiff_t>( cell.column );
+ const auto last = first + static_cast<std::ptrdiff_t>( cell.column_span );
+ const auto cell_width = std::accumulate( first, last, std::int64_t { 0 } )
+ + cell_gap * static_cast<std::int64_t>( cell.column_span - 1 );
+ auto box = blocks( cell.content, cell_width, overlay( style, { .strong = cell.header } ) );
+ const auto begin = heights.begin() + static_cast<std::ptrdiff_t>( row );
+ const auto end = begin + static_cast<std::ptrdiff_t>( cell.row_span );
+ const auto height = std::accumulate( begin, end, std::size_t { 0 } );
+ if ( box.rows > height ) {
+ heights[row + cell.row_span - 1] += box.rows - height;
+ }
+ cells.push_back( { &cell, row, std::move( box ) } );
+ }
+ }
+ std::vector<std::size_t> row_offsets( heights.size() + 1, 0 );
+ std::partial_sum( heights.begin(), heights.end(), row_offsets.begin() + 1 );
+ std::vector<std::int64_t> column_offsets( widths.size(), 0 );
+ for ( std::size_t column = 1; column < widths.size(); ++column ) {
+ column_offsets[column] = column_offsets[column - 1] + widths[column - 1] + cell_gap;
+ }
+ LayoutData result;
+ result.extent = std::accumulate( widths.begin(), widths.end(), gaps );
+ result.rows = std::max<std::size_t>( 1, row_offsets.back() );
+ for ( auto& cell : cells ) {
+ const auto column = column_offsets[cell.cell->column];
+ append_box( result, std::move( cell.box ), row_offsets[cell.row], column );
+ if ( cell.cell->column != 0 ) {
+ for ( auto row = row_offsets[cell.row]; row < row_offsets[cell.row + cell.cell->row_span]; ++row ) {
+ result.glyphs.push_back( { .position = { row, column - 2 },
+ .columns = 1,
+ .text = L"|",
+ .style = { .subdued = true },
+ .clip_right = result.extent,
+ .decoration = true } );
+ }
+ }
+ }
+ // A nested preformatted block scrolls inside its cell; its natural extent
+ // must not enlarge the enclosing table's horizontal scroll range.
+ result.extent = std::accumulate( widths.begin(), widths.end(), gaps );
+ horizontal( result, id, width );
+ return result;
+ }
+
+ LayoutData block_box( const Block& block, std::int64_t width, TextStyle style ) const
+ {
+ auto result = std::visit(
+ [this, &block, width, style]( const auto& value ) -> LayoutData {
+ using Value = std::decay_t<decltype( value )>;
+ if constexpr ( std::is_same_v<Value, Paragraph> ) {
+ return flow( inline_atoms( value.content, style ), width, true );
+ } else if constexpr ( std::is_same_v<Value, Heading> ) {
+ return flow( inline_atoms( value.content, overlay( style, { .strong = true } ) ), width, true );
+ } else if constexpr ( std::is_same_v<Value, Preformatted> || std::is_same_v<Value, Alternative> ) {
+ const Text& leaf = [&]() -> const Text& {
+ if constexpr ( std::is_same_v<Value, Preformatted> ) {
+ return value.content;
+ } else {
+ return value.description;
+ }
+ }();
+ const TextSpan span { &leaf, { 0, leaf.utf8.size() } };
+ std::vector<Atom> atoms;
+ constexpr bool preformatted = std::is_same_v<Value, Preformatted>;
+ text_run(
+ atoms, std::span( &span, 1 ), leaf.utf8, preformatted, overlay( style, { .code = preformatted } ) );
+ auto box = flow( atoms, width, !preformatted );
+ if constexpr ( preformatted ) {
+ horizontal( box, block.id, width );
+ }
+ return box;
+ } else if constexpr ( std::is_same_v<Value, List> ) {
+ return list_box( value, width, style );
+ } else if constexpr ( std::is_same_v<Value, Table> ) {
+ return table_box( value, block.id, width, style );
+ } else if constexpr ( std::is_same_v<Value, ControlPlaceholder> ) {
+ std::vector<Atom> atoms;
+ control_atoms( atoms, value.control, style );
+ return flow( atoms, width, true );
+ } else {
+ LayoutData box;
+ box.extent = width;
+ for ( std::int64_t column = 0; column < width; ++column ) {
+ box.glyphs.push_back( { .position = { 0, column },
+ .columns = 1,
+ .text = L"-",
+ .style = { .subdued = true },
+ .clip_right = width,
+ .decoration = true } );
+ }
+ return box;
+ }
+ },
+ block.content );
+ const bool has_text
+ = std::ranges::any_of( result.glyphs, []( const auto& glyph ) { return !glyph.decoration; } );
+ auto before = anchor( ObjectPosition { block.id, BoundarySide::Before }, {}, width );
+ auto after = anchor( ObjectPosition { block.id, BoundarySide::After },
+ { result.rows - 1, std::min( result.extent, width ) },
+ width );
+ before.boundary_only = has_text;
+ after.boundary_only = true;
+ result.glyphs.push_back( std::move( before ) );
+ result.glyphs.push_back( std::move( after ) );
+ return result;
+ }
+
+ const DocumentData& document_;
+ const TextService& text_;
+ std::span<const HorizontalScroll> scrolls_;
+ std::map<ControlId, const Control*> controls_ {};
+};
+
+LayoutPoint end_point( const LayoutGlyph& glyph )
+{
+ // Object decorations and empty fields do not create a second text boundary.
+ if ( glyph.source.start == glyph.source.end ) {
+ return glyph.position;
+ }
+ if ( glyph.line_break ) {
+ return { glyph.position.row + 1, glyph.line_start };
+ }
+ return { glyph.position.row, glyph.position.column + glyph.columns };
+}
+
+} // namespace
+
+DocumentLayout::DocumentLayout( const Document& document,
+ const TextService& text,
+ int columns,
+ std::span<const HorizontalScroll> scrolls )
+ : columns_( std::max( 1, columns ) )
+ , data_( LayoutBuilder( document.data(), text, scrolls ).blocks( document.data().blocks, columns_ ) )
+{
+ // An empty document still has a location for its cursor/viewport anchor.
+ if ( data_.glyphs.empty() ) {
+ data_.glyphs.push_back( anchor( DocumentStart {}, {}, columns_ ) );
+ }
+ for ( std::size_t index = 0; index < data_.glyphs.size(); ++index ) {
+ const auto& glyph = data_.glyphs[index];
+ if ( glyph.decoration || glyph.boundary_only ) {
+ continue;
+ }
+ // Wrapped object borders and hard-break anchors can repeat a logical point.
+ // Keep one reading stop, preferring actual text over its zero-width anchor.
+ if ( !stops_.empty() && data_.glyphs[stops_.back()].source.start == glyph.source.start ) {
+ if ( data_.glyphs[stops_.back()].columns == 0 ) {
+ stops_.back() = index;
+ }
+ } else {
+ stops_.push_back( index );
+ }
+ }
+ std::set<HitTarget> omitted;
+ for ( const auto& control : document.data().controls ) {
+ if ( control.disabled ) {
+ omitted.insert( control.id );
+ }
+ }
+ for ( std::size_t index = 0; index < stops_.size(); ++index ) {
+ const auto& target = data_.glyphs[stops_[index]].target;
+ // Skip ordinary text and disabled/previously encountered objects. A link's
+ // later fragments never create extra Tab stops, including across blocks.
+ if ( target && omitted.insert( *target ).second ) {
+ focus_.push_back( { *target, index } );
+ }
+ }
+}
+
+int DocumentLayout::columns() const
+{ return columns_; }
+std::size_t DocumentLayout::rows() const
+{ return data_.rows; }
+
+std::optional<LayoutPoint> DocumentLayout::locate( const DisplayPosition& position ) const
+{
+ if ( std::holds_alternative<DocumentStart>( position.point ) ) {
+ return data_.glyphs[stops_.front()].position;
+ }
+ std::optional<LayoutPoint> fallback;
+ for ( const auto& glyph : data_.glyphs ) {
+ if ( glyph.decoration ) {
+ continue;
+ }
+ if ( position.point == glyph.source.start ) {
+ if ( position.affinity == WrapAffinity::NextLine ) {
+ return glyph.position;
+ }
+ fallback = glyph.position;
+ }
+ if ( position.point == glyph.source.end ) {
+ if ( position.affinity == WrapAffinity::PreviousLine ) {
+ return end_point( glyph );
+ }
+ fallback = end_point( glyph );
+ }
+ }
+ return fallback;
+}
+
+std::optional<LayoutHit> DocumentLayout::hit( LayoutPoint point ) const
+{
+ // Blank space below the document means its last reading position, independent
+ // of the mouse column (especially when the final block is a table).
+ if ( point.row >= data_.rows ) {
+ const auto& glyph = data_.glyphs[stops_.back()];
+ return LayoutHit { last(), glyph.source, glyph.target };
+ }
+ const LayoutGlyph* nearest = nullptr;
+ auto distance
+ = std::tuple { std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::int64_t>::max(), true };
+ for ( const auto& glyph : data_.glyphs ) {
+ // Decorations and partially clipped wide glyphs have no independent text hit.
+ if ( glyph.decoration || glyph.boundary_only ) {
+ continue;
+ }
+ const auto left = std::max( glyph.position.column, glyph.clip_left );
+ const auto edge = std::min( glyph.position.column + glyph.columns, glyph.clip_right );
+ // Tab blanks may be clipped individually; a wide glyph remains indivisible.
+ if ( ( glyph.fill && left >= edge )
+ || ( !glyph.fill
+ && ( glyph.position.column < glyph.clip_left
+ || glyph.position.column + glyph.columns > glyph.clip_right ) ) ) {
+ continue;
+ }
+ const auto row_distance
+ = glyph.position.row > point.row ? glyph.position.row - point.row : point.row - glyph.position.row;
+ const auto right = std::max( left, edge - 1 );
+ const auto column_distance = std::max( { left - point.column, point.column - right, std::int64_t { 0 } } );
+ const auto candidate = std::tuple { row_distance, column_distance, glyph.columns == 0 };
+ if ( candidate < distance ) {
+ nearest = &glyph;
+ distance = candidate;
+ }
+ }
+ if ( !nearest ) {
+ return std::nullopt;
+ }
+ const bool after = !nearest->line_break && point.column >= nearest->position.column + nearest->columns;
+ return LayoutHit { .position = { after ? nearest->source.end : nearest->source.start,
+ after ? WrapAffinity::PreviousLine : WrapAffinity::NextLine },
+ .unit = nearest->source,
+ .target = nearest->target };
+}
+
+DisplayPosition DocumentLayout::stop( std::size_t index ) const
+{ return { data_.glyphs[stops_[index]].source.start, WrapAffinity::NextLine }; }
+
+DisplayPosition DocumentLayout::first() const
+{ return stop( 0 ); }
+DisplayPosition DocumentLayout::last() const
+{ return stop( stops_.size() - 1 ); }
+
+std::size_t DocumentLayout::nearest_stop( LayoutPoint point ) const
+{
+ std::size_t nearest = 0;
+ auto distance = std::pair { std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::int64_t>::max() };
+ for ( std::size_t index = 0; index < stops_.size(); ++index ) {
+ const auto& glyph = data_.glyphs[stops_[index]];
+ const auto row = glyph.position.row;
+ const auto right = glyph.position.column + std::max( 1, glyph.columns ) - 1;
+ const auto candidate = std::pair {
+ row > point.row ? row - point.row : point.row - row,
+ std::max( { glyph.position.column - point.column, point.column - right, std::int64_t { 0 } } ) };
+ if ( candidate < distance ) {
+ nearest = index;
+ distance = candidate;
+ }
+ }
+ return nearest;
+}
+
+DocumentLayout::ReadingPosition DocumentLayout::reading_position( const DisplayPosition& position ) const
+{
+ std::optional<std::size_t> endpoint;
+ for ( std::size_t index = 0; index < stops_.size(); ++index ) {
+ const auto& glyph = data_.glyphs[stops_[index]];
+ if ( glyph.source.start == position.point ) {
+ return { index, position.affinity == WrapAffinity::NextLine };
+ }
+ if ( glyph.source.end == position.point ) {
+ endpoint = index + 1;
+ }
+ }
+ if ( endpoint ) {
+ return { *endpoint, false };
+ }
+ // Structural block anchors have coordinates but are not extra character stops.
+ return { nearest_stop( locate( position ).value() ), true };
+}
+
+DisplayPosition DocumentLayout::adjacent( const DisplayPosition& position, int direction ) const
+{
+ const auto current = reading_position( position );
+ if ( direction < 0 ) {
+ return stop( current.index == 0 ? 0 : current.index - 1 );
+ }
+ return stop( std::min( stops_.size() - 1, current.index + static_cast<std::size_t>( current.at_start ) ) );
+}
+
+DisplayPosition DocumentLayout::word( const DisplayPosition& position, WordMotion motion ) const
+{
+ const auto current = reading_position( position );
+ if ( motion == WordMotion::Previous ) {
+ for ( auto index = current.index; index > 0; ) {
+ --index;
+ if ( data_.glyphs[stops_[index]].word_start ) {
+ return stop( index );
+ }
+ }
+ return first();
+ }
+ auto begin = current.index;
+ if ( current.at_start && ( motion == WordMotion::Next || data_.glyphs[stops_[begin]].word_end ) ) {
+ ++begin;
+ }
+ for ( auto index = begin; index < stops_.size(); ++index ) {
+ const auto& glyph = data_.glyphs[stops_[index]];
+ if ( motion == WordMotion::Next ? glyph.word_start : glyph.word_end ) {
+ return stop( index );
+ }
+ }
+ return last();
+}
+
+DisplayPosition DocumentLayout::on_row( LayoutPoint point ) const
+{ return stop( nearest_stop( point ) ); }
+
+DisplayPosition DocumentLayout::line_edge( const DisplayPosition& position, bool end ) const
+{
+ const auto row = locate( position ).value().row;
+ auto chosen = nearest_stop( { row, 0 } );
+ for ( std::size_t index = 0; index < stops_.size(); ++index ) {
+ const auto& glyph = data_.glyphs[stops_[index]];
+ const auto column = data_.glyphs[stops_[chosen]].position.column;
+ if ( glyph.position.row == row && ( end ? glyph.position.column > column : glyph.position.column < column ) ) {
+ chosen = index;
+ }
+ }
+ return stop( chosen );
+}
+
+const LayoutGlyph& DocumentLayout::cursor_glyph( const DisplayPosition& position ) const
+{
+ const auto current = reading_position( position );
+ const auto index = current.at_start || current.index == 0 ? current.index : current.index - 1;
+ return data_.glyphs[stops_[std::min( index, stops_.size() - 1 )]];
+}
+
+std::optional<HitTarget> DocumentLayout::target( const DisplayPosition& position ) const
+{ return cursor_glyph( position ).target; }
+
+std::optional<FocusStep> DocumentLayout::focus( const DisplayPosition& position, bool backward ) const
+{
+ if ( focus_.empty() ) {
+ return std::nullopt;
+ }
+ const auto object = target( position );
+ const auto current
+ = std::ranges::find_if( focus_, [&object]( const auto& entry ) { return object == entry.target; } );
+ auto origin = reading_position( position );
+ // Any fragment of an object uses that object's single place in the focus order.
+ // Plain text and disabled controls instead start from the current reading stop.
+ if ( current != focus_.end() ) {
+ origin = { current->stop, true };
+ }
+ if ( backward ) {
+ const auto previous = std::find_if(
+ focus_.rbegin(), focus_.rend(), [&origin]( const auto& entry ) { return entry.stop < origin.index; } );
+ const bool wrapped = previous == focus_.rend();
+ return FocusStep { stop( wrapped ? focus_.back().stop : previous->stop ), wrapped };
+ }
+ const auto next = std::ranges::find_if( focus_, [&origin]( const auto& entry ) {
+ // A line-end insertion boundary is before the next stop, not on its glyph.
+ return entry.stop > origin.index || ( !origin.at_start && entry.stop == origin.index );
+ } );
+ const bool wrapped = next == focus_.end();
+ return FocusStep { stop( wrapped ? focus_.front().stop : next->stop ), wrapped };
+}
+
+std::optional<LayoutPoint> DocumentLayout::cursor_cell( const DisplayPosition& position ) const
+{
+ const auto& glyph = cursor_glyph( position );
+ auto point = locate( position ).value();
+ if ( glyph.fill ) {
+ // Any visible part of an expanded tab represents the same logical unit.
+ const auto left = std::max( glyph.position.column, glyph.clip_left );
+ const auto right = std::min( glyph.position.column + glyph.columns, glyph.clip_right );
+ if ( left >= right ) {
+ return std::nullopt;
+ }
+ point.column = std::clamp( point.column, left, right - 1 );
+ return point;
+ }
+ if ( point.column == glyph.clip_right && point.row == glyph.position.row ) {
+ // An insertion boundary or LF immediately after a full row has no cell.
+ // Use the preceding complete glyph; do not reveal a cursor further offscreen.
+ auto column = glyph.clip_right - 1;
+ for ( const auto index : stops_ ) {
+ const auto& candidate = data_.glyphs[index];
+ if ( candidate.position.row == point.row && candidate.columns > 0
+ && candidate.position.column >= glyph.clip_left
+ && candidate.position.column + candidate.columns == glyph.clip_right ) {
+ column = candidate.position.column;
+ }
+ }
+ point.column = column;
+ }
+ // In a one-cell viewport a wide glyph is clipped entirely. Its cursor occupies
+ // the blank fallback cell, never paints half of the character.
+ const auto width = point.column == glyph.position.column
+ ? std::min<std::int64_t>( std::max( 1, glyph.columns ), glyph.clip_right - glyph.clip_left )
+ : 1;
+ if ( point.column < glyph.clip_left || point.column >= glyph.clip_right
+ || point.column + width > glyph.clip_right ) {
+ return std::nullopt;
+ }
+ return point;
+}
+
+std::vector<HorizontalScroll> DocumentLayout::reveal( const DisplayPosition& position ) const
+{
+ const auto& glyph = cursor_glyph( position );
+ auto column = glyph.position.column;
+ const auto width = std::max( 1, glyph.columns );
+ std::vector<HorizontalScroll> result;
+ for ( const auto& window : glyph.scrolls ) {
+ const auto delta
+ = column < window.left
+ ? column - window.left
+ : std::max<std::int64_t>(
+ 0, column + std::min<std::int64_t>( width, window.width ) - window.left - window.width );
+ const auto offset = std::clamp( window.offset + delta, std::int64_t { 0 }, window.maximum );
+ if ( offset != window.offset ) {
+ result.push_back( { window.block, static_cast<std::size_t>( offset ) } );
+ column -= offset - window.offset;
+ }
+ }
+ return result;
+}
+
+std::vector<HorizontalScroll> DocumentLayout::pan( const DisplayPosition& position, int direction ) const
+{
+ for ( const auto& window : cursor_glyph( position ).scrolls ) {
+ if ( window.maximum > 0 ) {
+ const auto offset = std::clamp( window.offset + direction, std::int64_t { 0 }, window.maximum );
+ if ( offset == window.offset ) {
+ return {};
+ }
+ return { { window.block, static_cast<std::size_t>( offset ) } };
+ }
+ }
+ return {};
+}
+
+DocumentRange DocumentLayout::unit( const DisplayPosition& position ) const
+{
+ const auto reading = reading_position( position );
+ if ( !reading.at_start ) {
+ return { position.point, position.point };
+ }
+ return data_.glyphs[stops_[reading.index]].source;
+}
+
+void DocumentLayout::append_frame( Frame& frame,
+ std::size_t first_row,
+ CellPoint origin,
+ ScreenSize viewport,
+ const DocumentText& content,
+ std::optional<DocumentRange> selection,
+ const DisplayPosition& cursor,
+ std::optional<HitTarget> focus ) const
+{
+ const auto selected = selection ? content.offsets( *selection ) : ByteRange {};
+ const auto caret = cursor_cell( cursor );
+ for ( const auto& glyph : data_.glyphs ) {
+ if ( glyph.position.row < first_row
+ || glyph.position.row - first_row >= static_cast<std::size_t>( std::max( 0, viewport.rows ) ) ) {
+ continue;
+ }
+ bool highlighted = false;
+ if ( selection && !glyph.decoration && !glyph.boundary_only ) {
+ const auto bytes = content.offsets( glyph.source );
+ highlighted = bytes.start < selected.end && selected.start < bytes.end;
+ // A control border has an object position but contributes no decoration to copy.
+ if ( bytes.start == bytes.end && std::holds_alternative<ObjectPosition>( glyph.source.start ) ) {
+ highlighted = selected.start <= bytes.start && bytes.start < selected.end;
+ }
+ }
+ const bool newline = glyph.line_break && highlighted;
+ if ( glyph.text.empty() && !newline ) {
+ continue;
+ }
+ auto style = glyph.style;
+ style.strong = style.strong || ( focus && glyph.target == focus );
+ const auto left = std::max<std::int64_t>( 0, glyph.clip_left );
+ const auto right = std::min<std::int64_t>( viewport.columns, glyph.clip_right );
+ const auto row = origin.row + static_cast<int>( glyph.position.row - first_row );
+ const bool at_cursor = caret && caret->row == glyph.position.row && caret->column >= glyph.position.column
+ && caret->column < glyph.position.column + std::max( 1, glyph.columns );
+ if ( glyph.fill ) {
+ // Expanded tabs are many blank cells but keep a single source range/hit unit.
+ for ( auto column = std::max( left, glyph.position.column );
+ column < std::min( right, glyph.position.column + glyph.columns );
+ ++column ) {
+ frame.glyphs.push_back( { .position = { row, origin.column + static_cast<int>( column ) },
+ .text = L" ",
+ .style = style,
+ .selected = highlighted,
+ .cursor = at_cursor && column == caret->column } );
+ }
+ } else if ( glyph.position.column >= left
+ && glyph.position.column + ( newline ? 1 : glyph.columns ) <= right ) {
+ frame.glyphs.push_back( { .position = { row, origin.column + static_cast<int>( glyph.position.column ) },
+ .width = newline ? 1 : glyph.columns,
+ .text = newline ? L" " : glyph.text,
+ .style = style,
+ .selected = highlighted,
+ .cursor = at_cursor } );
+ }
+ }
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/layout.hh b/ricochet/src/tui/layout.hh
new file mode 100644
index 0000000..f5db28c
--- /dev/null
+++ b/ricochet/src/tui/layout.hh
@@ -0,0 +1,164 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "paint.hh"
+#include "ricochet/tui/view.hh"
+#include "text.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <span>
+#include <variant>
+#include <vector>
+
+namespace ricochet::tui::detail {
+
+class DocumentText;
+
+using HitTarget = std::variant<LinkId, ControlId>;
+
+struct LayoutPoint
+{
+ std::size_t row { 0 };
+ std::int64_t column { 0 };
+};
+
+struct LayoutHit
+{
+ DisplayPosition position {};
+ DocumentRange unit {};
+ std::optional<HitTarget> target {};
+};
+
+struct FocusStep
+{
+ DisplayPosition position {};
+ bool wrapped { false };
+};
+
+struct ScrollWindow
+{
+ BlockId block {};
+ std::int64_t left { 0 };
+ std::int64_t width { 0 };
+ std::int64_t offset { 0 };
+ std::int64_t maximum { 0 };
+};
+
+enum class WordMotion : std::uint8_t
+{
+ Next,
+ Previous,
+ End
+};
+
+struct LayoutGlyph
+{
+ LayoutPoint position {};
+ int columns { 0 };
+ std::wstring text {};
+ TextStyle style {};
+ DocumentRange source {};
+ std::optional<HitTarget> target {};
+ // Clip bounds are in document-layout columns and also constrain hit testing.
+ std::int64_t clip_left { 0 };
+ std::int64_t clip_right { 0 };
+ // Structural boundaries locate anchors but do not displace nearby text hits.
+ bool boundary_only { false };
+ std::int64_t line_start { 0 };
+ bool decoration { false };
+ bool line_break { false };
+ bool fill { false };
+ bool word_start { false };
+ bool word_end { false };
+ // Inner-to-outer windows allow a code block inside a table to be revealed.
+ std::vector<ScrollWindow> scrolls {};
+};
+
+struct LayoutData
+{
+ // Reading order, including table cells in row/cell order; not screen order.
+ std::vector<LayoutGlyph> glyphs {};
+ std::size_t rows { 1 };
+ std::int64_t extent { 0 };
+};
+
+// A derived snapshot for one Document + initial control values + width/scrolls.
+// Editing will supply a separate draft snapshot; this stage uses revision zero.
+// No ncurses headers, terminal objects or business-module dependencies.
+class DocumentLayout
+{
+public:
+ DocumentLayout( const Document& document,
+ const TextService& text,
+ int columns,
+ std::span<const HorizontalScroll> scrolls );
+
+ int columns() const;
+ std::size_t rows() const;
+ std::optional<LayoutPoint> locate( const DisplayPosition& position ) const;
+ std::optional<LayoutHit> hit( LayoutPoint point ) const;
+ DisplayPosition first() const;
+ DisplayPosition last() const;
+ DisplayPosition adjacent( const DisplayPosition& position, int direction ) const;
+ DisplayPosition word( const DisplayPosition& position, WordMotion motion ) const;
+ DisplayPosition on_row( LayoutPoint point ) const;
+ DisplayPosition line_edge( const DisplayPosition& position, bool end ) const;
+ DocumentRange unit( const DisplayPosition& position ) const;
+ std::optional<HitTarget> target( const DisplayPosition& position ) const;
+ std::optional<FocusStep> focus( const DisplayPosition& position, bool backward ) const;
+ std::optional<LayoutPoint> cursor_cell( const DisplayPosition& position ) const;
+ std::vector<HorizontalScroll> reveal( const DisplayPosition& position ) const;
+ std::vector<HorizontalScroll> pan( const DisplayPosition& position, int direction ) const;
+ void append_frame( Frame& frame,
+ std::size_t first_row,
+ CellPoint origin,
+ ScreenSize viewport,
+ const DocumentText& content,
+ std::optional<DocumentRange> selection,
+ const DisplayPosition& cursor,
+ std::optional<HitTarget> focus = {} ) const;
+
+private:
+ struct ReadingPosition
+ {
+ std::size_t index { 0 };
+ bool at_start { true };
+ };
+ ReadingPosition reading_position( const DisplayPosition& position ) const;
+ std::size_t nearest_stop( LayoutPoint point ) const;
+ const LayoutGlyph& cursor_glyph( const DisplayPosition& position ) const;
+ DisplayPosition stop( std::size_t index ) const;
+
+ struct FocusEntry
+ {
+ HitTarget target {};
+ std::size_t stop { 0 };
+ };
+
+ int columns_;
+ LayoutData data_;
+ std::vector<std::size_t> stops_ {};
+ // First reading stop per enabled object, independent of viewport clipping.
+ std::vector<FocusEntry> focus_ {};
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/mailbox.cc b/ricochet/src/tui/mailbox.cc
new file mode 100644
index 0000000..89e2c19
--- /dev/null
+++ b/ricochet/src/tui/mailbox.cc
@@ -0,0 +1,93 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "mailbox.hh"
+
+#include <algorithm>
+#include <cerrno>
+#include <sys/eventfd.h>
+#include <system_error>
+#include <unistd.h>
+#include <utility>
+
+namespace ricochet::tui::detail {
+
+Mailbox::Mailbox() : event_fd_( eventfd( 0, EFD_CLOEXEC | EFD_NONBLOCK ) )
+{
+ if ( event_fd_ == -1 ) {
+ throw std::system_error( errno, std::generic_category(), "eventfd" );
+ }
+}
+
+Mailbox::~Mailbox()
+{ ::close( event_fd_ ); }
+
+PostResult Mailbox::post( HostUpdate update )
+{
+ const std::lock_guard lock( mutex_ );
+ // Closing rejects later producers while existing sender handles remain alive.
+ if ( closed_ ) {
+ return PostResult::Closed;
+ }
+ updates_.push_back( std::move( update ) );
+ int result;
+ do {
+ result = eventfd_write( event_fd_, 1 );
+ } while ( result == -1 && errno == EINTR );
+ // EAGAIN means the counter is already readable, so the loop is already awake.
+ if ( result == -1 && errno != EAGAIN ) {
+ const int error = errno;
+ updates_.pop_back();
+ throw std::system_error( error, std::generic_category(), "eventfd_write" );
+ }
+ return PostResult::Queued;
+}
+
+std::vector<HostUpdate> Mailbox::take( std::size_t limit )
+{
+ const std::lock_guard lock( mutex_ );
+ eventfd_t counter {};
+ int result;
+ do {
+ result = eventfd_read( event_fd_, &counter );
+ } while ( result == -1 && errno == EINTR );
+ // An empty queue has no wake token. Other read errors are transport failures.
+ if ( result == -1 && errno != EAGAIN ) {
+ throw std::system_error( errno, std::generic_category(), "eventfd_read" );
+ }
+ const auto count = std::min( limit, updates_.size() );
+ std::vector<HostUpdate> batch;
+ batch.reserve( count );
+ for ( std::size_t i = 0; i < count; ++i ) {
+ batch.push_back( std::move( updates_.front() ) );
+ updates_.pop_front();
+ }
+ return batch;
+}
+
+int Mailbox::descriptor() const noexcept
+{ return event_fd_; }
+
+void Mailbox::close() noexcept
+{
+ const std::lock_guard lock( mutex_ );
+ closed_ = true;
+ updates_.clear();
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/mailbox.hh b/ricochet/src/tui/mailbox.hh
new file mode 100644
index 0000000..099c5aa
--- /dev/null
+++ b/ricochet/src/tui/mailbox.hh
@@ -0,0 +1,50 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/session.hh"
+
+#include <cstddef>
+#include <deque>
+#include <mutex>
+#include <vector>
+
+namespace ricochet::tui::detail {
+
+class Mailbox
+{
+public:
+ Mailbox();
+ ~Mailbox();
+ Mailbox( const Mailbox& ) = delete;
+ Mailbox& operator=( const Mailbox& ) = delete;
+
+ PostResult post( HostUpdate update );
+ std::vector<HostUpdate> take( std::size_t limit );
+ int descriptor() const noexcept;
+ void close() noexcept;
+
+private:
+ int event_fd_;
+ std::mutex mutex_ {};
+ std::deque<HostUpdate> updates_ {};
+ bool closed_ { false };
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/paint.hh b/ricochet/src/tui/paint.hh
new file mode 100644
index 0000000..43bdf89
--- /dev/null
+++ b/ricochet/src/tui/paint.hh
@@ -0,0 +1,48 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/document.hh"
+#include "ricochet/tui/input.hh"
+
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace ricochet::tui::detail {
+
+// Layout supplies one printable spacing character and its combining marks per
+// glyph, with a measured width. Raw document text never goes directly to curses.
+struct PaintGlyph
+{
+ CellPoint position {};
+ int width { 1 };
+ std::wstring text {};
+ TextStyle style {};
+ bool selected { false };
+ bool cursor { false };
+};
+
+struct Frame
+{
+ std::vector<PaintGlyph> glyphs {};
+ std::optional<CellPoint> cursor {};
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/screen.cc b/ricochet/src/tui/screen.cc
new file mode 100644
index 0000000..2bb9c85
--- /dev/null
+++ b/ricochet/src/tui/screen.cc
@@ -0,0 +1,601 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "screen.hh"
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cerrno>
+#include <climits>
+#include <csignal>
+#include <curses.h>
+#include <cwchar>
+#include <langinfo.h>
+#include <locale.h>
+#include <map>
+#include <mutex>
+#include <poll.h>
+#include <stdexcept>
+#include <string_view>
+#include <system_error>
+#include <type_traits>
+#include <unistd.h>
+#include <utility>
+
+namespace ricochet::tui::detail {
+
+namespace {
+
+using Clock = std::chrono::steady_clock;
+using namespace std::chrono_literals;
+
+std::mutex terminal_mutex;
+// atomic_flag is guaranteed lock-free. Clear means pending; test_and_set consumes
+// the signal without a read/reset race, including delivery on a worker thread.
+std::atomic_flag interrupt_clear = ATOMIC_FLAG_INIT;
+std::atomic_flag termination_clear = ATOMIC_FLAG_INIT;
+
+extern "C" void record_signal( int signal )
+{
+ switch ( signal ) {
+ case SIGINT:
+ interrupt_clear.clear( std::memory_order_relaxed );
+ break;
+ case SIGTERM:
+ case SIGHUP:
+ termination_clear.clear( std::memory_order_relaxed );
+ break;
+ default:
+ break;
+ }
+}
+
+std::unique_lock<std::mutex> acquire_terminal()
+{
+ std::unique_lock lock( terminal_mutex, std::try_to_lock );
+ // The non-reentrant ncurses build has process-global current-screen state.
+ if ( !lock.owns_lock() ) {
+ throw std::runtime_error( "A terminal session is already active" );
+ }
+ return lock;
+}
+
+class ThreadLocale
+{
+public:
+ ThreadLocale() : locale_( newlocale( LC_CTYPE_MASK, "", nullptr ), freelocale ), previous_( nullptr )
+ {
+ if ( !locale_ ) {
+ throw std::system_error( errno, std::generic_category(), "newlocale" );
+ }
+ previous_ = uselocale( locale_.get() );
+ if ( previous_ == nullptr ) {
+ throw std::system_error( errno, std::generic_category(), "uselocale" );
+ }
+ }
+
+ ~ThreadLocale() { uselocale( previous_ ); }
+ ThreadLocale( const ThreadLocale& ) = delete;
+ ThreadLocale& operator=( const ThreadLocale& ) = delete;
+
+private:
+ std::unique_ptr<std::remove_pointer_t<locale_t>, decltype( &freelocale )> locale_;
+ locale_t previous_;
+};
+
+class Signals
+{
+public:
+ Signals()
+ {
+ for ( std::size_t i = 0; i < numbers_.size(); ++i ) {
+ if ( sigaction( numbers_[i], nullptr, &previous_[i] ) == -1 ) {
+ throw std::system_error( errno, std::generic_category(), "Reading signal disposition" );
+ }
+ }
+ }
+
+ ~Signals()
+ {
+ for ( std::size_t i = 0; i < numbers_.size(); ++i ) {
+ sigaction( numbers_[i], &previous_[i], nullptr );
+ }
+ }
+
+ Signals( const Signals& ) = delete;
+ Signals& operator=( const Signals& ) = delete;
+
+ void prepare()
+ {
+ interrupt_clear.test_and_set( std::memory_order_relaxed );
+ termination_clear.test_and_set( std::memory_order_relaxed );
+ struct sigaction action {};
+ sigemptyset( &action.sa_mask );
+ action.sa_handler = SIG_DFL;
+ // newterm owns SIGWINCH during the session; restore the host's handler later.
+ if ( sigaction( SIGWINCH, &action, nullptr ) == -1 ) {
+ throw std::system_error( errno, std::generic_category(), "Preparing resize signal disposition" );
+ }
+ }
+
+ void install()
+ {
+ for ( const auto number : numbers_ ) {
+ if ( number == SIGWINCH ) {
+ continue; // Keep the resize handler installed by newterm.
+ }
+ struct sigaction action {};
+ sigemptyset( &action.sa_mask );
+ switch ( number ) {
+ case SIGTSTP:
+ case SIGPIPE:
+ // No suspend support; output errors must reach the normal cleanup path.
+ action.sa_handler = SIG_IGN;
+ break;
+ default:
+ action.sa_handler = record_signal;
+ break;
+ }
+ if ( sigaction( number, &action, nullptr ) == -1 ) {
+ throw std::system_error( errno, std::generic_category(), "Installing signal disposition" );
+ }
+ }
+ }
+
+private:
+ static constexpr std::array numbers_ { SIGINT, SIGTERM, SIGHUP, SIGTSTP, SIGWINCH, SIGPIPE };
+ std::array<struct sigaction, numbers_.size()> previous_ {};
+};
+
+struct ScreenDeleter
+{
+ std::FILE* output;
+ bool paste_enabled { false };
+
+ void operator()( SCREEN* screen ) const noexcept
+ {
+ // Only undo the extension this session enabled. The rest belongs to ncurses.
+ if ( paste_enabled ) {
+ std::fputs( "\x1b[?2004l", output );
+ std::fflush( output );
+ }
+ mousemask( 0, nullptr );
+ endwin();
+ delscreen( screen );
+ }
+};
+
+Key character_key( char32_t character, Modifiers modifiers )
+{ return { .code = KeyCode::Character, .modifiers = modifiers, .character = character }; }
+
+InputEvent character_event( char32_t character )
+{
+ // These aliases are indistinguishable in the traditional terminal encoding.
+ switch ( character ) {
+ case U'\r':
+ case U'\n':
+ return Key { .code = KeyCode::Enter };
+ case U'\t':
+ return Key { .code = KeyCode::Tab };
+ case U'\b':
+ case 127:
+ return Key { .code = KeyCode::Backspace };
+ case 27:
+ return Key { .code = KeyCode::Escape };
+ case 0:
+ return character_key( U' ', { .control = true } );
+ case 28:
+ return character_key( U'\\', { .control = true } );
+ case 29:
+ return character_key( U']', { .control = true } );
+ case 30:
+ return character_key( U'^', { .control = true } );
+ case 31:
+ return character_key( U'_', { .control = true } );
+ default:
+ break;
+ }
+ // The remaining C0 letters encode Ctrl+A through Ctrl+Z.
+ if ( character >= 1 && character <= 26 ) {
+ return character_key( U'a' + character - 1, { .control = true } );
+ }
+ return TextInput { character };
+}
+
+Mouse mouse_event( const MEVENT& mouse )
+{
+ return { .position = { mouse.y, mouse.x },
+ .modifiers = { ( mouse.bstate & BUTTON_SHIFT ) != 0,
+ ( mouse.bstate & BUTTON_CTRL ) != 0,
+ ( mouse.bstate & BUTTON_ALT ) != 0 },
+ .pressed = { ( mouse.bstate & BUTTON1_PRESSED ) != 0,
+ ( mouse.bstate & BUTTON2_PRESSED ) != 0,
+ ( mouse.bstate & BUTTON3_PRESSED ) != 0 },
+ .released = { ( mouse.bstate & BUTTON1_RELEASED ) != 0,
+ ( mouse.bstate & BUTTON2_RELEASED ) != 0,
+ ( mouse.bstate & BUTTON3_RELEASED ) != 0 },
+ .motion = ( mouse.bstate & REPORT_MOUSE_POSITION ) != 0,
+ .wheel_steps = static_cast<int>( ( mouse.bstate & BUTTON5_PRESSED ) != 0 )
+ - static_cast<int>( ( mouse.bstate & BUTTON4_PRESSED ) != 0 ) };
+}
+
+} // namespace
+
+class Screen::Impl
+{
+public:
+ Impl( std::FILE& input, std::FILE& output, TerminalOptions options )
+ : lock_( acquire_terminal() )
+ , locale_()
+ , signals_()
+ , screen_( nullptr, ScreenDeleter { &output } )
+ , input_fd_( fileno( &input ) )
+ , options_( options )
+ {
+ // This backend requires a terminal, not a pipe with EOF-looking input.
+ if ( isatty( input_fd_ ) != 1 || isatty( fileno( &output ) ) != 1 ) {
+ throw std::runtime_error( "The TUI requires terminal input and output" );
+ }
+ if ( std::string_view( nl_langinfo( CODESET ) ) != "UTF-8" ) {
+ throw std::runtime_error( "The TUI requires a UTF-8 locale" );
+ }
+ signals_.prepare();
+ use_env( false );
+ use_tioctl( true );
+ screen_.reset( newterm( nullptr, &output, &input ) );
+ // Unlike initscr, newterm reports failure without exiting the host process.
+ if ( !screen_ ) {
+ throw std::runtime_error( "ncurses newterm failed" );
+ }
+ // ncurses documents application signal handlers being installed after newterm.
+ signals_.install();
+ if ( raw() == ERR || noecho() == ERR || nonl() == ERR || meta( stdscr, true ) == ERR
+ || keypad( stdscr, true ) == ERR ) {
+ throw std::runtime_error( "ncurses input-mode setup failed" );
+ }
+ const auto delay = std::min<std::chrono::milliseconds::rep>( options.escape_delay.count(), INT_MAX );
+ if ( set_escdelay( static_cast<int>( delay ) ) == ERR ) {
+ throw std::runtime_error( "ncurses rejected the escape delay" );
+ }
+ options_.escape_delay = std::chrono::milliseconds( delay );
+ wtimeout( stdscr, 0 );
+ curs_set( 0 ); // ERR means cursor visibility cannot be changed on this terminal.
+ mouseinterval( 0 ); // Deliver press/release immediately; no click aggregation delay.
+ capabilities_.colors = has_colors() && start_color() != ERR;
+ if ( options.capture_mouse ) {
+ const auto mask = mousemask( ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr );
+ capabilities_.mouse_buttons = ( mask & BUTTON1_PRESSED ) != 0;
+ capabilities_.mouse_motion = ( mask & REPORT_MOUSE_POSITION ) != 0;
+ }
+ install_keys();
+ if ( options.bracketed_paste ) {
+ if ( define_key( "\x1b[200~", paste_key_ ) == ERR ) {
+ throw std::runtime_error( "ncurses could not register the paste delimiter" );
+ }
+ // Mark before writing so partial initialization also disables the extension.
+ screen_.get_deleter().paste_enabled = true;
+ if ( std::fputs( "\x1b[?2004h", &output ) == EOF || std::fflush( &output ) == EOF ) {
+ throw std::system_error( errno, std::generic_category(), "Enabling bracketed paste" );
+ }
+ capabilities_.bracketed_paste = true;
+ }
+ }
+
+ ScreenSize size() const { return { getmaxy( stdscr ), getmaxx( stdscr ) }; }
+
+ void install_keys()
+ {
+ keys_ = { { KEY_ENTER, { .code = KeyCode::Enter } },
+ { KEY_BACKSPACE, { .code = KeyCode::Backspace } },
+ { KEY_BTAB, { .code = KeyCode::Tab, .modifiers = { .shift = true } } },
+ { KEY_LEFT, { .code = KeyCode::Left } },
+ { KEY_RIGHT, { .code = KeyCode::Right } },
+ { KEY_UP, { .code = KeyCode::Up } },
+ { KEY_DOWN, { .code = KeyCode::Down } },
+ { KEY_HOME, { .code = KeyCode::Home } },
+ { KEY_END, { .code = KeyCode::End } },
+ { KEY_PPAGE, { .code = KeyCode::PageUp } },
+ { KEY_NPAGE, { .code = KeyCode::PageDown } },
+ { KEY_IC, { .code = KeyCode::Insert } },
+ { KEY_DC, { .code = KeyCode::Delete } },
+ { KEY_SLEFT, { .code = KeyCode::Left, .modifiers = { .shift = true } } },
+ { KEY_SRIGHT, { .code = KeyCode::Right, .modifiers = { .shift = true } } },
+ { KEY_SR, { .code = KeyCode::Up, .modifiers = { .shift = true } } },
+ { KEY_SF, { .code = KeyCode::Down, .modifiers = { .shift = true } } },
+ { KEY_SHOME, { .code = KeyCode::Home, .modifiers = { .shift = true } } },
+ { KEY_SEND, { .code = KeyCode::End, .modifiers = { .shift = true } } } };
+ for ( unsigned int n = 1; n <= 63; ++n ) {
+ keys_.emplace( KEY_F( n ), Key { .code = KeyCode::Function, .function = n } );
+ }
+ const std::array extended { std::pair { "kLFT3", Key { .code = KeyCode::Left, .modifiers = { .alt = true } } },
+ std::pair { "kRIT3", Key { .code = KeyCode::Right, .modifiers = { .alt = true } } },
+ std::pair { "kUP3", Key { .code = KeyCode::Up, .modifiers = { .alt = true } } },
+ std::pair { "kDN3", Key { .code = KeyCode::Down, .modifiers = { .alt = true } } } };
+ int code = paste_key_ + 1;
+ for ( const auto& [name, key] : extended ) {
+ const char* sequence = tigetstr( name );
+ // tigetstr distinguishes absent capabilities from a non-string capability.
+ if ( sequence == nullptr || sequence == reinterpret_cast<char*>( -1 ) ) {
+ continue;
+ }
+ if ( define_key( sequence, code ) == ERR ) {
+ throw std::runtime_error( "ncurses could not register an extended key" );
+ }
+ keys_.emplace( code++, key );
+ }
+ }
+
+ void append_event( InputEvent event, std::vector<InputEvent>& events )
+ {
+ // Decoding a function key can itself consume the remaining Escape deadline.
+ if ( escape_deadline_ && Clock::now() >= *escape_deadline_ ) {
+ events.emplace_back( Key { .code = KeyCode::Escape } );
+ escape_deadline_.reset();
+ }
+ // An Escape prefix combines only with a key arriving inside its deadline.
+ if ( escape_deadline_ ) {
+ if ( auto* text = std::get_if<TextInput>( &event ) ) {
+ event = character_key( text->character, { .alt = true } );
+ } else if ( auto* key = std::get_if<Key>( &event ); key != nullptr && key->code != KeyCode::Escape ) {
+ key->modifiers.alt = true;
+ } else {
+ events.emplace_back( Key { .code = KeyCode::Escape } );
+ }
+ escape_deadline_.reset();
+ }
+ if ( const auto* key = std::get_if<Key>( &event ); key != nullptr && key->code == KeyCode::Escape ) {
+ escape_deadline_ = Clock::now() + options_.escape_delay;
+ return;
+ }
+ events.push_back( std::move( event ) );
+ }
+
+ void append_character( char32_t character, std::vector<InputEvent>& events )
+ {
+ if ( paste_ ) {
+ paste_->push_back( character );
+ constexpr std::u32string_view end = U"\x1b[201~";
+ if ( paste_->ends_with( end ) ) {
+ paste_->resize( paste_->size() - end.size() );
+ events.emplace_back( Paste { std::move( *paste_ ) } );
+ paste_.reset();
+ if ( keypad( stdscr, true ) == ERR ) {
+ throw std::runtime_error( "ncurses could not leave paste input mode" );
+ }
+ }
+ } else {
+ append_event( character_event( character ), events );
+ }
+ }
+
+ void decode_byte( unsigned char byte, std::vector<InputEvent>& events )
+ {
+ // wget_wch keeps its conversion buffer local to one call; nodelay can split a
+ // character across calls. Retain the standard conversion state across steps.
+ // Evidence: ncurses/widechar/lib_get_wch.c, wget_wch's local count/buffer.
+ const bool had_partial = std::mbsinit( &text_state_ ) == 0;
+ const char input = static_cast<char>( byte );
+ wchar_t character {};
+ const auto converted = std::mbrtowc( &character, &input, 1, &text_state_ );
+ if ( converted == static_cast<std::size_t>( -2 ) ) {
+ return; // The standard library needs continuation bytes, possibly next step.
+ }
+ if ( converted == static_cast<std::size_t>( -1 ) ) {
+ text_state_ = {};
+ append_character( U'\uFFFD', events );
+ // Retry the offending byte as a new sequence so an ASCII character following
+ // an incomplete sequence is not lost. Reset state bounds this to one retry.
+ if ( had_partial ) {
+ decode_byte( byte, events );
+ }
+ return;
+ }
+ append_character( static_cast<char32_t>( character ), events );
+ }
+
+ InputBatch read_events( std::size_t limit )
+ {
+ InputBatch batch;
+ if ( !termination_clear.test_and_set( std::memory_order_relaxed ) ) {
+ batch.events.emplace_back( TerminalClosed { TerminalCloseReason::TerminationSignal } );
+ return batch;
+ }
+ // Check disconnection even when continuous host updates prevent an idle wait.
+ if ( const auto closed = wait( -1, 0ms ) ) {
+ batch.events.emplace_back( *closed );
+ return batch;
+ }
+ if ( !interrupt_clear.test_and_set( std::memory_order_relaxed ) ) {
+ batch.events.emplace_back( character_key( U'c', { .control = true } ) );
+ escape_deadline_.reset();
+ }
+ if ( escape_deadline_ && Clock::now() >= *escape_deadline_ ) {
+ batch.events.emplace_back( Key { .code = KeyCode::Escape } );
+ escape_deadline_.reset();
+ }
+ for ( std::size_t n = 0; n < limit; ++n ) {
+ errno = 0;
+ const int value = wgetch( stdscr );
+ if ( value == ERR ) {
+ // nodelay has no byte available; signals may interrupt the ncurses reader.
+ if ( errno != 0 && errno != EAGAIN && errno != EINTR ) {
+ throw std::system_error( errno, std::generic_category(), "ncurses input" );
+ }
+ break;
+ }
+ batch.consumed = true;
+ // wgetch returns bytes below KEY_MIN and decoded function keys above it.
+ if ( value >= KEY_MIN ) {
+ // Resize is out-of-band and can occur between UTF-8 continuation bytes.
+ // Other function keys interrupt an unfinished text sequence.
+ if ( value != KEY_RESIZE && std::mbsinit( &text_state_ ) == 0 ) {
+ text_state_ = {};
+ append_character( U'\uFFFD', batch.events );
+ }
+ switch ( value ) {
+ case KEY_RESIZE:
+ batch.events.emplace_back( Resize { size() } );
+ break;
+ case KEY_MOUSE: {
+ MEVENT mouse {};
+ if ( getmouse( &mouse ) == ERR ) {
+ throw std::runtime_error( "ncurses mouse event could not be read" );
+ }
+ append_event( mouse_event( mouse ), batch.events );
+ break;
+ }
+ case paste_key_:
+ // A preceding standalone Escape cannot become a modifier on pasted text.
+ if ( escape_deadline_ ) {
+ batch.events.emplace_back( Key { .code = KeyCode::Escape } );
+ escape_deadline_.reset();
+ }
+ paste_.emplace();
+ // Preserve literal escape sequences inside pasted text. Only ncurses
+ // reads bytes; the backend recognizes the protocol's end delimiter.
+ if ( keypad( stdscr, false ) == ERR ) {
+ throw std::runtime_error( "ncurses could not enter paste input mode" );
+ }
+ break;
+ default: {
+ const auto found = keys_.find( static_cast<int>( value ) );
+ if ( found != keys_.end() ) {
+ append_event( found->second, batch.events );
+ } else {
+ append_event( Key { .code = KeyCode::Unsupported }, batch.events );
+ }
+ break;
+ }
+ }
+ } else {
+ decode_byte( static_cast<unsigned char>( value ), batch.events );
+ }
+ }
+ return batch;
+ }
+
+ std::optional<TerminalClosed> wait( int wake_fd, std::chrono::milliseconds max_wait ) const
+ {
+ // The signal may arrive on a producer thread. This bound also lets wgetch
+ // observe ncurses' SIGWINCH flag when no terminal bytes have arrived.
+ auto delay = std::clamp( max_wait, 0ms, 50ms );
+ if ( escape_deadline_ ) {
+ delay = std::min(
+ delay, std::max( 0ms, std::chrono::ceil<std::chrono::milliseconds>( *escape_deadline_ - Clock::now() ) ) );
+ }
+ std::array<pollfd, 2> descriptors { pollfd { input_fd_, POLLIN, 0 }, pollfd { wake_fd, POLLIN, 0 } };
+ const int result = poll( descriptors.data(), descriptors.size(), static_cast<int>( delay.count() ) );
+ if ( result == -1 && errno != EINTR ) {
+ throw std::system_error( errno, std::generic_category(), "poll" );
+ }
+ if ( ( descriptors[0].revents & ( POLLHUP | POLLERR | POLLNVAL ) ) != 0 ) {
+ return TerminalClosed { TerminalCloseReason::Disconnected };
+ }
+ if ( ( descriptors[1].revents & ( POLLHUP | POLLERR | POLLNVAL ) ) != 0 ) {
+ throw std::runtime_error( "The UI wake channel was disconnected" );
+ }
+ return std::nullopt;
+ }
+
+ void draw( const Frame& frame, bool redraw ) const
+ {
+ const auto dimensions = size();
+ if ( werase( stdscr ) == ERR ) {
+ throw std::runtime_error( "ncurses could not clear its window buffer" );
+ }
+ for ( const auto& glyph : frame.glyphs ) {
+ const auto [row, column] = glyph.position;
+ // A resize can clip a previously prepared frame. Never wrap clipped glyphs.
+ if ( row < 0 || row >= dimensions.rows || column < 0 || column >= dimensions.columns
+ || glyph.width > dimensions.columns - column ) {
+ continue;
+ }
+ // setcchar would silently truncate longer combining sequences.
+ if ( glyph.text.size() >= CCHARW_MAX ) {
+ throw std::runtime_error( "Glyph exceeds the ncurses complex-character capacity" );
+ }
+ const attr_t attributes
+ = ( glyph.style.strong ? A_BOLD : A_NORMAL ) | ( glyph.style.emphasis ? A_UNDERLINE : A_NORMAL )
+ | ( glyph.style.subdued ? A_DIM : A_NORMAL ) | ( glyph.style.warning ? A_REVERSE : A_NORMAL )
+ | ( glyph.selected ? A_REVERSE | A_UNDERLINE : A_NORMAL ) | ( glyph.cursor ? A_BOLD : A_NORMAL );
+ cchar_t cell {};
+ if ( setcchar( &cell, glyph.text.c_str(), attributes, 0, nullptr ) == ERR
+ || mvwadd_wchnstr( stdscr, row, column, &cell, 1 ) == ERR ) {
+ throw std::runtime_error( "ncurses could not paint a glyph" );
+ }
+ }
+ // add_wchnstr does not advance or wrap the cursor at the bottom-right cell.
+ if ( frame.cursor && frame.cursor->row >= 0 && frame.cursor->row < dimensions.rows && frame.cursor->column >= 0
+ && frame.cursor->column < dimensions.columns ) {
+ if ( wmove( stdscr, frame.cursor->row, frame.cursor->column ) == ERR ) {
+ throw std::runtime_error( "ncurses could not position the cursor" );
+ }
+ curs_set( 1 );
+ } else {
+ curs_set( 0 );
+ }
+ // An explicit Ctrl+L repairs the physical screen, not just our virtual frame.
+ // clearok is consumed by wrefresh; ordinary frames retain batched updates.
+ if ( redraw ) {
+ clearok( stdscr, TRUE );
+ }
+ if ( redraw ? wrefresh( stdscr ) == ERR : ( wnoutrefresh( stdscr ) == ERR || doupdate() == ERR ) ) {
+ throw std::runtime_error( "ncurses screen refresh failed" );
+ }
+ }
+
+ std::unique_lock<std::mutex> lock_;
+ ThreadLocale locale_;
+ Signals signals_;
+ std::unique_ptr<SCREEN, ScreenDeleter> screen_;
+ int input_fd_;
+ TerminalOptions options_;
+ TerminalCapabilities capabilities_ {};
+ std::map<int, Key> keys_ {};
+ std::optional<Clock::time_point> escape_deadline_ {};
+ std::optional<std::u32string> paste_ {};
+ std::mbstate_t text_state_ {};
+ static constexpr int paste_key_ = 0x10000;
+};
+
+Screen::Screen( std::FILE& input, std::FILE& output, TerminalOptions options )
+ : impl_( std::make_unique<Impl>( input, output, options ) )
+{}
+
+Screen::~Screen() = default;
+
+std::size_t Screen::glyph_capacity()
+{ return CCHARW_MAX - 1; }
+
+ScreenSize Screen::size() const
+{ return impl_->size(); }
+
+TerminalCapabilities Screen::capabilities() const
+{ return impl_->capabilities_; }
+
+InputBatch Screen::read_events( std::size_t limit )
+{ return impl_->read_events( limit ); }
+
+std::optional<TerminalClosed> Screen::wait( int wake_fd, std::chrono::milliseconds max_wait )
+{ return impl_->wait( wake_fd, max_wait ); }
+
+void Screen::draw( const Frame& frame, bool redraw )
+{ impl_->draw( frame, redraw ); }
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/screen.hh b/ricochet/src/tui/screen.hh
new file mode 100644
index 0000000..0ff1c58
--- /dev/null
+++ b/ricochet/src/tui/screen.hh
@@ -0,0 +1,60 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "paint.hh"
+#include "ricochet/tui/session.hh"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdio>
+#include <memory>
+#include <optional>
+#include <vector>
+
+namespace ricochet::tui::detail {
+
+struct InputBatch
+{
+ std::vector<InputEvent> events {};
+ bool consumed { false };
+};
+
+class Screen
+{
+public:
+ Screen( std::FILE& input, std::FILE& output, TerminalOptions options );
+ ~Screen();
+ Screen( const Screen& ) = delete;
+ Screen& operator=( const Screen& ) = delete;
+
+ ScreenSize size() const;
+ static std::size_t glyph_capacity();
+ TerminalCapabilities capabilities() const;
+ InputBatch read_events( std::size_t limit );
+ // Wait only polls descriptors; wgetch remains the sole terminal reader.
+ std::optional<TerminalClosed> wait( int wake_fd, std::chrono::milliseconds max_wait );
+ void draw( const Frame& frame, bool redraw = false );
+
+private:
+ class Impl;
+ std::unique_ptr<Impl> impl_;
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/session.cc b/ricochet/src/tui/session.cc
new file mode 100644
index 0000000..09af2ce
--- /dev/null
+++ b/ricochet/src/tui/session.cc
@@ -0,0 +1,1064 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ricochet/tui/session.hh"
+
+#include "document_view.hh"
+#include "editor.hh"
+#include "mailbox.hh"
+#include "screen.hh"
+
+#include <algorithm>
+#include <chrono>
+#include <exception>
+#include <map>
+#include <string_view>
+#include <utility>
+
+namespace ricochet::tui {
+
+namespace {
+
+using Clock = std::chrono::steady_clock;
+
+struct KeyPrefix
+{
+ char32_t character;
+ Clock::time_point deadline;
+};
+
+struct TabState
+{
+ TabInfo info {};
+ std::optional<RequestId> navigation {};
+ std::optional<detail::DocumentView> view {};
+ std::optional<ActionFailed> failure {};
+ std::optional<RequestId> copy_request {};
+ std::string copy_feedback {};
+};
+
+enum class AddressEntry : std::uint8_t
+{
+ Empty,
+ Current,
+ Selected
+};
+
+int address_label_columns( ScreenSize dimensions )
+{ return std::min( 5, std::max( 0, dimensions.columns - 1 ) ); }
+
+struct AddressInput
+{
+ TabId tab;
+ std::optional<ViewSnapshot> restore;
+ detail::TextEditor editor;
+ bool dragging { false };
+ std::wstring notice {};
+
+ AddressInput( TabId source,
+ std::optional<ViewSnapshot> snapshot,
+ std::string value,
+ const detail::TextService& text,
+ ScreenSize dimensions )
+ : tab( source )
+ , restore( std::move( snapshot ) )
+ , editor( std::move( value ), {}, text, { 1, dimensions.columns - address_label_columns( dimensions ) } )
+ {}
+};
+
+// Only fixed ASCII chrome uses this helper. Document text goes through the
+// Unicode text service and document layout.
+void append_chrome( detail::Frame& frame, CellPoint start, std::wstring_view text, TextStyle style = {} )
+{
+ int column = start.column;
+ for ( const wchar_t character : text ) {
+ frame.glyphs.push_back(
+ { .position = { start.row, column++ }, .text = std::wstring( 1, character ), .style = style } );
+ }
+}
+
+} // namespace
+
+class Session::Impl
+{
+public:
+ Impl( std::FILE& input, std::FILE& output, TerminalOptions options )
+ : mailbox_( std::make_shared<detail::Mailbox>() )
+ , screen_( std::make_unique<detail::Screen>( input, output, options ) )
+ , capabilities_( screen_->capabilities() )
+ {}
+
+ ~Impl() { close(); }
+
+ void close() noexcept
+ {
+ mailbox_->close();
+ address_.reset();
+ screen_.reset();
+ }
+
+ void request_exit()
+ {
+ // A held key or repeated SIGINT must not create concurrent exit requests.
+ if ( exit_request_ ) {
+ return;
+ }
+ prefix_.reset();
+ const RequestId request { next_request_++ };
+ std::vector<ViewSnapshot> views;
+ for ( const auto id : tab_order_ ) {
+ const auto& tab = tabs_.at( id );
+ if ( tab.view ) {
+ views.push_back( tab.view->snapshot() );
+ }
+ }
+ actions_.emplace_back( RequestExit { request, active_, std::move( views ) } );
+ exit_request_ = request;
+ dirty_ = true;
+ }
+
+ void apply( SyncTabs update, StepResult& )
+ {
+ // Cancel against the old tab map before a switch/removal destroys its view.
+ if ( address_
+ && ( update.active != address_->tab
+ || std::ranges::find( update.tabs, address_->tab, &TabInfo::id ) == update.tabs.end() ) ) {
+ finish_address();
+ }
+ if ( active_ != update.active ) {
+ stop_drag();
+ }
+ std::map<TabId, TabState> retained;
+ std::vector<TabId> order;
+ order.reserve( update.tabs.size() );
+ for ( auto& info : update.tabs ) {
+ order.push_back( info.id );
+ auto node = tabs_.extract( info.id );
+ if ( node.empty() ) {
+ const auto id = info.id;
+ retained.emplace( id, TabState { .info = std::move( info ) } );
+ } else {
+ node.mapped().info = std::move( info );
+ retained.insert( std::move( node ) );
+ }
+ }
+ tabs_ = std::move( retained );
+ tab_order_ = std::move( order );
+ if ( active_ != update.active ) {
+ prefix_.reset();
+ notice_.clear();
+ }
+ active_ = update.active;
+ dirty_ = true;
+ }
+
+ void apply( NavigationStarted update, StepResult& )
+ {
+ const auto found = tabs_.find( update.tab );
+ // Closed-tab results are stale; they must not recreate a tab.
+ if ( found == tabs_.end() ) {
+ return;
+ }
+ found->second.navigation = update.request;
+ found->second.info.loading_task = update.task;
+ found->second.failure.reset();
+ dirty_ = true;
+ }
+
+ void apply( CommitDocument update, StepResult& )
+ {
+ const auto found = tabs_.find( update.tab );
+ if ( found == tabs_.end() || found->second.navigation != update.navigation ) {
+ return;
+ }
+ auto& tab = found->second;
+ const auto version = update.document.data().version;
+ if ( tab.view && tab.view->document().data().version.document == version.document
+ && version.revision < tab.view->document().data().version.revision ) {
+ return;
+ }
+ const DocumentScope scope { update.tab, version };
+ ViewSnapshot snapshot { .scope = scope };
+ // Initial drafts are revision zero. A history view referring to edited values
+ // cannot be restored without the corresponding draft snapshot/mapping.
+ if ( update.restore && update.restore->scope == scope && update.restore->draft_revision == DraftRevision {} ) {
+ snapshot = std::move( *update.restore );
+ }
+ tab.info.title = update.document.data().metadata.title;
+ tab.info.display_url = update.document.data().metadata.display_url;
+ tab.info.loading_task.reset();
+ const auto dimensions = screen_->size();
+ tab.view.emplace( std::move( update.document ),
+ std::move( snapshot ),
+ text_,
+ ScreenSize { dimensions.rows - 2, dimensions.columns } );
+ if ( address_ && address_->tab == update.tab ) {
+ // Address editing survives a background commit, but cancellation must now
+ // return to the newly committed page, never restore the old view snapshot.
+ address_->restore.reset();
+ }
+ if ( active_ == update.tab ) {
+ prefix_.reset();
+ notice_.clear();
+ }
+ tab.failure.reset();
+ tab.copy_request.reset();
+ tab.copy_feedback.clear();
+ dirty_ = true;
+ }
+
+ void apply( CopyResult update, StepResult& )
+ {
+ const auto found = std::ranges::find_if(
+ tabs_, [&update]( const auto& entry ) { return entry.second.copy_request == update.request; } );
+ // Superseded requests, replaced documents, closed tabs and duplicate results
+ // have no pending request; they cannot replace the current operation's feedback.
+ if ( found == tabs_.end() ) {
+ return;
+ }
+ auto& tab = found->second;
+ tab.copy_request.reset();
+ switch ( update.status ) {
+ case CopyStatus::Confirmed:
+ tab.copy_feedback = "Copied";
+ break;
+ case CopyStatus::Dispatched:
+ tab.copy_feedback = "Copy request sent; clipboard acceptance unconfirmed";
+ break;
+ case CopyStatus::Unavailable:
+ tab.copy_feedback = "Clipboard unavailable; disable mouse capture for terminal selection";
+ break;
+ case CopyStatus::Failed:
+ tab.copy_feedback = "Copy failed | y retry";
+ break;
+ }
+ if ( !update.summary.empty() ) {
+ tab.copy_feedback += ": " + update.summary;
+ }
+ dirty_ = true;
+ }
+
+ void apply( ActionFailed update, StepResult& result )
+ {
+ const auto copy = std::ranges::find_if(
+ tabs_, [&update]( const auto& entry ) { return entry.second.copy_request == update.request; } );
+ if ( copy != tabs_.end() ) {
+ apply( CopyResult { update.request, CopyStatus::Failed, std::move( update.summary ) }, result );
+ return;
+ }
+ if ( !update.tab && exit_request_ == update.request ) {
+ exit_request_.reset();
+ dirty_ = true;
+ return;
+ }
+ if ( !update.tab ) {
+ return;
+ }
+ const auto found = tabs_.find( *update.tab );
+ if ( found == tabs_.end() || found->second.navigation != update.request ) {
+ return;
+ }
+ found->second.info.loading_task.reset();
+ found->second.failure = std::move( update );
+ dirty_ = true;
+ }
+
+ void apply( ExitDecision update, StepResult& )
+ {
+ if ( exit_request_ != update.request ) {
+ return;
+ }
+ exit_request_.reset();
+ if ( update.approved ) {
+ close();
+ }
+ dirty_ = true;
+ }
+
+ // Incremental updates and form editing still require the later editing layer.
+ void apply( UpdateDocument update, StepResult& result )
+ { result.unhandled_updates.emplace_back( std::move( update ) ); }
+
+ void apply( LocateView update, StepResult& )
+ {
+ const auto found = tabs_.find( update.scope.tab );
+ // A late anchor request must never move another tab/document revision.
+ if ( found == tabs_.end() || !found->second.view || found->second.view->snapshot().scope != update.scope ) {
+ return;
+ }
+ auto& view = *found->second.view;
+ const auto dimensions = screen_->size();
+ view.resize( { dimensions.rows - 2, dimensions.columns } );
+ std::optional<DocumentPoint> point;
+ if ( const auto* name = std::get_if<std::string>( &update.target ) ) {
+ const auto& anchors = view.document().data().anchors;
+ const auto anchor = std::ranges::find( anchors, *name, &Anchor::name );
+ if ( anchor != anchors.end() ) {
+ point = std::visit( []( const auto& value ) -> DocumentPoint { return value; }, anchor->point );
+ }
+ } else {
+ point = std::get<DocumentPoint>( update.target );
+ }
+ const bool located = point && view.locate( *point );
+ if ( active_ == update.scope.tab ) {
+ prefix_.reset();
+ notice_ = located ? L"" : L"Location not found";
+ }
+ dirty_ = true;
+ }
+
+ void apply( FormFeedback update, StepResult& result )
+ { result.unhandled_updates.emplace_back( std::move( update ) ); }
+
+ std::size_t receive( StepResult& result )
+ {
+ // A finite batch prevents continuous producers from starving terminal input.
+ auto updates = mailbox_->take( 64 );
+ const auto count = updates.size();
+ for ( auto& update : updates ) {
+ std::visit( [this, &result]( auto message ) { apply( std::move( message ), result ); }, std::move( update ) );
+ if ( !screen_ ) {
+ break; // An approved exit ends this batch before any further UI work.
+ }
+ }
+ return count;
+ }
+
+ detail::DocumentView* active_view()
+ {
+ if ( !active_ ) {
+ return nullptr;
+ }
+ const auto found = tabs_.find( *active_ );
+ const auto dimensions = screen_->size();
+ // Selection/copy/cancel still address the active document in compact mode.
+ if ( found == tabs_.end() || !found->second.view ) {
+ return nullptr;
+ }
+ auto& view = *found->second.view;
+ view.resize( { dimensions.rows - 2, dimensions.columns } );
+ return &view;
+ }
+
+ void move( detail::ViewMotion motion )
+ {
+ const auto dimensions = screen_->size();
+ if ( dimensions.rows < 3 || dimensions.columns < 1 ) {
+ return; // No body rows exist for a visual-row/page motion.
+ }
+ if ( auto* view = active_view() ) {
+ view->move( motion );
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ notice_.clear();
+ dirty_ = true;
+ }
+ }
+
+ void stop_drag()
+ {
+ if ( auto* view = active_view() ) {
+ view->stop_drag();
+ }
+ }
+
+ void focus( bool backward )
+ {
+ const auto dimensions = screen_->size();
+ if ( dimensions.rows < 3 || dimensions.columns < 1 ) {
+ return; // Object positioning requires a visible document region.
+ }
+ auto* view = active_view();
+ if ( !view || view->snapshot().selection ) {
+ return; // Tab must not move the cursor or extend/cancel an active selection.
+ }
+ const auto result = view->focus( backward );
+ switch ( result ) {
+ case detail::FocusResult::Empty:
+ notice_ = L"No focusable links or controls";
+ break;
+ case detail::FocusResult::Moved:
+ notice_.clear();
+ break;
+ case detail::FocusResult::Wrapped:
+ notice_ = backward ? L"Focus wrapped to last object" : L"Focus wrapped to first object";
+ break;
+ }
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ dirty_ = true;
+ }
+
+ void activate_link()
+ {
+ auto* view = active_view();
+ if ( !view || view->snapshot().selection ) {
+ return; // Selection mode consumes activation without navigating.
+ }
+ const auto* link = view->focused_link();
+ if ( !link ) {
+ return; // Plain text and controls do not produce link navigation requests.
+ }
+ const RequestId request { next_request_++ };
+ const auto source = view->snapshot().scope;
+ actions_.emplace_back( OpenLink { request, source, link->id, link->target, OpenTarget::CurrentTab } );
+ auto& tab = tabs_.at( source.tab );
+ // Correlate rejection even when the host fails before NavigationStarted.
+ tab.navigation = request;
+ tab.failure.reset();
+ tab.copy_feedback.clear();
+ notice_.clear();
+ dirty_ = true;
+ }
+
+ void copy_selection()
+ {
+ auto* view = active_view();
+ if ( !view ) {
+ return;
+ }
+ auto selected = view->selected_text();
+ if ( !selected || selected->empty() ) {
+ notice_ = selected ? L"Selection is empty" : L"No selection | v select";
+ dirty_ = true;
+ return;
+ }
+ const RequestId request { next_request_++ };
+ const auto& snapshot = view->snapshot();
+ actions_.emplace_back(
+ CopySelection { request, snapshot.scope, snapshot.draft_revision, std::move( *selected ) } );
+ auto& tab = tabs_.at( snapshot.scope.tab );
+ tab.copy_request = request;
+ tab.copy_feedback = "Copy pending";
+ notice_.clear();
+ dirty_ = true;
+ }
+
+ void open_address( AddressEntry entry )
+ {
+ const auto found = active_ ? tabs_.find( *active_ ) : tabs_.end();
+ if ( found == tabs_.end() ) {
+ notice_ = L"No active tab";
+ dirty_ = true;
+ return;
+ }
+ auto& tab = found->second;
+ if ( tab.view && tab.view->snapshot().selection ) {
+ return; // Address shortcuts belong to Browse, not the document selection mode.
+ }
+ stop_drag();
+ const auto snapshot = tab.view ? std::optional( tab.view->snapshot() ) : std::nullopt;
+ address_.emplace(
+ tab.info.id, snapshot, entry == AddressEntry::Empty ? "" : tab.info.display_url, text_, screen_->size() );
+ if ( entry == AddressEntry::Selected ) {
+ address_->editor.execute( detail::EditorCommand::SelectAll );
+ }
+ prefix_.reset();
+ notice_.clear();
+ dirty_ = true;
+ }
+
+ void finish_address()
+ {
+ auto& tab = tabs_.at( address_->tab );
+ const auto& restore = address_->restore;
+ if ( restore && tab.view && tab.view->snapshot().scope == restore->scope
+ && tab.view->snapshot().draft_revision == restore->draft_revision ) {
+ // Copy the shared document before emplace destroys the current view.
+ auto document = tab.view->document();
+ const auto dimensions = screen_->size();
+ tab.view.emplace(
+ std::move( document ), *restore, text_, ScreenSize { dimensions.rows - 2, dimensions.columns } );
+ }
+ address_.reset();
+ notice_.clear();
+ dirty_ = true;
+ }
+
+ void submit_address()
+ {
+ const auto& value = address_->editor.value();
+ if ( text_.analyze( value, false, false ).whitespace_only ) {
+ address_->notice = L"Enter an address | Esc cancel";
+ dirty_ = true;
+ return;
+ }
+ const RequestId request { next_request_++ };
+ actions_.emplace_back( OpenLocation { request, address_->tab, value, OpenTarget::CurrentTab } );
+ auto& tab = tabs_.at( address_->tab );
+ // A host rejection may precede NavigationStarted, just as for OpenLink.
+ tab.navigation = request;
+ tab.failure.reset();
+ tab.copy_feedback.clear();
+ finish_address();
+ }
+
+ void address_mouse( const Mouse& mouse )
+ {
+ const auto dimensions = screen_->size();
+ const auto label = address_label_columns( dimensions );
+ const CellPoint point { 0, mouse.position.column - label };
+ if ( address_->dragging ) {
+ if ( mouse.released.left || mouse.motion || mouse.pressed.left ) {
+ address_->editor.place( point, true );
+ address_->dragging = !mouse.released.left;
+ dirty_ = true;
+ }
+ return;
+ }
+ if ( mouse.pressed.left && mouse.position.row == 0 && mouse.position.column >= label
+ && mouse.position.column < dimensions.columns
+ && ( mouse.modifiers == Modifiers {} || mouse.modifiers == Modifiers { .shift = true } ) ) {
+ address_->editor.place( point, mouse.modifiers.shift );
+ address_->dragging = true;
+ dirty_ = true;
+ }
+ // Clicks and wheel events outside the input never reach the document underneath.
+ }
+
+ void address_input( const InputEvent& event )
+ {
+ if ( const auto* mouse = std::get_if<Mouse>( &event ) ) {
+ address_mouse( *mouse );
+ return;
+ }
+ const auto prefix = address_->editor.prefix_deadline();
+ const auto intent = address_->editor.handle( event );
+ if ( std::holds_alternative<Tick>( event ) ) {
+ dirty_ = dirty_ || prefix != address_->editor.prefix_deadline();
+ return;
+ }
+ address_->dragging = false;
+ address_->notice.clear();
+ dirty_ = true;
+ switch ( intent ) {
+ case detail::EditorIntent::Enter:
+ submit_address();
+ break;
+ case detail::EditorIntent::Escape:
+ case detail::EditorIntent::Cancel:
+ finish_address();
+ break;
+ case detail::EditorIntent::Redraw:
+ redraw_ = true;
+ break;
+ case detail::EditorIntent::Menu:
+ address_->notice = L"Menu unavailable | Enter open | Esc cancel";
+ break;
+ default:
+ break; // Tab/history/unbound keys have no second recipient in this mode.
+ }
+ }
+
+ void draw_address( detail::Frame& frame, ScreenSize dimensions )
+ {
+ const auto label = address_label_columns( dimensions );
+ append_chrome( frame, { 0, 0 }, std::wstring_view( L"URL: " ).substr( 0, label ), { .strong = true } );
+ address_->editor.resize( { 1, dimensions.columns - label } );
+ address_->editor.draw( frame, { 0, label } );
+ if ( dimensions.rows > 1 ) {
+ append_chrome( frame, { dimensions.rows - 1, 0 }, std::wstring( dimensions.columns, L' ' ) );
+ std::wstring_view status
+ = capabilities_.bracketed_paste
+ ? L"Address | Enter open | Esc cancel"
+ : L"Enter open | Esc cancel | Paste boundaries unavailable; multiline paste may confirm";
+ if ( !address_->notice.empty() ) {
+ status = address_->notice;
+ }
+ if ( address_->editor.prefix_deadline() ) {
+ status = L"Ctrl+X ... | Ctrl+U undo | Esc cancel prefix";
+ }
+ append_chrome( frame, { dimensions.rows - 1, 0 }, status );
+ }
+ }
+
+ void expire_prefix()
+ {
+ if ( prefix_ && Clock::now() >= prefix_->deadline ) {
+ prefix_.reset();
+ dirty_ = true;
+ }
+ }
+
+ void browse_text( char32_t character )
+ {
+ using Motion = detail::ViewMotion;
+ if ( prefix_ ) {
+ const auto prefix = std::exchange( prefix_, std::nullopt )->character;
+ dirty_ = true;
+ if ( prefix == U'g' && character == U'g' ) {
+ move( Motion::DocumentStart );
+ } else if ( prefix == U'z' && character == U'h' ) {
+ move( Motion::PanLeft );
+ } else if ( prefix == U'z' && character == U'l' ) {
+ move( Motion::PanRight );
+ }
+ // A mismatched sequence is consumed; gq must not turn into a quit action.
+ return;
+ }
+ switch ( character ) {
+ case U'o':
+ open_address( AddressEntry::Empty );
+ break;
+ case U'O':
+ open_address( AddressEntry::Current );
+ break;
+ case U'h':
+ move( Motion::Left );
+ break;
+ case U'j':
+ move( Motion::Down );
+ break;
+ case U'k':
+ move( Motion::Up );
+ break;
+ case U'l':
+ move( Motion::Right );
+ break;
+ case U'w':
+ move( Motion::NextWord );
+ break;
+ case U'b':
+ move( Motion::PreviousWord );
+ break;
+ case U'e':
+ move( Motion::WordEnd );
+ break;
+ case U'G':
+ move( Motion::DocumentEnd );
+ break;
+ case U'g':
+ case U'z':
+ prefix_ = KeyPrefix { character, Clock::now() + std::chrono::seconds( 1 ) };
+ dirty_ = true;
+ break;
+ case U'v':
+ if ( auto* view = active_view(); view && !view->snapshot().selection ) {
+ view->begin_selection();
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ notice_.clear();
+ dirty_ = true;
+ }
+ break;
+ case U'y':
+ copy_selection();
+ break;
+ case U' ': {
+ auto* view = active_view();
+ if ( view ) {
+ const auto target = view->focused_target();
+ // Selection owns Space; browse mode reserves control Space for activation.
+ if ( view->snapshot().selection || !target || !std::holds_alternative<ControlId>( *target ) ) {
+ move( Motion::PageDown );
+ }
+ }
+ break;
+ }
+ case U'q': {
+ const auto* view = active_view();
+ if ( !view || !view->snapshot().selection ) {
+ request_exit();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ void browse_key( const Key& key )
+ {
+ using Motion = detail::ViewMotion;
+ if ( key.code == KeyCode::Escape ) {
+ // A pending prefix is the top input layer; one Esc cancels only that layer.
+ if ( !prefix_ ) {
+ if ( auto* view = active_view() ) {
+ view->cancel_selection();
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ }
+ }
+ prefix_.reset();
+ notice_.clear();
+ dirty_ = true;
+ return;
+ }
+ if ( prefix_ ) {
+ // Ctrl+C cancels the prefix first; no key falls through into another action.
+ prefix_.reset();
+ dirty_ = true;
+ return;
+ }
+ if ( key.modifiers == Modifiers { .control = true } && key.code == KeyCode::Character ) {
+ switch ( key.character ) {
+ case U'l':
+ open_address( AddressEntry::Selected );
+ break;
+ case U'c':
+ if ( auto* view = active_view(); view && view->snapshot().selection ) {
+ view->cancel_selection();
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ notice_.clear();
+ dirty_ = true;
+ } else {
+ request_exit();
+ }
+ break;
+ case U'u':
+ move( Motion::HalfPageUp );
+ break;
+ case U'd':
+ move( Motion::HalfPageDown );
+ break;
+ case U'z':
+ notice_ = L"Suspend is unavailable";
+ dirty_ = true;
+ break;
+ default:
+ break;
+ }
+ return;
+ }
+ if ( key.modifiers == Modifiers { .alt = true }
+ && ( key.code == KeyCode::Left || key.code == KeyCode::Right ) ) {
+ const auto* view = active_view();
+ if ( view && view->snapshot().selection ) {
+ return; // Selection mode does not turn navigation into implicit activation.
+ }
+ if ( active_ ) {
+ const auto found = tabs_.find( *active_ );
+ if ( found != tabs_.end() ) {
+ std::optional<DocumentVersion> version;
+ if ( found->second.view ) {
+ version = found->second.view->document().data().version;
+ }
+ actions_.emplace_back(
+ Navigate { RequestId { next_request_++ },
+ *active_,
+ version,
+ key.code == KeyCode::Left ? NavigationCommand::Back : NavigationCommand::Forward } );
+ }
+ }
+ return;
+ }
+ if ( key.code == KeyCode::Tab
+ && ( key.modifiers == Modifiers {} || key.modifiers == Modifiers { .shift = true } ) ) {
+ focus( key.modifiers.shift );
+ return;
+ }
+ if ( key.modifiers != Modifiers {} ) {
+ return; // Modified keys have distinct bindings; do not alias them to arrows.
+ }
+ switch ( key.code ) {
+ case KeyCode::Function:
+ if ( key.function == 6 ) {
+ open_address( AddressEntry::Selected );
+ }
+ break;
+ case KeyCode::Enter:
+ activate_link();
+ break;
+ case KeyCode::Left:
+ move( Motion::Left );
+ break;
+ case KeyCode::Right:
+ move( Motion::Right );
+ break;
+ case KeyCode::Up:
+ move( Motion::Up );
+ break;
+ case KeyCode::Down:
+ move( Motion::Down );
+ break;
+ case KeyCode::Home:
+ move( Motion::LineStart );
+ break;
+ case KeyCode::End:
+ move( Motion::LineEnd );
+ break;
+ case KeyCode::PageUp:
+ move( Motion::PageUp );
+ break;
+ case KeyCode::PageDown:
+ move( Motion::PageDown );
+ break;
+ default:
+ break;
+ }
+ }
+
+ void browse_mouse( const Mouse& mouse )
+ {
+ auto* view = active_view();
+ if ( !view ) {
+ return;
+ }
+ const CellPoint point { mouse.position.row - 1, mouse.position.column };
+ // Capture owns motion/release outside the body as well, so releasing on the
+ // header/footer cannot leave a background autoscroll running.
+ if ( view->dragging() ) {
+ if ( mouse.released.left ) {
+ view->release( point );
+ } else if ( mouse.motion || mouse.pressed.left ) {
+ view->drag( point );
+ } else if ( mouse.wheel_steps != 0 ) {
+ view->wheel( mouse.wheel_steps );
+ }
+ dirty_ = true;
+ return;
+ }
+ const auto dimensions = screen_->size();
+ if ( point.row < 0 || point.row >= dimensions.rows - 2 || point.column < 0 || point.column >= dimensions.columns
+ || mouse.modifiers != Modifiers {} ) {
+ return;
+ }
+ if ( mouse.wheel_steps != 0 ) {
+ prefix_.reset();
+ view->wheel( mouse.wheel_steps );
+ dirty_ = true;
+ } else if ( mouse.pressed.left ) {
+ prefix_.reset();
+ view->press( point, Clock::now() );
+ tabs_.at( view->snapshot().scope.tab ).copy_feedback.clear();
+ notice_.clear();
+ dirty_ = true;
+ }
+ }
+
+ void dispatch( const InputEvent& event, StepResult& result )
+ {
+ if ( const auto* closed = std::get_if<TerminalClosed>( &event ) ) {
+ result.terminal_closed = *closed;
+ close();
+ } else if ( std::holds_alternative<Resize>( event ) ) {
+ stop_drag();
+ if ( address_ ) {
+ address_->dragging = false;
+ const auto dimensions = screen_->size();
+ address_->editor.resize( { 1, dimensions.columns - address_label_columns( dimensions ) } );
+ }
+ dirty_ = true;
+ } else if ( address_ ) {
+ address_input( event );
+ } else if ( const auto* tick = std::get_if<Tick>( &event ) ) {
+ if ( auto* view = active_view() ) {
+ dirty_ = view->tick( tick->now ) || dirty_;
+ }
+ } else if ( std::holds_alternative<Paste>( event ) ) {
+ stop_drag();
+ prefix_.reset();
+ notice_ = L"Enter an input field before pasting";
+ dirty_ = true;
+ } else if ( const auto* text = std::get_if<TextInput>( &event ) ) {
+ stop_drag();
+ browse_text( text->character );
+ } else if ( const auto* key = std::get_if<Key>( &event ) ) {
+ stop_drag();
+ browse_key( *key );
+ } else if ( const auto* mouse = std::get_if<Mouse>( &event ) ) {
+ browse_mouse( *mouse );
+ }
+ }
+
+ bool read_input( StepResult& result )
+ {
+ const auto batch = screen_->read_events( 64 );
+ for ( const auto& event : batch.events ) {
+ expire_prefix();
+ dispatch( event, result );
+ if ( !screen_ ) {
+ break;
+ }
+ }
+ expire_prefix();
+ return batch.consumed || !batch.events.empty();
+ }
+
+ void draw()
+ {
+ detail::Frame frame;
+ const auto dimensions = screen_->size();
+ // With fewer than three rows, header/body/footer cannot coexist.
+ if ( dimensions.rows < 3 ) {
+ const auto* view = active_view();
+ append_chrome( frame,
+ { 0, 0 },
+ view && view->snapshot().selection ? L"Resize | Esc cancel selection" : L"Resize | q quit" );
+ } else {
+ append_chrome( frame, { 0, 0 }, L"Ricochet", { .strong = true } );
+ std::wstring_view status = L"Browse | v select | q quit";
+ std::string_view copy_feedback;
+ if ( !capabilities_.bracketed_paste ) {
+ status = L"Paste boundaries unavailable; multiline paste may confirm input | q quit";
+ }
+ if ( active_ ) {
+ const auto found = tabs_.find( *active_ );
+ if ( found != tabs_.end() ) {
+ if ( found->second.view ) {
+ found->second.view->resize( { dimensions.rows - 2, dimensions.columns } );
+ found->second.view->draw( frame, { 1, 0 } );
+ if ( found->second.view->snapshot().selection && notice_.empty() ) {
+ status = L"Select | move extends | y copy | Esc cancel";
+ }
+ }
+ copy_feedback = found->second.copy_feedback;
+ if ( found->second.info.loading_task ) {
+ status = L"Loading | q quit";
+ }
+ if ( found->second.failure ) {
+ status = L"Navigation failed | q quit";
+ }
+ }
+ }
+ // Immediate input feedback (including focus wrap) must remain visible while
+ // the page has a background navigation status.
+ if ( !notice_.empty() ) {
+ status = notice_;
+ }
+ if ( prefix_ ) {
+ status = prefix_->character == U'g' ? L"g ... | Esc cancel" : L"z ... | Esc cancel";
+ }
+ if ( exit_request_ ) {
+ status = L"Waiting for exit decision";
+ }
+ if ( !copy_feedback.empty() && notice_.empty() && !prefix_ && !exit_request_ ) {
+ int column = 0;
+ for ( const auto& glyph : text_.analyze( copy_feedback, false, false ).graphemes ) {
+ if ( glyph.columns > dimensions.columns - column ) {
+ break; // Provider messages are clipped at a complete grapheme boundary.
+ }
+ frame.glyphs.push_back(
+ { .position = { dimensions.rows - 1, column }, .width = glyph.columns, .text = glyph.display } );
+ column += glyph.columns;
+ }
+ } else {
+ append_chrome( frame, { dimensions.rows - 1, 0 }, status );
+ }
+ }
+ if ( address_ ) {
+ draw_address( frame, dimensions );
+ }
+ screen_->draw( frame, std::exchange( redraw_, false ) );
+ dirty_ = false;
+ }
+
+ StepResult step( std::chrono::milliseconds max_wait )
+ {
+ StepResult result;
+ // Repeated step/close calls are part of the closed-session contract.
+ if ( !screen_ ) {
+ result.running = false;
+ return result;
+ }
+ try {
+ expire_prefix();
+ const auto received = receive( result );
+ if ( screen_ ) {
+ const bool input = read_input( result );
+ if ( screen_ && received == 0 && !input && !dirty_ ) {
+ auto wait = max_wait;
+ const auto deadline = address_ ? address_->editor.prefix_deadline()
+ : prefix_ ? std::optional( prefix_->deadline )
+ : std::nullopt;
+ if ( deadline ) {
+ wait = std::min( wait,
+ std::max( std::chrono::milliseconds::zero(),
+ std::chrono::ceil<std::chrono::milliseconds>( *deadline - Clock::now() ) ) );
+ }
+ const auto closed = screen_->wait( mailbox_->descriptor(), wait );
+ if ( closed ) {
+ dispatch( *closed, result );
+ } else {
+ receive( result );
+ if ( screen_ ) {
+ read_input( result );
+ }
+ }
+ }
+ }
+ if ( screen_ ) {
+ // Advance drag dwell even on a quiet terminal or a continuously busy input queue.
+ dispatch( Tick { Clock::now() }, result );
+ }
+ if ( screen_ && dirty_ ) {
+ draw();
+ }
+ } catch ( const std::exception& error ) {
+ // Restore the terminal before the host can print or present the failure.
+ close();
+ result.error = error.what();
+ }
+ result.running = screen_ != nullptr;
+ return result;
+ }
+
+ std::shared_ptr<detail::Mailbox> mailbox_;
+ std::unique_ptr<detail::Screen> screen_;
+ TerminalCapabilities capabilities_;
+ detail::TextService text_ { detail::Screen::glyph_capacity() };
+ std::map<TabId, TabState> tabs_ {};
+ std::vector<TabId> tab_order_ {};
+ std::optional<TabId> active_ {};
+ std::optional<RequestId> exit_request_ {};
+ std::optional<KeyPrefix> prefix_ {};
+ std::optional<AddressInput> address_ {};
+ std::uint64_t next_request_ { 1 };
+ std::vector<UserAction> actions_ {};
+ std::wstring notice_ {};
+ bool dirty_ { true };
+ bool redraw_ { false };
+};
+
+UpdateSender::UpdateSender( std::shared_ptr<detail::Mailbox> mailbox ) : mailbox_( std::move( mailbox ) ) {}
+
+PostResult UpdateSender::post_update( HostUpdate update ) const
+{ return mailbox_->post( std::move( update ) ); }
+
+Session::Session( TerminalOptions options ) : Session( *stdin, *stdout, options ) {}
+
+Session::Session( std::FILE& input, std::FILE& output, TerminalOptions options )
+ : impl_( std::make_unique<Impl>( input, output, options ) )
+{}
+
+Session::~Session() = default;
+
+UpdateSender Session::sender() const
+{ return UpdateSender( impl_->mailbox_ ); }
+
+PostResult Session::post_update( HostUpdate update )
+{ return impl_->mailbox_->post( std::move( update ) ); }
+
+StepResult Session::step( std::chrono::milliseconds max_wait )
+{ return impl_->step( max_wait ); }
+
+std::vector<UserAction> Session::take_actions()
+{ return std::exchange( impl_->actions_, {} ); }
+
+std::optional<ViewSnapshot> Session::snapshot( TabId tab ) const
+{
+ const auto found = impl_->tabs_.find( tab );
+ if ( found == impl_->tabs_.end() || !found->second.view ) {
+ return std::nullopt;
+ }
+ return found->second.view->snapshot();
+}
+
+TerminalCapabilities Session::capabilities() const
+{ return impl_->capabilities_; }
+
+void Session::close() noexcept
+{ impl_->close(); }
+
+} // namespace ricochet::tui
diff --git a/ricochet/src/tui/text.cc b/ricochet/src/tui/text.cc
new file mode 100644
index 0000000..ba24af2
--- /dev/null
+++ b/ricochet/src/tui/text.cc
@@ -0,0 +1,163 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "text.hh"
+
+#include <climits>
+#include <cwchar>
+#include <memory>
+#include <stdexcept>
+#include <unicode/ubrk.h>
+#include <unicode/uchar.h>
+#include <unicode/unistr.h>
+#include <unicode/utext.h>
+#include <utility>
+
+namespace ricochet::tui::detail {
+namespace {
+
+using UnicodeText = std::unique_ptr<UText, decltype( &utext_close )>;
+using Breaks = std::unique_ptr<UBreakIterator, decltype( &ubrk_close )>;
+
+Breaks breaks_for( UBreakIteratorType type, UText& text )
+{
+ UErrorCode status = U_ZERO_ERROR;
+ Breaks breaks( ubrk_open( type, "root", nullptr, 0, &status ), &ubrk_close );
+ if ( U_FAILURE( status ) ) {
+ throw std::runtime_error( u_errorName( status ) );
+ }
+ ubrk_setUText( breaks.get(), &text, &status );
+ if ( U_FAILURE( status ) ) {
+ throw std::runtime_error( u_errorName( status ) );
+ }
+ return breaks;
+}
+
+WordKind word_kind( UChar32 codepoint )
+{
+ if ( u_isUWhiteSpace( codepoint ) ) {
+ return WordKind::Space;
+ }
+ if ( u_hasBinaryProperty( codepoint, UCHAR_IDEOGRAPHIC ) ) {
+ return WordKind::Ideograph;
+ }
+ if ( codepoint == '_' || u_isalnum( codepoint ) ) {
+ return WordKind::Word;
+ }
+ return WordKind::Punctuation;
+}
+
+} // namespace
+
+TextService::TextService( std::size_t glyph_capacity ) : glyph_capacity_( glyph_capacity ) {}
+
+std::string TextService::encode( std::u32string_view input ) const
+{
+ // ICU's UTF-32 conversion takes a signed 32-bit length, like its break iterator.
+ if ( input.size() > INT32_MAX ) {
+ throw std::length_error( "Input exceeds ICU conversion length capacity" );
+ }
+ const std::vector<UChar32> codepoints( input.begin(), input.end() );
+ // fromUTF32 requires a non-null pointer even for a zero-length input.
+ const UChar32 empty = 0;
+ const auto converted = icu::UnicodeString::fromUTF32( codepoints.empty() ? &empty : codepoints.data(),
+ static_cast<int32_t>( codepoints.size() ) );
+ if ( converted.isBogus() ) {
+ throw std::runtime_error( "ICU could not convert editor input" );
+ }
+ std::string result;
+ converted.toUTF8String( result );
+ return result;
+}
+
+TextAnalysis TextService::analyze( std::string_view utf8, bool line_breaks, bool tabs ) const
+{
+ // ICU BreakIterator exposes int32_t native offsets even with a UTF-8 UText.
+ if ( utf8.size() > INT32_MAX ) {
+ throw std::length_error( "Text exceeds ICU break-iterator offset capacity" );
+ }
+ UErrorCode status = U_ZERO_ERROR;
+ UnicodeText text(
+ utext_openUTF8( nullptr, utf8.empty() ? "" : utf8.data(), static_cast<int64_t>( utf8.size() ), &status ),
+ &utext_close );
+ if ( U_FAILURE( status ) ) {
+ throw std::runtime_error( u_errorName( status ) );
+ }
+ auto characters = breaks_for( UBRK_CHARACTER, *text );
+ auto lines = breaks_for( UBRK_LINE, *text );
+ TextAnalysis result;
+ int32_t begin = ubrk_first( characters.get() );
+ int32_t line_end = ubrk_next( lines.get() );
+ WordKind previous = WordKind::Space;
+ for ( int32_t end = ubrk_next( characters.get() ); end != UBRK_DONE;
+ begin = end, end = ubrk_next( characters.get() ) ) {
+ while ( line_end != UBRK_DONE && line_end < end ) {
+ line_end = ubrk_next( lines.get() );
+ }
+ Grapheme glyph { .bytes = { static_cast<std::size_t>( begin ), static_cast<std::size_t>( end ) },
+ .break_after = line_end == end };
+ utext_setNativeIndex( text.get(), begin );
+ const auto first = utext_next32( text.get() );
+ result.whitespace_only = result.whitespace_only && u_isUWhiteSpace( first );
+ glyph.word = word_kind( first );
+ const auto source = utf8.substr( glyph.bytes.start, glyph.bytes.end - glyph.bytes.start );
+ if ( line_breaks && ( source == "\n" || source == "\r\n" ) ) {
+ glyph.kind = TextUnitKind::LineBreak;
+ glyph.columns = 0;
+ } else if ( tabs && source == "\t" ) {
+ glyph.kind = TextUnitKind::Tab;
+ glyph.display = L" ";
+ } else {
+ const int width = ::wcwidth( static_cast<wchar_t>( first ) );
+ glyph.display.push_back( static_cast<wchar_t>( first ) );
+ bool representable = width > 0 && !u_hasBinaryProperty( first, UCHAR_DEFAULT_IGNORABLE_CODE_POINT )
+ && !u_hasBinaryProperty( first, UCHAR_EMOJI_PRESENTATION );
+ while ( utext_getNativeIndex( text.get() ) < end ) {
+ const auto codepoint = utext_next32( text.get() );
+ result.whitespace_only = result.whitespace_only && u_isUWhiteSpace( codepoint );
+ glyph.display.push_back( static_cast<wchar_t>( codepoint ) );
+ // ncurses accepts one spacing character followed by nonspacing marks.
+ // Joiners, selectors and extra spacing characters use one visible fallback.
+ representable = representable && ::wcwidth( static_cast<wchar_t>( codepoint ) ) == 0
+ && u_charType( codepoint ) == U_NON_SPACING_MARK
+ && !u_hasBinaryProperty( codepoint, UCHAR_DEFAULT_IGNORABLE_CODE_POINT );
+ }
+ if ( !representable || glyph.display.size() > glyph_capacity_ ) {
+ glyph.display = L"?";
+ glyph.columns = 1;
+ } else {
+ glyph.columns = width;
+ }
+ }
+ if ( glyph.word != WordKind::Space ) {
+ if ( glyph.word == previous && glyph.word != WordKind::Ideograph ) {
+ result.words.back().end = glyph.bytes.end;
+ result.graphemes.back().word_end = false;
+ } else {
+ result.words.push_back( glyph.bytes );
+ glyph.word_start = true;
+ }
+ }
+ glyph.word_end = glyph.word != WordKind::Space;
+ previous = glyph.word;
+ result.graphemes.push_back( std::move( glyph ) );
+ }
+ return result;
+}
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/src/tui/text.hh b/ricochet/src/tui/text.hh
new file mode 100644
index 0000000..99fd7c9
--- /dev/null
+++ b/ricochet/src/tui/text.hh
@@ -0,0 +1,80 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/position.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace ricochet::tui::detail {
+
+enum class TextUnitKind : std::uint8_t
+{
+ Glyph,
+ LineBreak,
+ Tab
+};
+enum class WordKind : std::uint8_t
+{
+ Space,
+ Word,
+ Ideograph,
+ Punctuation
+};
+
+struct Grapheme
+{
+ ByteRange bytes {};
+ std::wstring display {};
+ int columns { 1 };
+ TextUnitKind kind { TextUnitKind::Glyph };
+ WordKind word { WordKind::Punctuation };
+ bool break_after { false };
+ bool word_start { false };
+ bool word_end { false };
+};
+
+struct TextAnalysis
+{
+ std::vector<Grapheme> graphemes {};
+ // All codepoints, including combining marks; an empty input is whitespace-only.
+ bool whitespace_only { true };
+ // Vim-style reading units: letters/numbers/underscore, punctuation runs,
+ // individual ideographs. Whitespace is excluded; offsets remain UTF-8 bytes.
+ std::vector<ByteRange> words {};
+};
+
+// Called on the UI thread in the same UTF-8 locale as Screen. ICU owns Unicode
+// segmentation/properties; POSIX wcwidth owns terminal widths. No curses types.
+class TextService
+{
+public:
+ explicit TextService( std::size_t glyph_capacity );
+ TextAnalysis analyze( std::string_view utf8, bool line_breaks, bool tabs ) const;
+ std::string encode( std::u32string_view input ) const;
+
+private:
+ std::size_t glyph_capacity_;
+};
+
+} // namespace ricochet::tui::detail
diff --git a/ricochet/tests/CMakeLists.txt b/ricochet/tests/CMakeLists.txt
new file mode 100644
index 0000000..00d7e4e
--- /dev/null
+++ b/ricochet/tests/CMakeLists.txt
@@ -0,0 +1,7 @@
+# file(GLOB_RECURSE ALL_TEST_SOURCES CONFIGURE_DEPENDS "*.cc")
+
+# add_executable(ricochet_tests ${ALL_TEST_SOURCES})
+# target_link_libraries(ricochet_tests PRIVATE ricochet_core)
+
+# include(CTest)
+# add_test(NAME ricochet_tests COMMAND ricochet_tests) \ No newline at end of file