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, and super
  • Modules and packages: module, use, internal, and egret.toml
  • Generics, norm constraints, and why Egret generics are more than just angle brackets
  • A first look at async and concurrency
  • How egret builds 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 module and use
  • Errors are handled explicitly, often with T, ErrCode

But Egret also has a few very recognizable design choices:

  1. main is often written as func main(argc: Int, argv: Int) -> Int
  2. Command-line arguments are usually read through std.argv_get(argc, argv, idx)
  3. Control flow centers on loop, not separate for and while
  4. Errors are commonly expressed with ErrCode and ?
  5. Generics often work together with norm constraints
  6. 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:

  1. func declares a function
  2. main is the program entry point
  3. argc and argv expose command-line arguments, much like C
  4. Returning 0 means 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
./hello

If you already installed Egret from the official script, you can usually run:

egret build hello.eg -o hello
./hello

2. 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 module
  • std.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:

  • let is more common for local bindings
  • var is 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 value
  • Ptr: raw pointer, for lower-level work
  • Any: boxed dynamic value
  • T?: optional value, meaning it may be nil

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, for and while can feel strangely arbitrary. Which one should you use, and why? In Egret, using one unified loop construct 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: continue is one of those keywords that often confuses beginners, because the name does not clearly describe what is being continued. Egret uses skip, 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:

  1. Fields usually use var
  2. The constructor is named init
  3. Methods usually write self explicitly 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 : Animal declares inheritance
  • super.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:

  • import in Go
  • use in Rust
  • import in 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.eg

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

  1. main.eg stays focused on the executable entry point
  2. Reusable logic lives in modules
  3. 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:

  • T may 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 utilities
  • strconv: number/string conversion
  • strings: string processing
  • collections: containers
  • fs and path: filesystem tools
  • time and os: 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 waiting
  • await: wait for an async result
  • spawn: start a task now
  • join: 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.bat

The README explains that these scripts help handle dependency checks, LLVM setup, building, and testing.

Manual Toolchain Build

The README also shows:

make -j4
make test

Build a Single-File Program

./egret build hello.eg -o hello
./hello

Conceptually, that command does something like this:

  1. egret build reads the source
  2. lexical, syntax, and semantic analysis happen
  3. backend code is generated
  4. the program is linked into an executable
  5. the result is written to the -o path

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.toml

If 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 --bins

The output directory is usually:

build/<bin-name>

A Practical Beginner Build Flow

For the early stage, this order works well:

  1. Start with a tiny single-file demo
  2. Compile it with egret build demo.eg -o demo
  3. Once it runs, split logic into modules
  4. When reuse appears, add egret.toml
  5. 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:

  • module
  • use
  • class
  • init
  • 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
  • norm constraints
  • Any only 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:

  1. Write hello.eg
  2. Add command-line arguments with std.argv_get
  3. Add a class Counter
  4. Store a batch of values in collections.Vector<Int>
  5. Write a function returning Int, ErrCode
  6. Split the code into two files with module and use
  7. Build a minimal package project with egret.toml
  8. Try writing Box<T> or contains<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, ErrCode

Error Propagation

let text: String = load()?;

Build

egret build hello.eg -o hello

Closing 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.