Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.
Embedded DSLs
Most languages handle SQL, HTTP routing, and logging through libraries. You import a package, call its functions, and pass strings or builder objects that the compiler treats as opaque data. A misspelled column name compiles fine. A log call with the wrong field type compiles fine. They all fail later, at runtime.
Vary treats a few domains differently. The compiler understands the domain directly. These are embedded DSLs: small languages inside Vary's grammar that the compiler type-checks and compiles to specialized bytecode. Mistakes are caught before your code runs.
There are four today: SQL, HTTP, structured logging, and JSON decode.
SQL
The SQL DSL replaces query strings with structured blocks that the compiler can verify.
A data type with SQL-compatible fields is a table schema. The compiler derives column names and types from its fields:
data Note {
id: Int
title: Str
body: Str
}
Queries, inserts, deletes, and counts each have their own syntax. The compiler checks that data type names exist, column references are valid, and types match. It generates parameterized SQL with bound parameters, so injection is not possible by construction.
let notes = query db {
from Note as n
where n.title == search_term
select {
id = n.id
title = n.title
}
}
insert db {
into Note
values {
id = next_id
title = "Untitled"
body = ""
}
}
let removed = delete db {
from Note
where Note.id == target_id
}
The where clause is a Vary expression, not a SQL fragment. The compiler walks the expression tree, extracts column references, and translates them into a prepared statement. Field access on query results is typed: notes[0].title is known to be Str at compile time.
Without this DSL, the same code would be string-based db.execute("SELECT ...") calls where the compiler sees a Str argument and nothing else. Column renames, type changes, and table deletions would all be silent runtime failures. With the DSL, they are compile errors.
See SQLite databases for the full reference.
HTTP exposure
The expose DSL turns a Vary interface into a full HTTP service with one statement.
interface NoteService {
def get_health(self) -> Str { }
def list_notes(self) -> List[NoteData] { }
def get_note(self, id: Int) -> NoteData? { }
def create_note(self, title: Str, body: Str) -> NoteData { }
def delete_note(self, id: Int) -> DeleteResult { }
}
expose NoteService via http
The compiler reads the interface, derives HTTP verbs from method name prefixes (get_ is GET, create_ is POST, delete_ is DELETE), maps parameters to path segments or request bodies based on their names and types, and generates JAX-RS resource classes. For methods that take multiple body parameters, it synthesizes a DTO class. For methods that return an optional type, it generates a 404 when the value is None.
Your source code contains no HTTP layer. No annotations, no route strings, no framework imports. The mapping rules are part of the language spec.
See HTTP services for the full reference.
Structured logging
The logging DSL replaces print() debugging with structured events.
log info "user_login" {
message = "User logged in"
field "username" = name
field "attempt" = count
}
log is a keyword. Each statement has a level (trace, debug, info, warn, error), a stable event ID, an optional message, and typed fields. The compiler checks that field values are primitive types (Int, Float, Bool, Str) and rejects log statements inside pure functions.
Logger blocks scope a name to nested log statements. Context blocks attach fields that propagate to every log statement inside them:
logger "auth" {
context {
field "service" = "api"
}
log info "login_attempt" "Checking credentials"
log warn "login_failed" {
message = "Bad password"
field "username" = user
}
}
The runtime dispatches events to pluggable sinks (stdout, color, JSON, file, memory). For testing, capture_logs() and captured_events() let you assert on log output without touching stdout.
Because the compiler knows the structure of log statements, mutation testing skips mutations in log message strings (which do not affect correctness) while still mutating field value expressions (which do). A library-based logger would be opaque to the mutation engine.
See Structured logging for the full reference.
JSON decode
The JSON decode DSL turns untyped JSON into typed Vary values with path-aware error messages.
Most languages parse JSON into a tree, then extract fields with string keys and manual casts. A typo in a key name returns null. A wrong type assumption throws at runtime. Error messages say "cannot cast Object to String" with no indication of where in the JSON the problem is.
Vary's Decoder tracks the path through the document as you navigate it. Every extraction is typed and returns a Result:
import json
let node = json.decode(raw_text)?
let name = node.field("name").str()?
let port = node.field("config").field("db").field("port").int()?
let role = node.field("role").str_or("guest")
If port is missing or has the wrong type, the error message includes the full path: "config.db.port: expected Int, found Str". The short methods (.str(), .int(), etc.) return Result[T, DecodeError], *_or_none() methods return T?, and *_or methods return T with a default. Validation predicates (validate_int, validate_str) and enum constraints (.enum(variants)) catch domain errors at the decode boundary.
The decoder composes with the ? propagation operator, so a function that decodes a complex structure reads as a flat sequence of field extractions. Any failure short-circuits with a DecodeError that names the exact field and what went wrong.
See JSON decode for the full reference.
Why these four
All four are common in real systems, are frequent sources of runtime bugs, benefit from static checking, and carry heavy framework overhead in other languages.
| Property | SQL | HTTP exposure | Logging | JSON decode |
|---|---|---|---|---|
| Runtime bugs | Misspelled columns, type mismatches, injection | Wrong routes, mismatched parameter types | Unstructured strings, missing context, wrong types | Wrong key names, type cast failures, missing fields with no context |
| What the compiler checks | Schema, column names, value types | Route table derived from types | Field types, purity violations | Field types, path tracking, required vs. optional |
| What you skip | ORMs, query builders, migration tools | Annotation processors, routing frameworks | Logging frameworks, format strings, serializers | Manual null checks, string-keyed accessors, custom error wrappers |
Each DSL turns a class of runtime failure into a compile error.
What these are not
You cannot define your own DSL in Vary. These are fixed extensions to the language grammar that the compiler team maintains alongside the rest of the language. There is no macro system, no plugin architecture, no metaprogramming facility.
That is intentional. A general-purpose DSL mechanism would make tooling harder and push language design decisions onto library authors. Keeping the set small means every DSL gets full integration with type checking, codegen, error reporting, and mutation testing.