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
- Getting Started
- Core Building Blocks
- Functional Programming
- Data Structures
- Flow Control
- Mini Project
- Testing
- Concurrency
- 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:
./tests/slug/std.slug$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:
./hello./hello.slug$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"]forslug 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: afn(){}value.task: a task handle, returned byspawn.
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 and println
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:
- CLI args:
--key=valueor-k=value. - Environment:
SLUG__db__port=5432. - Local
slug.tomlin the current directory. - Global
slug.tomlin$SLUG_HOME/lib/. - 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
- Concurrency is explicit: parallel work uses
spawn, suspension usesawait. - Lifetime is lexical: a scope owns the work it spawns.
- Values are values:
awaitproduces concrete values, not futures. - 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
currentNurserypointer. - Entering a
nurseryscope pushes a new nursery. - Leaving a
nurseryscope 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.
awaiton 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.
spawnregisters 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
Timeouterror 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
spawncalls in the current scope. - Excess spawns wait for capacity.
Timeouts
var v = await task within 1
- Timeouts are in milliseconds.
- A timeout raises
Timeoutand cancels the awaited task. - Handle errors via
defer onerroror 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 |