Functions
Functions are the building blocks of Psy programs. This chapter covers how to define functions and call them with various parameter types.
Function Definition
Functions are defined using the fn keyword:
#![allow(unused)] fn main() { // Basic function definition fn add(a: Felt, b: Felt) -> Felt { return a + b; } // Function without explicit return value (returns unit type) fn validate_inputs(a: Felt, b: Felt) { assert(a >= 0, "a must be non-negative"); assert(b >= 0, "b must be non-negative"); // Implicit return of unit type } // Function with multiple parameters of different types fn calculate(x: Felt, y: Felt, flag: bool, count: u32) -> Felt { if flag { x + y + (count as Felt) } else { x - y } } }
Function Calls
Parameter Passing
In Psy, function parameters are passed using different mechanisms depending on the data type:
Value Types (Copy Semantics):
Felt,u32,bool- These are copied when passed to functions- The original variable is not affected when the parameter is modified inside the function
Reference Types (Move/Borrow Semantics):
- Arrays
[T; N]- Passed by reference/move - Structs - Passed by reference/move
Tuples - Mixed Semantics:
- Tuples pass each member according to its own type
(Felt, Felt)- Both members copied (value types)(Felt, [Felt; 3])- First member copied, second member moved([Felt; 2], [Felt; 3])- Both members moved (reference types)
#![allow(unused)] fn main() { fn test_parameter_passing() { // Value types - copied let x: Felt = 10; let flag: bool = true; let count: u32 = 5u32; modify_values(x, flag, count); // x, flag, count remain unchanged // Reference types - moved/borrowed let mut arr = [1, 2, 3]; let mut point = new Point { x: 1, y: 2 }; modify_references(arr, point); // arr and point are moved into the function } fn modify_values(mut a: Felt, mut b: bool, mut c: u32) { a = 99; // Only affects the local copy b = false; // Only affects the local copy c = 0u32; // Only affects the local copy } fn modify_references(mut arr: [Felt; 3], mut point: Point) { arr[0] = 99; // Modifies the actual array point.x = 99; // Modifies the actual struct } // Examples of tuple parameter semantics fn process_value_tuple(tuple: (Felt, Felt)) -> Felt { // Both members copied - original tuple unaffected return tuple.0 + tuple.1; } fn process_mixed_tuple(tuple: (Felt, [Felt; 2])) -> Felt { // First member copied, array moved return tuple.0 + tuple.1[0]; } fn process_ref_tuple(tuple: ([Felt; 2], [Felt; 2])) -> Felt { // Both arrays moved return tuple.0[0] + tuple.1[1]; } }
Syntax Limitations
Important: Psy does not support early returns within control flow statements:
#![allow(unused)] fn main() { // ❌ Invalid: Early return in if statement fn invalid_early_return(x: Felt) -> Felt { if x > 10 { return x * 2; // This causes a compilation error }; return x; } // ✅ Valid: Use expression-based returns fn valid_conditional_return(x: Felt) -> Felt { if x > 10 { x * 2 } else { x } } // ✅ Valid: Set variable then return fn valid_variable_return(x: Felt) -> Felt { let mut result = x; if x > 10 { result = x * 2; }; return result; } }
Basic Function Calls
#![allow(unused)] fn main() { fn add(a: Felt, b: Felt) -> Felt { return a + b; } fn subtract(a: Felt, b: Felt) -> Felt { return a - b; } #[test] fn test_basic_calls() { // Simple function calls let sum = add(5, 3); let diff = subtract(10, 4); assert_eq(sum, 8, "5 + 3 should be 8"); assert_eq(diff, 6, "10 - 4 should be 6"); } }
Function Calls with Different Parameter Types
#![allow(unused)] fn main() { fn process_data(value: Felt, enabled: bool, iterations: u32) -> Felt { let mut result = value; if enabled { for i in 0u32..iterations { result = result + 1; } } else { result = 0; }; return result; } #[test] fn test_mixed_parameters() { // Calling function with mixed parameter types let result1 = process_data(10, true, 3u32); let result2 = process_data(10, false, 3u32); assert_eq(result1, 13, "10 + 3 iterations should be 13"); assert_eq(result2, 0, "disabled should return 0"); } }
Function Calls with Arrays
#![allow(unused)] fn main() { fn sum_array(arr: [Felt; 3]) -> Felt { return arr[0] + arr[1] + arr[2]; } fn modify_array(mut arr: [Felt; 3]) -> [Felt; 3] { arr[0] = arr[0] * 2; arr[1] = arr[1] * 2; arr[2] = arr[2] * 2; return arr; } #[test] fn test_array_parameters() { let numbers: [Felt; 3] = [1, 2, 3]; // Pass array to function let total = sum_array(numbers); let doubled = modify_array(numbers); assert_eq(total, 6, "1 + 2 + 3 should be 6"); assert_eq(doubled[0], 2, "first element doubled should be 2"); assert_eq(doubled[1], 4, "second element doubled should be 4"); } }
Function Calls with Tuples
#![allow(unused)] fn main() { fn distance(point1: (Felt, Felt), point2: (Felt, Felt)) -> Felt { let dx = point2.0 - point1.0; let dy = point2.1 - point1.1; // Simplified distance calculation (not actual Euclidean distance) return dx + dy; } fn create_point(x: Felt, y: Felt) -> (Felt, Felt) { return (x, y); } #[test] fn test_tuple_parameters() { let p1: (Felt, Felt) = (0, 0); let p2 = create_point(3, 4); let dist = distance(p1, p2); assert_eq(p2.0, 3, "x coordinate should be 3"); assert_eq(p2.1, 4, "y coordinate should be 4"); assert_eq(dist, 7, "distance should be 3 + 4 = 7"); } }
Function Calls with Structs
#![allow(unused)] fn main() { struct Point { pub x: Felt, pub y: Felt, } fn create_point_struct(x: Felt, y: Felt) -> Point { return new Point { x: x, y: y }; } fn move_point(mut point: Point, dx: Felt, dy: Felt) -> Point { point.x = point.x + dx; point.y = point.y + dy; return point; } fn get_x_coordinate(point: Point) -> Felt { return point.x; } #[test] fn test_struct_parameters() { let origin = create_point_struct(0, 0); let moved = move_point(origin, 5, 3); let x_coord = get_x_coordinate(moved); assert_eq(x_coord, 5, "x coordinate should be 5 after moving"); assert_eq(moved.y, 3, "y coordinate should be 3 after moving"); } }
Nested Function Calls
Functions can call other functions, creating nested calls:
#![allow(unused)] fn main() { fn double(x: Felt) -> Felt { return x * 2; } fn square(x: Felt) -> Felt { return x * x; } fn complex_calculation(x: Felt) -> Felt { // Nested function calls let doubled = double(x); let squared = square(doubled); return squared + double(x); } #[test] fn test_nested_calls() { let result = complex_calculation(3); // double(3) = 6, square(6) = 36, double(3) = 6 // result = 36 + 6 = 42 assert_eq(result, 42, "complex calculation should be 42"); } }
Function Inlining
Important: In Psy, functions are inlined by default during compilation. This means:
- Function calls are replaced with the function body during compilation
- Each function generates its own DPN opcodes
- There is no runtime function call overhead
- Recursive functions have limitations due to inlining
#![allow(unused)] fn main() { fn inline_example(x: Felt) -> Felt { return x + 1; } #[test] fn test_inlining() { // This call will be inlined during compilation let result = inline_example(5); assert_eq(result, 6, "5 + 1 should be 6"); } }