OxigenLang Angle Forms

Angle forms are one of Oxigen's core syntax families. The same <...> bracket shape is reused throughout the language for several different purposes:

  • type annotations in declarations
  • zero-initialization targets
  • typed walrus conversion
  • value constructors (creating error and success values)
  • explicit error and result handling
  • postfix effects on expressions (recovery with guard)
  • standalone logging with <log>
  • entry point marking with main

The syntax stays the same, but the meaning depends on where the angle form appears in a statement.

This guide explains every angle form from scratch. If you have never written Oxigen before, start here.


#Mental Model: How to Read Angle Forms

Whenever you see <...> in Oxigen, look at what comes before the opening < bracket. That tells you what the angle form is doing:

What comes before <What <...> meansExample
A variable nameType annotation — declares what type this variable holdsname <str> = "hello"
The keyword as or just <type> after a nameZero-initialize — create an empty value of this typecount <int>
Nothing (start of expression)Constructor or utility — build a value, transform, or log<Error>("failed"), <log>("msg")
A completed expressionEffect — recover from an error on that expressionexpr <guard>("fallback")

One bracket family, multiple roles. Position is everything.


#Understanding Oxigen's Two-Tier Error Model

Before diving into syntax, you need to understand how Oxigen thinks about errors. This is the foundation for everything that follows.

Oxigen has two fundamentally different kinds of error objects. Confusing them is the most common mistake when learning angle forms.

#Tier 1: Propagating Errors (the "halt everything" kind)

A propagating error is a runtime failure that immediately stops your program. When a propagating error occurs, it bubbles up through every function call and halts evaluation — nothing after it runs unless something explicitly catches it.

You create propagating errors with <fail> or fail:

Oxigen
<fail>("something went wrong")
fail "something went wrong"

Think of these like unhandled exceptions in Python or Java. If nothing catches them, your program stops.

Example — propagating error halts everything:

Oxigen
<fail>("broken")
println("this line never runs")
// The program stopped at <fail>. Nothing below it executes.

#Tier 2: Error Values (the "data" kind)

An error value is a piece of data that describes a failure but does not halt anything. It sits in a variable like a string or integer. You can inspect it, pass it to functions, store it in arrays, or ignore it entirely.

You create error values with <Error>:

Oxigen
err := <Error>("something went wrong")

This does not stop your program. It creates a value you can hold onto.

Example — error value does NOT halt anything:

Oxigen
err := <Error>("broken")
println("this line runs just fine")
println(err.msg)    // prints: broken
// The program keeps running. err is just a value sitting in a variable.

#Why Two Tiers?

Most languages force you into one model: either everything is an exception that you try/catch (Python, Java, JavaScript) or everything is a result type that you must unwrap (Rust, Go). Oxigen gives you both and lets you choose:

  • Use propagating errors (<fail>) when something is genuinely broken and the caller should not silently continue. The program will stop unless the caller explicitly handles it.
  • Use error values (<Error>) when you want to represent failure as data — to inspect it, branch on it, collect multiple errors, or pass it around between functions.

The angle form system lets you move between these two tiers explicitly. You can take a propagating error and turn it into a value (with <type<Error || Value>>), or take an error value and turn it into a propagating error (with <fail>).

#Key Behavioral Differences

Propagating Error (<fail>)Error Value (<Error>)
Halts program?Yes — stops evaluation immediatelyNo — just a value
Created with<fail>("msg") or fail "msg"<Error>("msg")
Is truthy?No (falsy)Yes (truthy — it is a real value)
Can be stored in variable?No — it propagates before assignmentYes — sits in a variable like any value
How to inspectMust catch it first (with guard or option <Error>)Access .msg and .tag directly

#1. Type Annotations

In declaration position (after a variable name, before = or :=), angle forms describe the type of a binding.

#What the Syntax Means

Oxigen
name <str> = "Oxigen"

Reading this left to right:

  1. name — the variable name
  2. <str> — the angle form says "this variable holds a string"
  3. = — strict assignment (the value must already be a string)
  4. "Oxigen" — the value being assigned

#More Examples

Oxigen
count <int> := 10           // count holds an integer, := means mutable
payload <map> := {"ok": True}  // payload holds a map
active <bool> = True         // active holds a boolean, = means immutable

#Why Type Annotations Exist

Without a type annotation, Oxigen infers the type from the value. But explicit types serve two purposes:

  1. Documentation — reading count <int> := 10 tells you the variable is meant to hold integers.
  2. Type safety — Oxigen will reject assignments of the wrong type:
Oxigen
count <int> := 10
count = "hello"    // error: type mismatch — count is locked to int

This is the same type system used across variables, struct fields, and function parameters.


#2. Zero Initialization

Angle forms create an empty value of the specified type when used with just a variable name and no assignment operator.

#What the Syntax Means

Oxigen
count <int>

Reading this left to right:

  1. count — the variable name
  2. <int> — the type to create a zero value for

After this line, count exists and holds 0 (the zero value for integers). No = or := is needed — the type alone is enough.

#The as Form

The as keyword is an optional alternative that does the same thing:

Oxigen
count as <int>

Both count <int> and count as <int> produce identical results. The convention in Oxigen is to use count <int> (without as) because it is shorter and consistent with how types appear everywhere else in the language.

#All Zero Values

Each type has a specific zero value:

TypeZero ValueWhat it looks like
<int>0The integer zero
<float>0.0The float zero
<str>""An empty string
<bool>FalseThe boolean false
<array>[]An empty array
<map>{}An empty map
<None>NoneThe None value

#When to Use Zero Initialization

Use zero initialization when you need a typed variable before you have a value for it. This is common when the value will be set inside a conditional or loop:

Oxigen
result <str>

option {
    ready -> result = fetch_data(),
    result = "not ready"
}

println(result)

Without result <str>, the variable result would not exist before the option block, and the assignments inside would fail.

#Why Not Just Write result <str> := ""?

You can. result <str> (zero initialization) is a shorthand that means the same thing as result <str> := "" for strings, result <int> := 0 for ints, etc. Use whichever reads more clearly. Zero initialization is especially useful when you do not have a meaningful initial value.


#3. Typed Walrus Conversion

With := (the walrus operator), angle forms can force a value into a different type when the conversion is supported.

#What the Syntax Means

Oxigen
num <int> := "10"

Reading this left to right:

  1. num — the variable name
  2. <int> — the target type
  3. := — the walrus operator, which means "convert if needed and allow mutation"
  4. "10" — the value (a string)

Oxigen sees that "10" is a string but the target type is <int>. Because := was used (not =), Oxigen attempts to convert the string "10" to the integer 10. This succeeds, and num holds 10.

#More Conversion Examples

Oxigen
label <str> := 42          // integer 42 becomes string "42"
chars <array> := "hello"   // string "hello" becomes ["h", "e", "l", "l", "o"]
flag <bool> := 1           // integer 1 becomes True

#Strict vs Walrus Assignment

The difference between = and := is critical:

Oxigen
num <int> = "10"     // ERROR: type mismatch — "10" is a string, not an int
num <int> := "10"    // OK: converts "10" to 10

= (strict):

  • The value must already be the correct type
  • No conversion is attempted
  • The binding is immutable (cannot be reassigned)

:= (walrus):

  • Oxigen will attempt type conversion
  • If conversion is impossible, you get an error
  • The binding is mutable (can be reassigned later)

#When Conversion Fails

If the conversion is not possible, you get a propagating error:

Oxigen
num <int> := "hello"    // error: cannot convert "hello" to int

#4. Error Construction with <Error>

<Error>(...) creates an error value (tier 2). This is a value-level operation — it does not halt your program.

#What the Syntax Means

Oxigen
<Error>("file not found")

Breaking this down:

  1. <Error> — the angle form that says "construct an error value"
  2. ( — open parenthesis, start of the argument
  3. "file not found" — the error message (any expression that produces a value)
  4. ) — close parenthesis

The result is an error value object. You typically assign it to a variable:

Oxigen
err := <Error>("file not found")

Now err holds an error value. Your program is still running. You can inspect it:

Oxigen
println(err.msg)    // prints: file not found
println(type(err))  // prints: ERROR_VALUE

#Tagged Errors

Add a tag inside nested angle brackets to categorize the error:

Oxigen
<Error<network>>("connection timed out")

Breaking this down:

  1. <Error — start of the error constructor
  2. <network> — the tag, nested inside the outer angle brackets
  3. > — close the outer angle bracket
  4. ("connection timed out") — the error message

The tag is a free-form string identifier. It does not need to be declared anywhere — just pick a name that describes the category of failure.

Oxigen
err := <Error<network>>("connection timed out")
println(err.msg)    // "connection timed out"
println(err.tag)    // "network"

#Why Tags Exist

Tags give you a way to group related errors without defining custom error types or classes. They are lightweight labels:

Oxigen
<Error<file>>("missing config")          // file-related error
<Error<auth>>("invalid token")           // authentication error
<Error<parse>>("unexpected character")   // parsing error
<Error<retry_error>>("max retries hit")  // retryable error

Later, when you catch errors with guard, you can filter by tag — only handling specific categories of errors and letting others propagate.

#Public Fields on Error Values

Every error value has two fields you can access with dot notation:

FieldTypeDescription
.msgstrThe error message you passed to <Error>
.tagstr or NoneThe tag if you used <Error<tag>>, otherwise None
Oxigen
tagged := <Error<network>>("offline")
println(tagged.msg)    // "offline"
println(tagged.tag)    // "network"

plain := <Error>("something broke")
println(plain.msg)     // "something broke"
println(plain.tag)     // None

#When to Use <Error>

Use <Error> when you want to represent a failure as data that you will inspect, store, or make decisions about later:

Oxigen
// Collect multiple validation errors instead of stopping at the first one
issues <array> := []
push(issues, <Error<validation>>("name is required"))
push(issues, <Error<validation>>("email is invalid"))

each issue in issues {
    println("{issue.tag}: {issue.msg}")
}
// prints:
// validation: name is required
// validation: email is invalid

This would not work with <fail> because the program would stop at the first error.


#5. Success Wrapping with <Value>

<Value>(...) wraps any value as an explicit success. It is the counterpart to <Error>.

#What the Syntax Means

Oxigen
<Value>("ok")

Breaking this down:

  1. <Value> — the angle form that says "wrap this as a success value"
  2. ("ok") — the value to wrap

The result is a Value object. You access the inner value with .value:

Oxigen
wrapped := <Value>("loaded successfully")
println(wrapped.value)    // "loaded successfully"
println(type(wrapped))    // "VALUE"

#Why <Value> Exists

On its own, <Value> might seem pointless — why wrap a value that is already fine? The answer is that <Value> pairs with <Error> to form the expected-result pattern.

When a function might succeed or fail, you want a uniform way to represent both outcomes as values. <Value> and <Error> give you that:

Oxigen
fun safe_divide(a <int>, b <int>) {
    option {
        b == 0 -> <Error<math>>("division by zero"),
        <Value>(a / b)
    }
}

result := safe_divide(10, 2)
println(result.value)    // 5

result := safe_divide(10, 0)
println(result.msg)      // "division by zero"
println(result.tag)      // "math"

Both paths return a value — one is a Value, the other is an ErrorValue. Neither halts the program. The caller decides what to do.


#6. Expected-Result Normalization with <type<Error || Value>>

This is one of the most powerful angle forms. It takes any expression — even one that might produce a propagating error — and converts the result into either a Value or an ErrorValue. The error is caught and turned into data instead of halting your program.

#The Problem It Solves

Normally, if a function fails with a propagating error, your program stops:

Oxigen
data := read_file("missing.txt")    // propagating error — program halts here
println("this never runs")

But sometimes you expect a call might fail and you want to handle both outcomes. You do not want the error to halt your program — you want to inspect it and decide what to do.

#What the Syntax Means

Oxigen
<type<Error || Value>>(read_file("config.txt"))

Breaking this down:

  1. <type — starts a type-conversion angle form
  2. <Error || Value> — the target type is "either an Error or a Value"
  3. > — closes the outer angle bracket
  4. (read_file("config.txt")) — the expression to evaluate

#How It Works, Step by Step

Oxigen
result := <type<Error || Value>>(read_file("config.txt"))

Here is what happens:

  1. Oxigen evaluates read_file("config.txt") inside the angle form.
  2. If the call succeeds: The return value is wrapped as Value(...). You can access it with result.value.
  3. If the call fails (produces a propagating error): The error is caught and converted to an ErrorValue. The propagation stops. You can access it with result.msg and result.tag.
  4. Either way, your program continues. The error is now a value, not a propagation.

#Tagged Variant: <type<Error<tag> || Value>>

You can include a tag in the Error part of the union type. This tag acts as a default — it is applied to any error that does not already have its own tag.

Oxigen
<type<Error<non_integer> || Value>>(int(value))

Breaking this down:

  1. <type — starts a type-conversion angle form
  2. <Error<non_integer> — the error side of the union, with a default tag of non_integer
  3. || Value> — the success side
  4. (int(value)) — the expression to evaluate

What happens step by step:

  1. int(value) is evaluated.
  2. If it succeeds: The result is wrapped as Value(...), same as the untagged form. The tag is ignored.
  3. If it fails with an untagged error: The error is caught and the tag non_integer is applied. The result is an ErrorValue with .tag == "non_integer" and .msg set to the error message.
  4. If it fails with an already-tagged error: The existing tag is preserved. The default tag from the union type is not used. This means tagged errors from deeper in the call stack keep their identity.

Example — default tag applied to untagged error:

Oxigen
result := <type<Error<non_integer> || Value>>(int("hello"))
println(result.tag)    // "non_integer" — applied because int() errors are untagged
println(result.msg)    // "int: cannot parse 'hello'"

Example — existing tag preserved:

Oxigen
result := <type<Error<fallback> || Value>>(<fail>(<Error<original>>("boom")))
println(result.tag)    // "original" — the existing tag wins over "fallback"
println(result.msg)    // "boom"

Either order works — Value || Error<tag> is the same as Error<tag> || Value:

Oxigen
result := <type<Value || Error<parse>>>(parse_json(input))

#When to Use the Tagged Variant

Use <type<Error<tag> || Value>> when you want every error from an expression to carry a category tag, even if the underlying function does not tag its errors:

Oxigen
fun convert_to_int(value <generic>) {
    converted <Error || Value> := <type<Error<non_integer> || Value>>(int(value))
    option {
        is_value(converted) -> converted.value,
        converted
    }
}

result := convert_to_int("hello")
println(result.tag)    // "non_integer"
println(result.msg)    // "int: cannot parse 'hello'"

Without the tag, result.tag would be None because int() does not tag its errors. The tagged union form adds the category for you.

#Checking Which Outcome You Got

After normalization, you have a value that is either a Value or an ErrorValue. Use the is_value() and is_error() builtins to check:

Oxigen
result := <type<Error || Value>>(read_file("settings.oxi"))

option {
    is_value(result) -> {
        // Success path — use result.value
        println("File contents: {result.value}")
    },
    {
        // Failure path — use result.msg
        println("Could not read file: {result.msg}")
    }
}

You can also check from the error side:

Oxigen
option {
    is_error(result) -> println("Failed: {result.msg}"),
    println("Success: {result.value}")
}

#With Typed Declarations

You can declare the variable with the Error || Value type annotation to document what it holds:

Oxigen
config <Error || Value> := <type<Error || Value>>(read_file("config"))

This reads as: "config holds either an Error or a Value, and we got it by normalizing read_file("config")."

#Quick Examples

Oxigen
// Success path — wraps "loaded" as a Value
ok := <type<Error || Value>>("loaded")
println(ok.value)    // "loaded"

// Failure path — catches the propagating error and wraps it as ErrorValue
failed := <type<Error || Value>>(<fail>("missing config"))
println(failed.msg)  // "missing config"
// Note: <fail> would normally halt the program, but <type<Error || Value>> caught it

#When to Use <type<Error || Value>>

Use it whenever you are calling something that might fail and you want to:

  • Keep the program running regardless of the outcome
  • Inspect the error as data instead of crashing
  • Make decisions based on success or failure

#7. Propagating Failures with <fail>

<fail>(...) creates a propagating runtime error (tier 1). This is a control-flow operation — it halts normal evaluation immediately.

#What the Syntax Means

Oxigen
<fail>("invalid input")

Breaking this down:

  1. <fail> — the angle form that says "produce a propagating error"
  2. ("invalid input") — the error message

After this line, nothing else in the current scope runs. The error propagates up through every caller until something catches it (a guard, an option <Error> arm, or a <type<Error || Value>> wrapper).

#From Plain Values

Any value passed to <fail> is converted to an error message string:

Oxigen
<fail>("bad input")     // message: "bad input"
<fail>(42)              // message: "42"
<fail>(True)            // message: "True"

#From Error Values

If you pass an ErrorValue to <fail>, it preserves the message and tag. This is how you upgrade a tier-2 error value into a tier-1 propagating error:

Oxigen
err := <Error<network>>("connection lost")

// err is just a value — the program is still running
// Now convert it to a propagating error:
<fail>(err)

// This propagating error has tag "network" and message "connection lost"
// The program halts here

#Keyword Form

The keyword form fail does the same thing as <fail>:

Oxigen
fail "bad input"

Both forms are supported. <fail> is the preferred modern form because it is visually consistent with other angle forms.

#When to Use <fail>

Use <fail> inside a function when you have encountered a condition where the function cannot continue and needs to signal failure to its caller:

Oxigen
fun parse_age(input <str>) {
    age <int> := input
    option {
        age < 0 -> <fail>(<Error<validation>>("age cannot be negative")),
        age > 150 -> <fail>(<Error<validation>>("age seems unrealistic")),
        age
    }
}

Here <fail> is used with <Error<tag>> together: <Error<validation>>(...) creates a tagged error value, and <fail>(...) converts it into a propagating error. The caller of parse_age will see a propagating error with tag "validation".


#8. error(...) vs <Error>(...) vs <fail>(...)

These three forms are related but serve different purposes. This section clarifies exactly when to use each one.

#error(...) — Legacy Form

Oxigen
error("missing file")

This is the old compatibility form. It creates a propagating error (like <fail>). Use it only if you are working with older Oxigen code. Prefer <fail> for new code.

#<Error>(...) — Create Error Value (Does NOT Halt)

Oxigen
err := <Error>("missing file")

Creates an error value. The program keeps running. You get an object with .msg and .tag fields. Use this when you want failure represented as data.

#<fail>(...) — Create Propagating Error (Halts Program)

Oxigen
<fail>("missing file")

Creates a propagating error. The program stops. Use this when the current function cannot continue and needs to tell its caller about the failure.

#Decision Guide

Ask yourself:

  1. Do I want the program to keep running? Use <Error>("msg") — it creates a value, not a halt.
  2. Do I want the program to stop and signal failure? Use <fail>("msg") — it propagates.
  3. Do I need a tag on the error? Use <Error<tag>>("msg") for error values, or <fail>(<Error<tag>>("msg")) to propagate a tagged error.
  4. Am I working with old code? error("msg") still works, but prefer <fail> in new code.

#Quick Comparison

FormWhat it producesHalts program?Best for
error("msg")Propagating errorYesLegacy code
<Error>("msg")Error valueNoRepresenting failure as data
<Error<tag>>("msg")Tagged error valueNoCategorized failure as data
<fail>("msg")Propagating errorYesSignaling failure to callers
<fail>(<Error<tag>>("msg"))Tagged propagating errorYesSignaling categorized failure

#9. Error Recovery with guard

guard catches propagating errors and replaces them with a fallback value. It converts a tier-1 propagating error back into normal program flow.

Think of guard as a safety net: "try this expression, but if it fails, use this fallback instead."

#Angle Form (Preferred)

Oxigen
name := read_name() <guard>("Guest")

What happens step by step:

  1. read_name() is evaluated.
  2. If it succeeds: Its return value is used. name gets that value. The <guard> part does nothing.
  3. If it fails (propagating error): The error is caught, discarded, and "Guest" is used instead. name gets "Guest".

Breaking down the syntax:

  1. read_name() — the expression that might fail
  2. <guard> — the angle effect that says "catch errors from the left expression"
  3. ("Guest") — the fallback value to use if the left expression fails

#With a Binding (Keyword Form)

When you need to access the caught error to decide the fallback, use the keyword form with guard err ->:

Oxigen
name := read_name() guard err -> "Unknown (error: {err.msg})"

What happens step by step:

  1. read_name() is evaluated.
  2. If it succeeds: Its return value is used. The guard does nothing.
  3. If it fails: The error is caught and bound to the variable err. The err variable is an error value with .msg and .tag fields. The expression after -> is evaluated and its result becomes the value.

The err binding is only visible inside the fallback expression (after ->). You cannot use it outside.

#Tagged Filtering

Add a tag filter to only catch specific categories of errors:

Oxigen
profile := fetch_profile() <guard<Error<retry_error>>>("Guest")

Breaking down the syntax:

  1. fetch_profile() — the expression that might fail
  2. <guard — start of the guard effect
  3. <Error<retry_error>> — the tag filter: only catch errors tagged retry_error
  4. > — close the guard angle bracket
  5. ("Guest") — the fallback

What happens step by step:

  1. fetch_profile() is evaluated.
  2. If it succeeds: The value passes through. Guard does nothing.
  3. If it fails with a retry_error tag: The error is caught. "Guest" is used.
  4. If it fails with a different tag (like network or auth) or no tag: The error is not caught. It keeps propagating. The guard only handles the specific tag you specified.

This is powerful because it lets you handle expected failures while allowing unexpected ones to propagate normally.

#What guard Does NOT Catch

guard only catches errors (both propagating errors and error values). It does not catch other values:

Oxigen
result := None guard err -> "fallback"
// result is None, NOT "fallback"
// guard did not fire because None is not an error

None is a value, not an error. Guard ignores it completely.

#Keyword Form at Statement Level

The keyword form can also be used at the statement level, without assignment:

Oxigen
println(fail "boom") guard err -> err.msg

Here:

  1. println(fail "boom") — fail "boom" creates a propagating error before println can run.
  2. guard err -> err.msg — catches the error, binds it to err, and evaluates err.msg which returns the string "boom".

#10. Logging with <log>

<log> is a standalone logging utility that writes timestamped messages to stdout. It is a prefix-position angle form — you call it on its own, not after an expression.

Think of <log> as a simple, structured way to record what your program is doing.

#Basic Form

Oxigen
<log>("server started")

This prints the following to stderr:

2026-03-28 14:30:00: server started

Breaking this down:

  1. <log> — the angle form that says "write a log message"
  2. ("server started") — the message to log

The timestamp is automatically added in YYYY-MM-DD HH:MM:SS format. You do not need to format it yourself.

#Tagged Form

Add a tag inside nested angle brackets to categorize the message:

Oxigen
<log<info>>("request received")

This prints:

2026-03-28 14:30:00: [INFO] request received

Breaking this down:

  1. <log — start of the log angle form
  2. <info> — the tag, nested inside the outer angle brackets
  3. > — close the outer angle bracket
  4. ("request received") — the message

The tag is uppercased in the output. You write <log<info>> in your code, but the output shows [INFO].

#Common Tags

You can use any identifier as a tag. Here are some common choices:

Oxigen
<log<info>>("user logged in")           // [INFO] user logged in
<log<warn>>("disk space low")           // [WARN] disk space low
<log<Error>>("file not found")          // [ERROR] file not found
<log<debug>>("cache hit ratio: 0.95")   // [DEBUG] cache hit ratio: 0.95

Tags are free-form — there is no fixed list. Pick names that make sense for your application.

#Nested Tags

For more specific categorization, nest tags inside tags:

Oxigen
<log<Error<network>>>("connection lost")

This prints:

2026-03-28 14:30:00: [ERROR:NETWORK] connection lost

Both the outer and inner tags are uppercased and joined with a colon. This is useful when you want to distinguish between different subcategories of errors or events.

#Tag-Only Form (No Message)

Sometimes you just want to mark that something happened, without a detailed message. You can omit the parentheses entirely:

Oxigen
<log<Error>>

This prints:

2026-03-28 14:30:00: [ERROR] generic

You can also use empty parentheses — the result is the same:

Oxigen
<log<custom_tag>>()

This prints:

2026-03-28 14:30:00: [CUSTOM_TAG]

Both forms (no parens and empty parens) are equivalent when there is no message.

If the tag refers to an in-scope error value, <log> uses that error's message automatically:

Oxigen
same_version_error <Error> = <Error<same_version_error>>("already installed")

<log<same_version_error>>
<log<Error<same_version_error>>>

Both print:

2026-03-28 14:30:00: [SAME_VERSION_ERROR] already installed

#Return Value

<log> always returns None. It exists purely for its side effect (writing to stderr). You would not normally assign its result to a variable, but if you do:

Oxigen
x := <log>("hello")
// x is None

#Key Facts

PropertyDetails
Output destinationstdout
Timestamp formatYYYY-MM-DD HH:MM:SS
Tag formattingUppercased in output
Return valueNone
Parens required?No — optional when there is no message
Allowed tagsAny identifier (info, Error, debug, my_tag, etc.)

#When to Use <log>

Use <log> whenever you want to record what your program is doing without affecting its output:

  • Debugging: Add <log<debug>>("entering parse loop") to trace execution flow.
  • Error reporting: After catching an error with guard or option, log the details with <log<Error>>(err.msg).
  • Monitoring: Log important events like <log<info>>("user {user_id} logged in") for later review.
  • Diagnostics: Use nested tags like <log<perf<slow>>>("query took 3.2s") to categorize performance observations.

Because <log> writes to stderr, you can redirect logs to a file while keeping your program's stdout output separate:

bash
oxigen my_program.oxi 2> app.log

#11. Angle Forms Inside option

option is Oxigen's multi-arm conditional. It evaluates conditions top to bottom and runs the first truthy arm. Angle forms extend option with explicit error handling through the <Error> arm.

#How option Works (Quick Review)

Oxigen
status := option {
    age >= 18 -> "adult",
    "minor"
}
  • Each arm has a condition (age >= 18) and a body ("adult"), separated by ->.
  • The last arm without -> is the default.
  • The first truthy condition wins. Its body is evaluated and becomes the result.

#The <Error> Arm

You can add an <Error> -> body arm to option. This arm catches any propagating error that occurs anywhere during the option evaluation:

Oxigen
result := option {
    risky_call() -> "success",
    <Error> -> "something went wrong"
}

Breaking down the syntax:

  1. option { ... } — the conditional block
  2. risky_call() -> "success" — a normal arm: if risky_call() is truthy, return "success"
  3. <Error> -> "something went wrong" — the error arm: if any propagating error occurs, return "something went wrong"

#When Does the <Error> Arm Fire?

The <Error> arm fires in three situations. Understanding all three is important.

Situation 1: A condition evaluation produces an error.

If evaluating the condition on the left side of -> produces a propagating error, the <Error> arm catches it:

Oxigen
result := option {
    missing_variable == 1 -> "found it",
    <Error> -> "condition failed"
}
// result is "condition failed"

Why this works: missing_variable is not defined. Evaluating missing_variable == 1 produces a propagating error ("identifier not found"). Normally this would halt the program, but the <Error> arm catches it and returns "condition failed".

Situation 2: A matched body produces an error.

If a condition is truthy but evaluating its body (the right side of ->) produces an error, the <Error> arm catches it:

Oxigen
result := option {
    True -> nonexistent_function(),
    <Error> -> "body failed"
}
// result is "body failed"

Why this works: The condition True is truthy, so Oxigen tries to evaluate the body nonexistent_function(). That produces a propagating error. The <Error> arm catches it.

Situation 3: The default body produces an error.

If no condition matches and the default arm (the last arm without ->) itself produces an error:

Oxigen
result := option {
    False -> "nope",
    nonexistent_function(),
    <Error> -> "default failed"
}
// result is "default failed"

Why this works: False does not match, so Oxigen falls through to the default arm nonexistent_function(). That errors. The <Error> arm catches it.

#Without an <Error> Arm

If there is no <Error> arm, errors propagate normally and can halt your program:

Oxigen
result := option {
    missing == 1 -> "found"
}
// This propagates the "identifier not found" error — program halts

The <Error> arm is optional. Only add it when you want option to handle errors internally.

#Rethrowing from the <Error> Arm

You can use <fail> inside the <Error> arm to propagate a different error with a clearer message:

Oxigen
result := option {
    connected -> fetch_data(),
    <Error> -> <fail>("request failed")
}

This catches whatever error occurred (maybe "connection refused" or "DNS lookup failed") and replaces it with a single clear message: "request failed". The new error propagates to the caller.

#Using Error Values in option Conditions

Because error values (created with <Error>) are truthy, they work as option conditions. This lets you branch on error properties:

Oxigen
err := <Error<auth>>("token expired")

message := option {
    err.tag == "auth" -> "please log in again",
    err.tag == "network" -> "check your connection",
    "unknown error: {err.msg}"
}
println(message)    // "please log in again"

This works because err is a value (tier 2), not a propagating error. It sits in a variable and you can inspect its .tag field in conditions.

#Complete option + Error Example

Here is a realistic example: a function that tries to read a config file and falls back to defaults if anything fails.

Oxigen
fun load_config() {
    // Wrap the file read so it doesn't propagate
    file_result <Error || Value> := <type<Error || Value>>(read_file("config.oxi"))

    config := option {
        is_value(file_result) -> file_result.value,
        {
            // File read failed — try environment variable
            env_result <Error || Value> := <type<Error || Value>>(get_env("APP_CONFIG"))

            option {
                is_value(env_result) -> env_result.value,
                <fail>(<Error<config>>("no config source available"))
            }
        }
    }

    config
}

Step by step:

  1. read_file("config.oxi") is wrapped with <type<Error || Value>> so it does not propagate.
  2. If the file read succeeded (type == "VALUE"), use its contents.
  3. If the file read failed, try the environment variable (also wrapped).
  4. If both failed, propagate a tagged error with <fail>.

#12. Angle Forms and choose Patterns

choose evaluates a subject value against named patterns. It is different from option — option checks boolean conditions, while choose matches a value against named patterns.

#How choose Works (Quick Review)

Oxigen
pattern is_even(n) when n % 2 == 0
pattern is_odd(n) when n % 2 != 0

choose 42 {
    is_even -> "even",
    is_odd -> "odd"
}

The subject (42) is passed to each pattern. The first pattern whose condition is truthy wins.

#Error Flow in choose

Errors can occur at three points during a choose. Unlike option, choose does not have a built-in <Error> arm. Errors propagate immediately.

Point 1: Subject evaluation errors.

If the subject expression itself errors, the error propagates and the choose stops:

Oxigen
choose nonexistent_function() {
    is_even -> "even",
    else -> "other"
}
// propagating error — program halts

Point 2: Pattern condition errors.

If evaluating a pattern's when condition produces an error, it propagates:

Oxigen
pattern bad_pattern(n) when n / 0 == 1

choose 42 {
    bad_pattern -> "matched",
    else -> "other"
}
// propagating error from the division by zero

Point 3: Arm body errors.

If the matched arm's body expression errors, it propagates:

Oxigen
choose 42 {
    is_even -> nonexistent_function(),
    else -> "other"
}
// propagating error from the body

#Protecting choose with guard

Since choose has no built-in error arm, use guard to recover from errors:

Oxigen
result := (choose some_value {
    is_valid -> process(some_value),
    else -> "unknown"
}) guard err -> "error during classification: {err.msg}"

The parentheses (...) around the choose let the guard apply to the whole choose expression. Without them, guard would only apply to the last arm's body.

#Protecting choose with option + <Error>

Wrap the choose inside an option to use the <Error> arm:

Oxigen
result := option {
    True -> {
        choose user_input {
            is_number -> parse_number(user_input),
            is_date -> parse_date(user_input),
            else -> user_input
        }
    },
    <Error> -> "failed to classify input"
}

Here option acts as a safety net around choose. If anything inside the choose errors, the <Error> arm catches it.

#Using Error Values as choose Subjects

Because error values are regular values, you can match on them with patterns:

Oxigen
pattern is_network_error(e) when e.tag == "network"
pattern is_auth_error(e) when e.tag == "auth"
pattern is_timeout(e) when e.tag == "timeout"

err := <Error<network>>("connection refused")

message := choose err {
    is_network_error -> "check your connection",
    is_auth_error -> "please log in again",
    is_timeout -> "try again later",
    else -> "unknown error: {err.msg}"
}
println(message)    // "check your connection"

This works because:

  1. err is an error value (tier 2), not a propagating error. It is a regular value.
  2. Each pattern receives err as its parameter and checks e.tag.
  3. The first matching pattern wins.

#Combining choose with Expected Results

A practical pattern — classify the outcome of a fallible operation using is_value() and is_error() inside option:

Oxigen
result <Error || Value> := <type<Error || Value>>(fetch_user(user_id))

option {
    is_value(result) -> println("Got user: {result.value}"),
    is_error(result) -> println("Failed: {result.msg}"),
    println("unexpected result type")
}

Or use patterns with choose for more complex routing:

Oxigen
pattern is_success(r) when is_value(r)
pattern is_failure(r) when is_error(r)

result <Error || Value> := <type<Error || Value>>(fetch_user(user_id))

choose result {
    is_success -> println("Got user: {result.value}"),
    is_failure -> println("Failed: {result.msg}"),
    else -> println("unexpected result type")
}

#13. Entry Point with main

Note: Although main is documented alongside angle forms for completeness (since it relates to module vs script behavior), it uses keyword syntax, not angle bracket syntax. main is a keyword, not an angle form.

main marks the entry point of a script. Code inside main only runs when the file is executed directly — it is skipped when the file is imported as a module via introduce.

Everything outside main (function definitions, struct definitions, patterns, etc.) runs in both contexts. This lets you write files that work as both libraries and scripts.

#What the Syntax Means

Oxigen
main {
    println("running as script")
}

Breaking this down:

  1. main — the keyword that says "this block is the script entry point"
  2. { ... } — the block of statements to execute only when run directly

#Library + Script Example

Oxigen
// math_utils.oxi
fun square(n <int>) { n * n }
fun cube(n <int>) { n * n * n }

main {
    println(square(5))
    println(cube(3))
}

When run directly (oxigen math_utils.oxi): prints 25 and 27.

When imported via introduce "math_utils": the functions square and cube are available, but nothing is printed.

#Return Value

main returns the value of the last expression in its body, or None if the body is empty. This is consistent with how all blocks work in Oxigen.

#Indent Mode

In #[indent] mode, use main: to start the block:

Oxigen
#[indent]

fun square(n <int>):
    n * n

main:
    println(square(5))

#When to Use main

Use main when you want a file to serve double duty — reusable definitions at the top, and a script entry point at the bottom. This is similar to Python's if __name__ == "__main__": pattern.


#14. Complete Error Handling Recipes

These recipes show how to combine angle forms for common scenarios. Each one explains what is happening and why it works.

#Recipe: Simple Try-Catch

The closest thing to try/catch in other languages. Try an expression, use a fallback if it fails:

Oxigen
result := risky_operation() <guard>("safe default")

Why it works: <guard> catches the propagating error from risky_operation() and replaces it with "safe default". The program continues normally.

#Recipe: Try-Catch with Error Inspection

Same idea, but examine the error to decide what fallback to use:

Oxigen
result := risky_operation() guard err -> {
    option {
        err.tag == "network" -> "offline mode",
        err.tag == "auth" -> "please log in",
        "unknown error"
    }
}

Why it works: The keyword guard err -> binds the caught error to err. The option block inspects err.tag to pick the right fallback message. The err variable is an error value with .msg and .tag fields available inside the fallback.

#Recipe: Log-Then-Recover

Log what you are about to do, then guard the operation with a fallback:

Oxigen
<log<info>>("fetching data...")
result := fetch_data() <guard>("cached fallback")

Why it works: <log> writes a timestamped message to stderr. Then fetch_data() runs. If it fails, <guard> catches the error and replaces it with "cached fallback". The log line is always written, regardless of whether fetch_data() succeeds or fails.

#Recipe: Log After Catching an Error

Normalize the operation, inspect the outcome, and log errors before deciding what to do:

Oxigen
result := <type<Error || Value>>(fetch_data())
option {
    is_error(result) -> {
        <log<Error>>(result.msg)
        "cached fallback"
    },
    result.value
}

Why it works: <type<Error || Value>> catches any propagating error and turns it into an error value. The option block checks whether the result is an error. If so, it logs the error message to stderr with <log<Error>> and returns a fallback. If the call succeeded, it unwraps the value.

#Recipe: Normalize and Branch

Wrap a fallible call into an expected-result, then branch on success or failure:

Oxigen
outcome <Error || Value> := <type<Error || Value>>(connect_to_database())

option {
    is_value(outcome) -> {
        db := outcome.value
        println("Connected to database")
    },
    {
        println("Database unavailable: {outcome.msg}")
        println("Running in offline mode")
    }
}

Why it works: <type<Error || Value>> catches the propagating error and converts it to an error value. The option block checks type(outcome) to decide which path to take. Both paths are normal program flow — no errors propagate.

#Recipe: Multiple Fallible Steps

When several steps might fail, normalize each one independently:

Oxigen
config <Error || Value> := <type<Error || Value>>(read_file("config.oxi"))
db <Error || Value> := <type<Error || Value>>(connect_db())

option {
    is_error(config) -> <fail>(<Error<startup>>("config: {config.msg}")),
    is_error(db) -> <fail>(<Error<startup>>("database: {db.msg}"))
}

// If we reach here, both succeeded
println("Config: {config.value}")
println("Database: {db.value}")

Why it works: Each call is individually normalized. If either fails, the option block detects the failure type and propagates a clear tagged error. If both succeed, the program continues to use .value on each.

#Recipe: Error Arm as Safety Net

Use option's <Error> arm as a catch-all when multiple branches might fail:

Oxigen
result := option {
    input_valid(data) -> process(data),
    needs_retry(data) -> retry(data),
    <fail>(<Error<validation>>("unrecognized input")),
    <Error> -> <Error<internal>>("unexpected failure during processing")
}

Why it works: If input_valid(data), process(data), needs_retry(data), or retry(data) produce a propagating error, the <Error> arm catches it and returns an error value (not a propagating error). The default arm uses <fail> intentionally — if no condition matches, it propagates a validation error.

#Recipe: Tagged Error Pipeline

Create errors with tags, propagate them, and catch only specific tags downstream:

Oxigen
fun validate(input <str>) {
    option {
        len(input) == 0 -> <fail>(<Error<validation>>("input is empty")),
        len(input) > 100 -> <fail>(<Error<validation>>("input too long")),
        input
    }
}

// Only catch validation errors — let all other errors propagate normally
cleaned := validate(user_input) <guard<Error<validation>>>("(invalid)")

Why it works: validate uses <fail> with tagged error values. The <guard<Error<validation>>> at the call site only catches errors tagged validation. If validate encountered a different kind of error (like a system-level failure), it would propagate past the guard.

#Recipe: Classify Errors with choose

Use patterns to route different categories of errors to different handlers:

Oxigen
pattern is_network(e) when e.tag == "network"
pattern is_auth(e) when e.tag == "auth"
pattern is_retryable(e) when e.tag == "retry_error"

err := <Error<network>>("connection refused")

action := choose err {
    is_retryable -> "retrying...",
    is_network -> "switching to offline mode",
    is_auth -> "redirecting to login",
    else -> "unhandled error: {err.msg}"
}

Why it works: The error value is a regular value, so choose can match on it using patterns. Each pattern checks .tag to classify the error. The matched arm's body becomes the result.


#15. Current Built-In Angle Forms Reference

#Type-side (declaration position)

Used after a variable name to declare its type:

Oxigen
<int>             // integer
<float>           // floating point
<str>             // string
<bool>            // boolean
<char>            // character
<byte>            // byte (0-255)
<uint>            // unsigned integer
<array>           // array
<map>             // map (dictionary)
<set>             // set
<tuple>           // tuple
<Error>           // error value type
<Error<tag>>      // tagged error value type
<Value>           // success value type
<Error || Value>  // expected-result union type

#Expression-side (prefix position)

Used at the start of an expression to construct or transform:

Oxigen
<Error>("message")                  // create error value (does NOT halt)
<Error<tag>>("message")             // create tagged error value (does NOT halt)
<Value>(expr)                       // wrap value as explicit success
<type<Error || Value>>(expr)        // normalize expression to expected-result
<type<Error<tag> || Value>>(expr)   // normalize with default tag on errors
<fail>("message")                   // propagate as runtime error (HALTS)
<fail>(<Error<tag>>("message"))     // propagate tagged error (HALTS)

#Logging (standalone)

Used as a standalone statement to write timestamped messages to stderr:

Oxigen
<log>("message")                       // log message to stderr
<log<tag>>("message")                  // log with tag to stderr
<log<Error>>("message")                // log with Error tag
<log<Error<sub_tag>>>("message")       // log with nested tags
<log<tag>>                             // log tag only (no message)

#Effect-side (postfix position)

Used after a completed expression to apply recovery:

Oxigen
expr <guard>("fallback")                        // recover with fallback value
expr <guard<Error<tag>>>("fallback")            // recover only if tag matches
expr guard err -> fallback_expr                 // recover with error binding (keyword form)

#Block-level (keyword syntax)

Used at the top level of a file to control execution context. Note that main uses keyword syntax, not angle bracket syntax:

Oxigen
main { ... }                                       // entry point — runs only when file is executed directly

#16. Design Rules

When reading or writing Oxigen code, these rules keep angle forms understandable:

  1. Declaration position = types. After a variable name, <...> is always a type annotation.
  2. Prefix position = construction. At the start of an expression, <...> constructs or transforms a value.
  3. Postfix position = effects. After a completed expression, <...> applies recovery (with guard).
  4. <Error> creates values, <fail> creates propagation. Never confuse the two. <Error> is data, <fail> is control flow.
  5. <Error<tag>> for categories. Use tags for lightweight error grouping without custom types.
  6. <type<Error || Value>> for expected failure. When you know a call might fail and you want to handle both paths explicitly without the program halting.
  7. guard for recovery. When you have a fallback and want to keep the program running.
  8. <log> for logging. Use <log> to write timestamped messages to stderr with optional tags.
  9. option + <Error> arm for structured recovery. When multiple conditions or bodies inside an option might fail and you want one safety net.
  10. choose does not have <Error>. Wrap choose in guard or option if you need error recovery.
  11. main for entry points. Use main (keyword syntax, not an angle form) to mark code that should only run when the file is executed directly, not when imported as a module.

#17. Error Messages

All runtime errors in Oxigen include helpful context to make debugging easier. When something goes wrong, the error output tells you where the problem is, what the source code looks like at that point, and how you might fix it.

#What You See

Every runtime error includes three pieces of information:

  1. Source position — the line number and column where the error occurred.
  2. Source code excerpt — the actual line of code, with a caret (^) pointing to the exact position of the problem.
  3. Hint — a suggestion for how to fix the issue.

#Example

If your code divides by zero:

Oxigen
result := 10 / 0

Oxigen prints:

error: division by zero
  --> line 3:10
  |
3 | result := 10 / 0
  |          ^
  = hint: ensure the divisor is not zero before dividing

Reading this output:

  1. error: division by zero — what went wrong.
  2. --> line 3:10 — the error is on line 3, column 10.
  3. 3 | result := 10 / 0 — the source code line.
  4. ^ — points to the exact position where the error was detected.
  5. = hint: ensure the divisor is not zero before dividing — a suggestion for fixing it.

#Why This Matters

Helpful error messages reduce the time you spend debugging. Instead of seeing a cryptic "runtime error" with no context, you get a clear pointer to the problem and a nudge toward the solution. This is especially useful when errors occur deep inside nested expressions or function calls — the source position and caret show you exactly where to look.