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
implonly 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::Mutexonly when shared-memory is the simpler model. - Bare numeric literals. Write
0, not0i64. 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/&strparameter 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 |> fdesugars tof(x).x |> f(a, b)desugars tof(a, b, x)— the piped value lands in the last positional slot.x |> recv.m(a)becomesrecv.m(a, x)— methods compose the same way.|>is left-associative with very low precedence, soa |> f |> greads asg(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 docrenders these andgos testruns 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.
&xread-shared,&mut xexclusive 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.1i32standing alone in an expression with no other width signal). Unsuffixed literals default toi64. - Casts.
x as i32— whitelist-checked (numeric ↔ numeric,bool/char→ integer,u8→char, same-type no-op). Every otherasshape is a hard error (GT0005). - Patterns. Wildcard
_, literals,name,mut name,Variant(…),Struct { … }, tuples(a, b), ranges1..=5, or-patternsa | b,@-bindingsx @ 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::Mutexonly when shared-memory updates are the simpler shape. gotakes 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 addselectarms 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::fmt—Display,Debug.std::io—Read,Write, buffered wrappers,stdin/stdout.std::os— process environment, argv, filesystem primitives.std::strings/std::strconv— string and numeric helpers.std::collections—Vec,HashMap,HashSet,BTreeMap.std::net—TcpListener,TcpStream,UdpSocket, DNS.std::net::url— URL parse + render + escape.std::http—Method,StatusCode,Headers,Request,Response,Handler,serve.std::encoding::{json, base64, hex, binary}.std::sync—Mutex,RwLock, atomics,channel,Once.std::time—Instant,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::testing—check,check_eq,Runner,check_ok.std::regex— wraps the Rustregexcrate.
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)¶
+onStringcopies; for heavy assembly usestd::bytes::Builderor amut Stringwith+=.- 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 runandgos buildboth route everygo 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 — preferstd::flagwith 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
pubitem 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-rolledforloops for transformations.xs |> iter::for_each(handle)instead offor x in xs { handle(x) }when the body is a single call.xs |> iter::sum_by(|n| n*n)instead of alet 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 instd::iterwith data-last argument order so they thread through|>. Keepforfor 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 amatchwhen 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/HashSetdo not carry.map/.filter/.foldmethods. Mutating helpers likexs.push,xs.sort,m.inc,m.or_insertstay as methods because they operate by side-effect on the receiver. - One statement per line; omit semicolons.
- Derive
Debug,Clone,PartialEqwhen cheap and meaningful; deriveDefaultfor 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 withhello_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.