-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
# 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](#installation) | ||
- [Getting Started](#getting-started) | ||
- [Compile a Script](#compile-a-script) | ||
- [Quick Evaluation](#quick-evaluation) | ||
- [Quick Formatting](#quick-formatting) | ||
- [Value System](#value-system) | ||
- [Example: Creating Values](#example-creating-values) | ||
- [Example: Type Checking](#example-type-checking) | ||
- [Custom Functions](#custom-functions) | ||
- [Registering Custom Functions](#registering-custom-functions) | ||
- [Example: Registering a Custom Function](#example-registering-a-custom-function) | ||
- [Error Handling](#error-handling) | ||
- [Example: Handling Errors](#example-handling-errors) | ||
- [Contributing](#contributing) | ||
- [License](#license) | ||
--- | ||
|
||
## 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): | ||
|
||
```cmake | ||
add_subdirectory(path/to/rift) | ||
target_link_libraries(${PROJECT_NAME} rift) | ||
``` | ||
|
||
3. Alternatively, you can use CPM: | ||
|
||
```cmake | ||
CPMAddPackage("gh:EclipseMenu/rift@v2") | ||
target_link_libraries(${PROJECT_NAME} rift) | ||
``` | ||
|
||
4. Include the RIFT headers in your source files: | ||
|
||
```cpp | ||
#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). | ||
|
||
```cpp | ||
#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: | ||
|
||
```cpp | ||
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: | ||
|
||
```cpp | ||
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: | ||
|
||
```cpp | ||
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 | ||
|
||
```cpp | ||
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 | ||
```cpp | ||
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: | ||
|
||
```cpp | ||
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: | ||
|
||
```cpp | ||
// 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 | ||
```cpp | ||
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](LICENSE) file for details. |