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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.