About

Varyonic programming

Early exploration. Vary and mutation testing are both early in their development. These guidelines reflect what we have learned so far and will evolve as the language and tooling mature.

Varyonic programming is the design style encouraged by Vary's verification-first model. It treats program behaviour as something that should be explicit, typed, observable, and testable under mutation.

Varyonic programming means writing code so that its decisions are typed, its guarantees are explicit, its effects are isolated, and its behaviour is observable.

Why this exists

Conventional code can look correct while hiding its real behaviour inside side effects, implicit error handling, stringly typed state, and broad orchestration functions. Varyonic programming is a response to that failure mode. Programs written this way have semantics that can be directly inspected, tested, and stressed by mutation.

This matters even more for LLM-generated code, which tends to pass conventional tests while hiding mutation-blind behaviour behind mixed concerns.

Behaviour must be observable

The behaviour of a program should be expressed in returned data, not hidden in logs or side effects.

Not Varyonic:

import process

def check_bad(pages: List[Str]) {
    print("broken links found")
    process.exit(1)
}

Varyonic:

data BrokenLink {
    source: Str
    target: Str
}

data CheckReport {
    files_checked: Int
    broken_links: List[BrokenLink]
    error_count: Int
}

Tests assert on structured results:

observe report.error_count == 1

If behaviour cannot be observed through returned values, mutation testing cannot measure it.

Domain models come first

The structure of the domain should be represented explicitly with data and enum.

Not Varyonic:

def deploy_bad(mode: Str) {
    if mode == "prod" {
        print("deploying to prod")
    }
}

Varyonic:

enum DeployTarget {
    Local
    Preview
    Production
}

data DocPage {
    slug: Str
    title: Str
    category: Str
    order: Int
}

Types encode meaning and reduce ambiguity.

Pure functions hold core logic

Important program behaviour should live in pure functions.

Pure logic includes classification, validation, transformation, planning, and report generation.

enum ChangeKind {
    Css
    Content
    Template
    StaticAsset
    Unknown
}

def classify_change(path: Str) -> ChangeKind {
    if path.endswith(".css") {
        return ChangeKind.Css
    }
    if path.endswith(".html") {
        return ChangeKind.Template
    }
    if path.endswith(".md") {
        return ChangeKind.Content
    }
    return ChangeKind.Unknown
}

Put reasoning in pure functions. Effects should execute decisions, not contain them.

Separate decisions from effects

Programs should be structured as:

Inputs -> Pure planning -> Execution -> Structured report
data BuildStep {
    source: Str
    output: Str
}

data BuildPlan {
    steps: List[BuildStep]
}

data BuildReport {
    files_built: Int
    errors: Int
}

def plan_build(sources: List[Str]) -> BuildPlan {
    mut steps: List[BuildStep] = []
    for s in sources {
        steps = steps + [BuildStep(s, s + ".out")]
    }
    return BuildPlan(steps)
}

def run_build(plan: BuildPlan) -> BuildReport {
    return BuildReport(len(plan.steps), 0)
}

The plan describes what should happen. Execution performs the side effects. Tests verify plan semantics separately from execution behaviour.

Errors are values

Error handling should use Result-style semantics.

import fs

data DocSource {
    path: Str
    content: Str
}

def load_doc(path: Str) -> Result[DocSource, Str] {
    if not fs.exists(fs.path(path)) {
        return Err("file not found: " + path)
    }
    let rp = fs.read_path(path).unwrap()
    return Ok(DocSource(path, fs.read_text(rp).unwrap()))
}

Usage:

def process_doc(path: Str) -> Result[Str, Str] {
    let doc = load_doc(path) ?else return Err("invalid document")
    return Ok(doc.content)
}

Errors become explicit branches that tests can evaluate.

Contracts express semantic guarantees

Contracts describe meaningful invariants.

data DocPage {
    slug: Str
    title: Str
    category: Str
    order: Int
}

def parse_doc(md: Str) -> DocPage {
    in {
        len(md) > 0
    }
    out (result) {
        result.slug != ""
    }
    return DocPage("intro", "Introduction", "docs", 1)
}

Contracts provide validation, documentation, and automatic verification. A mutant that violates a contract is killed without a dedicated test. See Contracts for syntax and Contracts in mutation for how they interact with mutation testing.

Effects are isolated

Side effects should be limited to small execution layers.

Common effects: filesystem, subprocess, HTTP server, logging.

data DeployPlan {
    target: Str
    source_dir: Str
}

data DeployReport {
    success: Bool
    files_copied: Int
}

def plan_deploy(target: Str) -> DeployPlan {
    return DeployPlan(target, "build/")
}

def run_deploy(plan: DeployPlan) -> DeployReport {
    return DeployReport(True, 0)
}

Effects should be thin shells around pure decisions.

Resource lifetimes are explicit

Use defer to control resource cleanup.

import process
import fs

def cleanup_example() {
    let dir = process.tempdir() + "/work"
    fs.mkdir(fs.path(dir))
    defer { fs.remove_dir(fs.path(dir)).unwrap() }
}

This ensures predictable cleanup and reduces hidden state.

Finite decision spaces

Use enums for closed decision sets.

enum PageKind {
    Doc
    Blog
    Landing
    Error
}

pure def page_template(kind: PageKind) -> Str {
    match kind {
        case PageKind.Doc { return "doc.html" }
        case PageKind.Blog { return "blog.html" }
        case PageKind.Landing { return "index.html" }
        case PageKind.Error { return "404.html" }
    }
}

Finite decision spaces allow mutation testing to exhaustively explore logic. When a match expression covers every variant, a mutant that swaps one branch for another is detectable.

Expression-oriented code

Use expression forms when appropriate.

def pick_title(title: Str?, slug: Str) -> Str {
    let result = if title is not None { title } else { slug }
    return result
}

Expression-oriented code reduces procedural scaffolding.

Constructors enforce invariants

Primary constructors should validate object state.

class ValidatedPage(slug: Str, title: Str) {
    init {
        if slug == "" { raise ValueError("empty slug") }
    }
}

This ensures invalid states cannot exist.

Interfaces isolate external systems

Interfaces encapsulate external dependencies.

interface FileSystem {
    def read(self, path: Str) -> Str {
    }
    def write(self, path: Str, content: Str) {
    }
}

External volatility stays outside core logic. See HTTP services for how interfaces map to service endpoints.

Architectural pattern

The canonical Vary program structure is:

CLI -> Load context -> Pure planning -> Execution layer -> Structured report -> CLI prints + exit
data Link {
    source: Str
    target: Str
}

data CheckPlan {
    pages: List[Str]
    known: List[Str]
}

data CheckReport {
    files_checked: Int
    broken_links: List[Link]
    error_count: Int
}

# Pure planning
def plan_check(pages: List[Str], known: List[Str]) -> CheckPlan {
    return CheckPlan(pages, known)
}

def run_check(plan: CheckPlan) -> CheckReport {
    mut broken: List[Link] = []
    for p in plan.pages {
        if not plan.known.contains(p) {
            broken = broken + [Link("site", p)]
        }
    }
    return CheckReport(len(plan.pages), broken, len(broken))
}

# Thin execution shell
import fs
import process

def main() {
    let pages = fs.list_dir(fs.dir_path("content/").unwrap())
    let known = fs.list_dir(fs.dir_path("build/").unwrap())
    let plan = plan_check(pages, known)
    let report = run_check(plan)

    if report.error_count > 0 {
        print("Found " + str(report.error_count) + " broken links")
        process.exit(1)
    }
    print("All links valid")
}

Tests verify plan semantics directly:

test "broken link detected" {
    let plan = plan_check(["a.html", "missing.html"], ["a.html"])
    let report = run_check(plan)

    observe report.broken_links.len() == 1
    observe report.files_checked == 2
    observe report.error_count == 1
}

test "all links valid" {
    let plan = plan_check(["a.html", "b.html"], ["a.html", "b.html"])
    let report = run_check(plan)

    observe report.broken_links.len() == 0
    observe report.error_count == 0
}

Varyonic module organization

A typical project separates domain logic from effects:

types.vary          # data and enum definitions
config.vary         # configuration loading
paths.vary          # path resolution

docs_parse.vary     # pure parsing logic
navigation.vary     # pure navigation building

render_plan.vary    # pure: what to render
render_execute.vary # effectful: write files

check_links.vary    # pure: find broken links
check_plan.vary     # pure: plan checks

deploy_plan.vary    # pure: plan deployment
deploy_execute.vary # effectful: run deployment

main.vary           # CLI entry point

Non-Varyonic patterns

The following patterns are discouraged:

PatternWhy it hurts
Magic strings instead of enumsMutations to string values go undetected
Functions returning None when behaviour occurredCallers cannot distinguish success from failure
Side effects mixed with decision logicPure logic becomes untestable
Logging instead of structured reportsNo observable return value to assert on
Tests asserting string fragmentsBrittle and mutation-blind
Implicit error handlingFailures are silently swallowed
Large functions that mix planning and executionToo many concerns to test or mutate cleanly
Free-form dictionaries instead of domain modelsNo type safety, no field-level coverage

Varyonic scripting

Scripts are where Varyonic discipline pays off most. A typical script starts small, accumulates side effects, and becomes untestable. The Varyonic approach structures scripts into five phases:

Resolve context → Decode inputs → Build plan → Execute effects → Emit report

Each phase is a pure function (except Execute) that returns typed data. Only main() calls system.exit() or writes to stderr. This makes every phase independently testable and every decision observable under mutation.

The five phases

PhaseResponsibilityPure?Returns
ResolveLocate files, determine pathsYesdata with resolved paths
DecodeParse and validate inputsYesResult[T, ScriptError]
PlanDecide what work to doYesdata describing work items
ExecutePerform side effectsNoResult[Report, ScriptError]
ReportFormat results for outputYesStructured report data

main() is a thin shell that calls each phase, handles errors, and prints results.

Case study: generate-test-docs

The generate-test-docs script reads a JSON test inventory, renders Markdown docs through templates, and writes the output files. Here is what the old version looked like and how it was refactored.

Before (stringly typed, exit-in-helpers):

# Helpers call system.exit() and system.stderr() directly
def load_json(path: Str) -> Json {
    let content = read_file(path)
    if content == "" {
        system.stderr().write("error: cannot read " + path + "\n")
        system.exit(1)
    }
    return Json.parse(content).unwrap()
}

def write_output(path: Str, content: Str) {
    let result = fs.write_text(fs.write_path(path).unwrap(), content)
    if result.is_err() {
        system.stderr().write("error: cannot write " + path + "\n")
        system.exit(1)
    }
}

def main() {
    # Path logic inlined and duplicated
    let base = system.cwd()
    let inv = load_json(base + "/ci-artifacts/test-inventory.json")

    # Template rendering mixed with file writing
    let engine = Template.create(base + "/programs/generate-test-docs/templates")
    let rendered = engine.render("test-inventory.md.peb", inv)
    write_output(base + "/website/content/docs/test-inventory.md", rendered)

    let rendered2 = engine.render("test-matrix.md.peb", inv)
    write_output(base + "/docs/test-matrix.md", rendered2)
    print("Done")
}

Problems with this style:

ProblemConsequence
load_json and write_output call system.exit()Tests cannot exercise error paths without killing the process.
No typed domain modelThe JSON is passed around as an opaque Json value with no validation.
Planning and execution are interleavedYou cannot test "what would be generated" without actually writing files.
Mutation testing cannot kill mutants in error handlingErrors terminate the process, making mutants invisible.

After (five-phase Varyonic structure):

import system
import path
import json
import template

# Phase 0: Domain types
data ScriptPaths {
    repo_root: Str
    template_dir: Str
    inventory_json: Str
}

enum ScriptError {
    Input(path: Str, message: Str)
    Decode(field: Str, message: Str)
    Render(template: Str, message: Str)
    Output(path: Str, message: Str)
}

data Inventory {
    suites: Dict[Str, SuiteTotals]
    totals: InventoryTotals
}

data GenerationTarget {
    template_name: Str
    output_rel_path: Str
}

data GenerationPlan {
    targets: List[GenerationTarget]
}

data GenerationReport {
    files_generated: Int
    output_paths: List[Str]
}

# Phase 1: Resolve context
def resolve_paths() -> ScriptPaths {
    let sd = system.script_dir()
    mut base: Str = system.cwd()
    if sd is not None {
        base = path.normalize(path.join(path.parent(sd), ".."))
    }
    return ScriptPaths(base,
        path.join(base, "programs/generate-test-docs/templates"),
        path.join(base, "ci-artifacts/test-inventory.json"))
}

# Phase 2: Decode inputs
def decode_inventory(inv: Json) -> Result[Inventory, ScriptError] {
    let suites_obj = inv.get_object("suites")
    if suites_obj is None {
        return Err(ScriptError.Decode("suites", "missing required field"))
    }
    # ... validate and decode each field, returning Err on failure
    return Ok(Inventory(suites_dict, totals))
}

# Phase 3: Build plan
def build_plan() -> GenerationPlan {
    return GenerationPlan([
        GenerationTarget("test-inventory.md.peb", "website/content/docs/test-inventory.md"),
        GenerationTarget("test-matrix.md.peb", "docs/test-matrix.md")
    ])
}

# Phase 4: Execute effects
def run_generation(plan: GenerationPlan, paths: ScriptPaths,
                   inventory: Json) -> Result[GenerationReport, ScriptError] {
    let engine = template.create(paths.template_dir)
    mut generated: Int = 0
    mut output_paths: List[Str] = []
    for target in plan.targets {
        # render + write, returning Err on failure
        generated = generated + 1
        output_paths = output_paths + [target.output_rel_path]
    }
    return Ok(GenerationReport(generated, output_paths))
}

# Phase 5: main (the only place with exit/stderr)
def main() {
    let paths = resolve_paths()
    let inv = load_inventory_json(paths.inventory_json)
    # ... match on Err, write to stderr, exit(1)
    let decoded = decode_inventory(inv.unwrap())
    # ... match on Err, write to stderr, exit(1)
    let plan = build_plan()
    let report = run_generation(plan, paths, inv.unwrap())
    # ... match on Err, write to stderr, exit(1)
    print(f"Generated {report.unwrap().files_generated} files")
}

What the phases enable

Testability. Each phase is a function that takes data and returns data. Tests call resolve_paths(), decode_inventory(), build_plan(), and run_generation() directly, with no mocking and no filesystem setup for the pure phases:

test "decode rejects missing suites" {
    let inv = Json.parse("{\"totals\": {\"total_files\": 1, \"total_test_cases\": 2}}")
    let result = decode_inventory(inv.unwrap())
    observe result.is_err()
}

test "plan contains expected targets" {
    let plan = build_plan()
    observe plan.targets.len() == 2
}

Mutation resistance. Because every decision returns a value, mutation testing can kill mutants by asserting on that value. In the old style, a mutant that changes system.exit(1) to system.exit(0) is invisible to tests because the process dies either way. In the new style, a mutant that changes ScriptError.Decode to ScriptError.Input is caught by a test that matches on the error variant.

LLM-assisted maintenance. An LLM modifying this script can:

ActionBenefit
Add a ScriptError variantThe compiler flags every match that needs updating
Add a GenerationTargetThe execution loop stays unchanged
Add checks to decode_inventoryPure function with clear inputs and outputs

The phase boundaries act as natural seams. An LLM does not need to understand the whole script to safely modify one phase.

Further reading

PageTopic
Designing for mutationHow to apply these principles to raise mutation strength
Golden pathThe mutation testing workflow
OraclesWriting effective test assertions
Contracts in mutationContracts and mutation testing
ContractsContract syntax reference
HTTP servicesInterfaces and service endpoints

Summary

Program semantics should be explicit, typed, and observable. The language provides tools for this: domain modelling (data, enum), contracts (in, out, old), pure logic (pure def), structured error flows (Result, ?else), observable tests (observe), and mutation analysis (vary mutate). When these tools work together, you can see whether the program is correct by reading it, testing it, and mutating it.

Language design choices →