OxigenLang Structs

OxigenLang supports structs — composite data types with typed fields, inheritance, and methods. Structs provide a way to group related data together and attach behavior to it.

#Defining a Struct

Use the struct keyword followed by a name and a list of typed fields:

Oxigen
struct Person {
    name <str>
    age <int>
}

Each field requires a type annotation using the same <type> syntax as typed variables. See the Type System guide for available types.

#Creating Instances

#Positional Instantiation

Pass values in the order fields are declared:

Oxigen
p := Person("Alice", 30)

#Named Instantiation

Specify fields by name using Name { field: value } syntax:

Oxigen
p := Person { name: "Alice", age: 30 }

Both forms perform strict type checking — the values must match the declared field types.

#Zero-Value Instantiation

Declare a struct-typed variable without providing values. All fields are initialized to their type's zero value:

Oxigen
p <Person>
println(p.name)
println(p.age)

See Zero-Value Declaration below for full details.

#Accessing Fields

Use dot notation to access fields:

Oxigen
p := Person("Alice", 30)
println(p.name)
println(p.age)

#Mutating Fields

Assign to a field using dot notation with =:

Oxigen
p := Person("Alice", 30)
p.age = 31
println(p.age)

Field mutation is type-checked — assigning a value of the wrong type produces an error:

Oxigen
p := Person("Alice", 30)
p.age = "thirty"

Field mutation works with postfix guards:

Oxigen
p.name = "Bob" when should_update

#Hidden Fields

The hide keyword marks a field as internal. Hidden fields are accessible inside methods but not from outside the struct:

Oxigen
struct Person {
    hide fname <str>
    hide lname <str>
    hide age <int>
}

Person includes {
    fun set_name(fn <str>, ln <str>) {
        self.fname = fn
        self.lname = ln
    }
    fun get_name() { self.fname + " " + self.lname }
}

p <Person>
p.set_name("John", "Doe")
println(p.get_name())

Use hide to encapsulate implementation details and control access through methods.

#What counts as "inside"

Access is allowed from code compiled as part of the struct's own chain, and refused everywhere else:

Where the code isHidden field
a method of the structallowed
a method it inherited, or a subclass's methodallowed
a lambda nested inside one of thoseallowed
a method of the same struct, reading another instanceallowed
a method of an unrelated structrefused
a plain top-level function, or mainrefused
Oxigen
Account includes {
    // both allowed — `other` is an Account too
    fun richer_than(other <Account>) { self.balance > other.balance }
}

fun audit(a <Account>) { a.balance }
// error: field 'balance' is hidden on Account
//   hint: it is declared `hide` — reach it through a method of the struct

#Building one

A hidden field cannot be supplied from outside, because setting the initial value from anywhere would make hide half a rule:

Oxigen
Account(balance=100, owner="ada")
// error: field 'balance' is hidden on Account and cannot be set from outside

Two ways to build one instead — zero-value declaration plus a method, or a factory inside the struct, which is not "outside" and may use the literal form:

Oxigen
a <Account>              // every field takes its zero value
a.open("ada", 100)       // a method sets the hidden one

Account includes {
    fun clone_with(b <int>) { Account(balance=b, owner=self.owner) }
}

#One thing hide does not do

It is an access rule, not a secrecy guarantee: println still shows the field, since printing is a diagnostic rather than a way to reach the value.

Oxigen
println(a)   // Account { balance: 100, owner: ada }

Do not treat hide as a place to put a secret that must never be displayed.

#Methods

Attach methods to a struct using a includes block:

Oxigen
Person includes {
    fun greet() { println(name) }
    fun is_adult() { age >= 18 }
}

#Implicit Self

Inside a method, all fields of the instance are accessible directly by name — there is no explicit self parameter. When you call p.greet(), the method body can reference name and age as if they were local variables:

Oxigen
struct Person {
    name <str>
    age <int>
}

Person includes {
    fun greet() { name + " is " + str(age) + " years old" }
}

p := Person("Alice", 30)
println(p.greet())

#Explicit self

You can also use self to explicitly reference the instance. This is required when you want to assign to a field within a method:

Oxigen
Person includes {
    fun set_name(new_name <str>) {
        self.name = new_name
    }
    fun set_age(new_age <int>) {
        self.age = new_age
    }
}

p <Person>
p.set_name("Alice")
p.set_age(30)

Use self.field = value to mutate the instance's field from within a method. Plain field access (name, age) reads the field value; self.field = value writes to it.

#Methods with Parameters

Methods can take additional parameters beyond the implicit fields:

Oxigen
Person includes {
    fun greet_with(greeting) { greeting + ", " + name }
}

p := Person("Alice", 30)
println(p.greet_with("Hello"))

#Returning Values

Methods can use give for early returns, or implicitly return the last expression:

Oxigen
Person includes {
    fun birth_year(current_year) { give current_year - age }
}

p := Person("Alice", 30)
println(p.birth_year(2026))

#Inheritance

A struct can inherit from a parent by specifying the parent name in parentheses:

Oxigen
struct Person {
    name <str>
    age <int>
}

struct American(Person) {
    nationality <str>
}

The child struct gets all parent fields first, followed by its own. Instantiation requires all fields in order — parent fields, then child fields:

Oxigen
a := American("John", 25, "USA")
println(a.name)
println(a.nationality)

Named instantiation works the same way:

Oxigen
a := American { name: "John", age: 25, nationality: "USA" }

#Multi-Level Inheritance

Inheritance can chain through multiple levels:

Oxigen
struct Person {
    hide fname <str>
    hide lname <str>
    hide age <int>
}

struct Nationality(Person) {
    hide country <str>
    hide title <str>
}

struct American(Nationality) {
    state <str>
}

American inherits all fields from Nationality, which inherits all fields from Person. The full field order is: fname, lname, age, country, title, state.

#Inheriting Methods

A child struct automatically inherits all methods defined on the parent:

Oxigen
Person includes {
    fun greet() { name }
}

struct American(Person) {
    nationality <str>
}

a := American("John", 25, "USA")
println(a.greet())

#Overriding Methods

Define a includes block on the child struct to add or override methods:

Oxigen
American includes {
    fun greet() { name + " from " + nationality }
}

a := American("John", 25, "USA")
println(a.greet())

The child's includes block only affects the child. The parent's methods remain unchanged for parent instances.

#Adding Methods at Each Level

Each level in an inheritance chain can define its own methods:

Oxigen
Person includes {
    fun set_name(fn <str>, ln <str>) {
        self.fname = fn
        self.lname = ln
    }
}

Nationality includes {
    fun set_country(country <str>) {
        self.country = country
    }
}

American includes {
    fun print_info() {
        println("Name: ", self.fname, self.lname)
        println("Age: ", self.age)
        println("Country: ", self.country)
        println("State: ", self.state)
    }
}

#Zero-Field Structs

Structs with no fields are valid:

Oxigen
struct Empty {
}

e := Empty()
println(type(e))

#Dot Chaining

If a method returns a struct instance, you can chain dot access:

Oxigen
struct Inner {
    val <int>
}

struct Outer {
    name <str>
}

Outer includes {
    fun make_inner() { Inner(42) }
}

o := Outer("test")
println(o.make_inner().val)

#Structs as Types

Struct names can be used as type annotations, just like built-in types. This locks a variable to a specific struct type.

#Strict Declaration

Oxigen
p <Person> = Person("Alice", 30)
p = Person("Bob", 25)

#Walrus Declaration

Oxigen
p <Person> := Person("Alice", 30)
p = Person("Bob", 25)
p = 42

#Zero-Value Declaration

Declare a typed variable with all fields set to their zero values. Both forms are equivalent:

Oxigen
p <Person>
// or equivalently: p as <Person>

println(p.name)
println(p.age)

p.name = "Alice"
p.age = 30

p = Person("Bob", 25)

#Different Struct Types

A variable locked to one struct type cannot be reassigned to a different struct:

Oxigen
struct Person { name <str> }
struct Dog { name <str> }

p <Person> := Person("Alice")
p := Dog("Rex")

#Struct-Typed Fields

Struct fields can use other structs as their type:

Oxigen
struct Address {
    city <str>
}

struct Person {
    name <str>
    addr <Address>
}

a := Address("NYC")
p := Person("Alice", a)
println(p.addr.city)

#Introspection

The type() built-in returns the struct name for instances:

Oxigen
struct Person {
    name <str>
    age <int>
}

p := Person("Alice", 30)
println(type(p))

Use is_mut() and is_type_mut() on fields:

Oxigen
m <Math> = Math { num_one: 10, num_two: 20 }
println(is_mut(m.num_one))
println(is_type_mut(m.num_one))

#Error Handling

OxigenLang produces clear errors for common mistakes:

Oxigen
Person("Alice")
Person { name: "Alice", height: 170 }
p.age = "thirty"
Person("Alice", "thirty")

#Indentation Mode

Structs and includes blocks work with indentation mode:

Oxigen
#[indent]

struct Person:
    name <str>
    age <int>

Person includes:
    fun greet():
        name

#Complete Example

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

struct Dog(Animal) {
    breed <str>
}

Animal includes {
    fun speak() { name + " says " + sound }
}

Dog includes {
    fun info() { name + " (" + breed + ")" }
}

dog := Dog("Rex", "Woof", "Labrador")
println(dog.speak())
println(dog.info())

dog.sound = "Bark"
println(dog.speak())

See also: