Quartz v5.25

Quartz Style Guide

The code is the interface. Make it beautiful.

v5.26 | Last updated March 2026


Philosophy

Quartz was built on a single heresy: systems code should be pleasant to read.

  1. Const by default — Immutability is the happy path. var is the exception, not the rule.
  2. Say what you mean — If there’s a shorter, clearer way, use it.
  3. Types guide, then vanish — Rich types at compile-time, pure i64 at runtime.
  4. Noise is a bug — Every character should earn its place.

At a Glance

This is what idiomatic Quartz looks like:

Point { x, y } = get_origin()

squares = [n * n for n in 0..10]

name = user?.profile?.name ?? "Anonymous"

return Err("invalid") if input.empty?

return 0 if opt is None

val = opt.unwrap_or(default)

evens = items.filter() { it % 2 == 0 }

config = data |> parse |> validate |> build

def double(x: Int): Int = x * 2

If your code doesn’t look like this, keep reading.


The Idiomatic Cheat Sheet

When multiple valid forms exist, Quartz picks a winner. This is that list. Use it as the reference for READMEs, marketing samples, demos, docs, and any code you generate. If the prescribed form doesn’t work for your situation, you’re free to use the alternative — but the prescribed form is the default, and deviating from it should have a reason you can name.

IdiomPrescribedAvoidWhy
Bindingx = 5var x = 5 (when unmutated), let x = 5 (no such keyword)Const is the default. var is the signal that mutation is coming.
Mutationvar n = 0let mut n = 0Quartz has var, not let/mut.
Function defdef double(x) = x * 2def double(x: Int): Int = x * 2 (unless at an API boundary)Inference wins. Annotate when the signature documents intent for callers.
Zero-arg defdef pi = 3.14159def pi(): Float = 3.14159Parens vanish when there are no params. Callers still write pi().
Return typedef greet(name) = "Hello, #{name}"def greet(name) -> StringNo ->. Return type goes after :, and only when you annotate at all.
Lambda bodyitems.filter() { it > 0 }items.filter(x -> x > 0), items.filter() |x| x > 0Implicit it is the default.
Lambda with named paramitems.map(user -> user.active_since)items.map() { it.active_since } (when name clarifies intent)Use x -> expr only when the name documents what x is, or when you need 2+ params.
Interpolationputs("Hello, #{name}!")puts("Hello, " + name + "!"), puts(format("Hello, %s!", name))#{} is the canonical interpolator.
Variant testreturn 0 if opt is Noneopt.is_none(), match opt | None => 0 | _ => ... (for a single-arm test)is is the predicate form. Reserve match for multi-arm dispatch.
Unwrapval = opt.unwrap_or(default)if opt is Some; val = opt!; else ...Method form reads left-to-right and composes.
Propagate errorvalue = try parse_int(s)$try(parse_int(s)) (retired), postfix ? (retired)try prefix is the marker; both legacy forms were retired in the Piezo landing.
Assert unwrapx = opt!x = opt.unwrap()! means “I proved this; crash if wrong.” Not catchable.
Method callv.push(x), m.get(k)vec_push(v, x), map_get(m, k)UFCS method style. Free-function style is legacy.
Nil-safetyport = config?.port ?? 8080port = if config != null and config.port != null then config.port else 8080?. + ?? compose cleanly; spell out the chain.
Safe navigationname = user?.profile?.name ?? "Anon"manual null checks at each levelChain as deep as you need.
For-infor x in itemsvar i = 0; while i < items.size()Use the high-level iterator. Reach for index only when you need the index.
Enumerate with indexfor (i, x) in items.enumerate()for i in 0..items.size(); x = items.get(i)Destructure the pair.
Rangefor i in 0..nvar i = 0; while i < n; i += 1; endRanges are the canonical counted loop.
Qualified variantmatch opt | Some(v) => ... | None => ...match opt | Option::Some(v) => ... | Option::None => ...Unqualified is the default when unambiguous. Qualify only when a variant name collides with another in-scope enum (e.g. a custom Opt::None when stdlib Option::None is also in scope).
Struct literalPoint { x: 3, y: 4 }Point::new(3, 4), point_new(3, 4)Literal construction is the default; constructors are for invariants.
Predicate nameempty?, some?, ok?is_empty(), is_some(), is_ok()? suffix reads as English.
Mutator namepush!, clear!, sort! (if defined as banged)N/A — banged suffix is a convention marker! marks “this mutates its receiver.”
Pipelinedata |> parse |> validate |> buildbuild(validate(parse(data)))Read left-to-right, data first.
Comprehensionsquares = [n * n for n in 0..10]squares = (0..10).map() { it * it }.to_vec() (works, but verbose)List comprehension is shorter for “transform a range into a vec.”
DestructuringPoint { x, y } = originx = origin.x; y = origin.ySingle-line unpack.
Struct field access inside impl@x, @nameself.x, self.name (allowed, but @-form is idiomatic)The @-prefix is the impl-block sigil.
@value struct@value struct Token | kind: Int | span: Int endHeap-allocate small value typesUse @value for small, copyable aggregates.
Endless def for one-linersdef sum(v) = v.fold(0) { it.0 + it.1 }Block form with explicit return for trivial functionsEndless def disappears when the body is one expression.
Zero emojis in codealwaysneverEmojis are for docs UX, not source.

The underlying pattern: Quartz has invested heavily in making the most common thing the shortest thing. If you’re typing something that feels longer than it should, check this table before committing.


Naming

ElementConventionExample
Functionssnake_caseparse_token, emit_ir
Variablessnake_casetoken_count, current_node
TypesPascalCaseToken, AstNode, TypeChecker
Enum variantsPascalCaseSome, None, IntLit
Type parametersSingle uppercaseT, U, E, K, V
NewtypesPascalCaseUserId, Meters
Modulessnake_caseimport frontend/lexer
TraitsPascalCaseEq, Ord, Hash, Show
ConstantsSCREAMING_SNAKEconst MAX_SIZE = 1024

Predicates and Mutators

? for questions. ! for mutations. No exceptions:

v.empty?             # Bool — "is it empty?"
opt.some?            # Bool — "does it hold a value?"
c.digit?             # Bool — "is it a digit?"

v.clear()            # Void — mutates in place

Abbreviations

Use freely: ctx, idx, len, buf, ptr, fn, arg, val, str, num Spell out: process, manager, helper, user, config Loop vars: i, j, k, n, x


Formatting

  • 2 spaces. No tabs. No debate.
  • 100 character line limit.
  • Space around operators: a + b, x: Int, f(a, b)
  • One blank line between top-level definitions.
  • No blank line after def or before end.

Newlines Are Statements

Newlines terminate statements. Continue with trailing operators or leading dots:

# Trailing operator continues
total = a +
  b +
  c

# Leading dot chains
result = data
  .parse()
  .validate()
  .save()

# Pipeline continuation
config = Config.new() |>
  set_name("app") |>
  build()

Functions

Endless Def

Always use endless def for single-expression functions. This is the signature Quartz style. Omit type annotations when inference handles it — annotate only at API boundaries or when it aids readability:

# Preferred — let inference work
def double(x) = x * 2
def is_valid(n) = n > 0 and n < 100
def greet(name) = "Hello, #{name}!"
def pi = 3.14159

# Annotate at API boundaries or for clarity
def origin(): Point = Point { x: 0, y: 0 }
def apply(f: Fn(Int): Int, x: Int): Int = f(x)

Zero-arg functions don’t need parens in the definition (callers still use pi(), origin()):

def default_port = 8080
def version = "1.0.0"

Block Form

Multi-statement functions use def/end with explicit return:

def process(items: Vec<Item>): Int
  return 0 if items.empty?
  var count = 0
  for item in items
    continue unless item.valid?
    count += 1
  end
  return count
end

First-Class Functions

Functions are values. No wrappers, no ceremony:

def double(x: Int): Int = x * 2
result = apply(double, 21)    # 42

Compile-Time Functions

const def square(n: Int): Int = n * n
const AREA = square(8)
static_assert(AREA == 64, "area must be 64")

Lambdas

Arrow syntax. That’s it.

# Nullary
tick = -> 42

# One param
double = x -> x * 2

# Multiple params
add = (x, y) -> x + y

# Typed when needed
transform = x: Int -> x * x

Trailing Blocks

Lambdas as the last argument get special syntax:

# Single-line: curly braces — prefer implicit `it` for single-param
items.each() { puts(it.to_s()) }
count = items.filter() { it > 0 }.size
items.map() { it * 2 }

# Explicit param when the name adds clarity
users.filter() { user -> user.active? }

# Multi-line: do...end (always use explicit param names)
items.map() do item ->
  name = item.name
  "Hello, #{name}!"
end

Convention: { } for one-liners, do...end for multi-line. Prefer implicit it in short curly blocks.

Closures

Lambdas capture by value at creation time:

def make_adder(n: Int): Fn(Int): Int = x -> x + n

add5 = make_adder(5)
add5(3)    # 8

Mutation after capture does not affect the closure.


Variables and Bindings

Const by Default

x = 5             # immutable
var y = 10         # mutable — explicit opt-in
y += 1             # OK
# x = 10           # ERROR

Rule: Default to immutable. Reach for var only when you must reassign.

Compound Assignment

Always. No exceptions:

count += 1         # not count = count + 1
total -= discount
index *= 2

Type Inference

Let the compiler work. Quartz infers types for variables, function parameters, and return types:

# Variables — always inferred
count = 0                # Int
name = "Quartz"          # String
items = vec_new()        # Vec — generic until used

# Parameters — inferred as Int (existential erasure)
def double(x) = x * 2
def add(a, b) = a + b

# Return types — omit unless you need to enforce a specific type
def greet(name) = "Hello, #{name}!"

Annotate only at module boundaries, for complex types, or when it aids readability:

# Complex types that benefit from annotation
var buffer: Vec<String> = vec_new()
def connect(host: String, port: Int, timeout: Int): Connection

Destructuring

Decompose structs and vecs in a single binding:

# Struct — type name required for immutable
Point { x, y } = get_point()

# Mutable — var keyword
var Point { x, y } = get_point()
var { x, y } = get_point()              # type inferred

# Rename fields
Point { x: px, y: py } = get_point()

# Partial — ignore the rest
Point3D { x, .. } = get_3d()

# Vec
[first, second, third] = my_vec
[head, ..tail] = my_vec                 # rest binding
[only, ..] = my_vec                     # head only

# In for-in loops
for Point { x, y } in points
  draw(x, y)
end

# In match arms
match point
  Point { x, y } => x + y
  { x, y } => x + y                     # inferred type
end

Control Flow

Postfix Guards

The single most important style rule in Quartz. Flatten everything:

def process(node: Int): Int
  return 0 if node == 0
  return -1 unless is_valid(node)
  # main logic — no nesting
end

for item in items
  continue unless item.active?
  break if item.terminal?
  process(item)
end

Postfix guards work with return, break, and continue:

return value if condition
return value unless condition
break if done
continue unless relevant

Never nest when a guard will do.

If / Elsif / Else

if x > 0
  puts("positive")
elsif x < 0
  puts("negative")
else
  puts("zero")
end

Unless

Reads better than if not:

unless logged_in
  redirect("/login")
end

Pattern Matching

Prefer match over if/elsif chains. Use unqualified variants (no Option:: prefix):

# As expression — no semicolons, no braces, just arms
size = match level
  Low => 5
  Medium => 10
  High => 20
end

# Destructuring — unqualified variants (preferred)
match option
  Some(v) => v                    # not Option::Some(v)
  None => 0                       # not Option::None
end

# For simple variant checks, prefer `is` keyword over match
return 0 if opt is None
return -1 if result is Err

# For simple unwrap, prefer .unwrap_or() over match
val = opt.unwrap_or(0)            # not: match opt { Some(v) => v / None => 0 }

Guards

match value n if n > 100 => “big” n if n > 0 => “small” _ => “zero or negative” end

Multi-statement arms

match choice A => { var g = Guard { id: 1 } process(g) result } B => simple_expr end

Struct patterns

match point Point { x, y } => x + y end

String and regex

match token “def” => TOK_DEF “end” => TOK_END ~r”\d+” => TOK_NUMBER _ => TOK_IDENT end


### Loops

```quartz
# Range — preferred for counted iteration
for i in 0..10           # 0 to 9 (exclusive)
  process(i)
end

for i in 0...5           # 0 to 5 (inclusive)
  process(i)
end

# Collection
for item in items
  process(item)
end

# Infinite
loop
  data = read()
  break if data == 0
  process(data)
end

# Labeled
@outer for i in 0..10
  for j in 0..10
    break @outer if found(i, j)
  end
end

Defer

Guaranteed cleanup, LIFO order:

def process_file(path: String): Result<Data, Error>
  f = file_open(path)
  defer file_close(f)

  data = parse(f)?
  transform(data)
end

Block Expressions

x = do
  tmp = compute()
  tmp * tmp
end

Collections

Literals

numbers = [1, 2, 3, 4, 5]
config = {name: "Quartz", version: 5, debug: false}
unique = {1, 2, 3}
empty = {:}

Always use {key: val} shorthand for symbol keys — never hashrocket.

Comprehensions

Prefer comprehensions over manual accumulation:

squares = [x * x for x in 0..10]
evens = [x for x in items if x % 2 == 0]
lookup = {x => x * 2 for x in 1..5}

Use explicit loops only for side effects, early termination, or complex bodies.

Unified API

Every collection speaks the same language:

v.size               # Vec, String, Map — all .size
v.empty?             # all .empty?
opt.some?            # Option
res.ok?              # Result

Never use .len, .length, is_empty, or is_some.


Pipelines and UFCS

Pipeline Operator (|>)

For data transformation chains — read left to right:

result = data |> parse |> validate |> transform

# With arguments
result = x |> add(10) |> multiply(2)

# Multi-line
config = Config.new() |>
  set_name("app") |>
  set_debug(true) |>
  build()

UFCS

For method-like calls on objects. Any function whose first parameter matches the receiver type can be called with dot syntax (open UFCS):

len = text.size
count = items.filter() { x -> x > 0 }.size
"hello".starts_with("he")

# Open UFCS: your own functions become methods automatically
def area(c: Circle): Int = c.radius * c.radius * 3
my_circle.area()       # calls area(my_circle)

When to Use Which

1 step2 steps3+ steps
f(x)whichever reads betterpipeline |> or UFCS .
# 1 step — just call it
len = items.size

# 2 steps — either works
result = validate(parse(data))        # direct
result = data |> parse |> validate    # pipeline

# 3+ steps — always chain
output = raw |> parse |> validate |> transform |> save

Strings

Interpolation

Preferred for embedding values:

message = "User #{name} has #{count} items"

Concatenation

For simple joins:

greeting = "Hello, " + name + "!"

Methods

s.size                 # length
s[i]                   # char at index
s[a:b]                 # slice
s.find("needle")       # find substring
s.starts_with("he")    # prefix
s.ends_with("!")       # suffix
s.contains("world")    # substring check
s.downcase()           # lowercase
s.upcase()             # uppercase
s.trim()               # trim whitespace
s.replace(old, new)    # replace all
n.to_s                 # Int to String
s.to_i                 # String to Int

Multi-Line and Raw

text = """
  Multi-line string.
  Interpolation #{works} too.
"""

path = r"C:\Users\data"        # no escape processing

Types

Structs

struct Point
  x: Int
  y: Int
end

p = Point { x: 10, y: 20 }

Associated Functions and Extend

def Point.origin(): Point = Point { x: 0, y: 0 }

extend Point {
  def sum(): Int = @x + @y
  def scale(n: Int): Point = Point { x: @x * n, y: @y * n }
}

Point.origin().sum()    # 0

Enums

enum Color
  Red
  Green
  Blue
end

enum Shape
  Circle(radius: Int)
  Rectangle(width: Int, height: Int)
  Point
end

enum Option<T>
  Some(value: T)
  None
end

Union Types

True union types — no wrappers, no boxing:

def process(value: Int | String): Void
  match value
    Int => puts("number")
    String => puts("text")
  end
end

Intersection Types

def print_if_equal<T: Eq & Ord>(a: T, b: T): Void
  puts("equal") if eq(a, b)
end

Record Types

Structural typing — if it has the fields, it fits:

def greet(who: { name: String }): Void
  puts("Hello, #{who.name}!")
end

Newtypes

Distinct types that prevent accidental mixing:

newtype UserId = Int
newtype Email = String

def get_user(id: UserId): User    # can't pass raw Int

Type Aliases

Transparent shortcuts for readability:

type Callback = Fn(Int): Bool
type IntTransform = Fn(Int): Int

Traits

trait Show
  def show(self): String
    return "<unknown>"
  end
end

impl Show for Point
  def show(self): String
    return "(#{self.x}, #{self.y})"
  end
end

Self Type

Self resolves to the implementing type inside trait/impl blocks:

trait Cloneable
  def clone(self): Self = self
end

impl Cloneable for Point
  def clone(self): Self = Point { x: @x, y: @y }  # Self = Point
end

Bounds

Use UFCS in bounded generics — call trait methods with dot syntax:

def max<T: Ord>(a: T, b: T): T
  return a if a.gt(b)
  return b
end

def compare<T: Eq + Ord>(a: T, b: T): Int
  return 0 if a.eq(b)
  return -1 if a.lt(b)
  return 1
end

Standard Traits

TraitPurpose
EqEquality (eq, ne)
OrdOrdering (lt, le, gt, ge)
HashHashing (hash)
ShowString representation (show)
CloneDeep copy (clone)
DropRAII cleanup (drop)

Operator Overloading

extend Vec2 {
  def +(other: Vec2): Vec2 = Vec2 { x: @x + other.x, y: @y + other.y }
  def ==(other: Vec2): Bool = @x == other.x and @y == other.y
}

a = Vec2 { x: 1, y: 2 }
b = Vec2 { x: 3, y: 4 }
c = a + b    # Vec2 { x: 4, y: 6 }

Error Handling

Result + ?

def divide(a: Int, b: Int): Result<Int, String>
  return Err("division by zero") if b == 0
  Ok(a / b)
end

def calc(x: Int, y: Int): Result<Int, String>
  a = divide(x, y)?    # propagates Err
  Ok(a * 2)
end

Safe Navigation + Nil Coalescing

name = user?.profile?.name ?? "Anonymous"
port = config?.port ?? 8080

Built-in Macros

length = $try(get_length(s))        # early return None on failure
val = $unwrap(get_value())          # unwrap or panic
$assert(x > 0)                      # panic if false
x = $debug(10 + 20)                 # stderr debug, returns value

Modules

Import Order

  1. Standard library
  2. Project (hierarchical)
  3. Flat imports
  4. Selective imports
import std/colors
import std/traits

import frontend/lexer
import frontend/parser

import token
from token import Token, Loc

Qualification

import math
result = math.add(1, 2)    # dot syntax — always

Visibility

struct Public { x: Int }       # accessible everywhere

private

struct Secret { value: Int }   # this module only

priv def helper(): Int = 42   # per-item

Import Styles

import lib                     # prefixed: lib.Point
from lib import *              # wildcard: Point (no prefix)
from lib import Point, greet   # selective
pub import lib                 # re-export

Symbols

For lightweight tags and map keys:

status = :ok

match status
  :ok => handle_success()
  :error => handle_failure()
end

config = {name: "Quartz", version: 5}

Symbols for simple flags. Enums when you need payloads or exhaustiveness.


Regex

pattern = ~r"hello"
email = ~r"[a-z]+@[a-z]+\.[a-z]+"

if s =~ ~r"\d+"
  puts("contains digits")
end

Macros

macro max(a: expr, b: expr) do
  "if #{a} > #{b} then #{a} else #{b} end"
end

result = $max(10, 20)

Concurrency

Quartz’s concurrency is colorless — the same code shape works for sync and async. go fires a task, await waits for a handle, channels carry values, select races multiple channels.

sched_init(4)              # worker threads — call once near `main`

# Fire-and-forget task
go background_worker()

# Producer / consumer over a typed channel
ch = channel_new(64)
go do ->
  for i in 0..100
    send(ch, i * i)
  end
  channel_close(ch)
end

for value in ch            # iterate until closed
  puts(value)
end

# Async/await with eager futures
future = async compute(42)
result = await future

# Select across channels
select
  recv(orders)  -> order => process(order)
  recv(cancels) -> id    => cancel(id)
  default               => idle()
end

sched_shutdown()

Rules of thumb:

  • ch = channel_new(64), future = async ..., result = await ... — all const bindings. The handle doesn’t change; only what you send through it does. Only use var when you actually re-assign the name.
  • Inside go do -> block bodies, the usual idioms apply — implicit it, #{} interpolation, UFCS chains.
  • For compute-heavy parallel iteration, reach for a parallel combinator (e.g. parallel_map) rather than hand-rolling spawns.

Effects (Piezo)

The algebraic effect system lives alongside the idiomatic rules above without replacing them. Code written invisibly (no can rows, no try, no with) still reads exactly like the cheat sheet prescribes — effects only appear when you reach for them.

# Invisible — just write the code
def greet(name)
  puts("Hello, #{name}!")
end

# Implicit — effects inferred, visible in LSP/docs, no source noise
def read_or_default(path)
  raw = try read_file(path)
  return parse_or_default(raw)
end

# Explicit — `can` rows at API boundaries, like any type annotation
def read_config(path: String): Config can FileIo, Throws<ConfigError>
  raw = try read_file(path)
  return try parse_toml(raw)
end

# Sandbox / handler installation
result = with catch (e: ConfigError) -> default_config() do ->
  read_config("/etc/app.toml")
end

# Reify — convert a throwing block into a Result<T, E>
res: Result<Int, ParseError> = reify { parse_int(input) }

When it’s time to write effect-aware code, apply the same rules:

  • Annotate can rows at API boundaries, not inside bodies. They carry the same weight as return-type annotations — document the contract for callers, omit when inference is obvious.
  • try is a visibility marker, not a coercion. Use it when the propagation point is non-obvious to the reader. Implicit propagation is fine when the call site already looks effectful.
  • with catch blocks use e -> ... named params (the name documents “this is the caught error”), not implicit it.
  • Never mix $try() with the new try prefix — $try() was retired in the Piezo Milestone A landing (Apr 2026). All new propagation points use the prefix form.

See EFFECTS.md for the canonical effect spec — the blessed effect set, error-handling tiers, handler semantics.


Memory and Systems

Linear Types

linear struct FileHandle
  fd: Int
end

def read_all(f: &FileHandle): String
  # borrow — doesn't consume
end

Placement Allocation

arr = @heap [1, 2, 3]

arena scratch do
  p = @scratch Point { x: 10, y: 20 }
end

Annotations

AnnotationPurpose
@valueValue-type struct (inline storage, copy semantics)
@cfg(os: "macos")Conditional compilation
@repr(C)C-compatible layout
@packedNo alignment padding
@nakedNo function prologue/epilogue
@thread_localThread-local storage
@field / @xImplicit self.x in extend blocks

FFI

extern "C" def puts(s: String): Int
extern "C" def malloc(size: Int): CPtr
extern "C" def free(ptr: CPtr): Void

Documentation

Use ## for doc comments:

## Adds two numbers.
##
## @param a - First operand
## @param b - Second operand
## @returns The sum
## @example
##   add(1, 2)  # => 3
def add(a: Int, b: Int): Int = a + b

Comment what’s non-obvious. Never comment what the code already says.


File Organization

  1. Imports
  2. Type definitions (structs, enums, newtypes)
  3. Constants
  4. Trait implementations
  5. Private helpers
  6. Public API
  7. main (if applicable)

The Rules

Every rule in this guide reduces to two principles:

Use the most expressive construct available:

def double(x: Int): Int = x * 2          # not block def
count += 1                                # not count = count + 1
squares = [x * x for x in 0..10]         # not manual loop
return 0 if x < 0                         # not block if
return 0 if opt is None                   # not match for bool check
val = opt.unwrap_or(0)                    # not match for simple unwrap
name = user?.name ?? "Anon"               # not nil check
items.filter() { it > 0 }                # not { x -> x > 0 }
v.push(x)                                # not vec_push(v, x)
Point { x, y } = get_point()              # not manual field access

Never nest when you can flatten:

# Yes
def process(n: Int): Int
  return 0 if n == 0
  return -1 unless valid(n)
  compute(n)
end

# No
def process(n: Int): Int
  if n != 0
    if valid(n)
      compute(n)
    else
      return -1
    end
  else
    return 0
  end
end

Modern Idioms (v5.26+)

UFCS-First

Prefer method-call style over free-function style for all collections:

# Yes
v.push(x)
v.pop()
m.set(k, v)
m.get(k)
s.find("needle")

# No
vec_push(v, x)
vec_pop(v)
map_set(m, k, v)
map_get(m, k)
str_find(s, "needle")

Option/Result Handling

Use UFCS methods, the is keyword, and the try prefix for Option/Result handling. Reserve match for true multi-arm dispatch.

# Predicate tests — `is` keyword
return 0 if opt is None
return -1 if result is Err

# Extract with default — UFCS
val = opt.unwrap_or(0)
val = opt.unwrap_or_else() { compute_default() }
count = result.unwrap_or(0)

# Propagate errors — `try` prefix (replaces the retired $try() macro
# and the retired postfix `?`)
def read_config(path): Result<Config, IoError>
  raw = try read_file(path)
  parsed = try parse_toml(raw)
  return Ok(Config { raw, parsed })
end

A single-arm match to unwrap is always a bad smell — use is, unwrap_or, or try instead. Save match for when there are multiple variants with real dispatch logic.

Unqualified Enum Variants

Module prefixes never appear in match arms — variants resolve from the scrutinee’s type:

match opt
  Some(v) => process(v)
  None => default()
end

Option::Some and Option::None are not valid pattern syntax. Not “discouraged” — the parser rejects them.

Implicit it

Implicit it is the default for any single-param trailing block. Reach for x -> only when the parameter name documents intent, or when the block takes two or more params.

# Default form — use `it`
items.filter() { it > 0 }
items.map() { it * 2 }
items.each() { puts("#{it}") }

# Named param — only when the name is load-bearing
users.map(user -> user.active_since)
# (the name `user` tells the reader what it is — `{ it.active_since }`
#  leaves the reader to guess from the collection name)

# Multi-param — always named
items.reduce(0) do acc, x -> acc + x end

If the body is long or has multiple statements, that’s not a signal to switch away from it — it’s usually a signal to extract a named function:

# When the body grows past one clear expression:
def is_eligible(u) = u.active? and u.age > 18 and u.region == "US"
users.filter(is_eligible)

Anti-Patterns

AvoidInstead
count = count + 1count += 1
v.len, v.lengthv.size
is_empty(v)v.empty?
vec_push(v, x)v.push(x)
map_get(m, k)m.get(k)
Option::Some(v) in matchSome(v)
option_is_some(opt)opt.is_some() or opt is Some
match opt { ... } for unwrapopt.unwrap_or(default)
{ x -> x * 2 } (trivial){ it * 2 }
Block def for single exprdef f(x): T = expr
Deep nestingPostfix guards + early return
Manual while for countingfor i in 0..n
Done-flag loopsloop + break
Hashrocket for symbol keys{key: val}
math$add(1, 2)math.add(1, 2)
Manual field extractionDestructuring

The Done-Flag

Never simulate break with a flag:

# No
var done = 0
while done == 0
  if pos >= len
    done = 1
  end
end

# Yes
loop
  break if pos >= len
  var ch = source[pos]
  break if ch == 10
  pos += 1
end