Chapter 4: Functions

**Master function definitions, parameters, and NanoLang's unique shadow testing system.**

NanoLang Mascot

Functions are the building blocks of NanoLang programs. This chapter covers how to define functions, work with parameters and return values, and write shadow tests.

4.1 Function Definitions

Functions in NanoLang follow a consistent, explicit syntax.

Basic Function Syntax


fn function_name(param1: type1, param2: type2) -> return_type {
    # function body
    return value
}

**Required components:**

1. fn keyword

2. Function name (lowercase with underscores)

3. Parameter list with types

4. Return type annotation ( -> type)

5. Function body in braces { }

6. return statement

Simple Function Example


fn add(a: int, b: int) -> int {
    return (+ a b)
}

shadow add {
    assert (== (add 2 3) 5)
    assert (== (add 0 0) 0)
    assert (== (add -5 5) 0)
}

Functions Without Parameters


fn get_pi() -> float {
    return 3.14159
}

shadow get_pi {
    let pi: float = (get_pi)
    assert (and (> pi 3.14) (< pi 3.15))
}

⚠️ **Watch Out:** Even parameterless functions need parentheses in their call: (get_pi) not get_pi.

Void Functions

Functions that don't return a value use void:


fn print_greeting(name: string) -> void {
    (println (+ "Hello, " name))
}

shadow print_greeting {
    (print_greeting "World")
}

**Note:** Void functions still execute code, they just don't return a value.

Naming Conventions

Use snake_case for function names:


 calculate_total
 is_valid
 get_user_name

 calculateTotal  # camelCase - not used
 Calculate_Total # PascalCase - not used

4.2 Parameters & Return Types

Functions can have multiple parameters and must declare their types explicitly.

Single Parameter


fn square(x: int) -> int {
    return (* x x)
}

shadow square {
    assert (== (square 5) 25)
    assert (== (square 0) 0)
    assert (== (square -3) 9)
}

Multiple Parameters


fn calculate_area(width: float, height: float) -> float {
    return (* width height)
}

shadow calculate_area {
    assert (== (calculate_area 5.0 10.0) 50.0)
    assert (== (calculate_area 0.0 10.0) 0.0)
}

Different Parameter Types


fn repeat_string(s: string, times: int) -> string {
    let mut result: string = ""
    let mut i: int = 0
    while (< i times) {
        set result (+ result s)
        set i (+ i 1)
    }
    return result
}

shadow repeat_string {
    assert (== (repeat_string "hi" 3) "hihihi")
    assert (== (repeat_string "x" 0) "")
}

Return Values

The return statement exits the function and provides the result:


fn max(a: int, b: int) -> int {
    if (> a b) {
        return a
    } else {
        return b
    }
}

shadow max {
    assert (== (max 5 3) 5)
    assert (== (max 3 5) 5)
    assert (== (max 4 4) 4)
}

Early Returns

You can return early from a function:


fn divide_safe(a: int, b: int) -> int {
    if (== b 0) {
        return 0  # Avoid division by zero
    }
    return (/ a b)
}

shadow divide_safe {
    assert (== (divide_safe 10 2) 5)
    assert (== (divide_safe 10 0) 0)
}

Multiple Return Paths


fn sign(x: int) -> int {
    if (< x 0) {
        return -1
    }
    if (> x 0) {
        return 1
    }
    return 0
}

shadow sign {
    assert (== (sign 5) 1)
    assert (== (sign -3) -1)
    assert (== (sign 0) 0)
}

4.3 Shadow Tests (Built-in Testing)

Shadow tests are NanoLang's unique compile-time testing feature. **Every function must have a shadow test** (except extern functions).

What Are Shadow Tests?

Shadow tests are code blocks that:

1. Run at compile time

2. Verify function correctness

3. Are mandatory for all functions

4. Use the shadow keyword

Basic Shadow Test


fn double(x: int) -> int {
    return (* x 2)
}

shadow double {
    assert (== (double 5) 10)
}

**Syntax:** shadow function_name { test code }

Why Shadow Tests Are Mandatory

**Benefits:**

1. **Catch bugs at compile time** - Tests run before your program executes

2. **Documentation** - Tests show how to use the function

3. **Confidence** - Every function is tested

4. **No separate test framework** - Testing is built into the language

**Philosophy:** If a function isn't worth testing, it isn't worth writing.

Writing Shadow Tests

**Rule 1: Test happy path**


fn add(a: int, b: int) -> int {
    return (+ a b)
}

shadow add {
    assert (== (add 2 3) 5)
}

**Rule 2: Test edge cases**


fn absolute(x: int) -> int {
    return (cond
        ((< x 0) (- 0 x))
        (else x)
    )
}

shadow absolute {
    assert (== (absolute 5) 5)     # Positive
    assert (== (absolute -5) 5)    # Negative
    assert (== (absolute 0) 0)     # Zero
}

**Rule 3: Test multiple scenarios**


fn is_even(n: int) -> bool {
    return (== (% n 2) 0)
}

shadow is_even {
    assert (is_even 4)         # Even positive
    assert (not (is_even 5))   # Odd positive
    assert (is_even 0)         # Zero
    assert (is_even -4)        # Even negative
    assert (not (is_even -5))  # Odd negative
}

Multiple Assertions

You can have multiple assertions in a shadow block:


fn factorial(n: int) -> int {
    if (<= n 1) {
        return 1
    }
    let mut result: int = 1
    let mut i: int = 2
    while (<= i n) {
        set result (* result i)
        set i (+ i 1)
    }
    return result
}

shadow factorial {
    assert (== (factorial 0) 1)
    assert (== (factorial 1) 1)
    assert (== (factorial 5) 120)
    assert (== (factorial 10) 3628800)
}

Testing with Different Types


fn string_length_valid(s: string) -> bool {
    return (and (>= (str_length s) 1) (<= (str_length s) 100))
}

shadow string_length_valid {
    assert (string_length_valid "hello")
    assert (not (string_length_valid ""))
    assert (string_length_valid "x")
}

Testing Void Functions

Void functions can have trivial shadow tests:


fn print_number(n: int) -> void {
    (println (int_to_string n))
}

shadow print_number {
    (print_number 42)
}

What If Tests Fail?

If a shadow test fails, compilation stops with an error:


# This won't compile:
# fn broken_add(a: int, b: int) -> int {
#     return (* a b)  # Bug: multiplying instead of adding
# }
#
# shadow broken_add {
#     assert (== (broken_add 2 3) 5)  # FAILS: 2*3 ≠ 5
# }

The compiler shows you which assertion failed, helping you fix the bug before running your program.

Shadow Test Best Practices

**1. Test boundary conditions**


fn safe_divide(a: int, b: int) -> int {
    if (== b 0) { return 0 }
    return (/ a b)
}

shadow safe_divide {
    assert (== (safe_divide 10 2) 5)    # Normal case
    assert (== (safe_divide 10 0) 0)    # Division by zero
    assert (== (safe_divide 0 5) 0)     # Zero numerator
    assert (== (safe_divide -10 2) -5)  # Negative numbers
}

**2. Keep tests simple and readable**


 Good:
shadow is_positive {
    assert (is_positive 5)
    assert (not (is_positive -3))
    assert (not (is_positive 0))
}

 Too complex:
shadow is_positive {
    assert (and (is_positive 5) (and (not (is_positive -3)) (not (is_positive 0))))
}

**3. Test realistic scenarios**


fn format_name(first: string, last: string) -> string {
    return (+ (+ first " ") last)
}

shadow format_name {
    assert (== (format_name "John" "Doe") "John Doe")
    assert (== (format_name "A" "B") "A B")
    assert (== (format_name "" "") " ")
}

4.4 Contracts: requires and ensures

NanoLang supports **design by contract** through requires (preconditions) and ensures (postconditions). These clauses specify what must be true when a function is called and what the function guarantees when it returns.

Preconditions with requires

Use requires to specify what must be true when the function is called:


fn safe_divide(a: int, b: int) -> int
requires (> b 0)
{
    return (/ a b)
}

shadow safe_divide {
    assert (== (safe_divide 10 2) 5)
    assert (== (safe_divide 100 5) 20)
}

fn main() -> int {
    (println (int_to_string (safe_divide 10 2)))
    return 0
}

shadow main { assert true }

**If the caller violates the precondition, the program exits with an error:**


Contract violation at line 3: (> b 0)

Postconditions with ensures

Use ensures to specify what the function guarantees about its return value:


fn abs_value(x: int) -> int
ensures (>= result 0)
{
    return (cond
        ((< x 0) (- 0 x))
        (else x)
    )
}

shadow abs_value {
    assert (== (abs_value 5) 5)
    assert (== (abs_value -5) 5)
    assert (== (abs_value 0) 0)
}

fn main() -> int {
    (println (int_to_string (abs_value -42)))
    return 0
}

shadow main { assert true }

The special result keyword refers to the function's return value in ensures clauses.

Combining requires and ensures

Functions can have multiple requires and ensures clauses:


fn bounded_increment(x: int, max: int) -> int
requires (>= x 0)
requires (> max x)
ensures (> result x)
ensures (<= result max)
{
    return (cond
        ((< (+ x 1) max) (+ x 1))
        (else max)
    )
}

shadow bounded_increment {
    assert (== (bounded_increment 5 10) 6)
    assert (== (bounded_increment 9 10) 10)
}

fn main() -> int {
    (println (int_to_string (bounded_increment 5 100)))
    return 0
}

shadow main { assert true }

Static Analysis

The compiler performs static analysis on contracts:

**Always-true conditions are elided:**


fn example() -> int
requires (> 5 0)   # Compiler comment: always true, elided
{
    return 42
}

**Always-false conditions generate warnings:**


fn broken() -> int
requires (< 0 0)   # Warning: contract condition is always false
{
    return 42
}

Best Practices

**1. Use requires for input validation**


fn process_age(age: int) -> string
requires (>= age 0)
requires (<= age 150)
{
    # age is guaranteed to be in valid range
    ...
}

**2. Use ensures to document function behavior**


fn string_length_safe(s: string) -> int
ensures (>= result 0)
{
    return (str_length s)
}

**3. Contracts complement shadow tests**

  • Shadow tests verify specific examples
  • Contracts express invariants that hold for all inputs

fn factorial(n: int) -> int
requires (>= n 0)
ensures (>= result 1)
{
    if (<= n 1) { return 1 }
    return (* n (factorial (- n 1)))
}

shadow factorial {
    # Shadow tests verify specific values
    assert (== (factorial 0) 1)
    assert (== (factorial 5) 120)
    # But contracts ensure n >= 0 and result >= 1 for ALL calls
}

Contract Violations at Runtime

When a contract is violated at runtime, the program prints a helpful error message showing the condition that failed:


Contract violation at line 5: (>= result 0)

This makes it easy to identify which contract was broken and where.

4.5 Recursion by Example

Recursion is when a function calls itself. It's a powerful technique for solving problems that have recursive structure.

Base Cases and Recursive Cases

Every recursive function needs:

1. **Base case** - When to stop recursing

2. **Recursive case** - How to break down the problem

Simple Recursion: Factorial


fn factorial_recursive(n: int) -> int {
    if (<= n 1) {
        return 1  # Base case
    }
    return (* n (factorial_recursive (- n 1)))  # Recursive case
}

shadow factorial_recursive {
    assert (== (factorial_recursive 0) 1)
    assert (== (factorial_recursive 1) 1)
    assert (== (factorial_recursive 5) 120)
    assert (== (factorial_recursive 6) 720)
}

**How it works:**

  • factorial_recursive(5) calls factorial_recursive(4)
  • Which calls factorial_recursive(3)
  • Which calls factorial_recursive(2)
  • Which calls factorial_recursive(1)
  • Base case returns 1
  • Results multiply back up: 1 * 2 * 3 * 4 * 5 = 120

Recursion: Sum of Array


fn sum_recursive(arr: array<int>, index: int) -> int {
    if (>= index (array_length arr)) {
        return 0  # Base case: past end of array
    }
    let current: int = (array_get arr index)
    let rest: int = (sum_recursive arr (+ index 1))
    return (+ current rest)
}

shadow sum_recursive {
    assert (== (sum_recursive [1, 2, 3, 4, 5] 0) 15)
    assert (== (sum_recursive [] 0) 0)
    assert (== (sum_recursive [10] 0) 10)
}

Recursion: Fibonacci


fn fibonacci_recursive(n: int) -> int {
    if (<= n 1) {
        return n  # Base cases: fib(0)=0, fib(1)=1
    }
    return (+ (fibonacci_recursive (- n 1)) (fibonacci_recursive (- n 2)))
}

shadow fibonacci_recursive {
    assert (== (fibonacci_recursive 0) 0)
    assert (== (fibonacci_recursive 1) 1)
    assert (== (fibonacci_recursive 5) 5)
    assert (== (fibonacci_recursive 10) 55)
}

Tail Recursion

Tail recursion is when the recursive call is the last operation:


fn sum_tail_recursive(arr: array<int>, index: int, accumulator: int) -> int {
    if (>= index (array_length arr)) {
        return accumulator
    }
    let next_acc: int = (+ accumulator (array_get arr index))
    return (sum_tail_recursive arr (+ index 1) next_acc)
}

shadow sum_tail_recursive {
    assert (== (sum_tail_recursive [1, 2, 3, 4, 5] 0 0) 15)
}

💡 **Pro Tip:** Tail recursion can be optimized by compilers to avoid stack growth.

Recursion vs Iteration

**When to use recursion:**

  • Problem has natural recursive structure (trees, nested data)
  • Code is clearer with recursion

**When to use iteration:**

  • Simple loops
  • Performance is critical
  • Risk of stack overflow

# Recursive version (elegant but can overflow)
fn count_down_recursive(n: int) -> void {
    if (<= n 0) { return }
    (println (int_to_string n))
    (count_down_recursive (- n 1))
}

# Iterative version (more efficient)
fn count_down_iterative(n: int) -> void {
    let mut i: int = n
    while (> i 0) {
        (println (int_to_string i))
        set i (- i 1)
    }
}

shadow count_down_recursive {
    (count_down_recursive 3)
}

shadow count_down_iterative {
    (count_down_iterative 3)
}

Complete Example: Binary Search


fn binary_search(arr: array<int>, target: int, left: int, right: int) -> int {
    if (> left right) {
        return -1  # Not found
    }
    
    let mid: int = (+ left (/ (- right left) 2))
    let mid_val: int = (array_get arr mid)
    
    if (== mid_val target) {
        return mid  # Found!
    }
    
    if (< target mid_val) {
        return (binary_search arr target left (- mid 1))
    } else {
        return (binary_search arr target (+ mid 1) right)
    }
}

shadow binary_search {
    let sorted: array<int> = [1, 3, 5, 7, 9, 11, 13]
    assert (== (binary_search sorted 7 0 6) 3)
    assert (== (binary_search sorted 1 0 6) 0)
    assert (== (binary_search sorted 13 0 6) 6)
    assert (== (binary_search sorted 8 0 6) -1)
}

Summary

In this chapter, you learned:

  • ✅ Function syntax: parameters, return types, body
  • ✅ Shadow tests are mandatory and run at compile time
  • ✅ Writing comprehensive tests for functions
  • ✅ Recursion: base cases and recursive cases
  • ✅ When to use recursion vs iteration

Practice Exercises


# 1. Write a function to compute power (x^n)
fn power(x: int, n: int) -> int {
    if (== n 0) {
        return 1
    }
    return (* x (power x (- n 1)))
}

shadow power {
    assert (== (power 2 3) 8)
    assert (== (power 5 0) 1)
    assert (== (power 10 2) 100)
}

# 2. Write a function to reverse a string
fn reverse_string(s: string) -> string {
    let len: int = (str_length s)
    if (== len 0) {
        return ""
    }
    let mut result: string = ""
    let mut i: int = (- len 1)
    while (>= i 0) {
        set result (+ result (string_from_char (char_at s i)))
        set i (- i 1)
    }
    return result
}

shadow reverse_string {
    assert (== (reverse_string "hello") "olleh")
    assert (== (reverse_string "") "")
    assert (== (reverse_string "a") "a")
}

# 3. Write a recursive function to find max in array
fn max_recursive(arr: array<int>, index: int, current_max: int) -> int {
    if (>= index (array_length arr)) {
        return current_max
    }
    let val: int = (array_get arr index)
    let new_max: int = (cond
        ((> val current_max) val)
        (else current_max)
    )
    return (max_recursive arr (+ index 1) new_max)
}

shadow max_recursive {
    assert (== (max_recursive [1, 5, 3, 9, 2] 1 1) 9)
    assert (== (max_recursive [10, 5, 8] 1 10) 10)
}

---

**Previous:** Chapter 3: Variables & Bindings

**Next:** Chapter 5: Control Flow