Modules¶
A PCL module is a named record. The composed root of a program is a record of modules; the IR layer evaluates it via CIA (Conflict-aware Inheritance with Arbitration) and produces, for each field, its winning contribution plus a provenance chain.
Definition¶
A module is module NAME { field = expr … }. Fields are typed by the checker; values can be:
- literals (
"foo",42,true, lists, records); - references to other modules' fields (
Other.field); - conditionals (
if cond then a else b); Setting<τ>(value, priority)for prioritised contributions (see scenarios below);- secret references (
{from_env="X"}or{from_file="P"}).
Cross-module references¶
Modules reference each other's fields by ModuleName.field:
module Caddy {
pname = "caddy"
version = "2.9.1"
…
}
module CaddyService {
package = Caddy # whole record
bin = Caddy.binary # one field — frontend canonicalises
}
The lower-er emits IrRef "Caddy__binary" (modules' fields are flattened into the top-level bindings table under <module>__<field>). The evaluator resolves this through normal substitution — there's no special "module import" mechanism. Cross-service refs are cross-module refs.
Whole-tree composition¶
PCL has no import. Every .pcl file passed to punix check / punix build is composed into one program. Modules from different files share the same flat namespace (<module>__<field>).
punix check pkgs/ # composes every *.pcl file under pkgs/
punix check pkgs/foo.pcl pkgs/bar.pcl # composes both
If two files both declare module Hello, the type checker raises a located [E3] (conflicting definitions). To layer overrides cleanly, use scenarios (below).
Scenarios¶
A scenario is a named delta that augments the base modules. Useful for "dev vs prod," "demo vs real," "force this version everywhere":
module Db {
port = 5432
host = "localhost"
}
scenario Prod {
Db.host = "db.prod.example"
Db.port = Setting<Int>(5433, priority = 100)
}
punix build --scenario Prod evaluates with the delta applied. CIA arbitration handles overlapping contributions: a Setting with higher priority wins; same-priority is a located [E4] (conflict).
Scenarios are first-class — they're part of the canonical derivation. Two builds with different scenarios produce different store paths (because the resolved values differ).
Required slots — option¶
module Database {
option dsn: Str # required, no default
option timeout: Int = 30 # required-with-default
}
option ℓ: τ declares a required slot. If no contribution provides it when the composed root is type-checked, the result is [E3] (required ∅).
What's in scope of what¶
The composed root is one big record. Every module is in scope of every other module within the same punix check. There's no "where to put this file" question — put it anywhere under the directory you pass to punix.
Where in the code¶
src/punix/frontend/parser.py— module/scenario syntax.src/punix/frontend/types.py— ScopeType +[E1]–[E6].src/punix/frontend/lower.py— module flattening into the IR's binding table.src/punix/ir/evaluator.py— CIA arbitration.
Related¶
- Overview — the language at a glance.
- Recipes —
recipe = "std.…"registry. - Concepts: content-addressing — what flows from modules into the canonical derivation.