Skip to content
📖 The Auto Programming LanguageBook contents

Functions and Control Flow ​

You now know how to store data in variables and transform it with operators. But a program that's just a list of statements is like a recipe with no sections — it works, but it's hard to reuse and hard to read. Functions let you name a block of code so you can call it from anywhere. Control flow lets you choose which blocks run and repeat them when needed. Together, they turn a flat script into a structured program.

In this chapter, we'll cover:

  • Defining and calling functions with fn
  • Returning values from functions (implicitly and explicitly)
  • Conditional branching with if / else if / else
  • Looping with a condition using for cond { ... }
  • Counted loops with for i in 0..n { ... }
  • Iterating over collections with for item in list { ... }
  • Controlling loops with break and continue
  • Returning early from functions

Defining Functions ​

A function is a named, reusable block of code. You define it once and call it as many times as you like. In Auto, you use the fn keyword to define a function:

auto
fn greet(name String) {
    print("Hello, " + name + "!")
}

This creates a function called greet that takes one parameter — name — with the type String. The type annotation goes after the parameter name, with no colon. The function body is wrapped in curly braces.

To call a function, write its name followed by arguments in parentheses:

auto
greet("Alice")

This runs the code inside greet with name set to "Alice".

A few things to notice:

  • Parameters have type hints. The caller must pass a value of the correct type. fn greet(name String) means name must be a String.
  • Return types are inferred. You don't write -> void or -> String. The compiler figures out what the function returns by examining the body.
  • No parentheses around the type. name String, not name: String or name: String.

Let's see functions in action.

Defining and calling a function

Look at how each target language handles string concatenation inside the function:

  • Rust uses println!("Hello, {}!", name) with a format string — more efficient than + on String values.
  • Python keeps it simple: "Hello, " + name + "!". Python's + works directly on strings.
  • C uses printf("Hello, %s!\n", name) with a format specifier.
  • TypeScript uses the same + concatenation as Auto, since JavaScript handles it natively.

Every language has its own way to combine strings, but the function structure — name, parameter, body — is universal.

Return Values ​

Functions don't just do things — they can also produce values. In Auto, a function returns the value of its last expression. No return keyword needed:

auto
fn add(a int, b int) {
    a + b
}

This function evaluates a + b and returns the result. The last expression in the function body is the return value. This is an expression-based design, inspired by languages like Rust.

You store the result in a variable, or pass it directly to another function:

auto
var result = add(3, 4)    // result is 7
print(add(10, 20))        // prints 30
Functions that return values without `return`

The key difference across languages is how they handle the return:

  • Auto and Rust both use expression-based bodies — the last expression is the return value, no return keyword needed. Rust does require an explicit return type annotation (-> i64), which the transpiler adds.
  • Python, C, and TypeScript all require the return keyword. The transpiler inserts it automatically.

This is one of Auto's design choices: expression-based functions reduce visual noise while keeping the semantics clear. The compiler always knows what a function returns by looking at the last expression.

Conditional Branching ​

Programs need to make decisions. The if keyword lets you run a block of code only when a condition is true:

auto
if n > 0 {
    print("positive")
}

You can chain conditions with else if and provide a fallback with else:

auto
if n > 0 {
    print("positive")
} else if n < 0 {
    print("negative")
} else {
    print("zero")
}

Auto evaluates the conditions from top to bottom. The first one that is true runs its block, and the rest are skipped. If none match, the else block runs.

The condition does not need parentheses. The curly braces are required.

Conditional branching with `if`

The structure is nearly identical across all five languages. The main differences are syntactic:

  • Python uses elif instead of else if, and colons + indentation instead of curly braces.
  • C requires parentheses around every condition and braces around every block.
  • Rust and TypeScript look almost exactly like Auto — parentheses are optional in Rust but required in TypeScript.

Looping with a Condition ​

Auto uses for for all loops. When you write for followed by a condition, it behaves like a while loop — it repeats as long as the condition is true:

auto
for count < 5 {
    print(count)
    count = count + 1
}

This reads naturally: "for as long as count is less than 5, keep going." Auto uses for instead of while as a deliberate design choice — one keyword for all loops keeps the language small.

When transpiled, the target languages use their native while keyword:

  • Rust: while count &lt; 5 { ... }
  • Python: while count &lt; 5: ...
  • C: while (count &lt; 5) { ... }
  • TypeScript: while (count &lt; 5) { ... }
Looping with a condition using `for`

This loop prints 0, 1, 2, 3, 4 — one number per line. Notice the pattern:

  1. Initialize a variable before the loop: var count = 0
  2. Check a condition at the top of each iteration: count &lt; 5
  3. Update the variable inside the loop body: count = count + 1

If you forget step 3, the loop runs forever. This is a common bug, which is why counted loops (next section) are preferred when you know the number of iterations in advance.

Counted Loops ​

When you know exactly how many times to repeat, use a counted loop with the .. range operator:

auto
for i in 0..5 {
    print(i)
}

This means "for each value i from 0 up to (but not including) 5, run the body." It prints 0, 1, 2, 3, 4.

The range 0..5 is exclusive on the right — it includes 0 but not 5. This matches Rust's range syntax. If you want 1 through 10, write 1..11.

The loop variable i is created automatically for each iteration. You don't declare it with var.

Counting with `for i in range`

Each target language expresses the same counted loop differently:

  • Rust uses the same 0..5 range syntax as Auto. The transpiler copies it directly.
  • Python converts 0..5 to range(0, 5) — Python's built-in function for generating integer sequences.
  • C expands the range into a classic for loop: for (long long i = 0; i &lt; 5; i++). The transpiler calculates the start, end, and step from the range.
  • TypeScript generates a similar classic for loop: for (let i = 0; i &lt; 5; i++).

Iterating Collections ​

You can also use for to iterate over the elements of a list (or any collection):

auto
var fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
    print(fruit)
}

Each iteration, the variable fruit takes on the next value from the list. This is the most readable way to process every item in a collection.

Iterating over a list

Each language iterates differently:

  • Rust iterates by reference with for fruit in &fruits, borrowing each element without taking ownership.
  • Python uses for fruit in fruits — clean and direct, the closest to Auto's syntax.
  • C falls back to an index-based loop: for (int i = 0; i &lt; fruits_len; i++). C has no built-in collection iterator, so the transpiler generates the index variable and length check automatically.
  • TypeScript uses for...of, which iterates over values (as opposed to for...in, which iterates over keys).

break and continue ​

Two keywords give you finer control inside loops:

  • break immediately exits the loop. No more iterations run.
  • continue skips the rest of the current iteration and jumps to the next one.
auto
for i < 10 {
    if i == 3 {
        continue    // skip printing 3
    }
    if i == 7 {
        break       // stop the loop entirely
    }
    print(i)
}
Controlling loop flow with `break` and `continue`

This program prints: 1, 2, 4, 5, 6.

Here's what happens step by step:

  1. i starts at 0, becomes 1 after the increment. Not 3, not 7. Prints 1.
  2. i becomes 2. Prints 2.
  3. i becomes 3. Hits continue. Skips print. Goes back to the top.
  4. i becomes 4. Prints 4.
  5. i becomes 5. Prints 5.
  6. i becomes 6. Prints 6.
  7. i becomes 7. Hits break. Loop ends immediately.

break and continue work in all four loop forms — for cond, for i in range, and for item in collection. They are universal loop controls.

Conditional Returns ​

Functions can return different values depending on a condition. In Auto, because functions are expression-based, you can write a conditional that evaluates to a value:

auto
fn is_even(n int) {
    if n % 2 == 0 {
        true
    } else {
        false
    }
}

Each branch of the if is an expression. The function returns whichever branch is taken — true or false. This works because the last expression in the function body is the return value, and the if/else itself is an expression.

Returning early from a function

This prints:

true
false

The transpiler handles the boolean return differently in each language:

  • Auto and Rust both treat if/else as an expression. No return keyword needed in either language (though the Rust output adds the -&gt; bool return type).
  • Python needs return True and return False — Python's if is a statement, not an expression, so return is mandatory.
  • C needs return true and return false, and prints booleans via a ternary operator since printf has no %bool specifier.
  • TypeScript needs return true and return false, and uses === for the strict equality check.

This pattern — an if/else where each branch is a value — is common in Auto. You'll see it everywhere, from simple checks like is_even to more complex logic that chooses between calculations.

Summary ​

In this chapter, you learned how to structure programs with functions and control flow:

  • Functions are defined with fn, take typed parameters, and return the value of their last expression. No return keyword needed in Auto.
  • Return values are inferred by the compiler. Expression-based bodies keep the code concise.
  • Conditional branching uses if, else if, and else. Conditions don't need parentheses. Braces are required.
  • Conditional loops use for cond { ... } — Auto's equivalent of while. The transpiler outputs while in every target language.
  • Counted loops use for i in 0..n { ... } with an exclusive range. The transpiler converts to each language's native syntax.
  • Collection iteration uses for item in list { ... } to process each element.
  • break and continue give you fine-grained loop control — exit early or skip an iteration.
  • Conditional returns let if/else branches produce the function's return value directly.

With functions and control flow, you can now write programs that make decisions, repeat work, and organize code into reusable pieces. In the next chapter, we'll look at how to group related data together using structs.

Released under the MIT License.