.. sectnum:: Contributing to TeachOS ======================= Thank you for contributing to TeachOS. This document outlines the workflow for developing, testing, formatting, and submitting contributions, as well as the coding idioms enforced across the codebase. Development Workflow -------------------- All **non-thesis** contributions follow a standard GitLab Merge Request workflow against the ``develop`` branch. When working on a semester of bachelor's thesis, first create a branch off ``develop`` with the name ``/develop`` (e.g., ``fa26/develop`` for a Fall 2026 thesis). Create feature branches off the thesis branch and submit Merge Requests against it, tagging your advisor as a reviewer if desired. Be prepared to rebase your thesis branch on top of ``develop`` periodically to keep up with changes in the mainline codebase. At the end of your thesis, submit a Merge Request from your thesis branch to ``develop`` for final review and integration. Branching and Merge Requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Base all feature branches off the latest ``develop`` or your thesis development branch. 2. Keep Merge Requests focused on a single logical change or subsystem feature. 3. Ensure CI pipelines pass cleanly without warnings or test regressions before requesting review. Commit Conventions ~~~~~~~~~~~~~~~~~~ Commit messages should be concise, written in the imperative mood, and scoped using a subsystem prefix: - Subsystems: ``kernel/vfs: ``, ``arch/x86_64: ``, ``kapi: ``, ``kstd: `` - Supporting areas: ``doc: ``, ``build: ``, ``chore: ``, ``ide: `` Building and Testing -------------------- TeachOS utilizes CMake presets to standardize build and test configurations across local environments and CI runners. Build-Host Tests (BHT) ~~~~~~~~~~~~~~~~~~~~~~ Unit and host-executable integration tests use Catch2 with address, leak, and undefined behavior sanitizers enabled by default: .. code-block:: sh cmake --preset bht cmake --build --preset bht-dbg ctest --preset bht-dbg For thread-safety and race condition analysis under ThreadSanitizer: .. code-block:: sh cmake --preset bht-stress cmake --build --preset bht-stress-dbg ctest --preset bht-stress-dbg Bootable Kernel Targets ~~~~~~~~~~~~~~~~~~~~~~~ To build the bootable debug ISO images (e.g. for x86-64): .. code-block:: sh cmake --preset x86_64 cmake --build --preset x86_64-dbg Quality and Compliance Verification ----------------------------------- Before submitting a Merge Request, verify the following checks locally, replacing ```` with the target architecture (e.g., ``x86_64``): 1. **Header Interface Verification**: .. code-block:: sh cmake --build --preset bht-dbg --target all_verify_interface_header_sets cmake --build --preset --target all_verify_interface_header_sets 2. **Formatting**: Ensure all C++ source and header files are formatted according to ``.clang-format``. 3. **Static Analysis**: Clang-Tidy runs during compilation when ``TEACHOS_ENABLE_LINTING`` is enabled (on by default). Builds must compile cleanly without warnings. 4. **License Compliance**: TeachOS adheres to the `REUSE specification `_. Run ``reuse lint`` to ensure all new files have valid copyright and license metadata (BSD-3-Clause for kernel code, CC0-1.0 for documentation and configuration). Code Style and Idioms ---------------------- This subsection outlines the coding idioms and conventions enforced across the TeachOS codebase. Language Standard and Vocabulary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TeachOS targets **C++23** without compiler extensions. Standard library features may be used freely in tests only. All other parts of the codebase rely on a freestanding variant of the standard library as shipped with the toolchain. Types that are not part of the toolchain's freestanding standard library are provided by the ``kstd`` library. Below is an overview of ``kstd`` replacements to be used in the **non-test** kernel code, including the bundled support libraries. This list does not claim completeness. =================== ========================= ======================================= Concept Preferred Replaces =================== ========================= ======================================= Dynamic array ``kstd::vector`` ``std::vector`` Strings ``kstd::string`` ``std::string`` Non-owning pointer ``kstd::observer_ptr`` raw ``T*`` for ownership-neutral access Shared ownership ``kstd::shared_ptr`` ``std::shared_ptr`` Unique ownership ``kstd::unique_ptr`` ``std::unique_ptr`` Printing ``kstd::println(...)`` ``std::println(...)``, ... String Formatting ``kstd::format(...)`` ``std::format(...)``, ... =================== ========================= ======================================= Common standard library parts that are available in the freestanding implementation include but are not limited to: - ``std::string_view`` - ``std::span`` - ``std::array`` - ``std::optional`` - ``std::byte`` - ``std::ranges`` - ``std::views`` These parts may be used freely throughout the codebase. Function Declarations — Trailing Return Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **All** functions and member functions use trailing return type syntax, including those returning `void`. See the following code snippet for examples: .. code-block:: cpp // Correct auto device_registry::get() -> device_registry &; auto bitmap_is_set(std::span bitmap, std::size_t index) -> bool; auto init() -> void; // Wrong device_registry & device_registry::get(); bool bitmap_is_set(std::span bitmap, std::size_t index); void init(); This rule applies to: free functions, member functions, lambdas with explicit return types, and virtual functions. The only exception is constructors and destructors, which have no return type at all. Parameter Passing Conventions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The choice of passing convention encodes intent and must be consistent. View and cheaply copyable types — pass by value ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Types that are designed to be non-owning views or are trivially copyable must be passed and returned **by value**. Passing them by ``const &`` is redundant and adds a pointer indirection with no benefit. This includes but is not limited to: - ``std::string_view`` - ``std::span`` - ``kstd::observer_ptr`` - ``kstd::bytes`` and similar unit types - ``kapi::capabilities::facet_id`` - ``kapi::memory::page`` - ``kapi::memory::frame`` - ``kapi::memory::physical_address`` - ``kapi::memory::linear_address`` See the following code snippet for examples: .. code-block:: cpp // Correct auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> std::observer_ptr; auto has(std::span data) -> bool; // Wrong auto resolve(kapi::capabilities::facet_id const & id, std::string_view const & name) -> std::observer_ptr; Large or non-trivially copyable types — pass by ``const &`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For types that own heap memory or are non-trivially copyable, use ``const &`` when the callee does not take ownership. See the following code snippet for examples: .. code-block:: cpp auto do_publish(kstd::string const & name) -> kstd::result; auto add_child(kstd::string const & child_name) -> void; Sink parameters — pass by value and move ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When a function is designed to take **ownership** of an argument, accept it by value and move it into its destination. This makes the transfer explicit at the call site. See the following code snippet for examples: .. code-block:: cpp // In the header auto add_child(kstd::shared_ptr child) -> void; // In the implementation auto bus::add_child(kstd::shared_ptr child) -> void { m_devices.push_back(std::move(child)); // ownership transferred here } Mutable subsystem references — pass by non-const reference ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Services and subsystems that are mutated in-place (e.g., ``page_mapper &``, ``driver_state &``, ``kapi::devices::bus &``) are passed by non-const reference. This expresses that the function operates on a shared, mutable context. See the following code snippet for examples: .. code-block:: cpp auto remap_kernel(kapi::memory::page_mapper & mapper) -> void; auto add_directory_entry(inode & directory, inode & child, driver_state & state, write_batch & batch) -> kstd::result; Error Handling ~~~~~~~~~~~~~~ TeachOS kernel code, and all library code used by it, cannot make use of exceptions. Use of exception related keywords, `try`, `catch`, `throw` in kernel code will cause compilation to fail. However, exceptions are allowed in test code. Recoverable errors — ``kstd::result`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Functions that can fail in an expected, recoverable way must return ``kstd::result``. Use the ``kstd::success()`` and ``kstd::failure()`` helpers consistently. Define per-subsystem error code if necessary. See the following code snippet for examples: .. code-block:: cpp auto mount(kstd::shared_ptr parent) -> kstd::result, state *>>; // In the implementation if (!device) { return kstd::failure(make_error_code(kstd::errc::invalid_argument)); } return kstd::success(result_value); Callers must check the result before using its value. The idiomatic check is: See the following code snippet for examples: .. code-block:: cpp auto result = some_function(); if (!result) { return kstd::failure(result.error()); // propagate } // use *result Monadic composition (``transform``, ``and_then``, ``or_else``) is preferred over manual if-check-and-return chains when it produces clearer code. Unrecoverable errors — ``kapi::system::panic`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Violations of kernel invariants (e.g., a subsystem used before being initialized, OOM during boot) call ``kapi::system::panic(...)``. The ``panic`` function is ``[[noreturn]]``, meaning control will never return from it. It must not be used for recoverable errors. Log prefix convention: ``[SUBSYSTEM:TAG] message``. Examples: ``[OS:DEV]``, ``[OS:VFS]``, ``[ARCH:DRV]``. See the following code snippet for examples: .. code-block:: cpp if (!instance) { system::panic("[OS:DEV] Device registry has not been initialized."); } Ownership and Lifetime ~~~~~~~~~~~~~~~~~~~~~~ Shared ownership — ``kstd::shared_ptr`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use ``kstd::shared_ptr`` when a resource is co-owned by multiple subsystems and its lifetime must be extended by any of them (e.g., ``device``, ``inode``, ``dentry``). Weak back-references that must not extend lifetime use ``kstd::weak_ptr``. Non-owning references — ``kstd::observer_ptr`` and raw references ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use ``kstd::observer_ptr`` to express a non-owning pointer where null is a valid state and the holder has no say in the lifetime of the target. Use a raw reference (``T &`` or ``T const &``) when null is not valid and the reference is short-lived (i.e. a function parameter or a local alias). Never use raw ``T *`` to mean "sometimes I own this, sometimes I don't". Ownership must be expressed unambiguously through the pointer type. Device driver data — ``kstd::shared_ptr`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Drivers attach their state to a ``device`` using an untyped ``kstd::shared_ptr`` via ``device::set_driver_data``. A driver retrieves its state via ``device::driver_data``. This allows the device tree to destroy driver data automatically when the device is released, without the device knowing the concrete driver type. Algorithm and Range Usage ~~~~~~~~~~~~~~~~~~~~~~~~~ Prefer ``std::ranges`` algorithms over manual loops ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When processing a range to search, filter, transform, or reduce, use the appropriate ``std::ranges`` algorithm or view pipeline instead of writing a raw ``for`` loop. See the following code snippet for examples: .. code-block:: cpp // Correct auto already_published = std::ranges::any_of(m_entries, [&](auto const & entry) { return entry.id() == id && entry.device().get() == device.get(); }); std::ranges::for_each(observers, [&](auto observer) { /* ... */ }); // Wrong — manual linear scan for a boolean result for (auto const & entry : m_entries) { if (entry.id() == id && entry.device().get() == device.get()) { return true; } } return false; Acceptable uses of explicit loops include: - Accumulation or mutation that modifies state in-place and cannot be cleanly expressed with a ranges algorithm. - Low-level routines dealing with raw memory arithmetic (allocators, page mappers). - Iterator-pair loops in library internals (``kstd::vector``, ``kstd::basic_string``). Prefer view composition over intermediate containers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Build processing pipelines using ``std::views::filter``, ``std::views::transform``, ``std::views::reverse``, ``std::views::split``, and ``std::ranges::subrange`` rather than materialising intermediate vectors. See the following code snippet for examples: .. code-block:: cpp // Correct auto descriptors = std::span{&__start_platform_drivers, &__stop_platform_drivers} | std::views::filter([](auto p) { return p != nullptr; }); auto modules_view = std::ranges::subrange(begin(), end()) | std::views::filter(filter_modules) | std::views::transform(transform_module); Do not call the same function twice to avoid storing the result ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If an intermediate value is needed more than once, store it in a local variable. This applies especially to factory calls and heap allocations. .. code-block:: cpp // Wrong — double invocation, two allocations, different objects for (auto driver : descriptors) { kstd::println("registering driver '{}'", driver->make_instance()->name()); registry.add(driver->make_instance()); } // Correct for (auto driver : descriptors) { auto instance = driver->make_instance(); kstd::println("registering driver '{}'", instance->name()); registry.add(std::move(instance)); } Class and Struct Design ~~~~~~~~~~~~~~~~~~~~~~~ Prefer ``struct`` over ``class`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The entire codebase uses ``struct`` for all type definitions with explicit ``private:`` sections where necessary. Do not introduce ``class``. Member ordering within a type ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Follow this ordering within a ``struct``: 1. Nested types and type aliases. 2. Static data members and static constexpr constants (e.g., ``static constexpr auto id = ...``). 3. Constructors and destructor. 4. Public member functions. 5. ``protected:`` section with virtual hooks. 6. ``private:`` section with helper functions, then data members. Data members are always in the ``private`` section and always prefixed with ``m_``. See the following code snippet for examples: .. code-block:: cpp struct facet_registry { struct entry { /* ... */ }; // 1. nested type facet_registry() = default; // 3. constructor auto static init() -> void; // 4. public interface auto static get() -> facet_registry &; auto publish(...) -> kstd::result; private: auto do_publish(...) -> kstd::result; // 6a. private helpers mutable tracked_mutex m_lock{}; // 6b. data members, m_ prefix kstd::vector m_entries; }; ``explicit`` on single-argument constructors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Mark every single-argument constructor ``explicit`` unless an implicit conversion is intentional and documented. A deliberate implicit constructor must be accompanied by a comment explaining the decision. See the following code snippet for examples: .. code-block:: cpp // Correct explicit device(kstd::string const & name); constexpr explicit facet_id(std::string_view name); // Intentional implicit — documented at the declaration //! This constructor allows implicit conversion from chunk<...> to page for //! convenience. It is deliberately not explicit. constexpr page(chunk other) : chunk{other} {} Declare deleted special members explicitly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If a type is not copyable or not movable, declare the deleted special members explicitly rather than relying on implicit suppression. See the following code snippet for examples: .. code-block:: cpp device(device const &) = delete; auto operator=(device const &) -> device & = delete; Virtual destructors ^^^^^^^^^^^^^^^^^^^ Every base class with virtual member functions must have a ``virtual`` destructor, defaulted if not otherwise needed. See the following code snippet for examples: .. code-block:: cpp virtual ~driver_descriptor() = default; virtual ~facet_registry_observer() = default; Static Singletons ~~~~~~~~~~~~~~~~~ Several subsystems expose a single global instance via an ``init()``/``get()`` pair. Follow this pattern: - Store the instance in an anonymous namespace as a ``constinit std::optional``. - ``init()`` asserts the instance is not yet constructed, then constructs it using ``emplace()`` it. - ``get()`` asserts the instance exists and returns a reference to it. - Both functions panic on violation rather than returning an error code, because incorrect call order is a programming error, not a recoverable runtime condition. See the following code snippet for examples: .. code-block:: cpp namespace { auto constinit instance = std::optional{}; } auto device_registry::init() -> void { if (instance) { system::panic("[OS:DEV] Device registry has already been initialized."); } instance.emplace(); } auto device_registry::get() -> device_registry & { if (!instance) { system::panic("[OS:DEV] Device registry has not been initialized."); } return *instance; } Enumerations ~~~~~~~~~~~~ All enumerations use ``enum struct`` (scoped enums), never plain ``enum``. Specify the underlying type explicitly when the representation matters (e.g., for hardware register fields). See the following code snippet for examples: .. code-block:: cpp // Correct enum struct state { uninitialized, present, bound, }; // Wrong enum state { uninitialized, present, bound }; Naming ~~~~~~ ================================== ============================= ===================================== Symbol Convention Example ================================== ============================= ===================================== Types (struct, enum) ``lower_case`` ``device_registry``, ``facet_id`` Functions and methods ``lower_case`` ``add_child``, ``make_instance`` Local variables ``lower_case`` ``entry``, ``block_index`` Private data members ``m_`` prefix, ``lower_case`` ``m_entries``, ``m_driver_data`` Template type parameters ``CamelCase`` ``ValueType``, ``FacetType`` Constants and constexpr variables ``lower_case`` ``page_size``, ``direct_block_count`` Type aliases ``lower_case`` ``value_type``, ``size_type`` Namespaces ``lower_case`` ``kapi::devices``, ``kernel::vfs`` ================================== ============================= ===================================== Namespaces reflect directory structure: ``kernel::vfs``, ``kernel::filesystems::ext2``, ``arch::devices``, etc. ``[[nodiscard]]`` ~~~~~~~~~~~~~~~~~ Mark any function ``[[nodiscard]]`` whose return value the caller should not silently discard. This includes in particular: - All functions returning ``kstd::result``. - All query functions (getters, lookups) that return computed data. - Factory functions and builder utilities. See the following code snippet for examples: .. code-block:: cpp [[nodiscard]] auto children() const -> kstd::vector>; [[nodiscard]] auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> void *; [[nodiscard]] auto request_resource(resource_type type, std::size_t index = 0) const -> kstd::result; ``constexpr`` and ``constinit`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Mark functions ``constexpr`` whenever they can be evaluated at compile time or in constant expressions, even if they are also called at runtime. Mark module-scope variables ``constinit`` to guarantee zero-initialization before any dynamic initialization runs. Documentation ~~~~~~~~~~~~~ Every non-trivial public type, function, and data member must be documented with a Doxygen comment using the ``//!`` line style. See the following code snippet for examples: .. code-block:: cpp //! A brief one-line description. //! //! Optional longer description providing context. //! //! @warning Any important warnings or preconditions. //! //! @param name Description of the parameter. //! @return Description of the return value. auto add_child(kstd::shared_ptr child) -> void; Doxygen grouping (``@name``, ``@{``, ``@}``) may be used to organize the API surface into logical sections visible in generated documentation. Header Guards ~~~~~~~~~~~~~ All headers use traditional include guards, not ``#pragma once``. The guard name follows the pattern ``TEACHOS___HPP``, where each path component is uppercased and separators are replaced by ``_``. The only exception to this pattern are the bundled libraries, which follow the pattern ``___HPP``. See the following code snippet for examples: .. code-block:: cpp #ifndef TEACHOS_KAPI_DEVICES_BUS_HPP #define TEACHOS_KAPI_DEVICES_BUS_HPP // ... #endif The closing ``#endif`` carries no comment with the guard name.