PCL: the language¶
PCL (Punix Configuration Language) is a small, typed, declarative language whose surface deliberately fits in two pages of grammar. It is a record/inheritance calculus, syntax sugar over Punix's IR (src/punix/ir/nodes.py), 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" }
]
}
Most programs use only the forms shown above. PCL has no imports (programs are whole-tree composed; see Modules), no subtyping, no classes, and no type variables. The underlying inheritance calculus is larger; the PCL frontend exposes only "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. 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 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.