Invisible link to canonical for Microformats

A Developers Guide


Welcome to Slug: A Practical Developer Course

Welcome. This guide teaches Slug through short, runnable lessons with beginner-friendly build-ups.

By the end, you will be able to:

  • Read and write idiomatic Slug.
  • Build scripts, modules, and data pipelines.
  • Use pattern matching, structs, and tagged functions confidently.
  • Handle errors and concurrency with defer, spawn, and await.

How to use this guide

Each module includes:

  • A quick mental model.
  • Small runnable examples.
  • Expected outputs for key snippets.
  • Common mistakes to avoid.
  • A Try it exercise.

Keep a terminal open while reading and run each snippet.

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

Let’s build with Slug.


Module 1: Getting Started

In this module, you will run Slug code, understand module resolution, and inspect CLI inputs.

Lesson 1.1: Your first program

Mental model

A Slug file is a sequence of statements. The CLI loads one entry module and executes it.

Create hello.slug:

println("Hello, Slug!")

Run it:

slug hello.slug

Expected output:

Hello, Slug!

Common mistakes:

  • Running from a different folder than the file path you pass.

Try it

Print two values with println("hello", 123) and verify both appear.

Lesson 1.2: How Slug resolves imports

Mental model

import("x") is resolved relative to the entry module first, then library paths.

If you run:

slug ./tests/bytes.slug

then import("slug.std") is searched in:

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

If the CLI target is not a local file, Slug treats it as a module name and searches local then library paths.

Common mistakes:

  • Assuming imports are always resolved from current working directory only.

Lesson 1.3: Program arguments

Mental model

Everything after the entry target is user input.

  • argv() returns raw argument list.
  • argm() returns parsed options + positional args.
println(argv())
println(argm())

Run:

slug script.slug --user knuckles input.txt

Typical shape:

argv: ["--user", "knuckles", "input.txt"]
argm: { options: {user: "knuckles"}, positional: ["input.txt"] }

Try it

Print just argm().options and argm().positional separately.


Module 2: Core Building Blocks

This module introduces the syntax you will use constantly.

Lesson 2.1: Core values and types

Mental model

Slug is expression-oriented: most constructs produce values.

Common value shapes:

  • nil, true, false
  • numbers (DEC64-inspired)
  • strings and bytes (0x"ff00")
  • lists ([1, 2, 3])
  • maps ({name: "Slug"})
  • functions (fn(...) { ... })
  • task handles (from spawn)
  • symbols (:ok, :"Content-Type")

Lesson 2.2: Comments

# line comment
// also a line comment
/* block comment */
/** doc comment */

Common mistakes:

  • Forgetting doc comments are a distinct form used for docs/metadata.

Lesson 2.3: Strings and interpolation

Build up in steps:

val name = "Slug"
val msg = "Hello "
val raw = 'C:\temp\file.txt'
println(msg, raw)

Expected output:

Hello Slug C:\temp\file.txt

Lesson 2.4: Numeric and bytes literals

val users = 1_000_000
val hex = 0x10_ff
val bytes = 0x"414243"
println(users, hex, bytes)

Expected output (shape):

1000000 4351 <bytes value>

Lesson 2.5: var vs val

Mental model

  • val: bind once.
  • var: reassignable binding.
var counter = 0
val label = "requests"
counter = counter + 1
println(counter, label)

Common mistakes:

  • Reassigning a val.

Lesson 2.6: Semicolons and newlines

val a = 1
val b = 2
println(a + b)

Line continuation example:

val sql =
    "select *"
    + " from users"

Common mistakes:

  • Breaking lines where parser expects expression completion.

Lesson 2.7: Trailing commas

val m = {
  user: "slug",
}

println(
  m,
)

Trailing commas are allowed in maps/lists/call args/tags.

Lesson 2.8: Everyday builtins

val {*} = import("slug.std")
println(len([1, 2, 3]))
print("hello")
println(" world")

Lesson 2.9: Modules and exports

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

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

Expected output:

5

Lesson 2.10: Functions, defaults, and named args

val greet = fn(name, title = "Mx") { "Hello  " }
println(greet("Slug"))
println(greet(name: "Slug", title: "Dr"))

Lesson 2.11: Pipelines (/>)

Mental model

x /> f(y) rewrites to f(x, y).

val double = fn(n) { n * 2 }
println(10 /> double)

Pipeline into match:

10 /> match {
  10 => "ten"
  _ => "other"
} /> println()

Common mistakes:

  • Assuming /> is method dispatch only; it is general call piping.

Lesson 2.12: Tagged dispatch

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

println(add(1, 2))
println(add("a", "b"))

Common type names: num, str, bool, list, map, bytes, fn, task, sym, chan.

Try it

Add a list overload for add that concatenates two lists, then print add([1], [2]).


Module 3: Functional Programming

This module focuses on transforming data with small composable functions.

Lesson 3.1: Map, filter, reduce

Mental model

  • map: transform each item
  • filter: keep some items
  • reduce: combine into one result
val {*} = import("slug.std")

val xs = [1, 2, 3, 4, 5]
val squares = xs /> map(fn(v) { v * v })
val evens = xs /> filter(fn(v) { v % 2 == 0 })
val sum = xs /> reduce(0, fn(acc, v) { acc + v })

println(squares)
println(evens)
println(sum)

Expected output:

[1, 4, 9, 16, 25]
[2, 4]
15

Lesson 3.2: Match expressions

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

println(classify(1))
println(classify(10))

Common mistakes:

  • Forgetting _ fallback case.

Lesson 3.3: Destructuring patterns

val headOrZero = fn(xs) {
  match xs {
    [h, ..._] => h
    [] => 0
  }
}

println(headOrZero([10, 20]))
println(headOrZero([]))

Pattern tools:

  • _ wildcard
  • ...rest spread capture
  • ^name pin
  • {| ... |} exact-map pattern
val expected = 42
match 42 {
  ^expected => println("matched")
  _ => println("nope")
}

Lesson 3.4: Higher-order functions

val applyTwice = fn(f, v) { f(f(v)) }
val inc = fn(x) { x + 1 }
println(applyTwice(inc, 10))

Try it

Write times(n, f, x) using recur.


Module 4: Data Structures

You will use lists, maps, symbols, and structs together.

Lesson 4.1: Lists

val xs = [10, 20, 30, 40]
println(xs[1])
println(xs[-1])
println(xs[1:3])
println(xs[0:2])
println(xs[2:])

Common mistakes:

  • Confusing slice bounds (start:end) with single index access.

Lesson 4.2: Maps and symbol keys

Mental model

Bare map keys are symbols.

val m = {name: "Slug", age: 2}
println(m[:name])
println(m.name)

Lesson 4.3: Symbols

println(:ok)
println(:"Content-Type")
println(sym("user id"))
println(label(:ok))

Lesson 4.4: Struct schemas and values

Step-by-step:

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

val u1 = User { name: "Slug", age: 2 }
val u2 = u1 copy { age: 3 }

println(type(u1))
println(keys(u1))
println(u2.age)

Lesson 4.5: Matching maps and structs

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

match m {
  {| :name: n, :age: a |} => println(n, a)
  _ => println("missing fields")
}

Common mistakes:

  • Using non-exact map patterns when exact field set is required.

Try it

Create Account with id, email, and default active = true, then copy it with a new email.


Module 5: Flow Control

This module covers branching, loops via recursion, and robust error handling.

Lesson 5.1: Conditionals

Mental model

if is an expression, so both branches should produce a value.

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

println(max(3, 5))

Expected output:

5

Lesson 5.2: Looping with tail recursion and recur

recur must be in tail position.

val sumTo = fn(n, acc = 0) {
  if (n == 0) {
    acc
  } else {
    recur(n - 1, acc + n)
  }
}

println(sumTo(5))

Expected output:

15

Common mistakes:

  • Calling recur(...) and then doing more work after it.

Lesson 5.3: Throwing errors

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

val divide = fn(a, b) {
  if (b == 0) {
    throw Error { type: "ValidationError", msg: "divisor cannot be zero" }
  }
  a / b
}

Lesson 5.4: defer, defer onsuccess, defer onerror(err)

Mental model

  • defer: always runs when scope exits.
  • defer onsuccess: only on success path.
  • defer onerror(err): only on thrown error path.
val run = fn(x) {
  defer { println("always") }
  defer onsuccess { println("success") }
  defer onerror(err) { println("failed:", err.msg) }

  if (x < 0) {
    throw Error { type: "ValidationError", msg: "x must be >= 0" }
  }

  x * 2
}

println(run(2))

Common mistakes:

  • Treating onsuccess or onerror as global keywords outside defer.

Try it

Write parsePositive(n) that throws on n <= 0 and logs failures with defer onerror(err).


Module 6: Mini Project - Data Pipeline

Build a complete mini project in small steps.

Goal

Given a list of numbers:

  1. Keep only odd numbers.
  2. Square each value.
  3. Sum the result.
  4. Return a structured report.

Step 1: Transform and sum

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

val run = fn(xs) {
  xs
    /> filter(fn(v) { v % 2 != 0 })
    /> map(fn(v) { v * v })
    /> reduce(0, fn(acc, v) { acc + v })
}

println(run([1, 2, 3, 4, 5]))

Expected output:

35

Step 2: Add input validation

val Error = struct {
  type:str = "Error",
  msg:str,
}

val runSafe = fn(xs) {
  if (len(xs) == 0) {
    throw Error { type: "InputError", msg: "expected at least one value" }
  }
  run(xs)
}

Step 3: Return a report map

val runReport = fn(xs) {
  val odds = xs /> filter(fn(v) { v % 2 != 0 })
  val squares = odds /> map(fn(v) { v * v })
  val total = squares /> reduce(0, fn(acc, v) { acc + v })

  {
    inputCount: len(xs),
    processedCount: len(odds),
    total: total,
  }
}

println(runReport([1, 2, 3, 4, 5]))

Common mistakes:

  • Mutating intermediate structures instead of recomputing immutable values.

Challenge

Add maxSquare to the report.


Module 7: Testing

This module walks through practical test-writing patterns.

Lesson 7.1: A basic behaviour test

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

val add = fn(a, b) { a + b }

@test
val addWorks = fn() {
  assertEqual(add(2, 3), 5)
}

Why this matters

A tiny, focused behaviour test is easier to debug than one large test.

Lesson 7.2: Parameterized tests with @testWith

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

@testWith(
  [3, 5], 8,
  [10, -5], 5,
  [0, 0], 0,
)
val addCases = fn(a, b) {
  a + b
}

Mental model

  • each input tuple is executed,
  • return value is compared to expected value.

Lesson 7.3: Testing errors

val Error = struct { type:str = "Error", msg:str }

val mustPositive = fn(n) {
  if (n <= 0) { throw Error { type: "ValidationError", msg: "n must be positive" } }
  n
}

@test
val mustPositiveThrows = fn() {
  fn() { mustPositive(0) } /> assertThrows()
}

Lesson 7.4: Running tests

slug test path_to_source.slug

Common mistakes:

  • Asserting too many behaviours in one test.
  • Using unclear test names.

Try it

Add one new @testWith case and one new error-path test.


Module 8: Concurrency

Slug concurrency is structured: tasks belong to a logical scope and are awaited explicitly.

Lesson 8.1: Core ideas

  • spawn starts child work.
  • await(handle) waits for result.
  • nursery defines ownership boundary.
  • errors/cancellation flow through the structure.

Lesson 8.2: Start with one spawned task

val work = nursery fn() {
  val t = spawn { 20 + 22 }
  await(t)
}

println(work())

Expected output:

42

Lesson 8.3: Add timeout-aware await

val getSlow = nursery fn() {
  val t = spawn { slowTask() }
  await(t, 500)
}

Mental model

  • await(handle) blocks until completion.
  • await(handle, timeoutMs) throws on timeout.

Lesson 8.4: Fan-out and fan-in

val fetchBoth = nursery fn(id) {
  val userT = spawn { fetchUser(id) }
  val postsT = spawn { fetchPosts(id) }

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

  { user: user, posts: posts }
}

Lesson 8.5: Limits with nursery limit

val handler = nursery limit 10 fn(ids) {
  ids /> map(fn(id) { spawn { fetchUser(id) } })
}

Use limits when fan-out can grow large.

Lesson 8.6: Error handling around async flows

val run = nursery fn() {
  defer onerror(err) { println("task flow failed:", err) }
  val t = spawn { riskyWork() }
  await(t)
}

Common mistakes:

  • Forgetting to await handles you care about.
  • Treating cancellation as immediate at every line (it is observed at suspension points).

Try it

Implement awaitAll(handles) that returns results in order and rethrows first failure.


Module 9: Reference

Use this as a quick syntax and precedence lookup.

Operator precedence (lowest to highest)

Level Operators Associativity
1 = Right
2 \|\| Left
3 && Left
4 == != Left
5 < <= > >= Left
6 \| Left
7 ^ Left
8 & Left
9 << >> Left
10 + - Left
11 * / % Left
12 :+ +: Left
13 prefix ! - ~ Right
14 /> Left
15 calls, indexing, dot access, struct init/copy Left

Quick syntax reminders

  • Symbols: :ok, :"content-type"
  • Exact map patterns: {| :id: id |}
  • Map literals vs blocks are structurally disambiguated in expression position.
  • Defer modes are contextual after defer: onsuccess, onerror(err).

Minimal examples

val x = 10 /> fn(v) { v * 2 }
val y = {name: "slug"}.name
val z = [1, 2, 3][1]