Skip to content

Language Guide: A Tour of Sushi

← Back to Documentation

This guide provides a friendly tour of Sushi's features. If you're new to Sushi, start here before diving into the detailed Language Reference.

Table of Contents

Hello World

Every Sushi program starts with a main function that serves as the entry point for execution:

fn main() i32:
    println("Mostly Harmless")
    return Result.Ok(0)

Key points: - fn declares a function - i32 is the return type (32-bit integer) - All functions actually return Result<T> - this is Sushi's approach to explicit error handling (more on this later) - println outputs text with a newline to standard output - The return Result.Ok(0) convention indicates successful program termination (exit code 0)

The main function must return an i32 (which the operating system uses as the exit code), and like all Sushi functions, it must explicitly wrap this value in Result.Ok() to indicate successful execution.

Variables and Types

Declaration and Rebinding

Sushi uses explicit variable declarations with type annotations for clarity and compile-time safety:

fn main() i32:
    # Declare with let
    let i32 answer = 42
    let string question = "Ultimate Question"

    # Rebind with :=
    answer := 54  # Wrong answer!

    return Result.Ok(0)

Important: Variables must be declared with let before you can rebind them with :=. This two-step approach makes it clear when a variable is first introduced versus when its value is being changed.

The declaration syntax follows the pattern let <type> <name> = <value>, where the type comes before the variable name. This makes it easy to scan code and immediately see what types are being used. Once declared, you can reassign values using the := operator, which indicates mutation rather than declaration.

Numeric Types

Sushi has explicit numeric types:

fn main() i32:
    # Signed integers
    let i8 tiny = 127
    let i32 normal = 2147483647
    let i64 huge = 9223372036854775807

    # Unsigned integers
    let u8 byte = 255
    let u32 unsigned = 4294967295

    # Floating-point
    let f32 pi = 3.14
    let f64 precise = 3.141592653589793

    return Result.Ok(0)

Type Conversion

All conversions must be explicit with as:

fn main() i32:
    let i32 x = 42
    let f64 y = x as f64      # int to float
    let u32 z = y as u32      # float to unsigned int

    return Result.Ok(0)

Strings

Strings have full UTF-8 support:

use <collections/strings>

fn main() i32:
    let string text = "Don't Panic"
    let string emoji = "🐬 🌍"

    println(text)
    println("Length: {text.len()}")     # Character count
    println("Size: {text.size()}")      # Byte count

    return Result.Ok(0)

String methods: - .len() -> i32 - Character count (UTF-8 aware) - .size() -> i32 - Byte count - .is_empty() -> bool - Check if string is empty - .find(string) -> Maybe<i32> - Find substring position - .split(string) -> string[] - Split into array by delimiter - .trim() -> string - Remove leading/trailing whitespace - .to_upper() -> string - Convert to uppercase (ASCII only) - .to_lower() -> string - Convert to lowercase (ASCII only)

See Standard Library: String Methods for detailed documentation.

String Interpolation

Embed expressions in strings with {...}:

fn main() i32:
    let i32 answer = 42
    let string name = "Arthur"

    println("Hello {name}, the answer is {answer}")
    println("Next answer: {answer + 1}")

    return Result.Ok(0)

Functions and Returns

Functions are the building blocks of Sushi programs. Every function follows a consistent pattern: explicit parameter types, explicit return type, and mandatory Result<T> wrapping for all returns.

Basic Functions

fn add(i32 a, i32 b) i32:
    return Result.Ok(a + b)

fn greet(string name) ~:  # ~ is "blank" type (no return value)
    println("Hello, {name}!")
    return Result.Ok(~)

fn main() i32:
    let i32 sum = add(5, 7).realise(0)
    greet("Ford")
    return Result.Ok(0)

Function syntax breakdown: - fn keyword declares a function - Parameters: type name pairs, comma-separated - Return type: comes after the parameter list - ~ ("blank" or "unit" type): used for functions that don't return a meaningful value - Body: indented block following the colon - Returns: must be Result.Ok(value) or Result.Err()

The blank type (~): When a function performs an action but doesn't produce a value (like printing or modifying a reference), it returns ~. You must still wrap it: return Result.Ok(~). This maintains consistency with Sushi's error handling model.

Multiple Parameters

fn describe(string name, i32 age, bool friendly) ~:
    println("{name} is {age} years old")
    if (friendly):
        println("They're quite friendly!")
    return Result.Ok(~)

fn main() i32:
    describe("Zaphod", 200, false)
    return Result.Ok(0)

Parameter passing: - Primitives and strings: Passed by copy (cheap, no ownership transfer) - Owning types (T[], List<T>, Own<T>): Passed by move — ownership transfers to the callee, which frees the value at scope exit; using the source afterward is CE2405 (use-after-move) - Structs: Passed by copy; a struct that owns heap memory (a dynamic-array field) is deep-copied so the caller and callee own independent buffers - References: Use &peek T for read-only or &poke T for read-write references without transferring ownership

If you need to keep using an owning value after passing it to a function, either pass a reference (&peek i32[] for read-only or &poke i32[] for modification) or explicitly clone it (.clone()) before passing. (One special case: main's string[] args is a borrowed view of the process argv — borrow it downstream, never move it by value.)

Control Flow

If-Elif-Else

fn main() i32:
    let i32 panic_level = 7

    # Parentheses required around conditions
    if (panic_level > 10):
        println("Serious panic!")
    elif (panic_level > 5):
        println("Mild panic")
    else:
        println("Stay calm")

    return Result.Ok(0)

While Loops

fn main() i32:
    let i32 countdown = 5

    while (countdown > 0):
        println("T-minus {countdown}")
        countdown := countdown - 1

    println("Liftoff!")
    return Result.Ok(0)

For-Each Loops

fn main() i32:
    let string[] names = from(["Arthur", "Ford", "Trillian"])

    foreach(name in names.iter()):
        println("Passenger: {name}")

    return Result.Ok(0)

Break and Continue

fn main() i32:
    let i32 x = 0

    while (x < 10):
        x := x + 1

        if (x == 3):
            continue  # Skip 3

        if (x == 8):
            break  # Stop at 8

        println(x)

    return Result.Ok(0)

Error Handling

Result

Sushi uses Result<T> as its fundamental approach to error handling. Every function in Sushi implicitly returns a Result<T> type, even if you declare the return type as just T. This design choice eliminates entire classes of bugs by making error handling explicit and impossible to ignore.

The Philosophy: In many languages, functions can fail silently or throw exceptions that might not be handled. Sushi takes a different approach inspired by Rust: if a function can fail, that failure is encoded in the type system. You must explicitly choose to handle errors or propagate them.

fn divide(i32 a, i32 b) i32:
    if (b == 0):
        return Result.Err(StdError.Error)  # Error case
    return Result.Ok(a / b)  # Success case

fn main() i32:
    let Result<i32, StdError> result = divide(42, 6)

    # Check if successful
    if (result):
        let i32 value = result.realise(0)
        println("Result: {value}")
    else:
        println("Division failed")

    return Result.Ok(0)

Key Concepts: - When you declare a function returning i32, it actually returns Result<i32> - Success values must be wrapped: return Result.Ok(value) - Failures are signaled with: return Result.Err(StdError.Error) - You can use Result values directly in conditionals: if (result) checks for success - The .realise(default) method extracts the value, using the default if the result is an error

This approach eliminates null pointer exceptions and ensures that error cases are always visible in the code. The compiler enforces that you handle or propagate errors - you cannot accidentally ignore them.

Unwrapping with .realise()

The .realise(default) method provides a safe way to extract values from Result<T>, similar to unwrapping in other languages but with a mandatory fallback value:

fn get_value() i32:
    return Result.Ok(42)

fn main() i32:
    # Unwrap with default value
    let i32 x = get_value().realise(0)
    println("Value: {x}")

    return Result.Ok(0)

The name "realise" reflects the operation of "making the value real" by extracting it from the Result wrapper. Unlike unsafe unwrap operations in other languages, .realise() always requires a default value, ensuring your code never crashes from unwrapping an error state.

Common patterns: - Numeric defaults: .realise(0) or .realise(-1) - String defaults: .realise("") for empty strings - Boolean defaults: .realise(false) for failure states - Using the default to indicate error: let i32 result = compute().realise(-1) where -1 signals failure

Error Propagation (?? operator)

The ?? operator provides elegant error propagation - it unwraps successful results and automatically returns errors to the caller. This is one of Sushi's most powerful features for writing clean error-handling code:

use <io/files>

fn read_config() string | FileError:
    # If open fails, ?? returns Err immediately
    let file f = open("config.txt", FileMode.Read())??

    # Only reaches here if open succeeded
    let string content = f.read()
    f.close()
    return Result.Ok(content)

fn main() i32:
    let Result<string, FileError> config = read_config()
    # Handle config...
    return Result.Ok(0)

How it works: 1. If the Result is Ok(value), ?? extracts and returns the value 2. If the Result is Err(), ?? immediately returns Result.Err() from the current function 3. The error propagates up the call stack until someone handles it

RAII Safety: The ?? operator is fully integrated with Sushi's RAII (Resource Acquisition Is Initialization) system. When an error is propagated, all resources in the current scope are properly cleaned up before the function returns. This means you never leak memory or file handles when errors occur.

Advantages over manual error checking:

# Without ??: verbose and error-prone
fn process() string:
    let Result<file> f_result = open("data.txt", FileMode.Read())
    if (not f_result):
        return Result.Err()
    let file f = f_result.realise(...)  # How do we get a default file?
    # ... more manual checks

# With ??: clean and safe
fn process() string:
    let file f = open("data.txt", FileMode.Read())??
    let string data = f.read()
    return Result.Ok(data)

The ?? operator makes error handling code read almost like non-error-handling code, while maintaining full safety and explicit error propagation.

Maybe for Optional Values

Maybe<T> is Sushi's type-safe way to represent values that might or might not exist. It replaces the dangerous practice of using sentinel values (like -1, null, or empty strings) to indicate "no value". With Maybe<T>, the absence of a value is encoded in the type system, making it impossible to forget to check.

The Problem with Sentinel Values: In many languages, you might return -1 to indicate "not found" or use null to mean "no value". This leads to bugs because: - The sentinel value might be a valid result (what if -1 is actually in your data?) - You can forget to check for the sentinel value - Different functions might use different sentinel values

The Maybe Solution: Maybe<T> has two variants: - Maybe.Some(value) - contains a value of type T - Maybe.None() - represents the absence of a value

fn find_first_even(i32[] numbers) Maybe<i32>:
    foreach(n in numbers.iter()):
        if (n % 2 == 0):
            return Result.Ok(Maybe.Some(n))
    return Result.Ok(Maybe.None())

fn main() i32:
    let i32[] data = from([1, 3, 5, 8])
    # Functions return Result<T>, so find_first_even returns Result<Maybe<i32>>
    let Result<Maybe<i32>, StdError> result = find_first_even(data)

    match result:
        Result.Ok(found) ->
            match found:
                Maybe.Some(value) ->
                    println("Found: {value}")
                Maybe.None() ->
                    println("No even numbers")
        Result.Err(_) ->
            println("Search failed")

    return Result.Ok(0)

Key Methods on Maybe: - .is_some() - returns true if the Maybe contains a value - .is_none() - returns true if the Maybe is empty - .realise(default) - extracts the value or returns the default if None - .expect(message) - extracts the value or terminates with an error message

Common Use Cases: - Search operations (return Maybe.Some(index) if found, Maybe.None() if not) - Optional configuration values (return Maybe.Some(config) or Maybe.None()) - Nullable references in data structures (use Maybe<T> instead of trying to represent null) - HashMap lookups (.get() returns Maybe<V> since the key might not exist)

Composing Maybe with Result: Since functions return Result<T>, you often see Result<Maybe<T>> - a result that might be an error, or might be a success with an optional value. The type system helps you handle all cases correctly.

Collections

Sushi provides multiple collection types, each optimized for different use cases. Understanding when to use each type is key to writing efficient code.

Arrays

Sushi has two array types: fixed-size arrays allocated on the stack, and dynamic arrays that can grow and shrink:

fn main() i32:
    # Fixed-size array
    let i32[3] fixed = [1, 2, 3]

    # Dynamic array
    let i32[] dynamic = from([1, 2, 3])
    dynamic.push(4)
    dynamic.push(5)

    println("Length: {dynamic.len()}")

    # Iteration
    foreach(n in dynamic.iter()):
        println(n)

    return Result.Ok(0)

Fixed vs Dynamic: - Fixed arrays (T[N]): Size known at compile time, allocated on the stack, cannot be resized. Fast and lightweight. - Dynamic arrays (T[]): Size can change at runtime, heap-allocated, supports push/pop operations. The from([...]) function converts a fixed array literal to a dynamic array.

Array methods: .len(), .get(), .push(), .pop(), .clone(), .iter(), .hash()

Memory Management: Dynamic arrays use RAII - they're automatically deallocated when they go out of scope. The destructor recursively cleans up all elements, so arrays of structs or nested arrays are properly freed. Arrays use move semantics: when you pass a dynamic array to a function, ownership transfers unless you explicitly .clone() it.

List

List<T> is a generic growable collection that provides more flexibility than raw dynamic arrays. It's similar to Vec<T> in Rust or ArrayList<T> in Java:

fn main() i32:
    let List<string> passengers = List.new()

    passengers.push("Arthur")
    passengers.push("Ford")
    passengers.push("Trillian")

    println("Passengers: {passengers.len()}")

    let Maybe<string> first = passengers.get(0)
    match first:
        Maybe.Some(name) -> println("First: {name}")
        Maybe.None() -> println("Empty list")

    return Result.Ok(0)

Key Features: - Generic: Works with any type - List<i32>, List<string>, List<MyStruct>, etc. - Automatic growth: Capacity doubles when full, giving amortized O(1) push operations - Safe access: .get() returns Maybe<T> instead of crashing on invalid indices - Efficient: Uses llvm.memmove for shifting elements during insert/remove operations

List methods: - Creation: .new(), .with_capacity(n) - Access: .get(index), .len(), .capacity(), .is_empty() - Modification: .push(value), .pop(), .insert(index, value), .remove(index), .clear() - Memory: .reserve(additional), .shrink_to_fit(), .free(), .destroy() - Iteration: .iter(), .debug()

When to use List vs raw arrays: Use List<T> when you need frequent insertions/removals at arbitrary positions, capacity management, or want the additional safety of Maybe<T> returns. Use raw dynamic arrays (T[]) for simpler use cases where you just need push/pop at the end.

HashMap

HashMap<K, V> provides O(1) average-case key-value lookups using open addressing with linear probing:

use <collections/hashmap>

fn main() i32:
    let HashMap<string, i32> ages = HashMap.new()

    ages.insert("Arthur", 42)
    ages.insert("Ford", 200)
    ages.insert("Trillian", 30)

    let Maybe<i32> age = ages.get("Arthur")
    match age:
        Maybe.Some(a) -> println("Arthur is {a}")
        Maybe.None() -> println("Not found")

    return Result.Ok(0)

Implementation Details: - Open addressing: Uses linear probing instead of chaining, giving better cache locality - Automatic resizing: Grows at 0.75 load factor to maintain performance - Power-of-two capacity: Allows fast modulo using bitwise AND operations - Auto-derived hashing: Any type with a .hash() method can be used as a key

HashMap methods: .new(), .insert(key, value), .get(key), .remove(key), .contains_key(key), .len(), .keys(), .values(), .entries(), .debug(), .free()

Memory Management: HashMaps use recursive destruction - when you call .free() or when the HashMap goes out of scope, it destroys all entries and their contents. This works correctly even for complex value types like structs containing arrays or nested enums.

Limitations: Keys must implement .hash() (auto-derived for most types). Storing an owning value that contains heap memory (a struct or enum with a dynamic-array field, List<T>, or Own<T>) currently crashes at runtime on get/free (issue #140); use a copyable value type, or hold such data outside the map for now.

Structs and Enums

Structs and enums are Sushi's primary ways to create custom data types. Structs group related data together, while enums represent types that can be one of several variants.

Defining Structs

Structs are product types - they contain multiple fields simultaneously:

struct Person:
    string name
    i32 age
    bool friendly

fn main() i32:
    let Person arthur = Person(name: "Arthur", age: 42, friendly: true)

    println("{arthur.name} is {arthur.age} years old")

    # Modify fields
    arthur.age := 43

    return Result.Ok(0)

Key features: - Named field initialization: Use StructName(field: value, ...) syntax for clarity and order-independence - Positional also supported: Person("Arthur", 42, true) for brevity - Named parameters prevent "boolean trap" bugs in structs with multiple bool fields - Cannot mix positional and named (all-or-nothing) - Field access: Use dot notation struct.field to read or modify fields - RAII cleanup: When a struct goes out of scope, all its fields are recursively destroyed - Auto-derived hashing: Structs automatically get a .hash() method combining all field hashes

Common patterns: - Data transfer objects: Group related data for passing between functions - Configuration: Struct fields for application settings - Complex state: Multiple related values that belong together

Defining Enums

Enums are sum types - a value can be exactly one variant at a time:

enum Status:
    Ready()
    Working(i32)
    Done()

fn main() i32:
    let Status current = Status.Working(75)

    match current:
        Status.Ready() ->
            println("Ready to start")
        Status.Working(progress) ->
            println("Progress: {progress}%")
        Status.Done() ->
            println("Completed")

    return Result.Ok(0)

Key features: - Variants with data: Each variant can contain different types of associated data - Type safety: The compiler ensures you handle all variants through exhaustive pattern matching - Memory efficient: Enums use a discriminant tag plus space for the largest variant - RAII for complex variants: If a variant contains a struct or array, it's properly cleaned up

How enums are implemented: Sushi enums compile to a struct containing: 1. A discriminant (tag) indicating which variant is active 2. Storage for the variant data (union of all variant types)

When you pattern match, the compiler generates a switch on the discriminant, then extracts and interprets the variant data appropriately.

Common use cases: - State machines: Represent different states with different associated data - Error types: Different error variants with relevant information - Optional complex data: Use Maybe<T> (which is an enum) for values that might not exist - Algebraic data types: Build sophisticated recursive data structures

Pattern Matching

Pattern matching is Sushi's way of deconstructing enums and handling different cases. The compiler enforces exhaustiveness checking - you must handle all possible variants, ensuring you never forget a case.

Match Expressions

enum Response:
    Success(i32)
    Error(string)

fn handle(Response resp) ~:
    match resp:
        Response.Success(code) ->
            println("Success code: {code}")
        Response.Error(msg) ->
            println("Error: {msg}")

    return Result.Ok(~)

fn main() i32:
    handle(Response.Success(200))
    handle(Response.Error("Not found"))
    return Result.Ok(0)

How matching works: 1. The match keyword examines an enum value 2. Each arm pattern specifies a variant: EnumName.VariantName(bindings) 3. If the pattern matches, the associated data is extracted and bound to variables 4. The code after -> executes for the matching pattern 5. The compiler verifies all variants are covered

Exhaustiveness checking: If you add a new variant to Response, any match expressions that don't handle it will cause compilation errors. This prevents bugs from forgetting to handle new cases.

Nested Patterns

Patterns can be nested to match complex enum structures in a single expression:

fn handle_file(FileResult result) ~:
    match result:
        FileResult.Ok(f) ->
            println("File opened successfully")
        FileResult.Err(FileError.NotFound()) ->
            println("File not found")
        FileResult.Err(FileError.PermissionDenied()) ->
            println("Permission denied")
        FileResult.Err(_) ->
            println("Other error")

    return Result.Ok(~)

Nested pattern matching: The pattern FileResult.Err(FileError.NotFound()) matches a FileResult whose Err variant contains a FileError enum with the NotFound variant. This lets you handle specific error combinations without nested match statements.

Wildcard patterns: The _ pattern matches anything, acting as a catch-all for remaining cases. It's useful for handling "all other errors" or "default" cases.

Zero-cost compilation: Pattern matching compiles to efficient jump tables or switch statements. There's no runtime overhead compared to hand-written if-else chains or switch statements in C.

Generics

Sushi's generics system provides zero-cost abstraction through compile-time monomorphization. Generic types are instantiated automatically based on usage, generating specialized code for each concrete type combination.

Generic Structs

struct Pair<T, U>:
    T first
    U second

fn main() i32:
    let Pair<i32, string> data = Pair(first: 42, second: "answer")

    println("Number: {data.first}")
    println("Label: {data.second}")

    return Result.Ok(0)

How it works: The compiler detects that you use Pair<i32, string> and generates a specialized version of the struct for that type combination. There's no runtime overhead - the generated code is as efficient as if you'd written a separate struct manually.

Generic Nesting: Sushi fully supports nested generics like Result<Maybe<T>>, List<Pair<K, V>>, or HashMap<string, List<i32>>. The type system correctly handles arbitrarily deep nesting.

Extension Methods

Extension methods let you add functionality to existing types without modifying their definitions. This is similar to extension methods in C#, Kotlin, or Swift, and is inspired by Rust's trait system. In Sushi, this is called UFCS (Uniform Function Call Syntax).

extend i32 squared() i32:
    return self * self

extend string shout() string:
    return "{self}!!!"

fn main() i32:
    let i32 x = 7
    println("Squared: {x.squared()}")

    let string msg = "Don't Panic"
    println(msg.shout())

    return Result.Ok(0)

How Extension Methods Work: 1. You define a function with extend <type> <method_name>(...) <return_type> 2. The first implicit parameter is self, referring to the value you're calling the method on 3. The call x.squared() is transformed at compile-time into squared(x) 4. This transformation is zero-cost - there's no runtime overhead

Generic Extension Methods:

struct Box<T>:
    T value

extend Box<T> unwrap() T:
    return self.value

fn main() i32:
    let Box<i32> b = Box(42)
    let i32 value = b.unwrap()  # Uses generic extension
    println("Unwrapped: {value}")
    return Result.Ok(0)

Extension methods can be generic over user-defined generic structs: the type parameter (T) is declared on the receiver type (Box<T>), and the compiler automatically instantiates the method for each concrete type used in your program. (Generic extensions on built-in generic types such as List<T> are not currently supported.)

Benefits: - Namespace organization: Group related functionality with the types they operate on - Discoverability: Methods appear natural on the type, making APIs easier to explore - Chainability: Method syntax enables fluent chaining: list.first().realise(0) - Zero cost: Compiles to the same code as a regular function call

Standard Library Use: The entire Sushi standard library is built using extension methods. String methods like .len(), .find(), and .split() are all extensions. Collection methods on List<T> and array methods are extensions. This means you can add your own methods using the same mechanism the standard library uses.

Memory Management

Sushi provides memory safety without garbage collection through a combination of RAII (Resource Acquisition Is Initialization), compile-time borrow checking, and move semantics. These features work together to prevent common memory bugs while maintaining C-like performance.

Borrowing, aka References

References allow functions to access values without taking ownership. Sushi has two borrow modes:

  • &peek T - Read-only borrow (multiple allowed simultaneously)
  • &poke T - Read-write borrow (exclusive access)

Sushi's borrow checker ensures that references are always valid and that aliasing rules are enforced at compile time:

fn increment(&poke i32 counter) ~:
    counter := counter + 1
    return Result.Ok(~)

fn display(&peek i32 value) ~:
    println("Value: {value}")
    return Result.Ok(~)

fn main() i32:
    let i32 count = 0
    increment(&poke count)
    increment(&poke count)
    display(&peek count)  # Read-only access
    println("Count: {count}")  # Prints: Count: 2

    return Result.Ok(0)

Borrowing Rules: - Multiple &peek allowed: You can have multiple read-only borrows of the same variable simultaneously - One &poke at a time: Mutable borrows require exclusive access - Cannot mix modes: Cannot have &peek and &poke borrows of the same variable at the same time - &poke coerces to &peek: A mutable reference can be passed where a read-only reference is expected - References must be valid: The borrow checker ensures references never outlive the data they point to - Zero cost: References compile to simple pointers with no runtime overhead

What references allow: - Reading function arguments without copying (use &peek) - Modifying function arguments in-place without copying large structures (use &poke) - Building efficient data structures that reference existing data - Avoiding expensive clones when you just need to read or modify a value

Example - avoiding copies:

struct LargeData:
    i32[1000] values

fn process(&poke LargeData data) ~:
    # Can access and modify data.values without copying 4000 bytes
    data.values[0] := 42
    return Result.Ok(~)

fn read_only(&peek LargeData data) i32:
    # Read-only access - cannot modify
    return Result.Ok(data.values[0])

The borrow checker runs at compile time (Pass 3 in the semantic analysis pipeline), so there's no runtime cost to these safety guarantees.

RAII (Automatic Cleanup)

RAII (Resource Acquisition Is Initialization) is Sushi's core memory management strategy. When a value goes out of scope, its destructor automatically runs, freeing any resources it owns. This applies recursively to nested structures:

struct Buffer:
    string[] lines

fn process() ~:
    let Buffer buf = Buffer(lines: from([]))
    buf.lines.push("Line 1")
    buf.lines.push("Line 2")
    # buf and buf.lines automatically destroyed when function returns
    return Result.Ok(~)

fn main() i32:
    process()
    return Result.Ok(0)

How RAII works in Sushi: 1. When a variable goes out of scope (end of function, end of block, error propagation with ??), its destructor runs 2. For structs, the destructor recursively destroys each field 3. For arrays, the destructor recursively destroys each element 4. For enums, the destructor examines the discriminant and destroys the active variant's data 5. Primitives and strings require no cleanup

The Recursive Destructor: Sushi's backend implements a general emit_value_destructor() function that handles cleanup for all types. It recursively traverses your data structures: - Structs with arrays: The array fields are freed, then the struct itself - Arrays of structs: Each struct is destroyed, then the array storage is freed - Enums with complex data: The discriminant determines which variant is active, then that variant's data is cleaned up - Nested collections: List<HashMap<string, List<i32>>> correctly frees all levels

Integration with error handling: RAII is crucial for the ?? operator. When an error is propagated, all variables in the current scope are destroyed before returning, preventing resource leaks even in error paths.

Benefits: - No manual memory management - no free() calls needed (except for explicit .free() on collections when you want early cleanup) - No memory leaks - resources are always freed when they go out of scope - Exception safety - errors can't cause resource leaks - Deterministic cleanup - you know exactly when destructors run

Own for Heap Allocation

Own<T> is Sushi's type for heap-allocated, owned values. It's essential for building recursive data structures like linked lists and trees, where a struct needs to contain an optional instance of itself:

struct Point:
    i32 x
    i32 y

# Recursive struct: the optional next pointer is Maybe<Own<Node>>
struct Node:
    i32 value
    Maybe<Own<Node>> next

fn main() i32:
    # Allocate a Point on the heap, then dereference it
    let Own<Point> ptr = Own.alloc(Point(10, 20))
    let Point loaded = ptr.get()
    println("Point: ({loaded.x}, {loaded.y})")

    # A linked-list node; next is an optional owned pointer
    let Node head = Node(1, Maybe.None())
    println("Head: {head.value}")

    if (head.next.is_some()):
        println("List has more nodes")
    else:
        println("End of list")

    return Result.Ok(0)

Why Own exists: Without Own<T>, you cannot create recursive types because the compiler needs to know the size of every struct at compile time. A Node containing another Node would have infinite size. Own<T> breaks the cycle by storing a pointer (fixed size) to heap-allocated data.

Own methods: - Own.alloc(value) - Allocates value on the heap and returns an Own<T> - .get() - Returns the contained value - .destroy() - Explicitly frees the heap memory (RAII calls this automatically)

Own<T> itself is always populated. To model an optional owned pointer (such as the next field of a list node), wrap it in Maybe<Own<T>> and use Maybe.Some(...) / Maybe.None() together with .is_some() / .is_none().

Memory management with Own: Own<T> integrates with RAII - when an Own<T> goes out of scope, it automatically calls .destroy() on the contained value, freeing the heap memory. This means recursive structures are properly cleaned up when they go out of scope, even if they're deeply nested.

Common patterns: - Linked lists: Each node owns the next node - Trees: Each node owns its children - Optional heap data: Use Own<T> instead of nullable pointers for optional heap-allocated data

Next Steps

This guide covered the basics. For more details:


Previous: Getting Started | Next: Language Reference