Functions

Functions are the primary way to organize and reuse code in Conjure. They allow you to encapsulate logic, accept parameters, and optionally return values to the caller.

Function Declaration

Functions are declared using the func keyword followed by a name, parameter list, and optional return type. If a function doesn’t return a value, it is assumed to return void and you can omit the return type entirely.

func greet() {
    c.printf("Hello, World!\n")
}

Parameters

Parameters are specified within parentheses, with each parameter having a name and type. Multiple parameters are separated by commas and each parameter may optionally specify a default value. The default value must be a compile-time constant.

Like variables, a function parameter that specifies a default value may omit the type specifier, in which case the type is inferred from the default value.

func greetNamed(name string) {
    c.printf("Hello, %s!\n", name)
}

func greetNamed(name = "Guest") {
    c.printf("Hello, %s!\n", name)
}

If you have multiple subsequent parameters with the same type and/or default value, you can use a shorthand syntax by separating the names with commas.

func setPosition(x, y, z i32 = 5) {
	c.printf("Position set to (%d, %d, %d)\n", x, y, z)
}

setPosition()           // Position set to (5, 5, 5)
setPosition(10)         // Position set to (10, 5, 5)
setPosition(10, 12)     // Position set to (10, 12, 5)
setPosition(10, 12, 15) // Position set to (10, 12, 15)

Returning Values

Use the return keyword to exit a function. If the function has no return type, return can be used without a value to exit early. If the function has a return type, you must provide a value of that type when returning.

func calculateArea(width f32, height f32) f32 {
	if width < 0 || height < 0 {
		return 0.0  // early return for invalid input
	}

    return width * height
}

Multiple Return Values Proposed

Functions can specify a second set of parentheses to return multiple values. Each return value must have a name and a type. This produces an anonymous struct type that can be used by the caller. The return values are assigned by name within the function body.

func divmod(a i32, b i32) (quotient i32, remainder i32) {
	quotient = a / b
	remainder = a % b
	return
}

var result = divmod(10, 3)
c.printf("Quotient: %d, Remainder: %d\n", result.quotient, result.remainder)

Function Calls

Call functions using their name followed by arguments in parentheses. Arguments are passed by value, meaning the function receives a copy of the argument. This can mitigated by passing references or pointers if needed.

Arguments that correspond to parameters with optional or default values can be omitted. If you want to skip some parameters but provide later ones, use named arguments.

greet() // prints "Hello, World!"
greet("Conjurer") // prints "Hello, Conjurer!"

Named Arguments

For better readability, especially with functions that have many parameters or boolean flags, you can use named arguments.

func createWindow(width i32, height i32, fullscreen bool, vsync bool) {
    // ...
}

createWindow(width = 800, height = 600, fullscreen = false, vsync = true)

You can mix positional and named arguments, but positional arguments must come first.

createWindow(800, 600, fullscreen = false, vsync = true)

Variadic Functions

Functions can accept a variable number of arguments using the ... syntax. The variadic parameter must be the last parameter in the function signature and is treated as a slice within the function body.

func sum(numbers ...i32) i32 {
    var total = 0
    for num in numbers {
        total += num
    }
    return total
}

var result = sum(1, 2, 3, 4, 5)  // 15

Function Pointers Proposed

Functions can be used as values, allowing them to be assigned to variables or passed as arguments. Additionally, you can avoid specifying parameter names when defining function types and avoid entire function signature definitions by using the .type property of an existing function to get its type.

func double(x i32) i32 {
    return x * 2
}

// Verbose function type declaration
var operation func(i32) i32 = double

var result = operation(5)  // 10

// Using .type property to infer the function type
var operation double.type

This is useful for callbacks and higher-order functions.

func apply(value i32, operation func(i32) i32) i32 {
    return operation(value)
}

var result = apply(10, double)  // 20

Function Prototypes

Functions can be declared without a body to create a prototype. This is useful for defining interfaces or when declaring external functions that will be implemented elsewhere.

@extern func externalFunction(param i32) i32

Template Parameters

Functions support generic programming through template parameters. Template parameters are specified within square brackets [] after the function name. They allow you to write functions that can operate on different types without sacrificing type safety.

See the page on Monomorphics for more details on how templates work in Conjure.

func max[T](a T, b T) T {
    if a > b {
        return a
    }
    return b
}

var maxInt = max[i32](5, 10)
var maxFloat = max[f32](3.14, 2.71)

Main Function

Every Conjure program needs an entry point. However, unlike languages like C or Go, this is not done by creating a function named main. Instead you define a function and annotate it with @entry. Conjure code bases can specify multiple entry points and selectively compile/run a specific one or all of them simultaneously.

Entry point functions must not take any parameters and must return either void, i32, or an error type.

@entry func main() {
    c.printf("Hello, Conjure!\n")
}

Annotations Proposed

Conjure provides several built-in compiler annotations to instruct the compiler to treat functions in specific ways. These annotations are prefixed with @ and placed immediately before the function declaration.

@extern

The @extern directive indicates that the function is implemented outside of Conjure, typically in C or another language. This is useful for interfacing with existing libraries or system calls. The function body should be omitted, leaving only a function prototype.

You can also specify a different name for the external function using the name argument, which is useful when the external function name does not conform to Conjure’s naming conventions.

When declaring external functions, you can also use unnamed variadic parameters using ... to represent C-style “varargs”. This creates a special anytype slice parameter that can accept any number of arguments of any type.

@extern(name = "printf") func cprintf(format string, ...) i32

@inline

The @inline directive suggests that the compiler should inline a function at its call sites. This can improve performance for small, frequently called functions by eliminating function call overhead. Conjure will automatically attempt to inline functions where it makes sense, but this annotation allows you to explicitly request it.

@inline func square(x i32) i32 {
    return x * x
}

@noinline

Inverse to @inline, the @noinline directive prevents the compiler from automatically attempting to inline a function. This can be useful for debugging, reducing code size, or when you want to ensure that a function call remains intact for profiling or stack traces.

@noinline func heavyComputation(x i32) i32 {
    // complex logic
    return x * x
}

@pure

The @pure directive indicates that a function has no side effects and its return value depends only on its input parameters. This allows the compiler to perform optimizations such as common sub-expression elimination and dead code elimination.

It will also be used to instruct our memory-management system about the safety of certain operations, resulting in ref-count insertions being elided in some cases.

@pure func reduceHealth(monster &Monster, amount i32) i32 {
	return monster.health - amount
}

@entry

The @entry directive marks the main entry point of a Conjure program. Multiple functions can be marked with @entry, but only one will be used as the actual entry point. You can also provide a name argument for the entry point to distinguish between multiple entry points in the same codebase.

@entry func main() {
    // game logic
}

@entry(name = "editor") func editorMain() {
	// editor logic
}

@test

The @test directive marks a function as a test case. Test functions must take no parameters and return no value. They can use assertions to validate behavior. Test functions are automatically discovered and run by the test framework.

The test case name is derived from the function name, but you can optionally provide a custom name using the name argument. Test functions are stripped from release builds and calls to test functions from non-test code are disallowed.

@test func testAddition() {
	var result = 2 + 2
	assert(result == 4, "2 + 2 should equal 4")
}

@test("Should multiply correctly")
func testMultiplication() {
	var result = 3 * 3
	assert(result == 9, "3 * 3 should equal 9")
}