Evaluated Functions
In Helix we have a special category of functions called evaluated functions. These functions are executed at compile time rather than at runtime, allowing for optimizations and computations to be performed before the program is run.
Definition
An evaluated function is defined using the eval keyword before the return signature (->) of the function. This indicates to the compiler that the function should be executed during the compilation phase.
Here’s an example of an evaluated function that adds two integers:
fn add(x: i32, y: i32) eval -> i32 {    return x + y}In this example, the add function takes two i32 parameters and returns their sum. The eval keyword indicates that this function will be evaluated at compile time.
Eval functions can also contain generics:
fn <T> max(a: T, b: T) eval -> T {    return if a > b { a } else { b }}Limitations
Evaluated functions have certain limitations compared to regular functions:
- No Side Effects: Evaluated functions cannot have side effects, such as modifying global state or performing I/O operations. They must be pure functions that always produce the same output for the same input.
- Limited Standard Library Access: Evaluated functions have restricted access to the standard library. Only a subset of functions and types are available for use within evaluated functions.
- No Recursion: Evaluated functions can be recursive, but the recursion depth is limited to prevent infinite loops during compilation.
- Type Restrictions: Evaluated functions can only use types that are also evaluable at compile time. This typically includes primitive types and certain user-defined types.
- Performance Considerations: While evaluated functions can improve performance by precomputing values, they can also increase compilation time if they are complex or called frequently.
- IO Operations: Evaluated functions cannot perform any input/output operations, as these are inherently runtime activities.
- Memory Management: Evaluated functions cannot allocate or deallocate memory dynamically. All memory usage must be predictable and manageable at compile time.
Example
Here is an example of using an evaluated function to compute the factorial of a number at compile time:
fn exponent(base: f64, exp: i32) eval -> f64 {    return if exp == 0 {        1.0    } else if exp % 2 == 0 {        exponent(base * base, exp / 2)    } else {        exponent(base * base, (exp - 1) / 2) * base    }}
fn cal_len_of_raw_string(const s: *char) eval -> usize {    var len: usize = 0
    while s[len] != '\0' {        ++len    }
    return len}
class Circle {    var radius: f64
    fn Circle(self, radius: f64) eval { // evaluated constructor        self.radius = radius    }
    fn area(self) eval -> f64 { // evaluated method        return 3.14159 * self.radius * self.radius    }}
// Usageconst r = 5.0const circle = Circle(r)const area = circle.area()
const len = cal_len_of_raw_string("Hello, World!")const exp_result = exponent(2.0, 10)
std::print(f"Area of circle with radius {r} is {area}")std::print(f"Length of raw string is {len}")std::print(f"2 raised to the power of 10 is {exp_result}")