Skip to content

Zynx By Example

This guide is a compact tour of common source patterns in the current language. The examples are intended to be runnable and practical, not a complete language reference. Each // doc-test: run block is expected to build and run under the current toolchain.

Read the page linearly for a quick language tour, or jump to the section that matches the construct you need. For complete rules, use the Language chapters.

Basics

Functions, variables, if, and loops use ordinary statement syntax.

Zynx
import std.io;
import std.mem.{ AllocError };

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

fn main() throws(AllocError) {
	var total = 0;
	var i = 0;

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

	io.println(try ^"total={total}");
}

Errors

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

Zynx
import std.io;
import std.mem.{ AllocError };

error FileError {
	NotFound,
	Denied,
}

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

fn load() throws(FileError) -> i32 {
	return try read_id(false);
}

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

	io.println(try ^"{ok}:{fallback}");
}

Nullable Values

T? stores either a T or null. Use an explicit null check before using the value when the next operation needs a non-null T.

Zynx
import std.io;

fn maybe_id(found: bool) -> i32? {
	if !found {
		return null;
	}
	return 7;
}

fn main() {
	let missing: i32? = maybe_id(false);
	let loaded: i32? = maybe_id(true);

	if missing == null && loaded != null {
		io.println("nullable");
	}
}

Structs And Methods

Methods live inside struct declarations. A mutable reference receiver can write through the receiver.

Zynx
import std.io;
import std.mem.{ AllocError };

struct Counter {
	var value: i32,

	fn add(&self, amount: i32) {
		self.value = self.value + amount;
	}

	fn get(self) -> i32 {
		return self.value;
	}
}

fn main() throws(AllocError) {
	var counter = Counter { value: 1 };
	counter.add(4);

	io.println(try ^"count={counter.get()}");
}

Interfaces

Interfaces can be used as static generic constraints, or as dynamic interface values when the concrete type should be erased.

Zynx
import std.io;
import std.mem.{ AllocError };

interface Named {
	fn code(self) -> i32;
}

struct User: Named {
	let value: i32,

	fn code(self) -> i32 {
		return self.value;
	}
}

fn label<T: Named>(item: T) -> i32 {
	return item.code();
}

fn main() throws(AllocError) {
	let user = User { value: 7 };
	io.println(try ^"{label(user)}");
}

The same object-safe interface can be used as a borrowed dynamic value:

Zynx
import std.io;
import std.mem.{ AllocError };

interface Reader {
	fn read(self) -> i32;
}

struct Const: Reader {
	let value: i32,

	fn read(self) -> i32 {
		return self.value;
	}
}

struct Doubler: Reader {
	let value: i32,

	fn read(self) -> i32 {
		return self.value * 2;
	}
}

fn show(reader: Reader) throws(AllocError) {
	io.println(try ^"read {reader.read()}");
}

fn main() throws(AllocError) {
	var a = Const { value: 7 };
	var b = Doubler { value: 7 };
	try show(a);
	try show(b);
}

Ownership, Move, And Copy

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

Zynx
import std.io;
import std.mem.{ AllocError };

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

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

	let text = try ("owned").clone();
	let moved = text;

	io.println(try ^"{point.x},{again.y},{moved.length}");
}

References And Explicit Borrow

Shared references use bare types. Mutable references are explicit with &; assigning through one writes the referent.

Zynx
import std.io;
import std.mem.{ AllocError };

fn print(value: i32) throws(AllocError) {
	io.println(try ^"{value}");
}

fn main() throws(AllocError) {
	var number = 5;

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

	try print(number);
}

Ranges, Slices, And Iteration

a..<b (end-exclusive) and a...b (end-inclusive) are first-class range values: bind them, pass them, return them, and iterate them. Subscripting an array with a range produces a zero-copy view that borrows the source, and reverse() reverses an array — or just a sliced span — in place.

Zynx
import std.io;

fn main() {
	let r = 1..<5;
	assert r.start == 1;
	assert r.end == 5;

	var sum = 0;
	for i in r {
		sum = sum + i;
	}
	assert sum == 10;

	let xs = [10, 20, 30, 40, 50];

	// Range subscript: a borrowed view, not a copy.
	var view = xs[1..<4];
	assert view.length == 3 as usize;
	assert view[0] == 20;

	// Writing through the view is visible in the source.
	view[0] = 99;
	assert xs[1] == 99;

	// In-place reversal of the whole array.
	xs.reverse();
	assert xs[0] == 50;

	io.println("ranges ok");
}

Defer And Cleanup

defer runs on scope exit. Owned values with drop methods are cleaned up deterministically.

Zynx
import std.io;
import std.mem.{ AllocError };

struct Resource {
	let id: i32,

	drop(&self) {
		io.println("drop");
	}
}

fn main() throws(AllocError) {
	let resource = Resource { id: 1 };
	defer io.println("defer");

	io.println(try ^"body {resource.id}");
}

Cold Futures

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

Zynx
import std.async;
import std.io;
import std.mem.{ AllocError };

fn answer() -> ^Future<i32> {
	await async.reschedule();
	return 42;
}

fn main() throws(AllocError) {
	let pending: ^Future<i32> = answer();
	let value = await pending;

	io.println(try ^"answer={value}");
}

Packages And Imports

import mod; binds the module namespace and makes exported symbols available by short name when the name is unambiguous. mod.Symbol is always available for disambiguation. A local declaration shadows an imported name. If two imported modules export the same name, using it unqualified is an error; qualify it (geometry.Point) to resolve the conflict. The auto-imported std prelude stays qualified (std.copy, std.Range).

Zynx
import geometry;            // exports `Point`
fn main() {
    let p = Point { x: 1, y: 2 };        // short name
    let q = geometry.Point { x: 3, y: 4 }; // qualified, same type
}

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.

Create a local package manifest and starter source in an empty directory with ./zynx init --name my_project. Dependency-free projects do not get a lockfile until dependencies are added. Use ./zynx package check to validate package state without fetching or changing the lockfile.

Zynx
import std.io as output;
import std.mem.{ AllocError };

fn main() throws(AllocError) {
	let text = ("imported").clone() catch { _ => ^"" };
	output.println(text);
}

Unsafe And Foreign

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

Zynx
import std.io;
import std.mem.{ AllocError };

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

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

fn main() throws(AllocError) {
	io.println(try ^"{magnitude(-9)}");
}

Text And Bytes

str is a shared UTF-8 view, &str is mutable, and ^str is owned. Use byte views when code needs the encoded bytes explicitly.

Zynx
import std.bytes;
import std.io;
import std.mem.{ AllocError };

fn main() throws(AllocError) {
	let text = try ("hi").clone();
	let view = bytes.Bytes(text);

	_ = view.at(0);
	io.println(try ^"{text}:{view.length}");
}

Next Steps

  • Use the Language chapters for the normative contracts.
  • Use the Package Reference for zynx.json, zynx.lock, and exact git dependencies.
  • Use the Standard Library pages for module-specific ownership, allocation, and error behavior.

Released under the MIT License.