OxigenLang Conventions

#Philosophy

Oxigen optimizes for code that states its intent plainly. A few principles run through every convention below:

  • Be explicit about types. A declared type is locked, which turns whole classes of bugs into errors and documents what a value is. Reach for a dynamic variable only when a type genuinely can't be known.
  • Read like intent. Prefer the construct that reads most naturally — option for a decision, unless/when for the clearer condition, named arguments at the call site, string interpolation over concatenation.
  • Returns are implicit. The last expression is the result; give is a rarely-used escape hatch, not the norm.
  • Public by default. Expose behavior by default and hide only what is genuinely internal.
  • Don't repeat yourself. Inherit and reuse rather than redeclare; reach for a struct only when there's shared state and behavior.
  • One tool per job. option for local decisions, pattern + choose for reusable matching; each for collections, repeat for conditions.
  • Pick a style and commit. One block style and snake_case names (PascalCase for structs), consistently.

#Simplicity Over Verbosity

Oxigen code should never be verbose when it can be simplified. It should read naturally and be reasoned about naturally — close to how you'd describe the problem out loud. This is the language's central goal, and it's why Oxigen deliberately removes redundant constructs and renames familiar ones: when there is exactly one clear way to express something, intent is unambiguous.

These choices are purpose-built, not accidental. Some will feel unfamiliar coming from other languages — that's intentional. Favor the simpler, more direct form: if a line can be said more plainly, say it that way.

#One Construct per Job

Oxigen drops overlapping constructs so there's a single, obvious tool for each job. Each has its own section below with the details.

Instead ofOxigen uses
if / else if / elseoption
for loopseach over an iterable
while loopsrepeat when (loop while true) / repeat unless (loop while false)
break / continuestop / skip
explicit returnimplicit return — give only for a rare early exit

#Declarations Encode Behavior

A variable's form states how it behaves — there's no const, let, or mut keyword. Mutability comes from how it's declared; the type supplies the initial value and drives automatic conversion where it's unambiguous, so values become what the context intends without manual ceremony.

FormBehavior
x <int>x initialized to the type's zero value (0)
x <int> := 0mutable, type-locked
x <int> = 0constant — immutable and type-locked

#Angle Forms Carry Meaning

The <...> angle forms are part of the same idea: one compact, consistent syntax that marks a type or an effect inline instead of spelling it out. Types (<int>, <Config>), the Error || Value system (<Error<tag>>, <Value>), and effects like <log>, <fail>, <guard>, and <test> all read as a single intentional mark rather than boilerplate.

#Variable Declarations

Oxigen has four declaration forms. Reach for the most explicit one that fits:

FormMeaning
x <type>Type-locked, zero-initialized, value-mutable
x <type> := valueType-locked with an initial value, value-mutable
x <type> = valueType-locked and immutable (a constant)
x := valueDynamic — both type and value can change (avoid)

#Always Declare the Expected Type

A type-locked variable prevents accidental type changes that cause subtle bugs, and the declared type documents intent. Prefer a type even when initializing with :=. Zero-initialized variables (x <type>) are the cleanest choice when the default value is what you want.

Oxigen
// Good — type-locked, zero-initialized (defaults: 0, "", [], False)
counter <int>
names <array>

// Good — type-locked with an initial value
limit <int> := 100
result <int> := compute()

// Avoid — dynamic; type and value can both drift
counter := 0
result := compute()

#Reserve Dynamic := for Genuinely Unknown Types

Use a bare := only when neither the type nor a sensible primitive can be known ahead of time — and even then, prefer <generic> to say "this is intentionally dynamic" rather than leaving a reader guessing whether it was deliberate.

Oxigen
// Acceptable — a generic callback result whose type isn't known
acc := initial

// Better — explicit that the type is dynamic
acc <generic> := initial

fun identity(val <generic>) {
    result <generic> := val
    result
}

#Change a Type by Shadowing, Not Reassignment

To change a variable's type, shadow it with a new binding. This keeps the change explicit instead of silently mutating the type in place.

Oxigen
// Good — shadowed with a clear type change
x <int> := 42
x <str> := str(x)

// Avoid — silent type change
x := 42
x := "now a string"

#Convert with <type>

Use explicit <type> conversion so the intent is visible at a glance.

Oxigen
value <int> := int("42")
ratio <float> := float(count) / float(total)

#Constants

Oxigen has no const keyword. A type-locked strict assignment (=) is both type-locked and immutable.

Oxigen
PI <float> = 3.14159
MAX_RETRIES <int> = 5
APP_NAME <str> = "Oxigen"

PI = 3.0          // error — cannot reassign an immutable variable
PI := "something" // error — type is locked

If a value is meant to change, declare it mutable with <type> := instead. You can override an immutable binding by shadowing, but only when the intent is clear.

Oxigen
// Good — mutable because it's expected to change
retries <int> := 5
retries = retries - 1

// Acceptable — shadowing to override with clear intent
MAX <int> = 100
MAX <int> := 200

// Avoid — walrus silently overriding an immutable binding
MAX <int> = 100
MAX := 200

#Scripts and Modules

#Use main for Script Entry Points

Keep reusable definitions at the top level and put executable script logic inside main. main is skipped when a file is brought in with introduce, so a file can serve as both a runnable script and an importable module.

Oxigen
introduce strings

fun banner(name <str>) {
    strings.upper("hello, {name}")
}

main {
    println(banner("Oxigen"))
}

Top-level statements still run when a file is executed directly, but main is the preferred home for script logic.

#Functions

#Named Functions Over Anonymous Functions

Use explicit named definitions (fun name() {}) for anything reusable. Reserve anonymous functions for inline callbacks.

Oxigen
// Good — named function, clear and reusable
fun calculate_area(width <int>, height <int>) {
    width * height
}

// Good — anonymous function as an inline callback
introduce array
doubled <array> := array.map([1, 2, 3], fun(x <int>) { x * 2 })

// Avoid — anonymous function bound to a name when a named function is clearer
calculate_area := fun(width, height) { width * height }

#Type Your Parameters

Always annotate parameter types — the signature should communicate what's expected. Use union types (<a> || <b>) for genuine flexibility.

Oxigen
// Good — typed parameters
fun greet(name <str>) {
    println("Hello, {name}!")
}

// Good — union types for flexibility
fun abs(x <int> || <float>) {
    option { x >= 0 -> x, -x }
}

// Avoid — untyped parameters
fun greet(name) {
    println("Hello, {name}!")
}

#Use Default Values Over None Checks

When a parameter has a natural default, declare it in the signature instead of checking for None in the body.

Oxigen
// Good — default value in the signature
fun connect(host <str>, port <int> = 8080) {
    println("Connecting to {host}:{port}")
}

// Avoid — checking None manually
fun connect(host <str>, port? <int>) {
    actual_port <int> := option { port == None -> 8080, port }
    println("Connecting to {host}:{actual_port}")
}

Use ? (optional parameters) only when there's genuinely no sensible default and the caller may or may not provide a value.

Oxigen
// Good — tag is truly optional, no sensible default
fun log(msg <str>, level <str> = "INFO", tag? <str>) {
    option {
        tag == None -> println("[{level}] {msg}"),
        println("[{level}] [{tag}] {msg}")
    }
}

#Use Named Arguments for Clarity at Call Sites

When a call has several same-typed parameters or boolean flags, use named arguments so the call documents itself. They also let you skip defaults.

Oxigen
fun create_user(name <str>, email <str>, admin <bool> = False) { ... }

// Good — intent is clear
create_user("Alice", email="alice@example.com", admin=True)

// Avoid — positional args are ambiguous
create_user("Alice", "alice@example.com", True)

// Good — skip port, only set tls
fun connect(host <str>, port <int> = 8080, tls? <bool>) { ... }
connect("example.com", tls=True)

#Returns Are Implicit — give Is a Last Resort

A function returns the value of its last expression. Because Oxigen has no semicolons, lean on this rather than an explicit return: write the value, don't announce it. Explicit return exists as give — named differently from return on purpose, so it isn't reached for out of habit. Use it only for the rare early exit where it genuinely reads clearer; to break a loop, prefer stop.

Oxigen
// Good — the option is the function body; its value is returned
fun is_even(val <int>) {
    option {
        val % 2 == 0 -> True,
        False
    }
}

// Not recommended — give adds a keyword and clutters with no benefit
fun is_even(val <int>) {
    option {
        val % 2 == 0 -> give True,
        give False
    }
}

#Conditionals and Pattern Matching

#Use option for Conditional Logic

option is the tool for conditional branching on expressions — inline decisions, ternary-style expressions, and multi-arm conditionals within a single file.

Oxigen
// Good — multi-arm option
status <str> := option {
    age < 13 -> "child"
    age < 20 -> "teenager"
    "adult"
}

// Good — ternary-style option
sign <int> := option { x >= 0 -> 1, -1 }

#Use pattern + choose for Reusable Matching

Patterns defined with pattern are named, reusable conditions meant to be shared across files through the module system; match against them with choose. For a condition that only matters in one place, use option instead.

Oxigen
// Good — reusable patterns shared across files
pattern is_even(n) when n % 2 == 0
pattern is_odd(n) when n % 2 != 0

each n in range(10) {
    choose n {
        is_even -> println(n, "is even")
        is_odd -> println(n, "is odd")
    }
}

// Avoid — pattern + choose for a one-off condition
pattern is_positive(x) when x > 0
choose value {
    is_positive -> println("positive")
    else -> println("not positive")
}

// Better — option for one-off conditions
option {
    value > 0 -> println("positive")
    println("not positive")
}

#Structs

#Instantiate with variable <Struct>

Create struct instances with the variable <Struct> form rather than writing new() factory functions. The struct definition is the source of truth for field types, so declaring the type at the instantiation site keeps it visible and lets the struct enforce its own constraints.

Oxigen
struct Config {
    host <str>
    port <int>
    debug <bool>
}

// Good — type is explicit, fields are checked by the struct
settings <Config> := Config("localhost", 8080, False)

// Zero-initialized struct
defaults <Config>

// Avoid — wrapping instantiation in a new() function
fun new_config(host <str>, port <int>, debug <bool>) {
    Config(host, port, debug)
}

#Inherit Behavior, Don't Redeclare It

A child struct automatically has its parent's methods. Only add new methods or override the ones that need to differ — never copy a parent method unchanged.

Oxigen
struct Animal {
    name <str>
    sound <str>
}

Animal includes {
    fun speak() { println(self.name, "says", self.sound) }
    fun describe() { println("Animal:", self.name) }
}

struct Dog(Animal) {
    breed <str>
}

// Good — add new behavior, override only what's different
Dog includes {
    fun fetch() { println(self.name, "fetches the ball!") }
    fun describe() { println("Dog:", self.name, "- Breed:", self.breed) }
}

// Avoid — redeclaring speak() when the parent's version already works
Dog includes {
    fun speak() { println(self.name, "says", self.sound) }
    fun fetch() { println(self.name, "fetches the ball!") }
}

#Structs Are for Shared Behavior, Not One-Off Logic

Use a struct to group related state and behavior. If there's no real state — just a single operation — a plain function is clearer.

Oxigen
// Good — groups related state and behavior
struct HttpResponse {
    status <int>
    body <str>
    headers <map>
}

HttpResponse includes {
    fun is_ok() { self.status >= 200 and self.status < 300 }
    fun is_error() { self.status >= 400 }
}

// Avoid — a struct wrapping a single function with no real state
struct Greeter {
    name <str>
}
Greeter includes {
    fun greet() { println("Hello, {self.name}!") }
}

// Better — just a function
fun greet(name <str>) {
    println("Hello, {name}!")
}

#No Empty or Untyped Structs

Every struct field should be typed. Don't create empty structs or leave fields untyped — the one exception is a <generic> field on a parent meant to be specialized by its children.

Oxigen
// Good — all fields typed
struct User {
    name <str>
    age <int>
    active <bool>
}

// Good — generic parent, children add specifics
struct Container {
    value <generic>
}
struct IntBox(Container) {
    label <str>
}

// Avoid — empty struct
struct Utils {}

// Avoid — untyped fields
struct User {
    name
    age
}

#Encapsulate with hide

Oxigen is public by default; everything is accessible unless marked with hide. Hide only genuine implementation details, and keep anything that's part of the public interface visible. It applies in two places: struct fields, and top-level functions in a module.

Oxigen
struct Account {
    name <str>
    hide balance <int>
    hide pin <int>
}

Account includes {
    fun open(n <str>, b <int>, p <int>) {
        self.name = n
        self.balance = b
        self.pin = p
    }
    fun deposit(amount <int>) { self.balance = self.balance + amount }
    fun get_balance() { self.balance }
}

a <Account>
a.open("Alice", 1000, 1234)
println(a.name)          // works — public
println(a.get_balance()) // works — public method
println(a.balance)       // error — hidden field

A hidden field cannot be supplied from outside, so Account("Alice", 1000, 1234) is refused too — declare the value and let a method set it, as above. See structs.md for the full access rules.

The same keyword makes a module's helpers private:

Oxigen
hide fun __normalize(s <str>) { strings.trim(strings.lower(s)) }

fun matches(a <str>, b <str>) { __normalize(a) == __normalize(b) }

__normalize stays callable inside its own file and is refused through the module namespace and through introduce {…} from. See imports.md.

#Always Use self.field in Methods

Inside methods, access fields through self.field. Oxigen allows the bare name, but explicit self makes it clear you're touching struct state, not a local. This matters most in setters, where a parameter shares a field's name.

Oxigen
struct Player {
    name <str>
    score <int>
}

// Good — explicit self
Player includes {
    fun add_score(points <int>) { self.score = self.score + points }
    fun set_name(name <str>) { self.name = name }
}

// Avoid — implicit field access
Player includes {
    fun add_score(points <int>) { score = score + points }
}

#Formatting

#Pick a Block Style and Stick With It

Braces {} are the default. Indentation blocks via #[indent] are welcome too — but commit to one style across a project; don't mix them.

Oxigen
// Good — braces throughout
fun greet(name <str>) {
    option {
        len(name) > 0 -> println("Hello, {name}!")
        println("Hello, stranger!")
    }
}
Oxigen
// Good — indentation throughout
#[indent]

fun greet(name <str>):
    option:
        len(name) > 0 -> println("Hello, {name}!")
        println("Hello, stranger!")

#Use String Interpolation Over Concatenation

Both work, but interpolation reads cleaner.

Oxigen
name <str> := "Oxigen"
version <int> := 1

// Good — interpolation
println("Welcome to {name} v{version}!")

// Avoid — concatenation
println("Welcome to " + name + " v" + str(version) + "!")

#Use Snake Case

Use snake_case for variables, functions, and fields. Struct names are the exception — they use PascalCase.

Oxigen
// Good
max_retries <int> := 3
fun get_user_name(user_id <int>) { ... }

struct HttpResponse {
    status_code <int>
    response_body <str>
}

// Avoid
maxRetries <int> := 3
fun getUserName(userId <int>) { ... }

#Loops

#each for Iterables, repeat for Conditions

Use each to iterate a known collection. Use repeat when / repeat unless for condition-based loops that could run indefinitely.

Oxigen
// Good — each over a collection
each item in items {
    println(item)
}

each i in range(10) {
    println(i)
}

// Good — repeat for condition-based loops
repeat unless queue_empty() {
    process_next()
}

repeat when retries > 0 {
    retries = retries - 1
    attempt()
}

#Pick when or unless for Readability

Choose whichever makes the condition read most directly — favor the form that avoids a double negative. It's about readability, not a rigid rule.

Oxigen
// Good — unless reads cleaner here
repeat unless i >= size { ... }

// Good — when reads cleaner here
repeat when retries > 0 { ... }