Skip to content

Quick and dirty runtime-based string interpolation and expression parsing library for C++

License

Notifications You must be signed in to change notification settings

EclipseMenu/rift

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RIFT Library

Runtime Interpreted Formatting Toolkit

RIFT is a lightweight scripting library designed for robust, extendable string formatting and simple expression evaluation, offering unique advantages such as flexibility, seamless API integration, and support for advanced data manipulation.


Key Features

  • Script Compilation: Parse and compile source scripts into reusable objects.
  • Dynamic Evaluation: Execute scripts with variable inputs.
  • Custom Value System: Seamlessly handle multiple data types like strings, numbers, arrays, and objects.
  • Error Handling: Structured error types for compilation and runtime.
  • Integration: Simple integration into C++ projects via an intuitive API.

Table of Contents


Installation

RIFT uses CMake for integration, making it simple to include in your project. Follow the steps below:

  1. Add RIFT to your project using CMake (you have to clone the repository first):

    add_subdirectory(path/to/rift)
    target_link_libraries(${PROJECT_NAME} rift)
  2. Alternatively, you can use CPM:

    CPMAddPackage("gh:EclipseMenu/rift@v2")
    target_link_libraries(${PROJECT_NAME} rift)
  3. Include the RIFT headers in your source files:

    #include <rift.hpp>

Getting Started

Compile a Script

Use rift::compile to compile a script into a Script object. This will give you a unique pointer to a rift::Script object that holds the precomputed AST of the script. You can then evaluate the script with different variables without recompiling it, saving time and resources (especially for complex scripts).

#include <rift.hpp>

auto source = "Hello, {name}!";
auto result = rift::compile(source);
if (result) {
    auto script = std::move(result.unwrap()); // script is a unique_ptr<rift::Script>
    // You can now store the script and reuse it later

    // Evaluate the script with variables
    rift::Object variables = { {"name", "John"} };
    auto res = script->run(variables);
    if (res) {
        auto value = res.unwrap();
        std::cout << value << std::endl; // Output: Hello, John!
    } else {
        auto error = res.unwrapErr();
        std::cerr << error.prettyPrint(source) << std::endl;
        // note that RuntimeErrors do not have the source code, so you need to pass it manually.
        // you can also just use error.message(), if you just want the error message
    }
} else {
    auto error = result.unwrapErr();
    std::cerr << error.prettyPrint() << std::endl;
    // prettyPrint() will make a human-readable error message
    // with an arrow pointing to the error location
}

There's also a way to compile a script in 'direct mode', which changes the behavior of the parser and turns it into an expression parser. This means that you don't need to use {} to enclose the script. In this mode, you can get the result directly as a rift::Value object:

auto source = "2 + 2 * 2";
auto result = rift::compile(source, true);
if (result) {
    auto script = std::move(result.unwrap());
    auto res = script->eval();
    if (res) {
        auto value = res.unwrap();
        std::cout << value.toInteger() << std::endl; // Output: 6
    } else {
        auto error = res.unwrapErr();
        std::cerr << error.prettyPrint(source) << std::endl;
    }
} else {
    auto error = result.unwrapErr();
    std::cerr << error.prettyPrint() << std::endl;
}

Quick Evaluation

You can use rift::evaluate to compile and evaluate a script with direct mode, in a single step. Note that this is less efficient than compiling the script once and reusing it, but can be used if you only need to evaluate the script once:

rift::Object variables = { {"a", 10}, {"b", 20} };
auto result = rift::evaluate("a + b", variables);
if (result) {
    auto value = result.unwrap();
    std::cout << value.toInteger() << std::endl; // Output: 30
} else {
    auto error = result.unwrapErr();
    // Handle evaluation error
}

Quick Formatting

Similar to rift::evaluate, you can use rift::format to compile and format a string with variables:

rift::Object variables = { {"name", "John"}, {"age", 30} };
auto result = rift::format("Hello, {name}! You are {age} years old.", variables);
if (result) {
    auto formattedString = result.unwrap();
    std::cout << formattedString << std::endl; // Output: Hello, John! You are 30 years old.
} else {
    auto error = result.error();
    // Handle formatting error
}

Value System

RIFT provides the rift::Value class for handling dynamic values in scripts. Supported types are grouped into categories for better clarity:

  • Primitives: Null, String, Integer, Float, Boolean

  • Collections: Array, Object

Example: Creating Values

rift::Value stringValue = "Hello, World!";
rift::Value intValue = 42;
rift::Value arrayValue = rift::Array{1, 2, 3, "four"};
rift::Value objectValue = rift::Object{{"key", "value"}};

Example: Type Checking

if (value.isString()) {
    std::string str = value.getString();
}
if (value.isObject()) {
    auto obj = value.getObject();
}

Custom Functions

RIFT allows you to extend its functionality by defining custom functions. You can register functions that can be called within scripts, providing additional behavior and logic. These functions are registered in the global configuration and can be invoked just like built-in RIFT functions.

Registering Custom Functions

To add a custom function to the RIFT library, use the Config::registerFunction method. This allows you to bind a C++ function to a name, making it available for use in your scripts.

Alternatively, you can create a function wrapper using Config::makeFunction, which handles argument unwrapping and return value wrapping.

Function Signature

Custom functions must follow the signature:

geode::Result<Value>(std::span<Value const>)

Where:

  • std::span<rift::Value const> is a span of the function arguments.
  • geode::Result<rift::Value> is the return type, indicating success with the result value, or an error.

Example: Registering a Custom Function

Here’s an example of how to register a custom function named multiply that multiplies two integers:

// You can use the function signature directly
// makeFunction will handle argument unwrapping and return value wrapping,
// so you don't need to do it manually
int64_t multiply(int64_t a, int64_t b) {
    return a * b;
}

// In some cases, when for example you don't know the number of arguments,
// you can use the direct function signature, and handle the arguments manually
rift::RuntimeFuncResult divide(std::span<rift::Value const> args) {
    if (args.size() != 2) {
        return geode::Err("Function 'divide' requires exactly 2 arguments");
    }
    if (!args[0].isInteger() || !args[1].isInteger()) {
        return geode::Err("Function 'divide' requires integer arguments");
    }
    auto a = args[0].toInteger();
    auto b = args[1].toInteger();
    if (b == 0) {
        return geode::Err("Division by zero");
    }
    return a / b;
}

int main() {
    // makeFunction will handle argument unwrapping and return value wrapping, and then store the function in the global configuration
    rift::Config::get().makeFunction("multiply", multiply);
    // registerFunction will simply store the function in the global configuration
    rift::Config::get().registerFunction("divide", divide);

    // Example script
    rift::Object variables = { {"x", 3}, {"y", 4} };
    auto result1 = rift::evaluate("multiply(x, y)", variables);
    std::cout << result1.unwrap().toInteger() << std::endl; // Output: 12

    auto result2 = rift::evaluate("divide(10, 2)");
    std::cout << result2.unwrap().toInteger() << std::endl; // Output: 5
}

Error Handling

RIFT uses geode::Result for error handling. Common errors include:

  • CompileError: Issues during script compilation.
  • RuntimeError: Errors during script execution.

Both error types hold the same information, except that RuntimeError does not have the source code. You can use the prettyPrint method to get a human-readable error message.

Example: Handling Errors

auto result = rift::evaluate("abc(123) + 4"); // Invalid function call
if (!result) {
    auto error = result.unwrapErr();
    std::cerr << error.prettyPrint() << std::endl;
    /** Output:
     * RuntimeError: Function 'abc' not found
     * abc(123) + 4
     * ^^^^^^^^
     */

    // You can also get the error message directly
    std::cerr << error.message() << std::endl;
    // Output:
    // RuntimeError: Function 'abc' not found
}

Contributing

Contributions are welcome! Feel free to submit issues or pull requests to improve the library.

License

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