Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.
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:
| Pattern | Why it hurts |
|---|---|
| Magic strings instead of enums | Mutations to string values go undetected |
Functions returning None when behaviour occurred | Callers cannot distinguish success from failure |
| Side effects mixed with decision logic | Pure logic becomes untestable |
| Logging instead of structured reports | No observable return value to assert on |
| Tests asserting string fragments | Brittle and mutation-blind |
| Implicit error handling | Failures are silently swallowed |
| Large functions that mix planning and execution | Too many concerns to test or mutate cleanly |
| Free-form dictionaries instead of domain models | No 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
| Phase | Responsibility | Pure? | Returns |
|---|---|---|---|
| Resolve | Locate files, determine paths | Yes | data with resolved paths |
| Decode | Parse and validate inputs | Yes | Result[T, ScriptError] |
| Plan | Decide what work to do | Yes | data describing work items |
| Execute | Perform side effects | No | Result[Report, ScriptError] |
| Report | Format results for output | Yes | Structured 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:
| Problem | Consequence |
|---|---|
load_json and write_output call system.exit() | Tests cannot exercise error paths without killing the process. |
| No typed domain model | The JSON is passed around as an opaque Json value with no validation. |
| Planning and execution are interleaved | You cannot test "what would be generated" without actually writing files. |
| Mutation testing cannot kill mutants in error handling | Errors 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:
| Action | Benefit |
|---|---|
Add a ScriptError variant | The compiler flags every match that needs updating |
Add a GenerationTarget | The execution loop stays unchanged |
Add checks to decode_inventory | Pure 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
| Page | Topic |
|---|---|
| Designing for mutation | How to apply these principles to raise mutation strength |
| Golden path | The mutation testing workflow |
| Oracles | Writing effective test assertions |
| Contracts in mutation | Contracts and mutation testing |
| Contracts | Contract syntax reference |
| HTTP services | Interfaces 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.