OxigenLang Data Types

OxigenLang supports a rich set of data types for building expressive programs. The language is dynamically typed by default, but offers optional type annotations to lock a variable's type. See the Type System guide for details on type annotations.

#Primitive Types

#Integer

64-bit signed integers. Supports standard arithmetic, comparison, and postfix increment/decrement.

Oxigen
x := 42
y := -7
z := 0

Type keyword: int. Zero value: 0.

Range is -9223372036854775808 to 9223372036854775807. +, -, and * wrap around on overflow; / and % raise an error instead. See Integer Overflow for the exact rules.

#Float

64-bit floating-point numbers. Supports arithmetic and comparison operations.

Oxigen
pi := 3.14
neg := -0.5
big := 1.5e3      // 1500.0; an exponent makes any literal a float
tiny := 2E-4

Type keyword: float. Zero value: 0.0. A literal too large for a 64-bit float is a compile error.

Floats and integers can be mixed in arithmetic — the result follows standard promotion rules.

#String

Unicode strings. Can be enclosed in double quotes or single quotes — both are equivalent:

Oxigen
greeting := "hello"
name := 'world'

Type keyword: str. Zero value: "" (empty string).

Single- and double-quoted strings are single-line: a raw newline before the closing quote is an error (write newlines with the \n escape instead).

#Multi-line Strings

Wrap text in triple quotes — """...""" or '''...''' — to write a string that spans multiple lines. Raw newlines inside the string are kept verbatim, so you don't need \n:

Oxigen
poem := """
Roses are red,
Violets are blue.
"""
println(poem)

The content is taken literally, including the leading newline right after the opening """ and the trailing newline before the closing """. Put the text on the same line as the fences to avoid them:

Oxigen
banner := """+----------+
|  Oxigen  |
+----------+"""

Triple-quoted strings support the same interpolation and escape sequences as single-line strings, and an interpolation expression may itself span multiple lines:

Oxigen
name := "world"
x := 10
y := 20

msg := """
Hello, {name}!
{x} + {y} = {x + y}
"""
println(msg)

// Interpolation expressions can span lines too:
total := """sum = {
    x +
    y
}"""

Both """ and ''' behave identically; choose whichever lets you embed the other quote style without escaping. A triple-quoted string is only "unterminated" if end-of-file is reached before its closing fence.

#String Operations

  • Concatenation: Use + to join strings:

    Oxigen
    full := "hello" + " " + "world"
  • String Interpolation: Use {} inside any string to embed expressions. The expression is evaluated and its result is converted to a string automatically:

    Oxigen
    name := "world"
    println("hello {name}")
    
    x := 10
    y := 20
    println("{x} + {y} = {x + y}")

    Interpolation works with variables, function calls, arithmetic, and any valid expression:

    Oxigen
    age := 25
    println("Next year you will be {age + 1}")
    
    items := [1, 2, 3]
    println("There are {len(items)} items")
    
    println("Type: {type(42)}")

    "Any valid expression" includes comparisons, %, and the logical keywords — there is no need to compute them into a variable first:

    Oxigen
    a := 1
    b := 2
    println("{a == b}")             // False
    println("{a < b and b % 2 == 0}")  // True
    println("{not (a == b)}")       // True
    println("{ {"k": 7}["k"] }")    // 7 — nested braces are fine

    Strings without {} remain plain strings — no special syntax is needed to opt out. Both double-quoted and single-quoted strings support interpolation.

  • Escape Sequences: Strings support the following escape sequences:

    EscapeCharacter
    \nNewline
    \tTab
    \rCarriage return
    \eEscape (ESC, useful for ANSI terminal sequences)
    \xHHCharacter by two hexadecimal digits, such as \x1b for ESC
    \\Backslash
    \"Double quote (inside "...")
    \'Single quote (inside '...')
    \0Null character
    \{Literal { (a bare { would start an interpolation)
    \}Literal }
    Oxigen
    println("line one\nline two")
    println("col1\tcol2")
    println("she said \"hello\"")
    println('it\'s fine')
    println("\e[31mred\e[0m")
    println("\{not an interpolation\}")   // {not an interpolation}

    Escape sequences work in both plain strings and interpolated strings.

  • Indexing: Access individual characters by position (returns a string):

    Oxigen
    "hello"[0]
    "hello"[4]
  • Slicing: Extract a substring with [start:end]:

    Oxigen
    "hello world"[0:5]
    "hello world"[6:]
    "hello world"[:5]
  • Length: Use len() to get the number of characters:

    Oxigen
    len("hello")
  • To characters: Use chars() to convert to an array of characters:

    Oxigen
    chars("abc")
  • To string: Use str() to convert other types to their string representation:

    Oxigen
    str(42)
    str(True)

#Character

A single Unicode character, enclosed in backticks:

Oxigen
letter := `a`
digit := `5`
symbol := `@`

Type keyword: char. Zero value: \0 (null character).

Characters are distinct from single-character strings. Use ord() to get the Unicode codepoint and chr() to convert back:

Oxigen
ord(`a`)
chr(97)

#Boolean

Boolean values are capitalized: True and False:

Oxigen
active := True
done := False

Type keyword: bool. Zero value: False.

#Truthiness

All values in OxigenLang have a truthiness. The following are falsy:

  • False
  • None
  • 0 (integer zero)
  • 0.0 (float zero)
  • "" (empty string)
  • \0 (null character)
  • [] (empty array)
  • () (empty tuple)
  • {} (empty map)
  • set() (empty set)
  • Any Error value

Everything else is truthy, including struct instances.

#None

Represents the absence of a value:

Oxigen
x := None

Functions that don't explicitly return a value produce None. None is falsy.

#Collection Types

#Array

Dynamic, ordered collection of values. Elements can be of any type:

Oxigen
nums := [1, 2, 3]
mixed := [1, "hello", True, 3.14]
empty := []

Type keyword: array. Zero value: [].

#Array Operations

  • Indexing: Access by position (zero-based):

    Oxigen
    arr := [10, 20, 30]
    arr[0]
    arr[2]
  • Index Assignment: Update an element at a specific position:

    Oxigen
    arr := [10, 20, 30]
    arr[0] = 99
    arr[-1] = 77       // negative index counts from the end
    println(arr)        // [99, 20, 77]
  • Out-of-range indices are an error, for both reads and writes. This applies to arrays, tuples, and strings; negative indices are valid down to -len:

    Oxigen
    arr := [10, 20, 30]
    arr[3]              // error: index 3 out of range for ARRAY of length 3
    arr[-4]             // error: index -4 out of range for ARRAY of length 3
    arr[3] = 1          // error — an out-of-range write does not append

    Use len(arr) to bounds-check, or push(arr, value) to grow an array. Slicing clamps instead of erroring, so arr[0:99] is the whole array. Maps are unaffected: a missing key is a lookup miss, and still returns None.

  • Slicing: Extract a sub-array with [start:end]:

    Oxigen
    arr := [10, 20, 30, 40, 50]
    arr[1:3]
    arr[2:]
    arr[:2]
    arr[:]

    Start and end can be any expression. Out-of-bounds values are clamped silently.

  • Add: push appends, insert puts an element at an index, and extend appends many. All three change the array in place:

    Oxigen
    arr := [1, 2]
    push(arr, 3)            // [1, 2, 3]
    insert(arr, 0, 0)       // [0, 1, 2, 3]
    extend(arr, [4, 5])     // [0, 1, 2, 3, 4, 5]
  • Remove: pop removes by position and returns the element; remove removes the first element equal to a value; clear empties the array:

    Oxigen
    arr := [10, 20, 30, 20]
    pop(arr)                // 20 (the last); arr is [10, 20, 30]
    pop(arr, 0)             // 10; arr is [20, 30]
    remove(arr, 30)         // arr is [20]
    clear(arr)              // arr is []
  • Copy: arrays are shared references, so b := a gives two names for one array. copy(a) makes a separate one.

  • First / Last / Rest:

    Oxigen
    first([1, 2, 3])
    last([1, 2, 3])
    rest([1, 2, 3])
  • Length: len([1, 2, 3]) returns 3.

  • Membership: has([1, 2, 3], 2) returns True. The array module searches with a predicate: find, find_index, index_of, filter.

#Tuple

Fixed-size, immutable ordered collection. Created with parentheses or the tuple() built-in:

Oxigen
point := (1, 2)
record := (1, "hello", True)
single := (42,)
empty := ()

Type keyword: tuple. Zero value: ().

Note: A single-element tuple requires a trailing comma to distinguish it from a grouped expression.

#Tuple Operations

  • Indexing: (1, 2, 3)[0] returns 1.
  • Slicing: (1, 2, 3, 4)[1:3] returns (2, 3).
  • Concatenation: (1, 2) + (3, 4) returns (1, 2, 3, 4).
  • Length: len((1, 2)) returns 2.
  • Membership: has((10, 20), 10) returns True.

#Map

Key-value pairs, enclosed in curly braces. A value can be any type; a key must be hashable:

Oxigen
person := {"name": "Alice", "age": 30}
mixed := {1: "one", "two": 2}
tuple_key := {(1, 2): "point"}
empty := {}

The hashable kinds are int, uint, float, bool, char, byte, str, None, and tuples whose elements are all hashable. Arrays, maps, sets, struct instances, and functions are not — they are mutable, so a key could be changed after insertion and no longer match its own entry. Using one is an error (E0035), not a silent fallback:

Oxigen
m := {[1, 2]: "v"}   // error[E0035]: ARRAY is not hashable

Type keyword: map. Zero value: {}.

Note: An empty map {} and an empty block {} use the same syntax. The parser distinguishes them by context — at the expression level, and in an option/choose arm, {} is a map literal.

#Map Operations

  • Indexing: Access by key:

    Oxigen
    m := {"a": 1, "b": 2}
    m["a"]
  • Index Assignment: Set or update a key directly using bracket syntax:

    Oxigen
    m <map>
    m["name"] = "Oxigen"
    m["version"] = "0.1.0"
    m["version"] = "0.2.0"    // updates existing key

    Both = and := work for index assignment.

  • Dot Access (string keys): Read and write string-keyed entries with .name syntax:

    Oxigen
    y <map> := {"name": "oxigen"}
    y.name              // "oxigen"  (same as y["name"])
    y.version = "0.1"   // inserts the key

    Dot access is limited to string keys. Non-string keys still require bracket syntax.

    Reading a missing key with the dot form is an error, the same as a missing struct field — so a mistyped key does not pass silently:

    Oxigen
    y.mising            // error: key 'mising' not found on map

    Bracket access is a lookup and still answers None for an absent key, so use it (or has) whenever a key may legitimately be missing. Writing through the dot form still inserts:

    Oxigen
    y["mising"]         // None
    has(y, "mising")    // False
    y.author = "ada"    // inserts, no error
  • Insert: Adds the key-value pair, or updates it, in place. extend copies in every entry of another map:

    Oxigen
    m := {"a": 1}
    insert(m, "b", 2)            // {a: 1, b: 2}
    extend(m, {"c": 3})          // {a: 1, b: 2, c: 3}
  • Remove: remove deletes a key in place; pop deletes it and returns its value (None if it wasn't there); clear empties the map:

    Oxigen
    m := {"a": 1, "b": 2}
    remove(m, "a")               // {b: 2}
    pop(m, "b")                  // 2; m is {}
  • Keys / Values:

    Oxigen
    keys({"a": 1, "b": 2})
    values({"a": 1, "b": 2})
  • Length: len({"a": 1}) returns 1.

  • Membership: has({"a": 1}, "a") returns True (checks keys).

  • Copy: copy(m) makes a separate map; n := m shares one.

#Set

A collection of unique values. Created with the set() built-in, from values or from an array:

Oxigen
s := set(1, 2, 3)
duped := set(1, 1, 2, 2)
from_array := set([1, 2, 2, 3])
empty := set()

Type keyword: set. Zero value: set().

Duplicates are automatically removed on creation. A set remembers the order values were added in, which is the order each and println show them. Two sets are equal when they hold the same values, in any order.

Set elements must be hashable, by the same rule and for the same reason as map keys — see Map above.

#Set Operations

  • Add: push(s, x) or insert(s, x) adds a value; adding one the set already holds does nothing. extend(s, items) adds every element of an array, tuple or set.
  • Remove: remove(s, x) deletes a value; pop(s) deletes and returns the most recently added one; clear(s) empties the set.
  • Membership: has(set(1, 2, 3), 2) returns True.
  • Length: len(set(1, 2, 3)) returns 3.
  • Copy: copy(s) makes a separate set.

All of these change the set in place:

Oxigen
s := set(1, 2)
push(s, 3)             // set(1, 2, 3)
remove(s, 1)           // set(2, 3)
extend(s, [3, 4])      // set(2, 3, 4)

Union, intersection, difference and subset tests are in the sets module:

Oxigen
introduce sets

sets.union(set(1, 2), set(2, 3))          // set(1, 2, 3)
sets.intersection(set(1, 2), set(2, 3))   // set(2)

#Byte

Unsigned 8-bit integer (0–255). Created with the byte() built-in:

Oxigen
b := byte(65)

Type keyword: byte. Zero value: 0.

Byte values can be created from integers (0–255), unsigned integers, and characters:

Oxigen
byte(65)
byte(uint(200))
byte(`A`)

Values outside 0–255 produce an error.

#Uint

Unsigned 64-bit integer. Created with the uint() built-in:

Oxigen
u := uint(42)

Type keyword: uint. Zero value: 0.

Uint values can be created from non-negative integers, floats (truncated), strings (parsed), and bytes:

Oxigen
uint(42)
uint(3.7)
uint("100")
uint(byte(255))

Negative values produce an error.

Range is 0 to 18446744073709551615. + and * wrap around on overflow, but a subtraction that would go below zero is an error rather than a wrap. See Unsigned Integer Overflow.

#Vectors

vec2, vec3 and vec4 hold two, three or four numbers: positions, directions, velocities and colours. Build one from ints or floats:

Oxigen
position := vec3(0, 1.5, -2)
velocity := vec3(1, 0, 0)

Read components with .x, .y, .z and .w. They come back as floats:

Oxigen
position.y   // 1.5

Vectors are values, like numbers: operators make new vectors, and a component cannot be assigned. Build a new vector instead:

Oxigen
position = position + velocity * 0.5
position = vec3(0, position.y, position.z)   // position.x = 0 is an error
ExpressionResult
a + b, a - bComponent by component; both the same size
a * bComponent by component; both the same size
v * n, n * v, v / nEvery component scaled by the number n
-vEvery component negated
a == bSame size and the same components

Mixing sizes (vec2(1, 2) + vec3(1, 2, 3)) is a type mismatch error. type() reports VEC2, VEC3 or VEC4. The math module has dot, cross, length, distance, normalize and lerp for vectors; see math.

Components are stored as 32-bit floats, as graphics hardware uses them, so a component reads back as the nearest 32-bit value: vec2(0.1, 0).x is 0.10000000149011612. Vectors display the shorter form, vec2(0.1, 0).

#Enums

Enumerations define a fixed set of named variants. See Enums for the full reference.

Oxigen
enum Stoplight { Red: 0, Yellow: 1, Green: 2 }

light <Stoplight> := Stoplight.Green
light.value   // 2

Variants may carry tuple or struct payloads (Rust-style). Type annotations support both <Enum> (any enum) and <EnumName> (a specific enum).

#Type Introspection

Use type() to get the string name of any value's type:

Oxigen
type(42)
type("hello")
type(3.14)
type(`a`)
type(True)
type(None)
type([1, 2])
type((1, 2))
type({"a": 1})
type(set(1, 2))
type(byte(65))
type(uint(42))

For struct instances, type() returns the struct name instead of a generic type:

Oxigen
struct Person { name <str> }
p := Person("Alice")
type(p)

Use is_type() to check a value's type directly against a type keyword:

Oxigen
is_type(42, int)
is_type("hello", str)
is_type(3.14, float)

See also: