Gossamer — Skill Card

Drop this file into a model's context to teach it how to write idiomatic Gossamer. Self-contained. Covers: what Gossamer is, surface syntax, forward-pipe style, the gos toolchain, error handling, concurrency, stdlib surface, and how to test. No prior context assumed.


1. What Gossamer is

A garbage-collected, goroutine-powered, fast-compiling systems language. Syntax is Rust-flavoured without lifetimes or a borrow checker. Runtime is Go-shaped: goroutines, channels, GC. Source files end in .gos. The toolchain binary is gos. Every project ships a project.toml manifest.

Status: pre-1.0.0. Surface is stable enough to write against; runtime and native codegen are partially wired — see "current gaps" at the bottom.

2. Idioms at a glance

Write clear, low-complexity, concise code. Names earn their length; helpers earn their existence. If a line reads cleanly the first time through, leave it alone — don't dress it up.

Prefer these shapes when writing Gossamer:

  • Left-to-right dataflow with |>. Chain calls with the forward-pipe operator instead of nesting.
  • Plain functions for free-standing logic. Reach for impl only when state is genuinely tied to a type.
  • Result<T, E> + ? for fallibility. Panic only for invariant violations.
  • Exhaustive match. Leave no _ => arm unless every unmatched case genuinely means the same thing.
  • Goroutines + channels for async work. Share by communicating; reach for sync::Mutex only when shared-memory is the simpler model.
  • Bare numeric literals. Write 0, not 0i64. Inference picks the type from the binding, the call site, or the return type. Only suffix when the literal stands alone with no contextual hint.
  • String literals are already String. Don't write "foo".to_string() — the literal is the owned value. &"foo" borrows it where a &String / &str parameter is expected.
  • Macros only for formatted output. println!, format!, print!, eprintln!, eprint!, panic! are the six macro entries — no others exist.

3. The |> forward-pipe operator

Prefer |> over nested calls whenever a value flows through two or more transformations.

  • x |> f desugars to f(x).
  • x |> f(a, b) desugars to f(a, b, x) — the piped value lands in the last positional slot.
  • x |> recv.m(a) becomes recv.m(a, x) — methods compose the same way.
  • |> is left-associative with very low precedence, so a |> f |> g reads as g(f(a)) without parentheses.
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 }
}

// Preferred — reads top-down.
let n = 3 |> double |> add(10) |> clamp(0, 100)

// Discouraged — the same meaning, but the eye has to unwind.
let same = clamp(0, 100, add(10, double(3)))

When a step is a closure, write it inline — |> still threads the value into the last slot:

let result = input
    |> parse_header
    |> validate
    |> |row| { row.body }
    |> write_out

4. Cheat sheet

use std::io

const PI: f64 = 3.14159
static MAX_RETRIES: u32 = 3

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,
        }
    }
}

fn main() {
    let mut total = 0
    for n in [1, 2, 3].iter() {
        total = total + *n
    }
    println!("total: {}", total)
}

5. Grammar essentials

  • Comments: // single-line and /* ... */ block are the only two forms — block comments do not nest, and there is no separate /// / //! doc-comment syntax. A run of // lines immediately above an item (no blank line between) is its documentation; gos doc renders these and gos test runs fenced code inside them.
  • Semicolons are optional at statement boundaries; one statement per line.
  • Expressions-as-statements. if, match, loop, and block expressions all yield values.
  • Bindings. let name = expr, let mut name = expr, let Point { x, y } = p (destructure), let (a, b) = pair.
  • References. &x read-shared, &mut x exclusive write. Aliasing intent only; the GC owns memory. No lifetimes, no borrow checker.
  • Types. bool, char, i8..i128, u8..u128, isize, usize, f32, f64, String, [T], (A, B), Option<T>, Result<T, E>, &T, &mut T, user types.
  • Integer literals are bare by default: 1, 255, 0. Inference picks the type from the binding, the call site, or the return type. Suffix only when no contextual hint exists (e.g. 1i32 standing alone in an expression with no other width signal). Unsuffixed literals default to i64.
  • Casts. x as i32 — whitelist-checked (numeric ↔ numeric, bool / char → integer, u8char, same-type no-op). Every other as shape is a hard error (GT0005).
  • Patterns. Wildcard _, literals, name, mut name, Variant(…), Struct { … }, tuples (a, b), ranges 1..=5, or-patterns a | b, @-bindings x @ 1..=3, rest ... Guards: Some(n) if n > 0 => ….

6. Formatted output (the only macros)

Gossamer has exactly six macros, all format-shaped. Every other name!(…) is a parse error.

Macro Returns Destination
format!("…", a, b) String
println!("…", a, b) () stdout + newline
print!("…", a, b) () stdout, no newline
eprintln!("…", a, b) () stderr + newline
eprint!("…", a, b) () stderr, no newline
panic!("…", a, b) ! unwinds with the rendered message

Each macro supports Rust-style {} placeholders and named-capture via {ident} for bindings in scope:

let name = "jane"
println!("hello, {name}!")
println!("value: {} / {}", answer, total)

The six macros lower to one allocation through the internal __concat builtin. For building a single String piece-by- piece, + concatenates without a separator:

let greeting = "hello, " + &name

7. Error handling

Fallible functions return Result<T, E>. Propagate with ? and build / wrap / inspect errors through std::errors:

use std::errors
use std::os

fn load_config(path: &String) -> Result<String, errors::Error> {
    os::read_file_to_string(path)
        .map_err(|e| errors::wrap(e, format!("reading {}", path)))
}
  • errors::new(msg) — build a free-standing error.
  • errors::wrap(cause, msg) — add a higher-level message.
  • errors::is(err, needle) — walk the cause chain.
  • errors::chain(err) — iterate the cause chain.
  • errors::join([err, err]) — combine several into one.

Panics abort the current goroutine (and, for now, usually the process). Reserve them for invariant violations, not recoverable failure.

8. Concurrency

Goroutines via go expr (fire-and-forget). For a result, spawn(f) runs f on a goroutine and returns a JoinHandle<T>; handle.join() blocks for Result<T, String>Ok(value), or Err(message) if the goroutine panicked:

let h = spawn(|| compute())
match h.join() {
    Ok(v) => println!("{}", v),
    Err(e) => eprintln!("worker failed: {}", e),
}

Typed channels via std::sync::channel(). select { } multiplexes receives and sends:

use std::sync::channel
use std::time

fn main() {
    let pair = channel()
    let tx = pair.0
    let rx = pair.1

    go tx.send(10)
    go tx.send(20)
    go tx.send(30)

    time::sleep(50)

    let mut total = 0
    loop {
        match rx.recv() {
            Some(v) => total = total + v,
            None => break,
        }
    }
    println!("total: {}", total)
}

select { } multiplexes:

select {
    x = rx_a.recv() => handle_a(x),
    y = rx_b.recv() => handle_b(y),
    default => do_something_else(),
}
  • Prefer channels for coordination; reach for sync::Mutex only when shared-memory updates are the simpler shape.
  • go takes a full expression — usually a function or method call. Closures work (go || { ... }()) but a named helper is easier to read and test.
  • Goroutines are real stackful coroutines on a work-stealing M:N scheduler. Blocking primitives (channel.recv, mutex.lock, time::sleep, network reads, fs syscalls) park the goroutine, not the OS thread — so writing blocking-shaped code is the right shape; do not pre-emptively add select arms or sleeps to "yield".

8a. Closures and higher-order fns

Lambdas use |param: T| body. Captures from the enclosing scope work as you'd expect (GC-managed, no move keyword).

For higher-order parameters, distinguish two callable types:

  • fn(args) -> ret — raw code pointer, accepts only non-capturing items (bare functions, lifted lambdas with no captures).
  • Fn(args) -> ret — callable trait, accepts both bare items and capturing closures. Fat pointer (env + code) under the hood; the conversion is implicit at the call site.
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
}

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

9. Data structures

  • [T] — growable array. Literal: [1, 2, 3].
  • (A, B, …) — tuple. Field access via .0, .1, ….
  • struct Foo { x, y } / struct Pair(A, B) — GC-managed value types.
  • enum E { A, B(Payload) } — sum types, pattern-matched exhaustively.
  • Option<T>Some(T) / None.
  • Result<T, E>Ok(T) / Err(E).
  • std::collections::{Vec, HashMap, HashSet, BTreeMap} — the richer containers; dispatch is wiring-dependent today, so verify with a small test if unsure.

10. The gos toolchain

Every subcommand takes a .gos file or a project directory. Bare gos drops into the REPL.

Command Purpose
gos check FILE Parse + resolve + typecheck + exhaustiveness.
gos run FILE Register-based bytecode VM. The walker is gone as a user-facing mode; if the VM hits an HIR shape it doesn't lower yet, it falls back internally — never user-selectable.
gos build FILE Native build via LLVM (opt -O0 \| llc -O0) + system linker.
gos build --release FILE Native build via LLVM (opt -O3 \| llc -O3 -mcpu=native) + system linker.
gos test PATH Discover and run #[test] functions.
gos bench PATH Discover and time #[bench] functions.
gos fmt [--check] FILE Rewrite canonically.
gos doc FILE Print item listing + doc comments.
gos lint [--deny-warnings] PATH Run the lint suite.
gos explain CODE Long-form rationale for a diagnostic code.
gos watch --command CMD PATH Re-run on file change.
gos new ID --path DIR Scaffold a project.
gos add SPEC / remove ID / tidy / fetch / vendor Package manager.

11. Writing tests

Unit tests live inside the file they cover, under #[cfg(test)] mod tests { … }. Integration tests live under tests/ in a project.

pub fn add(a: i64, b: i64) -> i64 { a + b }

#[cfg(test)]
mod tests {
    #[test]
    fn add_adds() {
        let total = super::add(2, 3)
        assert(total == 5)
    }
}

Doc-tests: fenced code inside a // doc-comment block (a run of // lines directly above an item) is compiled and executed by gos test. Mark non-runnable fences as ```text.

12. Standard library surface

  • std::fmtDisplay, Debug.
  • std::ioRead, Write, buffered wrappers, stdin / stdout.
  • std::os — process environment, argv, filesystem primitives.
  • std::strings / std::strconv — string and numeric helpers.
  • std::collectionsVec, HashMap, HashSet, BTreeMap.
  • std::netTcpListener, TcpStream, UdpSocket, DNS.
  • std::net::url — URL parse + render + escape.
  • std::httpMethod, StatusCode, Headers, Request, Response, Handler, serve.
  • std::encoding::{json, base64, hex, binary}.
  • std::syncMutex, RwLock, atomics, channel, Once.
  • std::timeInstant, Duration, sleep, now.
  • std::context — cancellation, deadlines, Context::background().
  • std::bytes / std::bufio — binary buffers and buffered IO.
  • std::errors — wrap / chain / join.
  • std::flag — CLI flag parser.
  • std::sort / std::utf8 / std::path / std::fs.
  • std::math::rand — deterministic RNG.
  • std::crypto::{rand, sha256, hmac, subtle} — narrow, audited.
  • std::slog — structured logging.
  • std::runtime — scheduler + GC knobs.
  • std::testingcheck, check_eq, Runner, check_ok.
  • std::regex — wraps the Rust regex crate.

Reality check: many modules exist in the manifest with partial implementations. Trust examples in the repo; write a small test when unsure.

13. Project layout

project.toml       # manifest: [project], [dependencies], [registries], optional [[bin]] / [lib]
src/
├── main.gos       # default binary entry  (override via [[bin]].path)
├── lib.gos        # default library root  (override via [lib].path)
└── subdir/
    └── mod.gos    # module `subdir`
tests/             # integration tests

project.toml:

[project]
id      = "example.com/widget"
version = "0.1.0"
authors = ["Jane Roe <jane@example.com>"]
license = "Apache-2.0"

[dependencies]
"example.org/lib" = "1.2.3"

# Optional. Without this section, the default is one binary
# named after the project id whose entry point is src/main.gos.
[[bin]]
name = "widget"
path = "src/main.gos"

# Optional. Without this section, presence of src/lib.gos is
# enough to build the library by convention.
[lib]
name = "widget"
path = "src/lib.gos"

14. Worked examples

CLI flags

use std::flag
use std::os

fn main() -> Result<(), flag::Error> {
    let mut fs = flag::Set::new("myapp")
    let port = fs.uint("port", 8080, "listen port")
    let verbose = fs.bool("verbose", false, "chatty output")
    let _ = fs.parse(os::args())?

    if *verbose {
        println!("starting on port {}", *port)
    }
    Ok(())
}

HTTP server

use std::http

struct App { }

impl http::Handler for App {
    fn serve(&self, r: http::Request) -> Result<http::Response, http::Error> {
        Ok(http::Response::text(200, format!("hi from {}", r.path)))
    }
}

fn main() -> Result<(), http::Error> {
    let app = App { }
    println!("listening on 0.0.0.0:8080")
    http::serve("0.0.0.0:8080", app)
}

15. Current gaps (pre-1.0.0)

  • + on String copies; for heavy assembly use std::bytes::Builder or a mut String with +=.
  • Method dispatch is name-global in places. Qualified path calls (Point::origin()) always work; method-style may collide across types until the resolver tightens.
  • The scheduler is M:N work-stealing with stackful coroutines. gos run and gos build both route every go fn(args) onto the same shared pool; channels, mutexes, sleeps, and network I/O all park the goroutine without holding a worker thread.
  • os::args() can return empty under some codegen paths — prefer std::flag with explicit defaults.

16. Style rules

  • Clear, low-complexity, concise. Plain reads beat clever ones. If a helper, type, or comment doesn't earn its space, drop it.
  • No emojis. Source, comments, commits, docs — all plain.
  • No TODO / FIXME committed; open an issue.
  • Doc every pub item with a single-line // directly above it (no blank line between); don't narrate self-evident code. Gossamer has no /// / //! form.
  • Pipe aggressively — if a value flows through more than one call, use |>.
  • iter::* over hand-rolled for loops for transformations. xs |> iter::for_each(handle) instead of for x in xs { handle(x) } when the body is a single call. xs |> iter::sum_by(|n| n*n) instead of a let mut total=0; for n in xs { ... }. The combinators (map, filter, filter_map, fold, reduce, for_each, find, group_by, partition, …) live as free functions in std::iter with data-last argument order so they thread through |>. Keep for for side-effect loops with complex state, break/continue, or early-return.
  • option::* / result::* for in-pipeline chaining. parse(s) |> result::map(render) |> result::default("") instead of a match when each arm is extract-or-default. ? remains the right tool for short-circuit propagation.
  • Free functions in std::iter, not methods on collections. Vec<T> / HashMap / HashSet do not carry .map / .filter / .fold methods. Mutating helpers like xs.push, xs.sort, m.inc, m.or_insert stay as methods because they operate by side-effect on the receiver.
  • One statement per line; omit semicolons.
  • Derive Debug, Clone, PartialEq when cheap and meaningful; derive Default for zero-valued types.

17. Where to read more

  • Language spec: SPEC.md (repo root).
  • Project style guide: GUIDELINES.md (repo root).
  • Rendered docs: docs_src/ (source) → site/ (built).
  • Examples: examples/ — start with hello_world.gos, function_piping.gos, go_spawn.gos, concurrency.gos.

18. When in doubt

Run it. gos check gives rustc-class diagnostics with source excerpts and did-you-mean suggestions. gos explain <CODE> expands any diagnostic code. The toolchain is your first debugger.