Nitro Language Documentation

Nitro is a modern, statically typed compiled language designed for performance and simplicity. It combines familiar syntax patterns from languages like Rust, Kotlin, and TypeScript with a focus on readable, maintainable code.

Key Features

  • Static typing with type inference for better reliability
  • Familiar syntax inspired by modern programming languages
  • Powerful type system with generics, options, and traits
  • Built-in collections no need to reinvent the wheel

Quick Start

Hello World

Here's your first Nitro program:

fun main() {
    println("Hello, World!")
}

Try Online

You can experiment with Nitro code in your browser using the Nitro Playground.

More Examples

Explore additional examples in the src/examples folder of the repository to see Nitro in action.

What's Next?

This documentation will guide you through:

  1. Getting Started - Basic syntax, variables, and language fundamentals
  2. Expressions and Operators - Working with values and operations
  3. Functions - Defining and calling functions, including generics
  4. Collections - Lists, maps, sets, and other data structures
  5. Types - Structs, options, enums, and the type system

Ready to learn Nitro? Start with Getting Started!

Getting Started

You can learn Nitro quickly, especially if you're familiar with modern programming languages like Rust, Kotlin, or TypeScript. This chapter covers the fundamental syntax and language features you need to start writing Nitro code.

Variables and Constants

Variables in Nitro are declared using the let keyword. By default, variables are mutable:

let name = "Alice"
let age = 25
let height = 5.6

age = 26 // Can be changed

Except for constants, when you use let on a global scope, it becomes a constant. Constants are immutable and must be initialized at declaration:

let counter = 0
// counter = counter + 1 // Compile time error: cannot assign to constant

Nitro has powerful type inference, so you often don't need to specify types explicitly. However, you can when needed:

let score: Int = 100
let message: String = "Hello, Nitro!"

Comments

Single line comments start with // and comment the rest of the line.

// This is a single line comment

Multi-line comments start with /* and end with */. They can span multiple lines.

/*
    This
    is
    a
    multi-line
    comment
*/

Nested multi-line comments are allowed.

/* 
 * This line opens a multi-line comment
 * Also /* nested comments are allowed */ the '* /' did not close the comment
 *
 */

Keywords and Identifiers

Reserved Keywords

These words are reserved and cannot be used as variable or function names:

this This fun let mod struct ret size_of option internal rec tag defer type_alias
enum nothing when match alias if else for in while repeat loop is as true false 
null include break continue use mut json! test! include_as_bytes! include_as_string!

Note: Some keywords like null are reserved for future use even though they're not currently part of the language.

Valid Identifiers

Identifiers name variables, functions, types, and other entities. Rules:

  • Must start with a letter (a-z, A-Z)
  • Can contain letters, digits, and underscores
  • Cannot start or end with an underscore
  • Are case-sensitive
foo           // ✓ Valid
fooBar        // ✓ Valid (camelCase)
BarFoo        // ✓ Valid (PascalCase)
foo_bar       // ✓ Valid (snake_case)
baz42         // ✓ Valid (with numbers)
ALL_CAPS      // ✓ Valid (constants)
_invalid      // ✗ Invalid (starts with underscore)
invalid_      // ✗ Invalid (ends with underscore)
42invalid     // ✗ Invalid (starts with number)

The language uses PascalCase for type names, snake_case for variables, constants, and functions.

Syntax Rules

Semicolons and Newlines

Semicolons are optional in Nitro, newlines serve the same purpose. Use semicolons when you want multiple statements on one line:

// These are equivalent:
let a = 1
let b = 2
let c = 3

// Same as:
let a = 1; let b = 2; let c = 3

Commas

Commas are optional in most contexts. Use them for clarity, especially in single-line collections:

// Single line - commas recommended
let numbers = [1, 2, 3, 4, 5]

// Multi-line with commas
let fruits = [
    "apple",
    "banana",
    "cherry",    // trailing comma is fine
]

// Multi-line without commas (also valid)
let colors = [
    "red"
    "green"
    "blue"
]

Omitting commas is allowed but not recommended outside DSLs (Domain Specific Languages).

For example, in a DSL like JSON:

json! {
    name: "Alice"
    age: 30
    is_student: false
}

That is valid Nitro code, and it will create a JSON object, isn't that neat?

Expressions and Operators

Expressions are the building blocks of Nitro programs - pieces of code that evaluate to a value. This chapter covers literals, operators, and how to combine them to create meaningful computations.

Literals

Literals are values written directly in your code. Nitro supports several types of literals:

Numbers

// Integers
let positive = 42
let negative = -23
let large = 1_000_000    // underscores for readability

// Floating point
let pi = 3.1416
let explicit_float = 5.0f
let scientific = 1e10    // 10,000,000,000

Strings and Characters

// Strings
let greeting = "Hello, world!"
let multiline = "
    This is a
    multi-line string
"
let raw_string = r#"This is a raw string with no escape sequences: \n"#

// Characters (Unicode)
let letter: Char = u"A"
let unicode_hex: Char = u"\x41"      // 'A' in hex
let unicode_full: Char = u"\u0041"   // 'A' in Unicode

// Characters ascii
let ascii_char: Int = a"A" // ASCII character 'A' (65 in decimal)
let ascii_hex: Int = a"\x41" // ASCII character 'A' in hex

Other Literals

// Boolean values
let is_ready = true
let is_finished = false

// Nothing (absence of value)
let empty = nothing

Function Calls

Functions are called using their name followed by arguments in parentheses:

// Built-in functions
println("Hello, world!")         // Print with newline
print("No newline")              // Print without newline

// Functions with multiple arguments
let result = add(5, 3)           // Custom function
let max_val = max(10, 20, 15)    // Variable arguments

Note: See the Functions chapter for details on defining your own functions.

Operators

Nitro provides a comprehensive set of operators for different types of operations:

Arithmetic Operators

let a = 10
let b = 3

let sum = a + b        // Addition: 13
let diff = a - b       // Subtraction: 7
let product = a * b    // Multiplication: 30
let quotient = a / b   // Division: 3
let remainder = a % b  // Reminder: 1

Note: '%' is the remainder operator, not modulo. For modulo behavior you can use a.modulo(b) that will always return a non-negative result.

Comparison Operators

let x = 5
let y = 10

let equal = x == y          // Equality: false
let not_equal = x != y      // Inequality: true
let less = x < y            // Less than: true
let greater = x > y         // Greater than: false
let less_equal = x <= y     // Less or equal: true
let greater_equal = x >= y  // Greater or equal: false
let ordering = x <=> y      // Value comparison: Ordering::Less, Ordering::Equals or Ordering::Greater

Logical Operators

let a = true
let b = false

let and_result = a && b     // Logical AND: false
let or_result = a || b      // Logical OR: true
let xor_result = a ^^ b     // Logical XOR: true
let not_result = !a         // Logical NOT: false

// Also valid
let and_result = a and b    // Logical AND: false
let or_result = a or b      // Logical OR: true
let xor_result = a xor b    // Logical XOR: true
let not_result = not a      // Logical NOT: false

Bitwise Operators

let x = 12  // Binary: 1100
let y = 10  // Binary: 1010

let and_bits = x & y        // Bitwise AND: 8 (1000) also x.bitwise_and(y)
let or_bits = x | y         // Bitwise OR: 14 (1110) also x.bitwise_or(y)
let xor_bits = x ^ y        // Bitwise XOR: 6 (0110) also x.bitwise_xor(y)
let left_shift = x << 2     // Left shift: 48
let right_shift = x >> 2    // Right shift: 3
let unsigned = x >>> 2      // Unsigned right shift: 3 (same as >> for positive numbers)

// There is no operator for bitwise NOT, but you can use the method:
let not_bits = x.bitwise_not()           // Bitwise NOT: -13 (0b11111111111111111111111111110011)

Assignment Operators

let value = 10

value += 5    // value = value + 5 -> 15
value -= 3    // value = value - 3 -> 12
value *= 2    // value = value * 2 -> 24
value /= 4    // value = value / 4 -> 6
value %= 3    // value = value % 3 -> 0

Special Operators

// Type checking and casting
let value = 42
let is_int = value is Int           // Type checking: true
let not_string = value !is String   // Negative type check: true
let as_int = value as Int           // Type casting, throws error if not possible

// Membership testing
let numbers = [1, 2, 3, 4, 5]
let contains = 3 in numbers         // Contains: true
let not_contains = 6 !in numbers    // Not contains: true

// Null coalescing (default values)
let optional_value: Optional<String> = None()
let result = optional_value ?? "default"  // Uses default if None

// Safe navigation operator (avoids null dereference)
let maybe_person: Optional<Person> = None()
let name = maybe_person?.name ?? "Unknown"  // Safe access, returns "Unknown" if person is None

// Early return operator, return if None(), unwrap if Some()
let definitly_a_person = maybe_person?

Precedence

Operators have a precedence that determines the order in which they are evaluated.

Operators sorted by precedence, from highest to lowest:

Prec.Operator
0()
1[] . ?. !! ? ??
2! - + (unary) and as is !is in !in
3* / %
4+ -
5..= ..<
6<< >> >>>
7&
8^
9|
10< > <= >=
11<=>
12== !=
13^^
14&&
15||
16= += -= *= /= %=
  • Precedence 0 (highest): Will evaluate first
  • Precedence 16 (lowest): Will be the last to evaluate
  • Operators in the same precedence level are evaluated left-to-right (left-associative)
  • Some operators cannot chain, such as += and -=, which require a single left-hand operand

Note: This precedence is subject to change in the future

String Interpolation

Nitro supports powerful string interpolation for embedding values and expressions directly in strings:

Basic Interpolation

let name = "Alice"
let age = 42

// Simple variable interpolation
let greeting = "Hello, $name!"

// Expression interpolation
let message = "You are $age years old"
let summary = "In 10 years, you'll be ${age + 10}"

Complex Expressions

Use ${} for complex expressions:

let numbers = [1, 2, 3, 4, 5]
let info = "The list has ${numbers.len} elements"

// Method calls
let text = "HELLO"
let formatted = "Lowercase: ${text.to_lowercase()}"

// Nested interpolation
let nested = "Result: ${"Value is ${42 * 2}"}"

Escape Sequences

let escaped = "Line one\nLine two\tTabbed"
let quote = "She said \"Hello!\""
let backslash = "Path: C:\\folder\\file.txt"
let dolar = "Price: \$100"

Ranges

Ranges represent a sequence of contiguous values efficiently by storing only the start and end values:

Creating Ranges

// Inclusive range (includes both endpoints)
let inclusive = 1..=10      // 1, 2, 3, ..., 9, 10

// Exclusive range (excludes the end)
let exclusive = 1..<10      // 1, 2, 3, ..., 8, 9

Note: The operators ..= and ..< can be implemented for any custom type by implementing magic functions.

Using Ranges

let range = 1..=10

// Check membership
println(5 in range)         // true
println(15 in range)        // false
println(10 in 1..<10)       // false (exclusive end)

// Iterate over ranges (in loops)
for i in 1..=5 {
    println(i)  // Prints 1, 2, 3, 4, 5
}

The Nothing Type

The nothing literal represents the absence of a value, similar to void or Unit in other languages:

// Functions that don't return a value
fun print_message() {
    println("This function returns nothing")
}  // Implicitly returns: nothing

// Explicit nothing return
fun do_something(): Nothing {
    println("Doing something...")
    return nothing
}

Important: Unlike null in other languages, nothing is a distinct type that cannot be mixed with other types. It is used to indicate that a function or expression intentionally does not produce a meaningful value.

The Never Type

The never type represents a situation that will never occur. It is used to make functions that will never return, for example, with an infinite loop or a critical error that will crash the program:

// Function that never returns
fun infinite_loop(): Never {
    loop {
        // Do something forever
    }
}

// Function that always crashes
fun print_and_exit(msg: String): Never {
    println(msg)
    runtime::exit(1)
}

Functions

Functions are the primary way to organize and reuse code in Nitro. They encapsulate behavior and can accept parameters, perform computations, and return values.

Definition and Calling

Basic Function Syntax

Functions are defined using the fun keyword:

// Function with parameters and return type
fun add(a: Int, b: Int): Int {
    return a + b
}

// Function without return value
fun greet(name: String) {
    println("Hello, $name!")
}

// Explicit Nothing return type (optional)
fun print_hello(): Nothing {
    println("Hello, world!")
}

Expression Functions

For simple functions, you can use the special expression syntax:

// Single expression function
fun square(x: Int): Int = x * x

// Important: if the type is not specified, it will be `Nothing`, causing unexpected behavior
fun double(x: Int) = x * 2  // Compile error

Note: This behavior is a limitation of the type inference. The return type must be known before inspecting the function body.

Calling Functions

Call functions by name with arguments in parentheses:

// Basic function calls
let result = add(5, 3)           // result = 8
greet("Alice")                   // prints "Hello, Alice!"
let squared = square(4)          // squared = 16

Module Functions

Functions defined in modules are called using the module path:

// Calling functions from specific modules
let builder = StringBuilder::new()              // Create new StringBuilder
let args = runtime::get_program_args()          // Get command line arguments
let file = runtime::fs::read_text("config.txt")  // Read file from filesystem module

The same applies to constants and types defined in modules.

Generics

Generic functions work with multiple types using type parameters. Type parameters are prefixed with #:

Explicit Generic Declaration

// Explicit type parameter declaration
fun get_value_in_box<#T>(box: Box<#T>): #T {
    return box.value
}

// Generic function with multiple type parameters
fun convert<#From, #To>(value: #From, converter: (#From) -> #To): #To {
    return converter(value)
}

Inferred Generic Parameters

Type parameters can be inferred when used in parameters or return types:

// Type parameter #T is inferred from usage
fun get_value_in_box(box: Box<#T>): #T {
    return box.value
}

// Multiple inferred type parameters
fun create_pair(first: #A, second: #B): Pair<#A, #B> {
    return Pair::of(first, second)
}

Using Generic Functions

let int_box = Box::new(42)
let string_box = Box::new("hello")

// Type inference at call site
let int_value = get_value_in_box(int_box)        // Returns Int
let string_value = get_value_in_box(string_box)  // Returns String

// Explicit type specification (when needed)
let pair = create_pair<String, Int>("age", 25)

Parameters

Function Parameters

Functions can accept multiple parameters with different types:

// Multiple parameters
fun calculate_area(width: Float, height: Float): Float {
    return width * height
}

// Parameters with different types
fun format_message(template: String, count: Int, is_urgent: Boolean): String {
    let urgency = if is_urgent { "URGENT: " } else { "" }
    return "$urgency$template (count: $count)"
}

Return Values

Functions can return values using the return keyword, or implicitly return the last expression:

// Explicit return
fun add_explicit(a: Int, b: Int): Int {
    return a + b
}

// Implicit return (last expression)
fun add_implicit(a: Int, b: Int): Int {
    a + b  // This value is returned
}

// Early return
fun safe_divide(a: Float, b: Float): Optional<Float> {
    if b == 0.0 {
        return None()  // Early return
    }
    Some(a / b)
}

Magic Functions

The language will search the project for functions with specific names to handle certain operations:

Operator Overloading

You can define functions to overload operators like +, -, *, etc.

OperatorFunction NameParametersReturn Type
+plusAnyType, AnyTypeAnyType
-minusAnyType, AnyTypeAnyType
*mulAnyType, AnyTypeAnyType
/divAnyType, AnyTypeAnyType
%remAnyType, AnyTypeAnyType
<=>get_orderingAnyType, AnyTypeOrdering
==is_equalsAnyType, AnyTypeBoolean
^^logical_xorAnyType, AnyTypeBoolean
&&logical_andAnyType, AnyTypeBoolean
||logical_orAnyType, AnyTypeBoolean
!logical_notAnyTypeBoolean
[]getAnyType, IntAnyType
[]!!unsafe_getAnyType, IntAnyType
a[i] =setAnyType, Int, AnyTypeNothing
a[] =addAnyType, AnyTypeNothing
..<range_up_toAnyType, AnyTypeAnyType
..=range_toAnyType, AnyTypeAnyType
<<bitwise_shift_leftAnyType, IntAnyType
>>bitwise_shift_rightAnyType, IntAnyType
>>>bitwise_shift_right_unsignedAnyType, IntAnyType
&bitwise_andAnyType, AnyTypeAnyType
^bitwise_xorAnyType, AnyTypeAnyType
| bitwise_orAnyType, AnyTypeAnyType
?is_returnable_errorAnyTypeBoolean
!!get_or_crashAnyTypeAnyType
for loopto_iteratorAnyTypeAnyType
for loopnext_itemAnyTypeOptional<AnyType>

Example of Operator Overloading

// Overloading the + operator for a custom type
fun Vec2.add(other: Vec2): Vec2 = Vec2::new(this.x + other.x, this.y + other.y)

The only requirement is that the function must be named according to the operator it overloads, and it should accept the appropriate parameters.

Getters and Setters

You can call any getter or setter function using the property syntax:

struct Person { ... }

fun Person.get_full_name(): String = "$first_name $last_name"

fun Person.set_full_name(name: String) {
    let parts = name.split(" ")
    first_name = parts[0]
    last_name = parts[1]
}

person.full_name  // Calls get_full_name()
person.full_name = "John Doe"  // Calls set_full_name("John Doe")

Future Features

Default Arguments: Currently not supported but planned for a future version.

// This will be supported in the future:
// fun greet(name: String, greeting: String = "Hello") { ... }

greet("Alice")  // Prints "Hello, Alice!"

Collections

Nitro provides several built-in collection types for storing and organizing data. Each collection type is optimized for different use cases and access patterns.

Lists

Lists are ordered, mutable collections that can grow and shrink dynamically.

Creating Lists

// Empty list
let empty_list = List::new<Int>()

// List with initial values
let numbers = [1, 2, 3, 4, 5]
let fruits = ["apple", "banana", "cherry"]

// Mixed types not allowed
let mixed = [1, "hello"] // Compile error: Mismatched types Int and String

Accessing Elements

let numbers = [10, 20, 30, 40, 50]

// Safe indexing (returns Optional)
let first: Optional<Int> = numbers[0]      // Some(10)
let invalid: Optional<Int> = numbers[10]   // None

// Unsafe indexing (crashes if out of bounds)
let first_value = numbers[0]!!             // 10
let last_value = numbers[numbers.len - 1]!! // 50

// Convenience methods
let first_safe = numbers.first()           // Optional<Int>
let last_safe = numbers.last()             // Optional<Int>

Modifying Lists

let my_list = [1, 2, 3]

// Add elements
my_list[] = 4              // Append: [1, 2, 3, 4]
my_list.add(5)             // Same as above: [1, 2, 3, 4, 5]
my_list.insert(2, 0)       // Insert at index: [1, 2, 0, 3, 4, 5]

// Modify elements
my_list[1] = 10            // [0, 10, 2, 3, 4, 5]

// Remove elements
let removed = my_list.remove_at(1)!!  // Removes and returns 10
let last = my_list.remove_last()!!    // Removes and returns last element (5)
let first = my_list.remove_first()!!  // Removes and returns first element (0)
my_list.clear()                       // Empty list: []

List Operations

let numbers = [1, 2, 3, 4, 5]

// Check membership
let contains_three = 3 in numbers      // true
let not_contains_ten = 10 !in numbers  // true

// Size and properties
let size = numbers.len                 // 5
let is_empty = numbers.is_empty()      // false
let opposite = numbers.is_not_empty()  // true

let first_index = numbers.index_of(3)        // Some(2) (returns index of first occurrence)
let last_index = numbers.last_index_of(4)    // Some(3) (returns index of last occurrence)

// Iteration
for number in numbers {
    println(number)
}

Maps

Maps store key-value pairs, providing fast lookups by key. All keys must be the same type, and all values must be the same type.

Creating Maps

// Empty map
let empty_map = Map::new<String, Int>()

// Map with initial values
let person = #[name: "John", surname: "Smith"]
let scores = #["alice": 95, "bob": 87, "charlie": 92]

// Type annotations when needed
let config: Map<String, Int> = #["timeout": 30, "retries": 3]

Accessing Values

let person = #[name: "John", age: "30", city: "New York"]

// Safe access (returns Optional)
let name: Optional<String> = person["name"]       // Some("John")
let country: Optional<String> = person["country"] // None

// Unsafe access (crashes if key doesn't exist)
let name_direct = person["name"]!!              // "John"

// Alternative access methods
let has_age = "age" in person                   // true

Modifying Maps

let person = #[name: "John", age: "30"]

// Add or update values
person["city"] = "Boston"               // Add new key-value pair
person["age"] = "31"                    // Update existing value
person[] = Pair::of("country", "USA")   // Alternative way

// Remove values
let removed_age = person.remove("age")  // Returns Optional<String>
person.clear()                          // Remove all entries

Map Operations

let scores = #["alice": 95, "bob": 87, "charlie": 92]

// Size and properties
let size = scores.len                    // 3
let is_empty = scores.is_empty()         // false
let is_not_empty = scores.is_not_empty() // true

// Get all keys or values
let players = scores.keys_to_list()              // List of keys
let all_scores = scores.values_to_list()         // List of values
let entries = scores.to_list()                   // List of key-value pairs

// Check membership
let has_alice = "alice" in scores        // true
let no_david = "david" !in scores        // true

Sets

Sets store unique elements with no duplicates. They're ideal for membership testing and eliminating duplicates.

Creating Sets

// Empty set
let empty_set = Set::new<Int>()

// Set with initial values
let fruits = %["apple", "banana", "cherry"]
let numbers = %[1, 2, 3, 4, 5]

// Type annotation when needed
let unique_ids: Set<Int> = %[101, 102, 103]

Set Operations

let fruits = %["apple", "banana"]

// Add elements (duplicates ignored)
fruits[] = "cherry"     // Add "cherry"
fruits[] = "apple"      // Ignored (already exists)
fruits.add("date")      // Desugared form

// Check size
let count = fruits.len                     // Number of unique elements

// Check membership (very fast)
let has_apple = "apple" in fruits          // true
let no_grape = "grape" !in fruits          // true

// Remove elements
let removed = fruits.remove("banana")      // Returns bool
fruits.clear()                             // Remove all elements

// Convert to list
let fruit_list = fruits.to_list()          // List of unique elements

Set Mathematics

let set_a = %[1, 2, 3, 4]
let set_b = %[3, 4, 5, 6]

// Union (all elements from both sets)
let union = set_a.union(set_b)             // %[1, 2, 3, 4, 5, 6]

// Intersection (common elements)
let intersection = set_a.intersection(set_b) // %[3, 4]

// Difference (elements in A but not in B)
let difference = set_a.difference(set_b)   // %[1, 2]

Other Collections

Pair

Pairs store exactly two values, useful for returning multiple values or creating simple associations:

// Creating pairs
let coordinates = Pair::of(10, 20)
let name_age = Pair::of("Alice", 25)

// Accessing elements
let x = coordinates.first    // 10
let y = coordinates.second   // 20

// Functions returning pairs
fun get_both_ends(numbers: List<Int>): Pair<Int, Int> {
    let first = numbers.first()!!
    let last = numbers.last()!!
    return Pair::of(first, last)
}

ArrayDeque

ArrayDeque is a resizable array implementation of a double-ended queue (deque) that provides efficient insertion and removal operations at both the front and back of the collection. This versatility allows ArrayDeque to function as both a Queue (FIFO - first in, first out) and a Stack (LIFO - last in, first out) data structure.

let deque = ArrayDeque::new()

// Add to both ends
deque.add_first(2)     // [2]
deque.add_first(1)     // [1, 2]
deque.add_last(3)      // [1, 2, 3]
deque.add_last(4)      // [1, 2, 3, 4]

// Remove from both ends
let first = deque.remove_first()!!   // 1, deque = [2, 3, 4]
let last = deque.remove_last()!!     // 4, deque = [2, 3]

JSON Collections

Nitro provides built-in JSON support for working with dynamic data:

// JSON objects and arrays
let user_data: Json = json! {"name": "Alice", "age": 42, "active": true}
let shopping_list: Json = json! ["apple", "banana", "cherry"]

// Enhanced JSON syntax
let computed_data = json! {
    name: "Bob",             // Quotes optional for keys
    age: (30 + 5),           // Expressions in parentheses
    birth_year: (2024 - 35),
    hobbies: ["reading", "coding"],
}

// Accessing JSON values
let name = user_data["name"]?.as_string()!!
let age = user_data["age"]?.as_int()!!

Collection Conversion

// Converting between collection types
let list = [1, 2, 3, 2, 1]
let unique_set = list.to_set()        // %[1, 2, 3]
let back_to_list = unique_set.to_list() // [1, 2, 3] (order may vary)

// List to map (with transformation)
let names = ["alice", "bob", "charlie"]
let name_lengths = names.map @{ name -> Pair::of(name, name.len) }.to_map()
// Result: #["alice": 5, "bob": 3, "charlie": 7]

Other Collection Types

Check the standard library at src/main/nitro/core/collections where all the collection types are defined.

Types

Nitro's type system provides powerful tools for creating custom types, handling errors safely, and writing generic code. This chapter covers structs, options, enums, and the trait system.

Structs

Structs group related data together into custom types. They're similar to classes in object-oriented languages but focus purely on data storage.

Defining Structs

// Basic struct definition
struct Book {
    title: String
    author: String
    isbn: String
    pages: Int
    published_year: Int
}

// Comas are optional
struct Point3D {
    x: Float,
    y: Float,
    z: Float,
}

// Struct with optional fields
struct User {
    name: String
    email: String
    age: Optional<Int>        // Age might not be provided
    profile_picture: Optional<String>
}

Creating and Using Structs

// Create struct instances
let book = Book @[
    title: "The Rust Programming Language",
    author: "Steve Klabnik",
    isbn: "978-1-59327-828-1",
    pages: 560,
    published_year: 2018,
]

let origin = Point3D @[x: 0.0, y: 0.0, z: 0.0]

// Access fields
let book_title = book.title           // "The Rust Programming Language"
let page_count = book.pages           // 560
let coordinates = "(${origin.x}, ${origin.y}, ${origin.z})"

// Modify fields (if struct is mutable)
let user = User @[
    name: "Alice",
    email: "alice@example.com",
    age: Some(25),
    profile_picture: None(),
]
user.age = Some(26)                   // Update age

Struct Methods

You can define methods for structs, the same way that any other type:

// Method syntax
fun Book.is_long(): Boolean {
    return this.pages > 300
}

// 100% the same as:
fun is_long(book: Book): Boolean {
    return book.pages > 300
}

// You can omit the `this` keyword and access fields directly
fun Point3D.distance_from_origin(): Float {
    return sqrt(x * x + y * y + z * z)
}

// Using methods
let is_thick_book = book.is_long()    // true (560 > 300)
let distance = origin.distance_from_origin()  // 0.0

Methods and functions are interchangeable in Nitro, they are only a syntactic difference. You can use the syntax that is more convenient in each case, for example, max(a, b) and book.is_long() are more readable than a.max(b) and is_long(book), is up to your preference.

Options and Enums

Option Types

Option types represent values that may have multiple variants, with each variant potentially carrying different data.

// Custom option type
option IceCreamChoice {
    Yes { flavor: String, scoops: Int }
    No { reason: String }
    Maybe { deadline: String }
}

// Creating option instances
let choice1 = IceCreamChoice::Yes @[flavor: "chocolate", scoops: 2]
let choice2 = IceCreamChoice::No @[reason: "I'm on a diet"]
let choice3 = IceCreamChoice::Maybe @[deadline: "after dinner"]

Built-in Option Types

Nitro provides two essential option types for common patterns:

Optional Type

Represents values that may or may not exist:

// Long form
let has_value = Optional::Some @[value: 42]
let no_value = Optional::None @[]

// Convenient shorthand
let has_value = Some(42)
let no_value = None()

// Working with Optional values
fun find_user(id: Int): Optional<User> {
    // Search logic here...
    return if user_exists(id) {
        Some(load_user(id))
    } else {
        None()
    }
}

// Safe access
let user = find_user(123)
let user_name = user?.name ?? "Unknown User"  // Safe navigation, returns "Unknown User" if None

// Unsafe access (crashes if None)
let user_name = find_user(123)!!.name

Result Type

Represents operations that can succeed or fail:

// Long form
let success = Result::Ok @[value: "Operation completed"]
let failure = Result::Err @[value: "Network timeout"]

// Convenient shorthand
let success = Ok("Data loaded successfully")
let failure = Err("File not found")

// Functions returning Results
fun divide(a: Float, b: Float): Result<Float, String> {
    if b == 0.0 {
        return Err("Division by zero")
    } else {
        return Ok(a / b)
    }
}

// Error handling
let result = divide(10.0, 2.0)
let value = result? // '?' will give the value if Ok, or return the function with the Err() variant, propagating the error

Enums

Enums define a fixed set of named constants, optionally with associated data:

Simple Enums

// Basic enum without data
enum Direction {
    North
    South
    East
    West
}

// Using enum values
let current_direction = Direction::North
let opposite = Direction::South

Enums with Data

// Enum with associated data
enum Direction {
    let x: Int
    let y: Int
    
    North @[x:  0, y:  1]
    South @[x:  0, y: -1]
    East  @[x:  1, y:  0]
    West  @[x: -1, y:  0]
}

// Accessing enum data
let north = Direction::North
let north_x = Direction::North.x      // 0
let north_y = Direction::North.y      // 1

// Pattern matching with enums
fun move_character(direction: Direction, distance: Int): Point {
    return match direction {
        Direction::North -> Point @[x: 0, y: distance]
        Direction::South -> Point @[x: 0, y: -distance]
        Direction::East -> Point @[x: distance, y: 0]
        Direction::West -> Point @[x: -distance, y: 0]
    }
}

Enum Utilities

Enums automatically provide useful methods:

// Get all enum values
let all_directions: List<Direction> = Direction::values()

// Convert from string name
let north_from_name = Direction::from_name("North")  // Optional<Direction>

// Convert from index
let north_from_index = Direction::from_variant(0)   // Optional<Direction>

// Get variant index
let north_index = Direction::North.variant          // 0

// Iterate over all values
for direction in Direction::values() {
    // to_string() is automatically generated
    println("Direction: ${direction}, Vector: (${direction.x}, ${direction.y})")
}

Tags

Tags (similar to traits in other languages) define a set of methods that types must implement. They enable polymorphism and code reuse through shared behavior.

The main difference in Nitro is that tags are automatically assigned to types that implement the required methods, instead of explicitly declaring that a type implements a tag.

Defining Tags

// Define a tag for types that can be printed
// `This` refers to the type that implements the tag
tag Printable {
    fun This.print()           // Method that implementers must provide
    fun This.print_debug()     // Another required method
}

// Define a tag for types that can be compared
tag Comparable {
    fun This.get_ordering(other: This): Ordering
}

// Define a tag with default implementations
tag Drawable {
    fun This.draw()                    // Required method
}

// This acts as a default implementation, since any Drawable type will have this function available, and any override will have higher priority
fun <#T: Drawable> #T.draw_with_color(color: String) {
    println("Drawing with color: $color")
    this.draw()
}

Implementing Tags

Types automatically implement tags when they have the required methods:

// Implement Printable for Int
fun Int.print() {
    println("Int value: $this")
}

fun Int.print_debug() {
    println("Debug - Int: $this (size: ${size_of<Int>()})")
}

// Implement Printable for String
fun String.print() {
    println("String: '$this'")
}

fun String.print_debug() {
    println("Debug - String: '$this' (length: ${this.len})")
}

// Now Int and String automatically implement Printable

Using Tags

Generic Functions with Tag Constraints

// Function that works with any Printable type
fun <#T: Printable> print_and_debug(value: #T) {
    value.print()
    value.print_debug()
}

// Function with multiple tag constraints
fun <#T: Printable & Comparable> process_item(item: #T) {
    // Can also use comparison methods
    if item > 10 {
        item.print()
    }
}

// Using the generic functions
fun main() {
    print_and_debug(42)              // Works with Int
    print_and_debug("Hello")         // Works with String
    
    let numbers = [3, 1, 4, 1, 5]
    for num in numbers {
        process_item(num)
    }
}

Built-in Tags

Nitro provides several built-in tags:

  • GetOrdering
  • ToString
  • ToBoolean

You can add tags to existing types and apply tags to new types you define, there is no limit. This allows easier interoperability with libraries, using library functions with your types and using your functions with library types.

GetOrdering Example

// Provides comparison between values of the same type
tag GetOrdering {
    fun This.get_ordering(other: This): Ordering
}

struct MyType { ... }

// Implementing GetOrdering for MyType
fun MyType.get_ordering(other: MyType): Ordering { ... }

fun main() {
    let a = MyType @[ ... ]
    let b = MyType @[ ... ]
    
    when {
        a < b -> println("a is less than b")
        a > b -> println("a is greater than b")
        else -> println("a is equal to b")
    }
}