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:
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:
p := Person("Alice", 30)#Named Instantiation
Specify fields by name using Name { field: value } syntax:
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:
p <Person>
println(p.name)
println(p.age)See Zero-Value Declaration below for full details.
#Accessing Fields
Use dot notation to access fields:
p := Person("Alice", 30)
println(p.name)
println(p.age)#Mutating Fields
Assign to a field using dot notation with =:
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:
p := Person("Alice", 30)
p.age = "thirty"Field mutation works with postfix guards:
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:
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 is | Hidden field |
|---|---|
| a method of the struct | allowed |
| a method it inherited, or a subclass's method | allowed |
| a lambda nested inside one of those | allowed |
| a method of the same struct, reading another instance | allowed |
| a method of an unrelated struct | refused |
a plain top-level function, or main | refused |
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:
Account(balance=100, owner="ada")
// error: field 'balance' is hidden on Account and cannot be set from outsideTwo 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:
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.
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:
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:
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:
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:
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:
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:
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:
a := American("John", 25, "USA")
println(a.name)
println(a.nationality)Named instantiation works the same way:
a := American { name: "John", age: 25, nationality: "USA" }#Multi-Level Inheritance
Inheritance can chain through multiple levels:
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:
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:
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:
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:
struct Empty {
}
e := Empty()
println(type(e))#Dot Chaining
If a method returns a struct instance, you can chain dot access:
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
p <Person> = Person("Alice", 30)
p = Person("Bob", 25)#Walrus Declaration
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:
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:
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:
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:
struct Person {
name <str>
age <int>
}
p := Person("Alice", 30)
println(type(p))Use is_mut() and is_type_mut() on fields:
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:
Person("Alice")
Person { name: "Alice", height: 170 }
p.age = "thirty"
Person("Alice", "thirty")#Indentation Mode
Structs and includes blocks work with indentation mode:
#[indent]
struct Person:
name <str>
age <int>
Person includes:
fun greet():
name#Complete Example
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:
- Type System — type annotations and zero values
- Functions — function syntax used in methods
- Variables and Assignments — typed variable declarations