Vary Course

Install Vary with Docker

Get Vary running locally with Docker, verify the compiler, and run a first file from your working directory.

Docker setup

Confirm Docker works

Check that Docker is installed before pulling the Vary image.

docker --version

Run: docker --version

Expected output:

^Docker version [0-9]+\.[0-9]+\.[0-9]+, build [0-9a-f]+$

Create the Vary Docker alias

Create a local vary command that runs the compiler container in your current directory.

alias vary='docker run --rm \
  -u "$(id -u):$(id -g)" \
  -v "$(pwd):/workspace" \
  -w /workspace \
  ghcr.io/ccollicutt/vary:latest'
vary --version

Run: vary --version

Expected output:

^Vary v[0-9]+-alpha\.[0-9]+ .+$

Run a Vary file through Docker

Print the working directory and verify it is the container's /workspace mount.

import system

let here: Str = system.cwd().to_str()
print("cwd=" + here)

Run: vary run main.vary

Expected output:

cwd=/workspace

Vary Introduction

Ten short programs that show what Vary is for: typed code, pure boundaries, contracts, and tests strong enough to survive mutation.

Why Vary exists

Readable code with visible types

Compose a deploy label from typed parts.

def deploy_label(env: Str, region: Str) -> Str {
    return env + "-" + region
}

print(deploy_label("prod", "us-east-1"))

Run: vary run main.vary

Expected output:

prod-us-east-1

Separate calculation from effects

Sum response bytes in a pure helper and print at the edge.

data Chunk {
    label: Str
    bytes: Int
}

pure def total_bytes(chunks: List[Chunk]) -> Int {
    mut sum = 0
    for chunk in chunks {
        sum = sum + chunk.bytes
    }
    return sum
}

let response = [
    Chunk("headers", 412),
    Chunk("body", 2048),
    Chunk("trailers", 96),
]

print("bytes=" + str(total_bytes(response)))

Run: vary run main.vary

Expected output:

bytes=2556

Think about test strength

Compute an exponential backoff so the next lessons have an obvious answer.

pure def backoff_ms(attempt: Int) -> Int {
    if attempt <= 0 {
        return 100
    }
    if attempt == 1 {
        return 200
    }
    if attempt == 2 {
        return 400
    }
    return 800
}

print("backoff=" + str(backoff_ms(2)) + "ms")

Run: vary run main.vary

Expected output:

backoff=400ms

Language choices

Model a closed choice

Choose a release path with an enum and a top-level match.

enum ReleaseKind {
    PATCH
    MINOR
    MAJOR
}

let release = ReleaseKind.MINOR

match release {
    case ReleaseKind.PATCH {
        print("release=patch")
    }
    case ReleaseKind.MINOR {
        print("release=minor")
    }
    case ReleaseKind.MAJOR {
        print("release=major")
    }
}

Run: vary run main.vary

Expected output:

release=minor

Make mutation explicit

Count rate-limited responses while keeping the response list immutable.

let responses: List[Int] = [200, 200, 429, 200, 503, 429, 200]
mut throttled = 0

for status in responses {
    if status == 429 {
        throttled = throttled + 1
    }
}

print("throttled=" + str(throttled))

Run: vary run main.vary

Expected output:

throttled=2

Target the JVM

Describe a deploy target with typed fields.

data DeployTarget {
    service: Str
    runtime: Str
    arch: Str
}

let target = DeployTarget("billing-api", "JVM 21", "linux/amd64")
print(target.service + " -> " + target.runtime + " on " + target.arch)

Run: vary run main.vary

Expected output:

billing-api -> JVM 21 on linux/amd64

Name assumptions

Return the first non-empty message and declare both ends of the contract.

pure def first_message(messages: List[Str]) -> Str {
    in {
        len(messages) > 0
    }
    out (value) {
        len(value) > 0
    }
    for message in messages {
        if len(message) > 0 {
            return message
        }
    }
    return "unknown"
}

let errors = ["", "timeout after 30s", "connection refused"]
print("first=" + first_message(errors))

Run: vary run main.vary

Expected output:

first=timeout after 30s

Workflow

Check before running

Define a typed record and a pure predicate suitable for vary check.

data Response {
    endpoint: Str
    status: Int
}

pure def is_2xx(response: Response) -> Bool {
    return response.status >= 200 and response.status < 300
}

let response = Response("/api/users", 200)

if is_2xx(response) {
    print("checked: " + response.endpoint)
}

Run: vary run main.vary

Expected output:

checked: /api/users

Prefer observable outputs

Compute a health verdict in a pure helper that a future test can assert against.

pure def health(failing: Int) -> Str {
    if failing == 0 {
        return "green"
    }
    if failing <= 2 {
        return "amber"
    }
    return "red"
}

let probe_results = [True, True, False, True]
mut failing = 0

for ok in probe_results {
    if not ok {
        failing = failing + 1
    }
}

print(health(failing))

Run: vary run main.vary

Expected output:

amber

The confidence loop

Sum the timeout budget across a typed list of pipeline stages.

data Stage {
    name: Str
    timeout_s: Int
}

let pipeline = [
    Stage("compile", 60),
    Stage("unit", 30),
    Stage("integration", 120),
    Stage("deploy", 90),
]

mut budget = 0
for stage in pipeline {
    budget = budget + stage.timeout_s
}

print("pipeline budget=" + str(budget) + "s")

Run: vary run main.vary

Expected output:

pipeline budget=300s

Vary Language Tour

Twelve runnable lessons covering typed values, functions, data, state, enums, dictionaries, nullable values, modules, and tests.

Values, functions, control flow

Values and types

Bind typed values and print a release string.

let project: Str = "acme-billing"
let major: Int = 2
let minor: Int = 14

print(project + " v" + str(major) + "." + str(minor))

Run: vary run main.vary

Expected output:

acme-billing v2.14

Functions and branches

Classify an HTTP status with a pure helper.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    if status >= 200 {
        return "2xx"
    }
    return "other"
}

print("404 -> " + class_of(404))

Run: vary run main.vary

Expected output:

404 -> 4xx

Lists and loops

Count how many requests exceeded a latency budget.

let durations_ms: List[Int] = [42, 180, 95, 240, 33]
mut slow: Int = 0

for d in durations_ms {
    if d > 100 {
        slow = slow + 1
    }
}

print("slow=" + str(slow))

Run: vary run main.vary

Expected output:

slow=2

Modeling data

Data types

Define an immutable record and read its generated toString.

data Response {
    status: Int
    bytes: Int
}

let resp = Response(200, 1024)
print(str(resp))

Run: vary run main.vary

Expected output:

Response(status=200, bytes=1024)

Classes and state

Track remaining retries behind a small stateful class.

class RetryBudget(max: Int) {
    mut remaining: Int = max

    def consume(self) -> None {
        self.remaining = self.remaining - 1
    }

    def left(self) -> Int {
        return self.remaining
    }
}

let budget = RetryBudget(3)
budget.consume()
print("remaining=" + str(budget.left()))

Run: vary run main.vary

Expected output:

remaining=2

Enums and exhaustive match

Advance a build state machine with exhaustive matching.

enum BuildState {
    QUEUED
    RUNNING
    PASSED
    FAILED
}

pure def advance(state: BuildState) -> BuildState {
    match state {
        case BuildState.QUEUED {
            return BuildState.RUNNING
        }
        case BuildState.RUNNING {
            return BuildState.PASSED
        }
        case BuildState.PASSED {
            return BuildState.PASSED
        }
        case BuildState.FAILED {
            return BuildState.FAILED
        }
    }
}

print(str(advance(BuildState.QUEUED)))

Run: vary run main.vary

Expected output:

RUNNING

Nullable values

Fall back to a default when a configured value is missing.

let configured: Int? = None
mut port: Int = 8080

if configured is not None {
    port = configured
}

print("port=" + str(port))

Run: vary run main.vary

Expected output:

port=8080

Lookups and modules

Typed maps

Look up a service timeout from a config map.

let timeouts: Dict[Str, Int] = {"db": 30, "http": 10, "cache": 60}

print("http=" + str(timeouts["http"]) + "s")

Run: vary run main.vary

Expected output:

http=10s

Module-ready helpers

Format an artifact coordinate with a pure helper.

pure def coord(group: Str, name: Str, version: Str) -> Str {
    return group + ":" + name + ":" + version
}

print(coord("acme", "billing", "1.4.2"))

Run: vary run main.vary

Expected output:

acme:billing:1.4.2

Tests

Shape helpers for assertion

Print the inputs and outputs the next lesson will assert on.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    return "2xx"
}

print(class_of(503) + "," + class_of(200))

Run: vary run main.vary

Expected output:

5xx,2xx

Run passing assertions

Run two observe assertions against the helper.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    return "2xx"
}

test "server errors classify as 5xx" {
    observe class_of(503) == "5xx"
}

test "client errors classify as 4xx" {
    observe class_of(404) == "4xx"
}

Run: vary test tests/grade_test.vary

Expected output:

Results: 2 passed, 0 failed

Read a failing test

Run a file with one intentional boundary mistake and find the summary line.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    return "2xx"
}

test "server errors classify as 5xx" {
    observe class_of(503) == "5xx"
}

test "boundary 500 should be 4xx" {
    observe class_of(500) == "4xx"
}

Run: vary test tests/grade_test.vary

Expected output:

Results: 1 passed, 1 failed

Vary Testing

Seven runnable lessons covering test, observe, edge cases, error paths, output, and naming.

First tests

The smallest test

Verify one fact about one function.

pure def is_even(n: Int) -> Bool {
    return n % 2 == 0
}

test "4 is even" {
    observe is_even(4) == True
}

Run: vary test tests/even_test.vary

Expected output:

Results: 1 passed, 0 failed

Multiple observations per test

Group several related facts under one named behaviour.

pure def normalize_path(path: Str) -> Str {
    if path.startswith("/") {
        return path
    }
    return "/" + path
}

test "normalize_path enforces a single leading slash" {
    observe normalize_path("api") == "/api"
    observe normalize_path("/api") == "/api"
    observe normalize_path("") == "/"
}

Run: vary test tests/path_test.vary

Expected output:

Results: 1 passed, 0 failed

Multiple tests per file

Cover distinct behaviours with separate named tests.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    if status >= 200 {
        return "2xx"
    }
    return "other"
}

test "2xx range" {
    observe class_of(200) == "2xx"
}

test "4xx range" {
    observe class_of(404) == "4xx"
}

test "5xx range" {
    observe class_of(503) == "5xx"
}

test "below 200 is other" {
    observe class_of(100) == "other"
}

Run: vary test tests/status_test.vary

Expected output:

Results: 4 passed, 0 failed

Edge cases and errors

Test the boundaries

Assert both sides of every edge where behaviour changes.

pure def is_2xx(status: Int) -> Bool {
    return status >= 200 and status < 300
}

test "200 is the lower edge" {
    observe is_2xx(200) == True
    observe is_2xx(199) == False
}

test "299 is the upper edge" {
    observe is_2xx(299) == True
    observe is_2xx(300) == False
}

Run: vary test tests/range_test.vary

Expected output:

Results: 2 passed, 0 failed

Test error paths

Use observe throws to assert a block raises.

def parse_port(s: Str) -> Int {
    let n = int(s)
    if n < 1 or n > 65535 {
        raise "port out of range: " + s
    }
    return n
}

test "valid port parses" {
    observe parse_port("8080") == 8080
}

test "out of range raises" {
    observe throws { parse_port("99999") }
}

test "non-numeric raises" {
    observe throws { parse_port("nope") }
}

Run: vary test tests/port_test.vary

Expected output:

Results: 3 passed, 0 failed

Running and reading

Read the test output

Locate the pass/fail summary line and understand the exit code.

pure def retry_delay_ms(attempt: Int) -> Int {
    if attempt <= 0 {
        return 100
    }
    if attempt >= 5 {
        return 16000
    }
    mut delay: Int = 100
    mut i: Int = 0
    while i < attempt {
        delay = delay * 2
        i = i + 1
    }
    return delay
}

test "first attempt is 100ms" {
    observe retry_delay_ms(0) == 100
}

test "doubles each attempt" {
    observe retry_delay_ms(1) == 200
    observe retry_delay_ms(2) == 400
    observe retry_delay_ms(3) == 800
}

test "saturates at 16000ms" {
    observe retry_delay_ms(5) == 16000
    observe retry_delay_ms(10) == 16000
}

Run: vary test tests/backoff_test.vary

Expected output:

Results: 3 passed, 0 failed

Names are documentation

Write test names that read like the spec.

pure def truncate(s: Str, limit: Int) -> Str {
    if len(s) <= limit {
        return s
    }
    return s[:limit - 1] + "…"
}

test "truncate returns the input when it fits inside the limit" {
    observe truncate("hello", 10) == "hello"
}

test "truncate replaces the last character with an ellipsis when too long" {
    observe truncate("hello world", 5) == "hell…"
}

test "truncate handles the exact-fit boundary without modification" {
    observe truncate("hello", 5) == "hello"
}

Run: vary test tests/truncate_test.vary

Expected output:

Results: 3 passed, 0 failed

Mutation Testing

Five runnable lessons covering vary mutate, scores, survivors, boundary mutations, and the inner-loop workflow.

First mutations

Run your first mutation pass

See that one happy-path test leaves most mutants alive.

pure def has_free_shipping(order_total: Int) -> Bool {
    return order_total >= 50
}

test "larger order qualifies" {
    observe has_free_shipping(75) == True
}

Run: vary mutate free_shipping.vary --tests free_shipping.vary --quick

Expected output:

(?m)Mutation score: \d{1,2}%

Pin down the survivors

Add a boundary and an opposite-branch test until the score hits 100%.

pure def has_free_shipping(order_total: Int) -> Bool {
    return order_total >= 50
}

test "larger order qualifies" {
    observe has_free_shipping(75) == True
}

test "smaller order does not qualify" {
    observe has_free_shipping(20) == False
}

test "exactly 50 qualifies" {
    observe has_free_shipping(50) == True
}

test "49 does not qualify" {
    observe has_free_shipping(49) == False
}

Run: vary mutate free_shipping.vary --tests free_shipping.vary --quick

Expected output:

(?m)Mutation score: 100%

Common survivors

Boundary mutants survive without edge tests

See how range predicates leak mutants when tests skip the edges.

pure def is_2xx(status: Int) -> Bool {
    return status >= 200 and status < 300
}

test "200 is 2xx" {
    observe is_2xx(200) == True
}

test "404 is not 2xx" {
    observe is_2xx(404) == False
}

Run: vary mutate is_2xx.vary --tests is_2xx.vary --quick

Expected output:

(?m)Mutation score: \d{1,2}%

Lock in every edge

Cover all four boundaries of a range predicate.

pure def is_2xx(status: Int) -> Bool {
    return status >= 200 and status < 300
}

test "200 is the lower edge" {
    observe is_2xx(200) == True
    observe is_2xx(199) == False
}

test "299 is the upper edge" {
    observe is_2xx(299) == True
    observe is_2xx(300) == False
}

test "404 is not 2xx" {
    observe is_2xx(404) == False
}

Run: vary mutate is_2xx.vary --tests is_2xx.vary --quick

Expected output:

(?m)Mutation score: 100%

Practical use

Mutation in the inner loop

Hit 100% on a multi-branch classifier with one edge pair per threshold.

pure def class_of(status: Int) -> Str {
    if status >= 500 {
        return "5xx"
    }
    if status >= 400 {
        return "4xx"
    }
    if status >= 200 {
        return "2xx"
    }
    return "other"
}

test "200 is the 2xx lower edge" {
    observe class_of(200) == "2xx"
    observe class_of(199) == "other"
}

test "400 is the 4xx lower edge" {
    observe class_of(400) == "4xx"
    observe class_of(399) == "2xx"
}

test "500 is the 5xx lower edge" {
    observe class_of(500) == "5xx"
    observe class_of(499) == "4xx"
}

test "deep 5xx still classifies as 5xx" {
    observe class_of(503) == "5xx"
}

Run: vary mutate class_of.vary --tests class_of.vary --quick

Expected output:

(?m)Mutation score: 100%

Install and Start Via Server

Prepare an Ubuntu host, confirm Vary and Via are installed, install Via prerequisites, initialize the control plane, and verify the Via systemd services.

Install

Verify Vary and Via commands

Confirm the host already has the Vary compiler and Via server CLI on PATH.

# Confirm the Vary compiler is installed and available on PATH.
command -v vary
vary --version

# Confirm the Via server CLI is installed and available on PATH.
command -v via
via --version

Run: via --version

Expected output:

(?m)^Via\s+.+$

Install host prerequisites

Install Ubuntu packages for Git, Docker, Caddy, and the Via Docker network.

# Install Git, Docker, Caddy, and the Via Docker network.
via install --print-prereq-script | sudo bash

# Create Via users, directories, systemd units, and admin group access.
via install

Run: via install

Expected output:

(?m)^\s*Installed changes applied$|^\s*Installed no changes \(already installed\)$

Initialize the control plane

Write server config, create the database, and mint signing keys.

# Write config, create the database, mint keys, and start Via services.
via init --domain vary.example.com

# Confirm the host layout, services, Docker, Git, and proxy are healthy.
via doctor

Run: via doctor

Expected output:

(?m)^.*PASS.*$

Verify

Verify Via services

Confirm the control plane, builder, runner, config server, test runner, and managed Caddy service are active.

# Show every Via service and whether it is active.
via status

# Re-run the full host and runtime health checks after startup.
via doctor

Run: via status

Expected output:

(?m)\bactive\b

Push and Test an App on Via

Create the first operator session, connect a local Vary project, test it, push a deploy, and inspect deploy output.

Provision

Create an operator and app on the server

Mint a one-time admin token and create the app record that will receive source pushes.

# Create a one-time admin token for the first operator.
via admin create alice

# Create the server-side app record and bare Git repo.
via app ensure my-api --owner alice

Run: via app ensure my-api --owner alice

Expected output:

(?i)created|exists|ensured|my-api

Log in from your workstation

Exchange the one-time admin token for workstation credentials.

# Exchange the one-time admin token for workstation credentials.
vary login https://vary.example.com --name alice --token-stdin

Run: vary login https://vary.example.com --name alice --token-stdin

Expected output:

(?i)session|credentials|logged in|expires

Connect a local project

Record the Via server and app id in the project's vary.toml.

# Work from the local Vary project you want to deploy.
cd my-api

# Record the Via server URL and app name in vary.toml.
vary app init --server https://vary.example.com --app my-api

# Confirm only the deployment config changed.
git status --short

# Commit the config so deploys can push a clean tree.
git add vary.toml
git commit -m "Configure Via deployment"

Run: vary app init --server https://vary.example.com --app my-api

Expected output:

(?m)^.*vary\.toml.*$

Test and push

Test locally and deploy committed source

Run the project's tests, push the current Git commit to Via, and wait for the deploy result.

# Run local tests before pushing source to Via.
vary test

# Via deploys expect committed source by default.
git status --short

# Push the current commit and wait for the deploy result.
vary app deploy

# Confirm the app's current deploy and runtime state.
vary app status

Run: vary app deploy

Expected output:

(?i)\brunning\b

Inspect build output from your workstation

Find the build id, inspect the build detail, and stream test logs for the deploy.

# List recent server builds for the app.
vary app builds --app my-api

# Inspect one build record in detail.
vary app build <build-id> --app my-api

# Stream the test logs when a result needs detail.
vary app logs --tests <build-id> --app my-api

Run: vary app builds --app my-api

Expected output:

(?i)build|running|passed|failed

Read the test result on the server

Confirm the server saw the same test result by reading Via's local state directory.

# Read the server-side test result directly from the Via state directory.
vary app tests <build-id> --app my-api --local --state-dir /var/lib/via

Run: vary app tests <build-id> --app my-api --local --state-dir /var/lib/via

Expected output:

(?i)passed|failed|tests

Route and observe

Attach traffic and read logs

Request a hostname, verify ownership, add a route, and stream runtime logs.

# Ask Via to create a domain ownership challenge.
vary app domain request my-api my-api.apps.example.com --target prod

# Confirm the required DNS record is visible.
vary app domain verify my-api my-api.apps.example.com --target prod

# Attach the verified hostname to the running app target.
vary app route add my-api my-api.apps.example.com --target prod --port 8080

# Stream runtime logs from the active container.
vary app logs --runtime

Run: vary app logs --runtime

Expected output:

(?m)^.*my-api.*$