Skip to content

unrays/Lynx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 

Repository files navigation

EXOTIC.lynx

Build License C++ Version Release

Lynx is a C++ compile-time metaprogramming library for building composable operator pipelines. Part of the EXOTIC collection, it is intended for developers who want to build DSLs using type-safe, compile-time operator composition.


Table of Contents

  1. Motivation
  2. Installation
  3. Usage
  4. Examples
  5. Contributing
  6. Roadmap
  7. License

Motivation

Originally, my plan was to implement a fluent design system for the front end of my ECS game engine. I started a week ago and I had never dealt with this kind of concept before. I then got to work and began developing a small, entirely compile-time system. Gradually, this small side project transformed into a full-fledged project, which itself evolved into a library.

The main problem with chaining compile-time elements is that, since these elements are compiled before being received in the previous node, it is not directly possible to interact with or modify its templated types. The solution, using template and using statements, allows me to model and recreate subsequent nodes from scratch with their original attributes, but adding the types and information of the current node, such as the data container or size constraints, for example.

This project is the culmination of my journey learning metaprogramming in C++, which I naively began a little less than two months ago. This is my first library ever, and I'm sure there are many things that can be improved. Feel free to share your suggestions!


Installation

Instructions on how to install, include, or build the library.

Requirements

  • C++ Standard: C++17 or later

  • Compilers: GCC 9+, Clang 10+, MSVC 2019+ (any compiler supporting C++17 to C++26)

  • Dependencies: Only the C++ standard library (<tuple>, <type_traits>, <concepts>, <utility>, <iostream>). No external dependencies.

Include

This library is header-only, so you just need to include the main header in your project:

#include "lynx.hpp"
using namespace EXOTIC::lynx;

Usage

Basic usage of the library involves creating operator chains and terminating them with Result or your own implementation.

Step 1: Create a pipeline

#include "lynx.hpp"
using namespace EXOTIC::lynx;

// Create a FunctionOperator pipeline
auto pipeline = FunctionOperator<SubscriptOperator<>>{};

// Equivalent explicit template version specifying arity, state, and next operator
auto pipeline = FunctionOperator<0, std::tuple<>, SubscriptOperator<0, std::tuple<>, DefaultEndOperator>>{};

Step 2: Execute the pipeline

#include "lynx.hpp"
using namespace EXOTIC::lynx;

// Provide some arguments; the pipeline collects them internally
pipeline(0, 250, 500)[750, 1000];

Step 3: End the pipeline

#include "lynx.hpp"
using namespace EXOTIC::lynx;

// Automatically terminates when pipeline reaches End
auto final_state = pipeline(10, 20)[30, 40, 50]; // returns collected arguments (tuple by default)

Examples

Example 1: Specify the size of arguments

#include "lynx.hpp"
using namespace EXOTIC::lynx;

auto pipeline = SubscriptOperator<3,
                    FunctionOperator<5,
                        SubscriptOperator<> // 0 = no constraints by default
                    >
                >{};

pipeline[0, 10, 20](30, 40, 50, 60, 70)[80]; // Compiles

pipeline[0, 10](20, 30, 40)[50]; // Doesn't compile

Example 2: Implement your own operator

One of the base provided by the API

#include "lynx.hpp"
using namespace EXOTIC::lynx;

template<typename>
struct FunctionOperatorBase;

template<
    template<std::size_t, typename, typename> class DerivedOperator,
    std::size_t Arity,
    typename Next,
    typename State
>
struct FunctionOperatorBase<DerivedOperator<Arity, Next, State>> {
    using Derived_t = DerivedOperator<Arity, Next, State>;

    template<typename... Args>
    auto operator()(Args&&... args)
        -> std::enable_if_t<
            (Arity == 0 || sizeof...(Args) == Arity),
            decltype(std::declval<Derived_t>().onOperated(std::forward<Args>(args)...))
        >
    {
        return static_cast<Derived_t*>(this)
            ->onOperated(std::forward<Args>(args)...);
    }
};

Example of implementation using this base

#include "lynx.hpp"
using namespace EXOTIC::lynx;

template<
    std::size_t Arity, // Number of arguments required
    typename Next, // Represents the subsequent structure type
    typename CurrentState // Type of the stored state
>
struct EntityIndexerOperator_: //Operator used for an ECS, for example: insert[entity](component)
    OperatorTraits<EntityIndexerOperator_<Arity, Next, CurrentState>>, // Allows for type introspection 
    SubscriptOperatorBase<EntityIndexerOperator_<Arity, Next, CurrentState>>, // Crtp base for generic operator attributes
    StatefulOperator<CurrentState> // Allows support for State by providing constructor, storage, etc...
{
    using StatefulOperator<CurrentState>::StatefulOperator; // Using the base's constructor
    friend SubscriptOperatorBase<EntityIndexerOperator_<Arity, Next, CurrentState>>; // Allows private implementation for onOperated

private:
    template<typename... Args>
    auto onOperated(Args&&... args) { // hooked function called when the base operator is called.
        // Adds the arguments to the current state using std::tuple_cat()
        auto concat_state_args = std::tuple_cat(
            this->state_,
            std::make_tuple(std::make_tuple(std::forward<Args>(args)...))
        );

        // Checks if the next node is normal or terminal
        if constexpr (is_end_operator<Next>::value) {
            if constexpr (has_onOperated_dummy<Next>::value) // Checks if terminal implements onOperated()
                return Next{ concat_state_args }; // If so, returns the terminal node with the current state 
            else
                return concat_state_args; // Otherwise, returns directly the state as it's raw type (tuple in this case) 
        }
        else
            return Next::template template_type<
                sizeof...(args), // Limits the next node to accepting the same number of arguments
                                 // In this case, we want the number of components to be equal to the number of entities

                typename Next::next_type, // Uses the same typename Next as normal, no change here

                decltype(concat_state_args) // Resolves the type of the current state and sends it
                                            // This allows the next node to store the tuple as a member

            >(std::move(concat_state_args)); // std::move the current state and passes it to the next node via its constructor
    }

     LINKLY_GENERATE_OPERATOR_ALIAS(EntityIndexerOperator, EntityIndexerOperator_);
     // In this case, this generates:
     //    EntityIndexerOperator<{Next operator (default=DefaultEndOperator)}, {State (default=std::tuple<>)}> 
     //    EntityIndexerOperator_n<{arity (default=0)}, {Next operator (default=DefaultEndOperator)}, {State (default=std::tuple<>)}>
};

Contributing

Contributions are welcome! You can help by:

  • Reporting bugs or issues
  • Suggesting new features or improvements
  • Submitting pull requests with fixes or new functionality

Please follow these guidelines:

  1. Fork the repository
  2. Create a new branch for your feature or bug fix
  3. Make your changes and write tests if applicable
  4. Submit a pull request describing your changes

Roadmap

Planned features and improvements for future releases:

  • Add support for additional operator types
  • Improve compile-time diagnostics and error messages
  • Extend examples and documentation
  • Fix SFINAE to support C++17 and prior

This roadmap may evolve as the library grows.


License

This project is licensed under the BSL License. See the LICENSE file for details.

© Félix-Olivier Dumas 2026