Skip to content

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

τ ::= Bool | Int | Str | Path | List<τ> | Rec<…> | Setting<τ>

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:

hello.pcl:5:3: error: [E1] type mismatch in 'version': expected Str, got Int

→ 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/Setting distributed; SecretRef flattened to IrSecret(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 in recipeArgs.
  • Concepts: Eval/realise seam: why PCL is shaped this way.