Syntax tour

Gossamer's surface is Rust with two simplifications:

  • No lifetime annotations. References express aliasing intent; the GC owns the memory.
  • Semicolons are optional at statement boundaries.

See the full grammar in grammar/ once it is committed.

Comments

Two forms, no others:

  • // ... — line comment to end of line.
  • /* ... */ — block comment. Does not nest.

There is no separate /// or //! doc-comment syntax. A run of // lines immediately above an item (no blank line between) is its documentation; a run at the top of a file is the module's. Tooling reads these by position.

Items

const PI: f64 = 3.14159
static MAX: u32 = 1024

struct Point { x: f64, y: f64 }
struct Pair(i64, i64)

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

trait Area {
    fn area(&self) -> f64;
}

impl Area for Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => 3.14159 * r * r,
            Shape::Rect { w, h } => w * h,
        }
    }
}

Generic structs

A struct may carry one or more type parameters. The typechecker infers each parameter from the field values at the construction site — no turbofish annotation is needed:

struct Pair<A, B> { fst: A, snd: B }
struct Cell<T>    { value: T }

fn main() {
    // Parameters inferred: Pair<i64, String>
    let p = Pair { fst: 42, snd: "answer" }
    println!("{} = {}", p.fst, p.snd)   // 42 = answer

    // Same struct, different instantiation: Pair<i64, i64>
    let nums = Pair { fst: 10, snd: 32 }
    println!("{}", nums.fst + nums.snd)  // 42

    let c = Cell { value: 99 }
    println!("{}", c.value)              // 99
}

Field reads carry the per-instance concrete type. When two fields share the same parameter (Pair<i64, i64>), arithmetic across them typechecks directly — no extra annotation required.

Up to three type parameters are supported in 0.5.0. Generic methods (impl Pair<A, B> { ... }) are tracked for a later release; field access works across all tiers today.

Expressions

Everything is an expression. Blocks evaluate to their tail:

let max = if x > y { x } else { y }
let label = match status {
    200 => "ok",
    404 => "missing",
    _ => "other",
}

Forward pipe (|>)

The forward-pipe operator threads a value through a chain of calls. x |> f desugars to f(x); x |> f(a, b) to f(a, b, x) — the piped value lands in the last positional slot. Methods work the same way: x |> recv.m(a) becomes recv.m(a, x). |> is left-associative with very low precedence, so a |> f |> g reads as g(f(a)) with no parentheses needed:

fn double(x: i64) -> i64 { x * 2 }
fn add(a: i64, b: i64) -> i64 { a + b }
fn clamp(lo: i64, hi: i64, x: i64) -> i64 {
    if x < lo { lo } else if x > hi { hi } else { x }
}

// Reads left-to-right instead of inside-out.
let n = 3 |> double |> add(10) |> clamp(0, 100)

// Equivalent nested form:
let same = clamp(0, 100, add(10, double(3)))

Pattern matching

  • _ — wildcard.
  • name / mut name — bind.
  • Some(inner) / None — variant destructure.
  • Point { x, y } — struct destructure.
  • (a, b) — tuple destructure.
  • 1..=5 — range.
  • a | b — or-pattern.
  • x @ 1..=3@-binding.
  • .. — rest.

Guards: Some(n) if n > 0 => ...

Loops

loop { ... break value }
while cond { ... }
for item in iter { ... }

break value returns a value from loop. continue jumps to the top.

Error handling

fn load(path: String) -> Result<String, io::Error> {
    let raw = os::read_file_to_string(&path)?
    Ok(raw)
}

? propagates the Err variant. Wrap with std::errors::wrap(err, "while loading config") for context.

Arenas

arena {
    let tree = build_tree(16)
    total += check(&tree)
}

Everything allocated inside an arena { } block is bump-allocated and freed wholesale when the block exits — on every exit path, including early return and ?. Allocation becomes a pointer bump; reclamation is O(slabs) with no per-object teardown; small-enum nodes drop their runtime header entirely (a two-pointer tree node is exactly 16 bytes). The contract: nothing allocated inside the block may be referenced after it exits. See the memory model for the full semantics.

Concurrency

let (tx, rx) = channel::<i64>()
go fn() { tx.send(42) }()
let n = rx.recv()

select {
    a = rx_a.recv() => handle_a(a),
    b = rx_b.recv() => handle_b(b),
    _ = time::after(5000) => timeout(),
}

go expr spawns a goroutine — a real stackful coroutine on the M:N scheduler. Blocking primitives (channel ops, mutex contention, time::sleep, network reads, filesystem syscalls) park the goroutine, freeing the worker thread to run other goroutines. Channels are typed and bounded; select multiplexes receives.

Closures and higher-order fns

Lambdas use |param: T| body; captures from the enclosing scope work transparently (GC-managed, no move).

Higher-order parameters distinguish two callable types:

Type Accepts Representation
fn(args) -> ret non-capturing items only raw code pointer
Fn(args) -> ret bare items and capturing closures env+code fat pointer
fn apply(f: Fn(i64) -> i64, x: i64) -> i64 { f(x) }

fn main() {
    let scale = 10
    let scaled = |y: i64| scale * y    // captures `scale`
    println!("{}", apply(scaled, 5))   // 50

    fn add_one(y: i64) -> i64 { y + 1 }
    println!("{}", apply(add_one, 41)) // 42 — bare fn coerces
}

The conversion at the call boundary is implicit. Single trait variant — FnMut / FnOnce parse but lower to the same Fn(_) shape (the borrow-style split Rust draws is unnecessary in a fully GC'd world).

Attributes

#[test]
fn add_adds() { ... }

#[bench]
fn bench_hot_path() { ... }

#[lint(allow(unused_variable))]
fn scratch() { let x = 1 }

Modules

use std::http
use std::http::{Handler, Request, Response}
use example.org/other::widget

A project's module tree is file-based: src/foo.gos becomes mod foo, src/bar/mod.gos becomes mod bar.

Numeric literals

Write bare literals by default. Inference picks the type from the binding, the call site, or the return type; suffixes are reserved for the rare standalone case with no contextual hint.

  • 42 — plain int, inferred type. Defaults to i64.
  • 42i32 / 42u64 — explicit width when context can't pin it.
  • 0xff / 0b1010 / 0o777 — bases.
  • 1_000_000 — underscore separator.
  • 1.0 — plain float, inferred type. Defaults to f64.
  • 1.0f32 — explicit float width.

String literals

  • "hello" — ordinary double-quoted string. Spans multiple lines without extra syntax; embedded newlines are preserved.
  • "\n" / "\t" / "\\" / "\"" — standard escapes.
  • r"raw" / r#"with embedded "quotes""# — raw strings.
  • b"bytes" / b'c' — byte literals for binary protocols.

Formatted output

Gossamer has no macro system and no ! syntax. Formatted output goes through plain variadic builtins:

let name = "jane"
let age = 30
println("hello, ", name, "! you are ", age, " years old.")
let greeting = format("welcome, ", name)

Every builtin below stringifies each argument and joins them with a single space:

Builtin Effect
format(a, b, …) Returns a String.
println(a, b, …) Writes to stdout + newline.
print(a, b, …) Writes to stdout, no newline.
eprintln(a, b, …) Writes to stderr + newline.
eprint(a, b, …) Writes to stderr, no newline.
panic(a, b, …) Unwinds with the rendered message.

For the single-String output shape, + concatenates without adding a separator:

let greeting = "hello, " + &name

Writing name!(…) is a hard parse error — the ! suffix is reserved for no purpose today.