Egret in Twenty Minutes
Note: This document was first drafted with AI assistance and then reviewed and corrected by a human. It is not a fully AI-only artifact.
This is a beginner-friendly introduction to egret-lang. The goal is not to memorize every syntax rule in one sitting, but to spend about twenty minutes building a practical mental map: how programs are written, how modules are organized, how classes and generics work, how errors are handled, and how the compiler toolchain fits together.
Before learning any new programming language, the first useful question is: what kind of language is it? Egret-lang is a statically typed, strongly typed language for the AI era, with built-in GC support.
What You Will Learn in Twenty Minutes
- The basic shape of an Egret program
- Common syntax: variables, types, expressions, branching, loops, and
match - Functions, default parameters, lambdas, error handling, and
? - Object-oriented programming:
class, fields,init, methods, inheritance, andsuper - Modules and packages:
module,use,internal, andegret.toml - Generics,
normconstraints, and why Egret generics are more than just angle brackets - A first look at async and concurrency
- How
egretbuilds a single file and a package project
Start With the Big Picture
If you already know C, C++, Go, or Rust, a lot of Egret will feel familiar:
- It is a statically typed, strongly typed language
- Top-level functions use
func - Conditions and loops use braces
- Classes use
class - Generics use
<T> - Modules use
moduleanduse - Errors are handled explicitly, often with
T, ErrCode
But Egret also has a few very recognizable design choices:
mainis often written asfunc main(argc: Int, argv: Int) -> Int- Command-line arguments are usually read through
std.argv_get(argc, argv, idx) - Control flow centers on
loop, not separateforandwhile - Errors are commonly expressed with
ErrCodeand? - Generics often work together with
normconstraints - Modules and packages are first-class parts of the language workflow
1. Run Your First Egret Program
Start with the smallest possible working program:
func main(argc: Int, argv: Int) -> Int {
print("hello egret");
return 0;
}There are four important ideas here:
funcdeclares a functionmainis the program entry pointargcandargvexpose command-line arguments, much like C- Returning
0means the program finished successfully
If you come from Go, you can think of this as “Go-style func main with an explicit C-style exit code.” If you come from C or C++, the shape will feel very natural.
Build and Run
The most direct way to compile a single file is:
./egret build hello.eg -o hello
./helloIf you already installed Egret from the official script, you can usually run:
egret build hello.eg -o hello
./hello2. The Basic Program Skeleton: main, Arguments, and Comments
Read Command-Line Arguments
A common pattern looks like this:
use std;
func main(argc: Int, argv: Int) -> Int {
if argc > 1 {
let name: String = std.argv_get(argc, argv, 1);
print("hello " + name);
} else {
print("hello world");
}
return 0;
}Two things are worth remembering immediately:
use std;imports the standard library modulestd.argv_get(argc, argv, 1)reads the first user argument
This is different from many other languages:
- Go uses
os.Args[1] - Python uses
sys.argv[1] - C uses
argv[1] - Egret recommends reading arguments through a standard library API
Comments
Single-line comments look just like C, C++, and Go:
// This is a comment.
func main(argc: Int, argv: Int) -> Int {
print("hello");
return 0;
}Statement Endings
Most Egret statements end with semicolons:
let x: Int = 1;
x += 2;
print("done");This is close to C and C++, but in actual Egret projects the preferred style is still “one clear statement per line.”
3. Common Syntax: Variables, Types, Expressions, Branching, and Loops
This section covers the syntax you will use most often.
Variables: let and var
Local values are usually written with let:
let age: Int = 18;
let name: String = "egret";
let ok: Bool = true;Type inference also works:
let age = 18;
let name = "egret";
let ok = true;var can also be used for local variables:
var total: Int = 0;
total += 1;But in common Egret style:
letis more common for local bindingsvaris more common for class fields
Common Primitive Types
You will see types like these all the time:
let a: Int = 42;
let b: Int64 = 123456789;
let pi: Float = 3.14;
let ok: Bool = true;
let name: String = "egret";You will also encounter:
Void: no return valuePtr: raw pointer, for lower-level workAny: boxed dynamic valueT?: optional value, meaning it may benil
Example:
class User {
var name: String;
var email: String?;
func init(self: User, name: String) -> Void {
self.name = name;
self.email = nil;
}
}String? means “this field may have no value.” That is similar in spirit to Rust’s Option<T>, Swift optionals, or nullable types in TypeScript: the absence of a value is part of the type, not hidden behind magic conventions.
Expressions and Operators
Arithmetic, comparison, and boolean operators are familiar:
let a: Int = 10;
let b: Int = 3;
let add: Int = a + b;
let sub: Int = a - b;
let mul: Int = a * b;
let div: Int = a / b;
let rem: Int = a % b;
let eq: Bool = a == b;
let gt: Bool = a > b;
let ready: Bool = (a > 0) && (b > 0);String Concatenation
let first: String = "egret";
let second: String = "lang";
let full: String = first + "-" + second;
print(full);if / else
let score: Int = 78;
if score >= 90 {
print("excellent");
} else if score >= 60 {
print("pass");
} else {
print("failed");
}Notice that Egret does not require extra parentheses around the condition. That makes it feel closer to Go or Rust than to classic C syntax.
loop: The Core Loop Construct in Egret
This point matters a lot. Egret does not emphasize separate for and while forms. Instead, it centers the language around loop.
A small side note: when many people first learn programming,
forandwhilecan feel strangely arbitrary. Which one should you use, and why? In Egret, using one unifiedloopconstruct makes the control-flow model simpler, cleaner, and easier for both humans and AI tools to reason about.
Three-Part Loop
use strconv;
func main(argc: Int, argv: Int) -> Int {
loop (let i: Int = 0; i < 5; i = i + 1) {
print(strconv.itoa(i));
}
return 0;
}You can think of this as “Egret’s version of a for loop.”
Conditional Loop
let n: Int = 0;
loop n < 3 {
print("running");
n += 1;
}This behaves like “Egret’s version of a while loop.”
Infinite Loop
loop true {
print("tick");
break;
}break and skip
break exits the loop. skip is the Egret equivalent of continue.
Another side note:
continueis one of those keywords that often confuses beginners, because the name does not clearly describe what is being continued. Egret usesskip, which makes the meaning much more direct: skip the rest of this iteration and move on.
use strconv;
func main(argc: Int, argv: Int) -> Int {
loop (let i: Int = 0; i < 6; i += 1) {
if i % 2 == 0 {
skip;
}
print("odd=" + strconv.itoa(i));
}
return 0;
}That is worth remembering very early: in Egret, the keyword is skip;, not continue;.
match
match is a great fit for discrete branches:
func level_name(level: Int) -> String {
return match level {
0 => "debug",
1 => "info",
2 => "warn",
3 => "error",
_ => "unknown",
};
}If you know Rust, this will feel very familiar. If you come from Go or C, think of it as a safer, more expression-oriented switch.
4. Functions: Parameters, Return Values, Default Parameters, and Lambdas
Ordinary Functions
func add(a: Int, b: Int) -> Int {
return a + b;
}Call it like this:
let result: Int = add(20, 22);Functions Without a Return Value
func log_line(message: String) -> Void {
print(message);
}Void means the function performs work but does not return a value.
Default Parameters
use strconv;
func connect(host: String, port: Int = 80) -> String {
return host + ":" + strconv.itoa(port);
}So you can write:
print(connect("example.com"));
print(connect("example.com", 443));Default parameters must appear at the end of the parameter list. That is similar to the common rules in Python and TypeScript.
Function Types
Egret can treat functions as values:
func apply(a: Int, b: Int, op: (Int, Int) -> Int) -> Int {
return op(a, b);
}
func add(a: Int, b: Int) -> Int {
return a + b;
}Lambda
func main(argc: Int, argv: Int) -> Int {
let inc = |x: Int| -> Int {
return x + 1;
};
print(inc(41));
return 0;
}A lambda is just an anonymous function. It plays the same broad role as a Go function literal, a Rust closure, or a JavaScript arrow function.
5. Error Handling: ErrCode, T, ErrCode, and ?
This is one of the most important parts of Egret.
Why Egret Prefers Explicit Error Handling
Many languages rely heavily on exceptions. Egret usually prefers to make failure visible in the function signature itself.
That means: if a function can fail, the failure path should not be hidden.
Returning Only an Error Code
use std;
func validate_port(port: Int) -> ErrCode {
if port <= 0 || port > 65535 {
return std.ERR_INVALID;
}
return std.OK;
}Returning a Value Plus an Error Code
This is a very common Egret pattern:
use std;
use strconv;
func parse_port(text: String) -> Int, ErrCode {
let port: Int = strconv.atoi(text);
if port <= 0 || port > 65535 {
return 0, std.ERR_INVALID;
}
return port, std.OK;
}Call it like this:
let port, err = parse_port("8080");
if err != std.OK {
print("invalid port");
return err;
}
print(port);This feels similar to Go’s value, err := ..., but Egret makes the error side more explicitly tied to the ErrCode model.
?: Propagate Errors Upward
If you simply want “return early if the inner call fails,” Egret gives you ?.
use fs;
use std;
func load_text(path_text: String) -> String, ErrCode {
let text: String = fs.read_text(path_text)?;
return text, std.OK;
}The idea is:
- Success: unwrap the real value
- Failure: return from the current function immediately
If you know Rust, the spirit is very close to Rust’s ?. If you know Go, it is like compressing the standard error-propagation pattern into a single symbol.
6. Object-Oriented Programming: class, Fields, Methods, and Inheritance
Egret supports a natural, readable OOP style.
Define a Class
class Counter {
var value: Int;
func init(self: Counter, value: Int = 0) -> Void {
self.value = value;
}
func inc(self: Counter) -> Void {
self.value += 1;
}
func get(self: Counter) -> Int {
return self.value;
}
}Key ideas:
- Fields usually use
var - The constructor is named
init - Methods usually write
selfexplicitly as the first parameter
That differs from languages like Java or C#, where this is implicit. Egret makes the receiver visible in the method signature.
Create an Object
let c = new Counter();
c.inc();
print(c.get());Access Fields
let c = new Counter(10);
print(c.value);
c.value = 20;Inheritance
Egret currently uses single inheritance:
class Animal {
var name: String;
func init(self: Animal, name: String) -> Void {
self.name = name;
}
func speak(self: Animal) -> String {
return "unknown";
}
}
class Dog : Animal {
func init(self: Dog, name: String) -> Void {
super.init(name);
}
func speak(self: Dog) -> String {
return "woof";
}
}Two important things are happening:
class Dog : Animaldeclares inheritancesuper.init(name)calls the parent initialization logic
Method Overriding and Dynamic Dispatch
func print_speak(a: Animal) -> Void {
print(a.speak());
}If you pass a Dog, the actual call will resolve to Dog.speak(). That is the usual object-oriented virtual dispatch model you may know from Java, C++, or C#.
internal: Define a Boundary
If you do not want outside modules touching internal details directly, use internal:
class Buffer {
internal var handle: Int;
var len: Int;
}This is similar to package-private or module-private visibility in other languages. Its practical value is simple: it protects your ability to refactor later.
7. Modules and Packages: Organize Real Code
For a tiny demo, one file is enough. For a real project, modules matter quickly.
module
module hello_egret_package;
func print_hello_world() -> Void {
print("hello world");
}module declares the logical namespace of the current file.
use
use hello_egret_package;
func main(argc: Int, argv: Int) -> Int {
_ = hello_egret_package.print_hello_world();
return 0;
}use serves the same general purpose as:
importin Gousein Rustimportin Python
It tells the compiler which external module you want to bring in.
Module Aliases
use net.smtp as smtp;When names get long, aliases help keep call sites readable.
A Minimal Package Project Layout
hello-egret-package/
├── egret.toml
├── main.eg
└── src/
└── hello_egret_package/
└── hello_egret_package.egegret.toml:
[package]
name = "hello_egret_package"
version = "0.1.0"
[lib]
modules = ["hello_egret_package"]main.eg:
use hello_egret_package;
func main(argc: Int, argv: Int) -> Int {
_ = hello_egret_package.print_hello_world();
return 0;
}src/hello_egret_package/hello_egret_package.eg:
module hello_egret_package;
func print_hello_world() -> Void {
print("hello world");
}This structure is useful because:
main.egstays focused on the executable entry point- Reusable logic lives in modules
- The project can grow naturally without a redesign
8. Generic Programming: Stop Repeating the Same Code
If the only difference between several implementations is the type they work on, generics are probably the right tool.
The Simplest Generic Class
class Box<T> {
var value: T;
func init(self: Box<T>, value: T) -> Void {
self.value = value;
}
func get(self: Box<T>) -> T {
return self.value;
}
}Use it like this:
let int_box = new Box<Int>(42);
let str_box = new Box<String>("egret");Think of T as a placeholder that gets filled in when you instantiate the generic type.
Generic Functions
func identity<T>(value: T) -> T {
return value;
}
let a: Int = identity<Int>(1);
let b: String = identity<String>("x");The compiler can often infer the type:
let a: Int = identity(1);Default Generic Parameters
use collections;
class VecBox<T = Int> {
var values: collections.Vector<T>;
func init(self: VecBox<T>) -> Void {
self.values = new collections.Vector<T>();
}
}So this works:
let xs = new VecBox();
xs.values.push(1);The default generic argument is Int.
Value Generic Parameters
Egret generics do not only support type parameters. They can also support compile-time value parameters:
class FixedArray<N> {
var len: Int;
func init(self: FixedArray<N>) -> Void {
self.len = N;
}
}
let a = new FixedArray<16>();This is similar in spirit to non-type template parameters in C++. It is useful when a type depends not only on another type, but also on a fixed size, mode, tag, or compile-time policy.
9. norm: One of Egret’s Most Distinctive Generic Designs
If you only remember that “Egret supports generics,” that still misses something important. A very Egret-flavored idea is norm.
What Is a norm?
You can think of it as something like:
- a Rust trait
- a Go interface used as a capability constraint
- a Haskell type class
Its job is to say: not every type is allowed here. Only types with a certain ability are allowed.
Define a norm
norm Eq<T> {
func eq(a: T, b: T) -> Bool;
}That means: if a type wants to satisfy Eq, it must know how to compare two values of that type.
Implement a norm for a Concrete Type
norm impl Eq<Int> {
func eq(a: Int, b: Int) -> Bool {
return a == b;
}
}Use a Constraint in Generic Code
use collections;
func contains<T: Eq>(items: collections.Vector<T>, value: T) -> Bool {
loop (let i: Int = 0;
i < items.len();
i += 1) {
if eq(items.get(i), value) {
return true;
}
}
return false;
}T: Eq means:
Tmay be many possible types- but not just any type
- it must implement
Eq
That is far safer and clearer than pushing everything into Any and trying to recover structure later.
A More Practical Example: Hashable
norm Hashable<T> {
func hash(value: T) -> Int;
func eq(a: T, b: T) -> Bool;
}You can read this as: “if a type wants to be used as a Map or Set key, it must support hashing and equality.”
This kind of design becomes especially valuable in larger codebases because it forces capability boundaries into the source code.
10. Standard Library and Common Collections
Egret already has a useful standard library surface.
Common Modules
use std;
use strconv;
use strings;
use collections;
use fs;
use path;
use time;
use os;Roughly speaking:
std: core utilitiesstrconv: number/string conversionstrings: string processingcollections: containersfsandpath: filesystem toolstimeandos: system capabilities
collections.Vector
use collections;
func main(argc: Int, argv: Int) -> Int {
let xs = new collections.Vector<Int>();
xs.push(10);
xs.push(20);
print(xs.get(0) + xs.get(1));
return 0;
}If you come from:
- C++: think
std::vector - Java: think a lower-level, strongly typed
ArrayList - Go: think a role somewhere between slices and a container abstraction
collections.Map
use collections;
func main(argc: Int, argv: Int) -> Int {
let m = new collections.Map<String, Int>();
m.set("alice", 95);
m.set("bob", 88);
if m.has("alice") {
print(m.get_or("alice", 0));
}
return 0;
}collections.Set
use collections;
func main(argc: Int, argv: Int) -> Int {
let s = new collections.Set<String>();
s.add("egret");
if s.has("egret") {
print("found");
}
return 0;
}11. Async and Concurrency: Learn to Read It Early
You may not write a lot of concurrent Egret code on day one, but you should at least be comfortable reading it.
async func
async func fetch_number() -> Int {
return 42;
}await
async func run() -> Int {
let value: Int = await fetch_number();
return value;
}spawn
async func work(id: Int) -> Int {
return id * 2;
}
async func demo() -> Int {
let fut = spawn work(21);
return await fut;
}join
async func demo() -> Int {
let f1 = spawn work(1);
let f2 = spawn work(2);
let a: Int = join f1;
let b: Int = join f2;
return a + b;
}A useful mental model:
async func: this function may suspend while waitingawait: wait for an async resultspawn: start a task nowjoin: collect the result of a started task
Egret’s official docs also emphasize that async is not magic speed dust. It is mainly a better way to organize waiting and concurrency.
12. Build Tools and the Compile Process
This part matters a lot. A language is only real in practice when you can build and run code confidently.
Build the Egret Toolchain
Inside the Egret source repository, you may see:
./build-mac.sh
./build-linux.sh
build-windows.batThe README explains that these scripts help handle dependency checks, LLVM setup, building, and testing.
Manual Toolchain Build
The README also shows:
make -j4
make testBuild a Single-File Program
./egret build hello.eg -o hello
./helloConceptually, that command does something like this:
egret buildreads the source- lexical, syntax, and semantic analysis happen
- backend code is generated
- the program is linked into an executable
- the result is written to the
-opath
You can think of it as Egret’s version of a go build, rustc, or clang entry point.
Build a Package Project
If your project has egret.toml, you can build from the manifest:
./egret build --manifest-path egret.tomlIf the manifest defines multiple executable entries:
[[bin]]
name = "server"
main = "cmd/server.eg"
[[bin]]
name = "client"
main = "cmd/client.eg"Then you can run:
./egret build --manifest-path egret.toml --bin server
./egret build --manifest-path egret.toml --binsThe output directory is usually:
build/<bin-name>A Practical Beginner Build Flow
For the early stage, this order works well:
- Start with a tiny single-file demo
- Compile it with
egret build demo.eg -o demo - Once it runs, split logic into modules
- When reuse appears, add
egret.toml - When the project grows, then think about multiple binaries, libraries, and tests
Do not over-engineer the structure on day one. The first goal is to build a reliable loop: write, compile, run, change, repeat.
13. A Small Integrated Example
Here is a compact example that combines functions, classes, modules, and collections.
src/todo/item.eg
module todo.item;
class TodoItem {
var title: String;
var done: Bool;
func init(self: TodoItem, title: String, done: Bool = false) -> Void {
self.title = title;
self.done = done;
}
func mark_done(self: TodoItem) -> Void {
self.done = true;
}
}src/todo/repo.eg
module todo.repo;
use collections;
use todo.item;
class TodoRepo {
var items: collections.Vector<todo.item.TodoItem>;
func init(self: TodoRepo) -> Void {
self.items = new collections.Vector<todo.item.TodoItem>();
}
func add(self: TodoRepo, value: todo.item.TodoItem) -> Void {
self.items.push(value);
}
func count_done(self: TodoRepo) -> Int {
let count: Int = 0;
loop (let i: Int = 0;
i < self.items.len();
i += 1) {
let item = self.items.get(i);
if item.done {
count += 1;
}
}
return count;
}
}main.eg
use strconv;
use todo.item;
use todo.repo;
func main(argc: Int, argv: Int) -> Int {
let repo = new todo.repo.TodoRepo();
let a = new todo.item.TodoItem("learn syntax");
let b = new todo.item.TodoItem("build first app");
b.mark_done();
repo.add(a);
repo.add(b);
print("done=" + strconv.itoa(repo.count_done()));
return 0;
}This single example already includes:
moduleuseclassinit- methods
Vector<T>loop- string concatenation
- integer-to-string conversion
That combination covers a lot of the everyday Egret workflow.
14. The Most Common Beginner Mistakes
1. Forgetting That loop Is the Main Loop Form
Many beginners instinctively search for for or while. In Egret, loop is the thing to internalize first.
2. Ignoring Error Codes
Whenever you see T, ErrCode, you should quickly form the habit:
let value, err = some_call();
if err != std.OK {
return err;
}Or:
let value: String = some_call()?;3. Keeping Everything Inside main.eg
That is fine for the first hello-world demo, but the moment logic becomes reusable, split it into modules.
4. Reaching for Any Too Early
Any is flexible, but flexibility often means weaker structure.
Prefer this order:
- concrete types
- generics
normconstraintsAnyonly when it is genuinely needed
5. Treating Egret as “Just Another Familiar Syntax”
Egret is not just a language that looks a bit like others. Some of its identity really comes from:
- explicit error handling
norm-based abstraction- module and package organization
- generics plus specialization-style capabilities
- an engineering-oriented standard library, async model, and toolchain
The earlier you understand those pieces, the more your code will actually feel like Egret rather than another language wearing Egret syntax.
15. What to Practice After These Twenty Minutes
This is a good order for practice:
- Write
hello.eg - Add command-line arguments with
std.argv_get - Add a
class Counter - Store a batch of values in
collections.Vector<Int> - Write a function returning
Int, ErrCode - Split the code into two files with
moduleanduse - Build a minimal package project with
egret.toml - Try writing
Box<T>orcontains<T: Eq>
If you can move through those eight steps smoothly, you are already past the pure beginner stage and ready to build small tools.
16. One-Page Cheat Sheet
Minimal Entry Point
func main(argc: Int, argv: Int) -> Int {
return 0;
}Read Arguments
use std;
let arg: String = std.argv_get(argc, argv, 1);Branching
if cond {
} else {
}Loop
loop (let i: Int = 0; i < 10; i += 1) {
}Skip the Current Iteration
skip;Class
class User {
var name: String;
func init(self: User, name: String) -> Void {
self.name = name;
}
}Module
module app.user;
use std;Generics
class Box<T> {
var value: T;
}Error Handling
func load() -> String, ErrCodeError Propagation
let text: String = load()?;Build
egret build hello.eg -o helloClosing Thought
If you had to summarize the Egret beginner path in one sentence, it would be this:
Treat Egret first as an engineering-oriented, statically typed language. Learn func, class, module, loop, ErrCode, and generics first. Once those feel natural, go deeper into norm, specialization, async, the system library, and larger project patterns.
You do not need to become an Egret expert on day one. But you do want to get this workflow running as early as possible:
write a program -> compile it -> run it -> split it into modules -> handle errors -> introduce classes and generics
Once that path feels natural, the rest of the language becomes much easier to learn.