Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.

Language design choices

Every language is shaped by trade-offs. This page documents the ones worth explaining.

High-level design

PrincipleWhat it means in practice
Readable by defaultReadable syntax with braces. If you know Python, you can read Vary.
Explicit over implicitNo annotations, no macros, no implicit conversions. You see what the code does in the code itself.
One way to do thingsOne string type, one integer type, one formatter style, one test DSL. Fewer choices means less time arguing.
Mutation testing is first-classvary mutate is built into the compiler, not a plugin. The compiler has direct access to the AST and bytecode, so mutations are precise.
Mutation results you actually readMost mutation tools dump a table of survived mutants and leave you to figure out why. Vary tells you what each survivor means, groups them by the kind of test gap they reveal, and says what to write next. The output is designed so a developer who has never heard of mutation testing can act on it in under a minute.
Single CLIRun, test, format, check, and mutate all ship as one command. No toolchain assembly.
JVM underneath, Vary on topVary compiles to JVM bytecode and uses Java libraries like Quarkus internally, but does not expose Java interop to users. You write Vary, not Java.
Correctness over clevernessStatic types, null safety, and mutation testing are the defaults. The language is designed to catch mistakes, especially from AI-generated code.

Compilation target

ChoiceDecisionRationale
RuntimeJVM bytecode30 years of GC, JIT, threading, and production infrastructure for free. The alternative was LLVM or a custom VM, both of which would have consumed the entire project budget on runtime engineering.
Execution modelCompiled, not interpretedCatches type errors before runtime. Bytecode generation through ASM gives full control over what the JVM executes.
Numeric typesInt maps to long, Float maps to doubleOne integer type and one float type. No width selection, no signed/unsigned distinction. Simplicity over precision control.

Syntax

ChoiceDecisionRationale
Block delimitersBraces { } onlyStarted with Python-style indentation, switched to braces. Indentation-sensitive parsing created ambiguity in nested blocks and made code generation from tools harder. Braces are unambiguous.
Statement terminatorNewlineNo semicolons. Newlines terminate statements. Keeps the visual simplicity of Python without the parsing complexity of significant whitespace.
Type annotationsRequired on parameters, optional on localsFunction signatures are documentation. Local variables benefit from inference where the type is obvious from context.
Immutability defaultlet is immutable, mut is opt-in mutableImmutable by default pushes you toward fewer moving parts. Mutation is available when you need it, but you have to ask for it.

Type system

ChoiceDecisionRationale
Null safetyT? optional types with flow narrowingNull pointer errors are caught at compile time. if x is not None { } narrows x from T? to T inside the block. No wrapper types, no monadic syntax.
GenericsSquare brackets, type erasure, always inferred[T] instead of <T> to avoid parser ambiguity with comparison operators. Type parameters are checked at compile time and erased in bytecode. No bounds, no variance annotations. See the generics page for details.
String typeStr with value semanticsOne string type. No char type, no byte string distinction. s[i] returns a single-character Str, not an integer.

Toolchain

ChoiceDecisionRationale
Single binaryCompiler, test runner, formatter, and mutation tester in one CLINo toolchain assembly. vary run, vary test, vary format, vary mutate all ship together. Fewer moving parts, fewer version mismatches.
Mutation testingBuilt into the compilerMutation testing is too useful to be an afterthought plugin. The compiler has direct access to the AST and bytecode, so mutations are precise and fast.
Artifact cachingContent-addressed compilation cache (SHA-256 of source + compiler version + flags)The CLI maintains a persistent cache of compiled bytecode in .vary/artifacts/. If the source has not changed, recompilation is skipped entirely. Multiple variants (normal, debug, coverage, mutation) of the same file coexist without conflict. This makes repeated vary run, vary test, and vary mutate invocations fast even on large projects.
FormatterOpinionated, minimal configOne style with two settings in vary.toml: indent size and import sorting. Everything else is fixed. vary format rewrites files in place.

Web and services

ChoiceDecisionRationale
HTTP runtimeQuarkus under the hoodVary does not implement its own HTTP server. Quarkus handles routing, request parsing, and lifecycle management. You get a production-grade server without Vary having to build one.
Expose capabilityexpose Interface via http statementDefine an interface with methods named by HTTP verb (get_health, create_order, delete_item), implement it in a class, then write expose MyService via http. The compiler maps method prefixes to HTTP verbs and generates all the routing. No annotations, no decorators, no framework boilerplate.

For example, a market API in Vary looks like this:

interface SimService {
    def get_health(self) -> Str { }
    def get_state(self) -> Str { }
    def create_step(self) -> Str { }
    def delete_order(self, order_id: Int, account_id: Int) -> Str { }
}

expose SimService via http

The method name tells the compiler everything: get_ becomes a GET endpoint, create_ becomes POST, patch_ becomes PATCH, delete_ becomes DELETE. Parameters named *_id become path parameters. For GET, the rest become query parameters; for POST/PUT/PATCH, they become a JSON request body. DELETE methods only accept id parameters. See HTTP services for the full rules.

Testing

ChoiceDecisionRationale
Testing DSLtest "name" { } blocks with observe keywordTests are a language-level construct, not a library convention. test is a keyword. The compiler generates test discovery and execution directly, so vary test works with zero configuration.
Mutation testingBuilt into the compiler via vary mutateThe compiler has direct access to the AST and bytecode, so it can apply precise mutations and run your tests against each one. This catches tests that pass by accident, assert too little, or never actually exercise the code they claim to cover.
Observe keywordobserve expr as the assertion primitiveA single keyword that marks the oracle boundary: where the test defines correctness. throws { } handles exception testing. No matcher libraries, no assertion chaining.
Contractsin {} preconditions, out (r) {} postconditions, old(expr)Function-level invariants checked at runtime on every call. Preconditions document caller responsibilities, postconditions document implementation promises. Failed contracts throw ContractViolation and count as mutation kills. No annotation syntax needed.

Things we chose not to do

If you can't tell what code does by reading it, the language has failed. Annotations, implicit wiring, and convention-over-configuration frameworks all hide what's actually happening.

OmissionRationale
No annotations or decoratorsThe @ symbol is not part of the language. There are no user-defined annotations, no annotation processors, and no decorators. Features that other languages implement with annotations (testing, HTTP routing, dependency injection) are handled through language-level constructs: test "name" { } blocks for tests, expose Interface via http for routing.
No macrosMacros make tooling harder. Code that rewrites itself is code that editors, linters, and AI assistants cannot reason about.
No arithmetic operator overloading+, -, *, /, and the comparison operators do what they look like they do. + is addition or string concatenation, never a custom method. The one exception is structural equality: a class can define equals(other: Self) -> Bool and hash() -> Int to opt into ==/!= and collection-key behavior. This exists because user classes need structural equality to work as Dict/Set keys and to be meaningfully mutation-tested, and a separate is_equivalent() method would silently diverge from what == does. See classes.
No exceptions as control flowtry/except exists for error handling, but the language does not encourage throwing exceptions for normal control flow.
No implicit conversionsInt widens to Float automatically (64-bit integer to 64-bit double, which is lossless for most values). Beyond that single numeric widening, there are no implicit conversions. Str does not become Int, Bool does not become Int, and collections do not convert element types.
No direct Java interopVary does not let you import or call Java libraries directly. If a Java library is useful (Quarkus for HTTP, ASM for bytecode), we bring it in through the compiler, runtime, or stdlib where it gets a Vary-native API. You write Vary, not Java-through-Vary. This keeps the language surface clean and means we can swap underlying libraries without breaking user code.
No convention-over-configurationMethod names like get_health map to GET endpoints not because of a naming convention buried in documentation, but because the expose ... via http statement explicitly tells the compiler to do it. The mapping rules are part of the language, not a framework you have to learn separately.