Every language is shaped by trade-offs. This page documents the ones worth explaining.
High-level design
| Principle | What it means in practice |
| Readable by default | Readable syntax with braces. If you know Python, you can read Vary. |
| Explicit over implicit | No annotations, no macros, no implicit conversions. You see what the code does in the code itself. |
| One way to do things | One string type, one integer type, one formatter style, one test DSL. Fewer choices means less time arguing. |
| Mutation testing is first-class | vary 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 read | Most 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 CLI | Run, test, format, check, and mutate all ship as one command. No toolchain assembly. |
| JVM underneath, Vary on top | Vary 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 cleverness | Static types, null safety, and mutation testing are the defaults. The language is designed to catch mistakes, especially from AI-generated code. |
Compilation target
| Choice | Decision | Rationale |
| Runtime | JVM bytecode | 30 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 model | Compiled, not interpreted | Catches type errors before runtime. Bytecode generation through ASM gives full control over what the JVM executes. |
| Numeric types | Int maps to long, Float maps to double | One integer type and one float type. No width selection, no signed/unsigned distinction. Simplicity over precision control. |
Syntax
| Choice | Decision | Rationale |
| Block delimiters | Braces { } only | Started 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 terminator | Newline | No semicolons. Newlines terminate statements. Keeps the visual simplicity of Python without the parsing complexity of significant whitespace. |
| Type annotations | Required on parameters, optional on locals | Function signatures are documentation. Local variables benefit from inference where the type is obvious from context. |
| Immutability default | let is immutable, mut is opt-in mutable | Immutable by default pushes you toward fewer moving parts. Mutation is available when you need it, but you have to ask for it. |
Type system
| Choice | Decision | Rationale |
| Null safety | T? optional types with flow narrowing | Null 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. |
| Generics | Square 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 type | Str with value semantics | One string type. No char type, no byte string distinction. s[i] returns a single-character Str, not an integer. |
Toolchain
| Choice | Decision | Rationale |
| Single binary | Compiler, test runner, formatter, and mutation tester in one CLI | No toolchain assembly. vary run, vary test, vary format, vary mutate all ship together. Fewer moving parts, fewer version mismatches. |
| Mutation testing | Built into the compiler | Mutation 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 caching | Content-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. |
| Formatter | Opinionated, minimal config | One 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
| Choice | Decision | Rationale |
| HTTP runtime | Quarkus under the hood | Vary 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 capability | expose Interface via http statement | Define 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
| Choice | Decision | Rationale |
| Testing DSL | test "name" { } blocks with observe keyword | Tests 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 testing | Built into the compiler via vary mutate | The 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 keyword | observe expr as the assertion primitive | A single keyword that marks the oracle boundary: where the test defines correctness. throws { } handles exception testing. No matcher libraries, no assertion chaining. |
| Contracts | in {} 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.
| Omission | Rationale |
| No annotations or decorators | The @ 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 macros | Macros 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 flow | try/except exists for error handling, but the language does not encourage throwing exceptions for normal control flow. |
| No implicit conversions | Int 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 interop | Vary 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-configuration | Method 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. |