Invisible link to canonical for Microformats

A Developers Guide


Welcome to Slug: An Intro Course for Developers

Hi! In this course-style guide, I am going to walk you through Slug step by step, just like a hands-on bootcamp. You will start with a tiny program, then build confidence through short lessons, and finish with mini projects and tests.

By the end, you will be able to:

  • Read and write idiomatic Slug.
  • Build small scripts and data pipelines.
  • Use functions, pattern matching, and collections effectively.
  • Test your code and reason about concurrency.

How this guide works

Each module includes:

  • A quick concept intro.
  • Short examples you can run.
  • A tiny challenge to lock it in.

Keep a terminal open and run the samples as you go. You will learn fastest by typing the code yourself.

Course map

  1. Getting Started
  2. Core Building Blocks
  3. Functional Programming
  4. Data Structures
  5. Flow Control
  6. Mini Project
  7. Testing
  8. Concurrency
  9. Reference

Ready? Let’s build something with Slug.


Module 1: Getting Started

In this module, you will run your first Slug program and learn how the runtime finds your files.

Lesson 1.1: Your first program

Create a file called hello.slug:

println("Hello, Slug!")

Run it:

slug hello.slug

Expected output:

Hello, Slug!

Try it

Change the string, run again, and make sure the new text shows up. You just completed your first Slug program.

Lesson 1.2: How Slug resolves imports

Slug resolves the entry module and all import(...) paths relative to the command-line target first, then falls back to the global library directory ($SLUG_HOME/lib).

1) When the CLI target is a file path

Example:

slug ./tests/bytes.slug

Imports are searched relative to the directory of the entry file. For example:

import("slug.std")

is resolved as ./slug/std.slug and searched in this order:

  1. ./tests/slug/std.slug
  2. $SLUG_HOME/lib/slug/std.slug

2) When the CLI target is not a file path

If the command-line target is not found locally, Slug treats it as a library or module name.

Example:

slug hello world

Slug attempts to load the entry module hello in this order:

  1. ./hello
  2. ./hello.slug
  3. $SLUG_HOME/lib/hello.slug

Lesson 1.3: Program arguments

In all cases, remaining command-line tokens after the entry target are available via argv() and argm().

  • argv() == ["world"] for slug hello world

Try it

Run a program with two extra args and print argv() to confirm the list order.


Module 2: Core Building Blocks

In this module, you will learn the essential pieces of the language: types, variables, functions, and the builtins you will use all the time.

Lesson 2.1: Types at a glance

Slug has a small, focused set of core types:

  • nil: absence of a value.
  • true / false: booleans.
  • number: DEC64-inspired floating decimal values.
  • string.
  • list: ordered collection, e.g. [1, 2, 3].
  • map: key-value collection, e.g. {k: v}.
  • bytes: byte sequence, e.g. 0x"ff00".
  • function: a fn(){} value.
  • task: a task handle, returned by spawn.

Lesson 2.2: Comments

Slug supports two comment styles:

  • // C-style comments.
  • # for script-friendly shebang usage, like #!/usr/bin/env slug.

Lesson 2.3: Strings

Slug supports regular strings, raw strings, and interpolation.

val name = "Slug"
val greeting = "Hello !"
val path = 'C:\temp\file.txt' // raw string, no escapes

Lesson 2.4: Numeric literals

Numbers can include underscores for readability:

val maxUsers = 1_000_000

Lesson 2.5: Variables

Use var for mutable values and val for constants:

var counter = 0
val greeting = "Hello"

counter = counter + 1
counter /> println()

Lesson 2.6: Semicolons are optional

Statements end at newlines, not semicolons:

var a = 1
var b = 2
(a + b) /> println

A line continues when it clearly should:

var sql =
    "select *"
    + " from users"
    + " where active = true"

A line ends when the next token would be confusing:

f(x)     // valid
f
(x)      // invalid

Lesson 2.7: Dangling commas

Slug supports trailing commas in lists, maps, tags, and call arguments. It does not allow them in function definitions.

var {*} = import(
    "slug.std",
)

val map = {
    k: 50,
}

var list = [
    1,
    [1, 2,],
    11,
]

println(map, list,)

Lesson 2.8: Built-in functions you will use first

import

val {*} = import("slug.std")

len

val size = len([1, 2, 3])
val textLength = len("hello")
print("Hello", "Slug!")
println("Welcome to Slug!")

Lesson 2.9: Modules and exports

Use @export to expose values from a module. import(...) returns a map of exports.

// math.slug
@export
val add = fn(a, b) { a + b }

// app.slug
val math = import("math")
math.add(2, 3) /> println()

Imports are live bindings. In cyclic imports, accessing a value before it is initialized raises a clear runtime error.

Lesson 2.10: Command-line arguments

Slug provides two tiny, explicit builtins for arguments:

  • argv() returns raw args as a list.
  • argm() returns a parsed map and positionals.
// slug playground.slug -abc --user john foo.txt

argv()
// => ["-abc", "--user", "john", "foo.txt"]

argm()
// => { options: { a: true, b: true, c: true, user: "john" }, positional: ["foo.txt"] }

Typical usage:

var cli = argm()

match cli.options {
  {help: true} => showHelp()
  {user: u} => run(u, cli.positional[0])
  _ => fail("missing --user")
}

Lesson 2.11: Unified configuration with cfg()

val port = cfg("port", 8080)
val dbUrl = cfg("db.url", "postgres://localhost:5432")

Precedence is:

  1. CLI args: --key=value or -k=value.
  2. Environment: SLUG__db__port=5432.
  3. Local slug.toml in the current directory.
  4. Global slug.toml in $SLUG_HOME/lib/.
  5. In-code default.

Keys without dots are automatically namespaced to the current module.

Lesson 2.12: Functions and closures

val add = fn(a, b) { a + b }
add(3, 4) /> println()

Closures capture their environment:

val multiplier = fn(factor) {
    fn(num) { num * factor }
}

val double = multiplier(2)
double(5) /> println()

Lesson 2.13: Default parameters

Defaults are evaluated at call time in the function’s defining module.

val dbHost = cfg("db.host", "localhost")

val connect = fn(host = dbHost, port = 5432) {
    println(host, port)
}

connect()

Lesson 2.14: Named parameters

Named parameters are supported in function calls:

val greet = fn(name, title) { "Hello  " }
greet(title: "Mr", name: "Slug") /> println()

Lesson 2.15: Pipelines with the trail operator

The trail operator (/>) passes the value to the next function, left to right:

var double = fn(n) { n * 2 }
var map = { double: double }
var lst = [nil, double]

10 /> map.double /> lst[1] /> println("is 40")

// Equivalent to:
println(lst[1](map.double(10)), "is 40")

Lesson 2.16: Function dispatch and type tags

Slug can dispatch by argument count and type tags:

fn add(@num a, @num b) { a + b }
fn add(@str a, @str b) { a + b }

Supported tags: @num, @str, @bool, @list, @map, @bytes, @fn, @task.

Try it

Write an add overload for lists that concatenates two lists, then call it with [1] and [2].


Module 3: Functional Programming

Slug shines when you lean into functional patterns. Let’s practice the big three: map, filter, reduce, plus pattern matching.

Lesson 3.1: Map, filter, reduce

var {*} = import("slug.std")

val list = [1, 2, 3, 4, 5]

val squares = list /> map(fn(v) { v * v })           // [1, 4, 9, 16, 25]
val evens = list /> filter(fn(v) { v % 2 == 0 })     // [2, 4]
val sum = list /> reduce(0, fn(acc, v) { acc + v })  // 15

squares /> println()
evens /> println()
sum /> println()

Lesson 3.2: Pattern matching

match lets you destructure values directly.

val classify = fn(value) {
    match value {
        0 => "zero"
        1 => "one"
        _ => "other"
    }
}

classify(1) /> println()
classify(5) /> println()

You can match lists too:

val sumList = fn(list) {
    match list {
        [h, ...t] => h + sumList(t)
        [] => 0
    }
}

sumList([1, 2, 3]) /> println()

Pattern matching extras

Pin an existing value with ^name:

val expected = 42

match value {
    ^expected => println("matched 42")
    _ => println("nope")
}

Use ... to capture the rest of a list:

match list {
    [head, ...tail] => println(head, tail)
    [] => println("empty")
}

Lesson 3.3: Higher-order functions

val applyTwice = fn(f, v) { f(f(v)) }

val increment = fn(x) { x + 1 }
applyTwice(increment, 10) /> println()

Try it

Write a function times that takes n and a function f, then applies f to an input value n times.


Module 4: Data Structures

In this module, you will get comfortable with the two workhorse collections: lists and maps.

Lesson 4.1: Lists

val list = [10, 20, 30]

list[1] /> println()    // 20
list[-1] /> println()   // 30
list[0:1] /> println()  // [10]
list[1:] /> println()   // [20, 30]

Use lists for ordered data, pipelines, and batches of work.

Lesson 4.2: Maps

Maps store key-value pairs:

var myMap = {}
myMap = put(myMap, :name, "Slug")
get(myMap, :name) /> println()

Lesson 4.3: Symbols

Symbols are interned labels used as map keys, struct fields, and type tags. They are written with a : prefix:

:ok
:"Content-Type"

Use sym() to create symbols from strings and label() to get the raw text:

sym("foo bar")      // :"foo bar"
label(:ok)          // "ok"

Maps with bare keys use symbols by default:

val headers = {contentType: "text/plain"}
headers[:contentType] /> println()

When a map uses symbol keys, you can use dot access as shorthand:

headers.contentType /> println()

Lesson 4.4: Structs

Structs are schema-backed, immutable records. You define a schema with struct, then construct values from it.

val User = struct {
    name,
    @num age,
    active = true,
}

val u1 = User { name: "Slug", age: 2 }
u1.name /> println()

Update with copy:

val u2 = u1 copy { age: 3 }

Structs support introspection through type() and keys():

type(u1) == User /> println()
keys(u1) /> println()    // [:name, :age, :active]

Match on structs:

match u2 {
    User { name, age } => println(name, age)
    _ => println("unknown")
}

Try it

Create a map that stores a user id and name, then print a sentence using both values.


Module 5: Flow Control

Now you can build logic: conditions, loops via tail recursion, and error handling.

Lesson 5.1: Conditionals

val max = fn(a, b) {
    if (a > b) {
        a
    } else {
        b
    }
}

max(3, 5) /> println()

Lesson 5.2: Tail-recursive looping with recur

recur restarts the current function in tail position without growing the call stack.

// Sum 1..n using tail recursion in an anonymous function
fn(n, acc) {
    if (n == 0) {
        acc
    } else {
        recur(n - 1, acc + n)
    }
}(5, 0) /> println()

Lesson 5.3: Error handling with throw and defer onerror

val process = fn(value) {
    defer onerror(err) { println("Caught error:", err.msg) }

    if (value < 0) {
        throw Error { type: "ValidationError", msg: "Negative value not allowed" }
    }

    value * 2
}

process(-1) /> println()

Standard Error payloads (slug.std)

Slug treats errors as values. The standard library defines a conventional Error struct for consistency:

val Error = struct {
    @str type = "Error",
    @str msg,
    code = nil,
    data = nil,
    cause = nil,
}

Use struct literals at the throw site to keep stacktraces accurate:

throw Error { type: "IOError", msg: "failed to read config", data: { path: path } }

Lesson 5.4: defer, defer onsuccess, and defer onerror

Use defer to run cleanup or logging when a scope exits.

val writeFile = fn(path, text) {
    defer { println("closing file") }
    defer onsuccess { println("write ok") }
    defer onerror(err) { println("write failed:", err) }

    // ... write logic here ...
}

Try it

Write a function that divides two numbers and throws an error when the divisor is zero.


Module 6: Mini Project - Data Pipeline

Time to build a tiny project. You will process a list of numbers by:

  • Squaring each number.
  • Filtering for even numbers.
  • Summing the remaining values.
var {*} = import("slug.std")

val numbers = [1, 2, 3, 4, 5, 6]

val result = numbers
    /> map(fn(x) { x * x })
    /> filter(fn(x) { x % 2 == 0 })
    /> reduce(0, fn(acc, x) { acc + x })

println("Result:", result)

Challenge

Change the pipeline so it squares only odd numbers, then sums them.


Module 7: Testing

Slug has built-in testing tags so you can keep tests next to the code they verify.

Lesson 7.1: Parameterized tests with @testWith

@testWith(
    [3, 5], 8,
    [10, -5], 5,
    [0, 0], 0
)
var parameterizedTest = fn(a, b) {
    a + b
}
  • Each pair is inputs plus the expected output.
  • The test runner executes the function for each pair.

Lesson 7.2: Standard tests with @test

var {*} = import("slug.test")

@test
var simpleTest = fn() {
    val result = 1 + 1
    result /> assertEqual(2)
}

Lesson 7.3: Running tests

Run tests for a module with:

slug test path_to_source.slug

Example output:

Results:

Tests run: 33, Failures: 0, Errors: 0

Total time 1ms

Try it

Add a new @testWith case that checks subtraction, then run the tests.


Module 8: Concurrency

Slug uses structured concurrency. That means every task has a clear owner and a clear lifetime. It is explicit and predictable on purpose.

Lesson 8.1: Core principles

  1. Concurrency is explicit: parallel work uses spawn, suspension uses await.
  2. Lifetime is lexical: a scope owns the work it spawns.
  3. Values are values: await produces concrete values, not futures.
  4. Errors and cancellation are structural: failures bubble up, cancellations flow down.

Lesson 8.2: nursery

nursery marks a function or block as suspending.

var fetchUser = nursery fn(id) {
    ...
}

Key rules:

  • Each task has a currentNursery pointer.
  • Entering a nursery scope pushes a new nursery.
  • Leaving a nursery scope joins or cancels its children.
  • Ordinary function calls do not create a nursery.
  • spawn { ... } registers with the nearest enclosing nursery, not the immediate call frame.

Escaping task handles:

  • A task handle can be returned or stored in data.
  • If it escapes its nursery scope, it is guaranteed to be settled.
  • await on an escaped handle is always idempotent and returns immediately (or re-throws the stored error).

Lesson 8.3: spawn

spawn creates a child task and returns a task handle.

var t = spawn {
    work()
}

Semantics:

  • Child tasks are owned by the current nursery scope.
  • Spawned tasks run on a managed worker pool.
  • A nursery cannot exit until its children settle.
  • spawn registers with the nearest enclosing nursery.

Lesson 8.4: await

await suspends the current task until a handle completes.

var {*} = import("slug.channel")
var value = await(taskHandle)

With a timeout:

var value = await(taskHandle, 500)

Notes:

  • Suspension happens only at await.
  • On timeout, a Timeout error is raised.
  • Errors propagate like normal runtime errors.

Lesson 8.5: Task type tags

Task handles are first-class values. Use @task with tagged dispatch:

var awaitAll = fn(@list hs) {
    hs /> map(fn(@task h) { await(h) })
}

await is idempotent: awaiting an already-settled handle returns immediately (or re-throws its error).

Lesson 8.6: Concurrency limits and timeouts

Limits

var handler = nursery limit 10 fn() {
    ...
}
  • Default limit is 2 * CPU cores or 4.
  • Limits apply only to direct spawn calls in the current scope.
  • Excess spawns wait for capacity.

Timeouts

var v = await task within 1
  • Timeouts are in milliseconds.
  • A timeout raises Timeout and cancels the awaited task.
  • Handle errors via defer onerror or other constructs.

Lesson 8.7: Failure and cancellation

  • If a child task fails, siblings are cancelled and the error propagates.
  • If a parent scope exits early, all children are cancelled.
  • Cancellation is observed at await.
  • The runtime attempts to detect circular awaits and raises Deadlock.

Lesson 8.8: Fan-out and fan-in example

var fetchUser  = nursery fn(id) { ... }
var fetchPosts = fn(id) { ... }
var renderProfile = fn(user, posts) { ... }

var showProfile = nursery limit 10 fn(id) {
    var userT  = spawn { id /> fetchUser }
    var postsT = spawn { id /> fetchPosts }

    var user  = await(userT, 500)
    var posts = await(postsT, 1000)

    renderProfile(user, posts)
}

Lesson 8.9: Pipelines and await

Because await is syntax, use a helper for pipelines:

var awaitWithin = fn(v, dur) {
    await(v, dur)
}

var user = userT /> awaitWithin(500)

Lesson 8.10: What Slug does not provide

Slug intentionally avoids:

  • Actors or mailboxes.
  • Implicit futures.
  • Automatic parallelization.
  • Implicit blocking on variable reads.
  • Global cancellation tokens.
  • Detached background tasks without explicit APIs.

Module 9: Reference

Use this as a quick look-up when you forget operator precedence.

Operator precedence and associativity

Prec Operator Description Associates
1 () [] . Grouping, Subscript, Method call Left
2 - ! ~ Negate, Not, Complement Right
3 * / % Multiply, Divide, Modulo Left
4 + - Add, Subtract Left
6 « » Left shift, Right shift Left
7 & Bitwise and Left
8 ^ Bitwise xor Left
9 | Bitwise or Left
10 < <= > >= Comparison Left
12 == != Equals, Not equal Left
13 && Logical and Left
14 || Logical or Left
15 ?: Conditional* Right
16 = Assignment Right