Learn Zynx
A linear first tour through current Zynx syntax, errors, ownership, modules, and packages.
This tutorial is the beginner path. It avoids SDK, package-cache, ABI, and internals detail until you have run a few small programs.
Zynx is 0.0.0-dev, so syntax can still change. The goal here is to teach the
current shape, not promise a final language.
Create main.zx:
import std.io;
fn main() {
io.println("Hello, Zynx");
}
No output is produced yet; this block is the source file. The next command runs it.
Run it:
./zynx run main.zx
# Hello, Zynx
import std.io binds the standard I/O module as io. io.println is for
human-facing command output and intentionally ignores low-level write errors.
Use let for mutable local bindings and const for immutable ones. Many
examples rely on inference, but written types are available:
import std.io;
fn main() {
const name: str = "Ada";
let score: i32 = 40;
score = score + 2;
io.println("{name}: {score}");
}
Expected output:
Ada: 42
str is borrowed UTF-8 text. Owned text uses std.string.String.
Blocks use braces and statements end with semicolons. Ranges use ... for an
inclusive end and ..< for an exclusive end.
import std.io;
fn main() {
let total = 0;
for value in 1...4 {
total += value;
}
if total == 10 {
io.println("ok");
} else {
io.println("bad");
}
}
Expected output:
ok
Recoverable failures use error unions. A function that can fail writes
throws(ErrorSet) before the return type. Use try to propagate and catch
to recover.
import std.io;
error ParseError {
Empty,
}
fn parse_id(text: str) throws(ParseError) -> i32 {
if text == "" {
throw ParseError.Empty;
}
return 42;
}
fn main() {
let id = parse_id("") catch {
.Empty => 0,
};
io.println("id={id}");
}
Expected output:
id=0
Bare fn main() can use try without listing every propagated error; the
runtime reports an unhandled propagated error and exits nonzero.
Most owned values move on assignment or call. Copy values, such as integers
and structs that opt into Copy, can still be used after copying.
import {
String
} from std.string;
fn main() {
let text = try String("owned");
let moved = text;
assert moved.length() == 5;
}
Expected output: none. The assertion passes silently.
After let moved = text;, use moved; text is no longer available.
Checked references use &T. Assigning through a reference writes to the
referenced value; it does not rebind the reference:
fn set_to_answer(value: &i32) {
value = 42;
}
fn main() {
let number = 0;
set_to_answer(&number);
assert number == 42;
}
Expected output: none. The assertion passes silently.
When a function expects &T or const &T, a value argument may be borrowed for
the duration of that call. Long-lived references still need explicit &.
Imports bind modules or exported names:
import std.io;
import std.os as raw_os;
import {
String
} from std.string;
Expected output: none. These are import forms, not a complete program.
Top-level declarations are private unless exported:
export fn add(left: i32, right: i32) -> i32 {
return left + right;
}
Expected output: none. This declaration is used by importing or calling it from another file.
Normal source uses import. Dynamic require(...) is experimental/internal.
Create a project:
./zynx new hello
cd hello
../zynx run
Expected output:
Hello from Zynx
zynx new itself does not print a success message.
zynx new writes zynx.json and src/main.zx. Dependencies are exact git
dependencies in zynx.json and are locked in zynx.lock.
Check package state without fetching or changing the lockfile:
../zynx package check
Expected output:
package check: ok
Use Packages when you need dependency and lockfile details.
Read Hello World for a slightly more complete first program, Zynx by Example for more runnable patterns, then use Current Language Surface as the compact reference for current source boundaries.