tl;dr: Pull the official Vary image, then run examples including hello world, FizzBuzz, tests, and mutation testing. Everything is pre-installed.


If you have Docker and just want to quickly try out Vary, use our Docker image. Everything is pre-installed. The container deletes itself when you exit, so nothing touches your machine. Once you feel comfortable, you can install Vary properly on Linux, macOS, or Windows.

This walkthrough takes about five minutes. By the end you will have run a Vary program, executed tests, and seen mutation testing find a gap in a test suite.

Start a shell

Pull the image and drop into a shell:

docker run -it --rm --hostname vary --entrypoint bash ghcr.io/ccollicutt/vary:latest

The --entrypoint bash overrides the default entrypoint (which is the vary command) to give you a shell. The --rm flag removes the container when you exit. The Vary compiler, test runner, formatter, mutation engine, and REPL are all pre-installed.

Verify it works:

vary@vary:/workspace$ vary --version
Vary v113-alpha.1

Run hello world

The image ships with example programs in /opt/vary/examples/. List them:

vary@vary:/workspace$ ls /opt/vary/examples/
cli_demo.vary      enums.vary         fibonacci.vary     game_of_life.vary  math_utils.vary    ...
counter.vary       factorial.vary     fizzbuzz.vary      hello.vary         tuples.vary        ...

Run the simplest one:

vary@vary:/workspace$ vary run /opt/vary/examples/hello.vary
Hello, World!

That is the entire program. Vary runs top-level code directly. No main function, no boilerplate.

Run FizzBuzz

vary@vary:/workspace$ vary run /opt/vary/examples/fizzbuzz.vary
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...

This prints the classic FizzBuzz sequence from 1 to 100. The source is 13 lines of straightforward code:

mut i = 1
while i <= 100 {
    if i % 15 == 0 {
        print("FizzBuzz")
    } elif i % 3 == 0 {
        print("Fizz")
    } elif i % 5 == 0 {
        print("Buzz")
    } else {
        print(i)
    }
    i = i + 1
}

If you have written Python, this should look familiar. The differences: braces instead of indentation, explicit types on function parameters, and mut for mutable variables.

Run tests

Vary has a built-in test runner. The examples include test files for the Fibonacci module:

vary@vary:/workspace$ vary test /opt/vary/examples/fibonacci_test.vary

Found 1 test file(s)

fibonacci_test.vary:
  PASS: fib base cases
  PASS: fib sequence
  PASS: fib larger

==================================================
Results: 3 passed, 0 failed

Look at what the test file contains:

from fibonacci import fib

test "fib base cases" {
    observe fib(0) == 0
    observe fib(1) == 1
}

test "fib sequence" {
    observe fib(2) == 1
    observe fib(3) == 2
    observe fib(4) == 3
    observe fib(5) == 5
    observe fib(6) == 8
}

test "fib larger" {
    observe fib(10) == 55
}

Tests are test "name" { } blocks with observe assertions. No test framework to install, no imports to remember. The compiler knows what tests are.

Try mutation testing

This is where things get interesting. Tests tell you the code works. Mutation testing tells you the tests are actually checking something.

Run mutation testing against the math utils example:

vary@vary:/workspace$ vary mutate /opt/vary/examples/math_utils.vary

The mutation engine takes the math_utils.vary source, makes small changes to it (swapping + for -, changing * to +, replacing return values), and reruns the tests for each change. If the tests still pass after a change, that mutant "survived," which means the tests have a gap.

The output shows a score: the percentage of mutants your tests caught. A function like square that returns x * x might have a surviving mutant where * becomes +, but only if the test does not check values where x * x differs from x + x (which is every value except 0 and 2).

You can ask the engine to explain a specific survivor:

vary@vary:/workspace$ vary mutate /opt/vary/examples/math_utils.vary --why

This prints what changed, where, and what test you could write to catch it.

Write your own program

Create a file right in the container:

vary@vary:/workspace$ cat > /tmp/greet.vary << 'EOF'
def greet(name: Str) -> Str {
    return f"Hello, {name}!"
}

print(greet("Docker"))
print(greet("Vary"))
EOF

Run it:

vary@vary:/workspace$ vary run /tmp/greet.vary
Hello, Docker!
Hello, Vary!

Types are checked at compile time. Try changing the function to return an Int instead of Str and the compiler will catch it before anything runs.

Try the REPL

For quick experiments, Vary has an interactive mode:

vary@vary:/workspace$ vary repl
>>> 2 ** 10
1024
>>> let name = "Vary"
>>> f"Hello, {name}!"
Hello, Vary!
>>> exit()

Clean up

Type exit to leave the container. Because you used --rm, Docker deletes the container and everything in it. Your host machine is untouched.

vary@vary:/workspace$ exit

What to do next

If you liked what you saw and want to install Vary on your machine, see the platform guides for Linux, macOS, or Windows. For a persistent Docker setup with volume mounts instead of a throwaway container, see the Docker guide.

Once installed, the getting started page walks through the language in more depth. The test DSL page covers writing tests, and the mutation testing page explains the mutation engine in full.

More articles

What's new in Vary v122-alpha.1 v122-alpha.1 is out. The headline is vary var, a new top-level command that runs check, test, mutation, and review under a cost budget. The mutation engine was rewritten around reachability tracing, kill-first scheduling, and a hot-swap backend. Frugal, a native PEG parser library ported from Parsimonious, also lands.
Vary mutation testing speed: comparing to AST and PIT Vary now measures mutation-testing performance directly on real benchmark programs, including a project-scale parser workload and a PIT-style comparison fixture, and the current results are strong enough to talk about in concrete terms.