PCL — the language¶
PCL (Punix Configuration Language) is a small, typed, declarative language whose surface deliberately fits in two pages of grammar. It's neither a DSL for one job nor a general-purpose language with packaging on the side — it's a record/inheritance calculus, syntax sugar over the MIXINv2 IR, with one merge rule and no escape hatches.
A 30-second tour¶
# A package module
module Curl {
pname = "curl"
version = "8.5.0"
recipe = "std.autotools"
source = {
type = "url"
url = "https://curl.se/download/curl-8.5.0.tar.xz"
hash = "..."
hashType = "sha256"
}
deps = [Openssl, Zlib]
}
# A service-stack module — same syntax, distinguished by having `backend`
module CurlAPI {
backend = "systemd"
storeModules = ["Curl"]
services = [{
name = "curl-api"
package = "Curl"
binary = "curl"
args = ["--listen", "0.0.0.0:8080"]
dependsOn = []
environment = {
API_TOKEN = { from_env = "API_TOKEN" } # secret
LOG_LEVEL = "info" # literal
}
}]
configFiles = [
{ path = "/etc/curl-api.conf"
content = "verbose = true\n" }
]
}
That's the whole surface for most uses. No imports (PCL programs are whole-tree composed; see Modules). No subtyping. No classes. No type variables. The IR can express more (the MIXINv2 core), but the PCL frontend pins what's reachable to "the smallest set that makes the examples work."
The type system¶
Six base types. Records are structural (Rec<key1: τ1, key2: τ2, …>); equality is string-structural; List<*> unifies any List<τ>. There is no subtyping object. This isn't a simplification to revisit — the finite-tree decidability proof depends on it. (See concepts/eval-realise-seam for why.)
The type checker (punix check) is a single syntax-directed pass over a finite named tree. It cannot diverge. Every error is located:
→ See Reference: error codes E1–E6 for the full surface.
What flows through PCL¶
Every PCL value flows down the seam:
PCL source bytes
│
│ parse → typed AST (located E1–E6)
│ check → ScopeType assigned to every contribution
│ lower → canonical MIXINv2 IR
│
▼
canonical derivation hash → store path
By the time a value reaches the IR it's:
- Canonical: top-level fields sorted by key; list elements ordered;
if/Settingdistributed;SecretRefflattened toIrSecret(kind, name). - Order-independent w.r.t. source: permute the source PCL ⇒ byte-identical IR ⇒ byte-identical canonical derivation ⇒ byte-identical store path. A conformance test ships exactly this property.
What PCL deliberately doesn't have¶
| Why not | |
|---|---|
| Imports | Tree composition. Every module is in scope of every other module within the same punix check/build invocation. See Modules. |
| String concatenation | The frontend has no ++. Compose paths in code (f"{package_path}/bin/{binary}") at the deploy boundary. Keeps the IR simple. |
| Lambdas / higher-order | Decidability anchor. The whole language is first-order over typed records. |
| Subtyping | The decidability proof depends on no <:. Adding it is a feature-procedure violation, not a feature request. |
| First-class type members | Same. |
| Removal | "This contribution never existed" — modelled as data, not as an effect (move © in the feature-decision procedure). |
| IFD (import-from-derivation) | Provably-host (b). Belongs below the seam, as a deploy-time fetch. |
Reading further¶
- Modules — how composition works; whole-tree scope;
module/scenario/option. - Secrets in PCL —
{from_env=…}/{from_file=…}syntax and what the frontend does with them. - Recipes — the
recipe = "std.shell"/"std.autotools"registry; what each recipe expects inrecipeArgs. - Concepts: Eval/realise seam — why PCL is shaped this way.