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.
- 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.
RIFT uses CMake for integration, making it simple to include in your project. Follow the steps below:
-
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)
-
Alternatively, you can use CPM:
CPMAddPackage("gh:EclipseMenu/rift@v2") target_link_libraries(${PROJECT_NAME} rift)
-
Include the RIFT headers in your source files:
#include <rift.hpp>
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;
}
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
}
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
}
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
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"}};
if (value.isString()) {
std::string str = value.getString();
}
if (value.isObject()) {
auto obj = value.getObject();
}
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.
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.
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.
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
}
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.
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
}
Contributions are welcome! Feel free to submit issues or pull requests to improve the library.
This project is licensed under the MIT License. See the LICENSE file for details.