radlang — Project config (cfg)

cfg is a compiler-injected namespace that exposes your project’s config.yaml to the program. It needs no import — the compiler fills it in from the config at the project root and bakes the values into the binary.

Its purpose is to keep two different notions of “environment” cleanly apart:

  • cfg.env(key)project configuration: the config.yaml env: block (baked into the binary at compile time) overlaid at runtime by a .radenv file. The .radenv layer is not baked — it is read when the program runs, so it is the right home for secrets.
  • sys.env(key) — the real OS environment at runtime, nothing else (see sys.md).

The two are fully separate: cfg.env reads only your project config (never the OS), and sys.env reads only the OS (never your config). The baked config.yaml defaults travel inside the binary, so a standalone build binary still runs with its configured defaults even if no .radenv is present. To let the OS override a config value, combine them explicitly: sys.env("PORT") or cfg.env("PORT").


Members

Project dependency roots are configured under paths.include in config.yaml:

paths:
  include:
    - "../shared-radlang"
    - "../common"

Each included project contributes its lib/*.rlc files and its src/namespace/*.ns.rad sources. The compiler, rtest, and the LSP use the same ordered search path. Paths are resolved relative to config.yaml; earlier entries take precedence, and duplicate roots are canonicalized.

All members read as strings.

cfg.name           // project.name    from config.yaml  ("" if unset)
cfg.version        // project.version from config.yaml  ("" if unset)
cfg.profile        // "release" for `radlang build`, "debug" for `radlang run`
cfg.buildDir       // paths.build      (default "output")
cfg.srcDir         // paths.src        (default "src")
cfg.staticDir      // paths.static     (default "public"; see server.useStatic)
cfg.libDir         // paths.lib        (resolved path, "" if disabled)
cfg.testsDir       // paths.tests      ("" if unset)
cfg.namespaceDir   // paths.namespace  (default "namespace")

cfg.env

cfg.env(key: string) : string

Returns the configured value for key, or the empty string if there is no such key. Resolution is project-config only:

  1. The runtime .radenv value, if a .radenv provides that key (see below).
  2. Otherwise the value baked from config.yaml’s env: block.
  3. Otherwise "".

It does not read the OS environment — that’s sys.env’s job. When you want a deploy-time override, ask for it explicitly:

fn main() {
    string port = sys.env("PORT")            // OS override, if set
    if port == "" { port = cfg.env("PORT") } // else the config default
    HttpServer server = httpserver.serve(Int(port))
    sys.output("{cfg.name} v{cfg.version} on port {port}")
    server.listen()
}

Because the config keys are known at compile time, a literal key that isn’t in your config env is caught as a compiler warning (not an error, so config and code can be edited in either order) — cfg.env("PROT") when only PORT exists warns rather than silently returning "". Keys computed at runtime (a variable) are not checked.


config.yaml

project:
  name: "radlang_api"
  version: "1.0.0"
  cores: 4            # optional: HTTP prefork worker count (omit = CPU count)

paths:
  build: "output"
  lib: "lib"
  tests: "src/test"
  namespace: "src/namespace"

env:
  PORT: 8085
  MODE: "dev"

project.cores sets how many worker processes an HttpServer.listen() preforks across CPU cores. It is baked into the compiled binary. If omitted (or 0) the server uses the machine’s online CPU count; cores: 1 disables prefork (classic single process). The RAD_HTTP_WORKERS environment variable overrides it at runtime for a given launch. Values are capped at 256.

Per-profile values

An env: key may hold per-build-profile values instead of a single one, so cfg.env returns something different for radlang build (release) vs radlang run (debug) — resolved at compile time, still baked into the binary:

env:
  PORT: 8085                         # plain: same for every profile
  DB:
    debug:   "sqlite://dev.db"
    release: "mysql://prod-host/app"
  API_BASE:
    release: "https://api.example.com"
    default: "http://localhost:9000" # used by any profile without its own entry
  LOG:
    debug: "verbose"                 # release has no LOG → cfg.env("LOG") is ""

Resolution for the active profile: its own variant → default: if present → otherwise the key is omitted and cfg.env(key) returns "". A key defined this way still counts as declared, so cfg.env("LOG") never triggers the unknown-key warning even when it resolves to "". .radenv keys override the whole config key regardless of profile.

.radenv — runtime secrets

A .radenv file supplies environment values at run time — its contents are not baked into the binary, so it is the right place for secrets (passwords, API keys, tokens). Its keys override the baked config.yaml env: values.

# .radenv — runtime secrets, gitignored
DB_PASSWORD=s3cr3t
API_TOKEN=abc123
MODE=local

Because .radenv is read at run time, secrets never enter the compiled artifact — strings on the binary cannot recover them. The baked config.yaml env: block stays inside the binary as the default; .radenv overlays it. So the split is:

Put it in…WhenBaked into binary?
config.yaml env:Non-secret defaults (ports, modes, base URLs)Yes (compile time)
.radenvSecrets and per-deploy overridesNo (read at run time)

Keep .radenv out of version control (the api template’s .gitignore already ignores it).

Discovery. At the first cfg.env call the program loads one .radenv, in this order: the path in $RADLANG_ENV_FILE if set, else ./.radenv in the working directory, else .radenv next to the executable. Format: KEY=value per line, # comments, blank lines ignored; one pair of surrounding double quotes on a value is stripped (K="a b"a b).

The OS environment is never consulted by cfg.env — read it separately via sys.env. (sys.env remains the option for secrets you’d rather not place in a file at all, e.g. injected by a container orchestrator.)


cfg vs sys.env

NeedUse
A value declared in config.yaml / .radenvcfg.env("KEY")
A project path or name/versioncfg.buildDir, cfg.name, …
A value from the real OS environment onlysys.env("KEY")

cfg is a reserved name — you cannot declare a variable, function, or type called cfg.