← Back to projects

WCL

WCL

What is WCL?

WCL (Wil’s Configuration Language) is a statically-typed, block-structured configuration language I built for my own projects that need to deal with complex data structures. It sits somewhere between YAML and a full programming language — giving you the structure and safety of a real type system without the overhead of writing actual code.

If you’ve ever found yourself fighting YAML anchors, wishing TOML supported nesting properly, or stacking HCL hacks to get around its limitations, WCL is my answer to that frustration.

Key Features

Static Typing & Schema Validation — Every config file can be validated against a schema at parse time, not at runtime. Catch mistakes before they hit production.

Block Structure — Configs are organized into nested blocks rather than relying on indentation or bracket soup. Blocks can be typed, named, and composed.

Expressions — Interpolation, arithmetic, conditionals, and string operations are first-class. No more templating your config through an external tool.

Macros — Define reusable config patterns and expand them wherever needed. Reduce boilerplate without sacrificing readability.

Decorators — Attach metadata and transformations to blocks declaratively.

Data Tables — Define structured tabular data inline, useful for lookup tables, feature flags, and matrix configs.

Query Engine — Query and filter config data programmatically, making it easy to extract exactly what you need from complex configurations.

Syntax Examples

Schemas

Schemas define the shape of your configuration and are enforced at parse time:

schemas/server.wcl
123456789101112131415
schema "server" {
  name         = string
  region       = string
  instance     = string
  replicas     = number @default(1)
  public       = bool   @default(false)

  tags = map(string)

  healthcheck {
    path     = string @default("/health")
    interval = number @default(30)
    timeout  = number @default(5)
  }
}

Using it in a config file gives you immediate validation — misspell a field or pass the wrong type and you get an error before anything runs:

production.wcl
12345678910111213141516171819
import "schemas/server.wcl"

server "api" {
  name     = "api-prod-01"
  region   = "ap-southeast-2"
  instance = "t3.large"
  replicas = 3
  public   = true

  tags = {
    environment = "production"
    team        = "platform"
  }

  healthcheck {
    path     = "/api/health"
    interval = 15
  }
}

Data Tables

Data tables let you define structured tabular data inline — useful for feature flags, environment matrices, or lookup tables:

environments.wcl
123456789
table "environments" {
  columns = ["name", "region", "instance", "replicas", "debug"]

  row { name = "dev"     region = "ap-southeast-2" instance = "t3.small"  replicas = 1 debug = true  }
  row { name = "staging" region = "ap-southeast-2" instance = "t3.medium" replicas = 2 debug = true  }
  row { name = "prod-au" region = "ap-southeast-2" instance = "t3.large"  replicas = 3 debug = false }
  row { name = "prod-us" region = "us-west-2"      instance = "t3.large"  replicas = 3 debug = false }
  row { name = "prod-eu" region = "eu-west-1"      instance = "t3.large"  replicas = 3 debug = false }
}

You can then query this table to extract what you need:

12345
# Get all production environments
prod_envs = query(environments, name startswith "prod")

# Get all environments in a specific region
au_envs = query(environments, region == "ap-southeast-2")

Partial Blocks

Partial blocks let you spread a block’s definition across multiple files using the partial keyword. Each partial with the same type and ID gets merged together into a single block. This is useful for splitting large configs into logical chunks — for example, keeping networking config separate from application config while both contributing to the same server definition:

base.wcl
12345
partial server "web-1" {
  host     = "0.0.0.0"
  port     = 8080
  replicas = 3
}
networking.wcl
12345
partial server "web-1" {
  tls      = true
  cert     = "/etc/ssl/web-1.pem"
  dns_name = "web-1.example.com"
}

These merge into a single server "web-1" block with all six fields. Unlike regular blocks where IDs must be unique per type within a scope, partials are explicitly designed to share an ID — that’s how WCL knows which blocks to merge.

Language Bindings

WCL is written in Rust and most bindings are powered by WASM, meaning the Rust implementation is the only true implementation. Every language gets the exact same parsing, validation, and evaluation behaviour — there’s no drift between bindings. The exceptions are C/C++ and Zig, which link directly against the Rust library instead.

Supported languages:

  • Rust (native)
  • Python (WASM)
  • JavaScript / TypeScript (WASM)
  • Go (WASM)
  • .NET (WASM)
  • Java (WASM)
  • Ruby (WASM)
  • Zig (native lib)
  • C / C++ (native lib)

LSP Support

WCL ships with a full Language Server Protocol implementation, giving you autocompletion, diagnostics, hover info, and go-to-definition in any editor that supports LSP. Because schemas define the exact shape of your config, the LSP can provide full syntax validation for domain-specific blocks used by your application — not just generic config linting, but real validation that understands your app’s structure.

Why Not Just Use…

FormatTrade-off
YAMLImplicit typing, indentation footguns, no schemas without external tools
JSONNo comments, no expressions, verbose
TOMLFalls apart with deep nesting, limited expressiveness
HCLI really liked HCL — WCL is syntactically based on it, but adds everything I found missing: static typing, macros, decorators, data tables, and a query engine, without being tied to Terraform’s ecosystem

WCL is not trying to replace any of these everywhere — it’s built for the cases where you need more structure and safety than they can offer, without reaching for a general-purpose language.