Zynx by Example

A compact tour of current Zynx source patterns.

These examples are meant to be runnable and practical. The focused reference pages remain the source for full rules.

Save any standalone example as main.zx and run it with:

./zynx run main.zx

Basics

import std.os;

fn clamp_min(value: i32, minimum: i32) -> i32 {
    if value < minimum {
        return minimum;
    }
    return value;
}

fn main() {
    let total = 0;
    let i = 0;

    while i < 3 {
        total = total + clamp_min(i, 1);
        i = i + 1;
    }

    _ = os.write("total={total}\n");
}

Output:

total=3

What this demonstrates:

  • functions use fn name(args) -> Type
  • let bindings are mutable locals
  • interpolation formats {total} before writing text

Common mistake: assert(...) is not the normal assertion spelling. Write assert condition; or, in tests, assert condition, "message";.

Errors

Use throws(E) for fallible functions, throw for errors, try to propagate, and catch to recover.

error FileError {
    NotFound,
    Denied,
}

fn read_id(fail: bool) throws(FileError) -> i32 {
    if fail {
        throw FileError.NotFound;
    }
    return 42;
}

fn main() {
    let ok = try read_id(false);
    let fallback = read_id(true) catch {
        .NotFound => 0,
        .Denied => -1,
    };

    assert ok == 42;
    assert fallback == 0;
}

This example has no output when the assertions pass. A failed assertion aborts the program.

What this demonstrates:

  • throws(FileError) marks a fallible function
  • try propagates a recoverable error
  • catch chooses a fallback value with match-like arms

Common mistake: returning FileError.NotFound with return is rejected. Use throw FileError.NotFound;.

Ownership and Copy

Most owned values move on assignment or call. Types that implement the builtin Copy marker can still be used after copying.

import { String } from std.string;

struct Point: Copy {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 3, y: 4 };
    let again = point;

    let text = try String("owned");
    let moved = text;

    assert point.x == 3;
    assert again.y == 4;
    assert moved.length() == 5;
}

What this demonstrates:

  • Point: Copy opts into copy-after-assignment behavior
  • String is owned text and moves by default
  • clone or constructor APIs can allocate, so they are fallible

References

Long-lived references are created explicitly with &. Assigning through a reference writes to the referenced value; it does not rebind the reference. This is intentionally different from C-style *ref = value syntax.

fn print(value: const &i32) {
    assert value > 0;
}

fn main() {
    let number = 5;

    {
        let ref: &i32 = &number;
        ref = 8;
    }

    print(&number);
}

What this demonstrates:

  • let ref: &i32 = &number; creates a reference that can outlive one call.
  • ref = 8; writes through the reference.
  • print(&number) passes an explicit read-only borrow because print expects const &i32.

Cold Futures

Calling an async fn creates a cold std.future.Future<T>. The body starts when the future is consumed by await or a structured future API.

import std.future;

async fn answer() -> i32 {
    await future.yield();
    return 42;
}

async fn main() {
    let pending: future.Future<i32> = answer();
    let value = await pending;
    assert value == 42;
}

Packages and Imports

import binds module namespaces. Aliases make the local binding explicit. External packages are resolved through zynx.json and zynx.lock; code still imports modules by identity.

import std.os as output;
import { String } from std.string;

fn main() {
    let text = String("imported") catch { _ => String() };
    _ = output.write("{text.str()}\n");
}

Output:

imported

CLI Arguments and Files

std.env exposes borrowed process arguments, std.path provides lexical path helpers, and std.fs provides narrow text-file read/write helpers for CLI tools.

import std.env;
import std.fs;
import std.io;
import std.path;

fn main() {
    let args = env.args();
    if args.length < 2 {
        io.eprintln("usage: copy-text <input>");
        return;
    }

    let input = args[1];
    let output = try path.join(path.dirname(input), "copy.txt");
    let text = try fs.read_file(input);

    try fs.write_file(output.str(), text.str());
    io.println("wrote {output.str()}");
}

Run it with an input file path:

./zynx run main.zx notes.txt
# wrote copy.txt

JSON Tools

The source tree includes small CLI examples that exercise std.json and package metadata. examples/jsoncheck validates strict JSON input. examples/jsonset mutates one top-level JSON object key and can write through the one-shot encoder or streaming encoder.

cat > input.json <<'EOF'
{"name":"old","items":[1]}
EOF
./zynx run examples/jsonset/main.zx -- input.json name '"new"'

Output:

{"name":"new","items":[1]}

Unsafe and Foreign

Foreign calls and other low-level operations require an explicit unsafe context. Safe wrappers should keep the unsafe block narrow.

foreign "C" fn abs(value: i32) -> i32;

fn magnitude(value: i32) -> i32 {
    unsafe {
        return abs(value);
    }
}

Text and Bytes

String and str are UTF-8 text. Use byte views when code needs the encoded bytes explicitly.

import std.bytes;
import { String } from std.string;

fn main() {
    let text = try String("hi");
    let view = text.utf8();

    assert view[0] == 104 as u8;
    assert text.length() == 2;
}