Skip to content

Commit

Permalink
Create README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
Prevter authored Dec 29, 2024
1 parent 5f68961 commit 60c9a83
Showing 1 changed file with 290 additions and 0 deletions.
290 changes: 290 additions & 0 deletions README.md
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.

0 comments on commit 60c9a83

Please sign in to comment.