# Rank > Deterministic configuration language and bootstrap compiler Complete markdown-oriented Rank documentation bundle for offline reading and LLM ingestion. This file is generated from the Docusaurus docs build and follows the docs ordering used on the website. ## Introduction Rank is a **declarative, deterministic, finite configuration language** for generating structured outputs such as JSON and YAML. Write a program that describes your output. Run it. Get the same result every time. Use Rank when you want typed configuration, reproducible data transforms, provider-backed reads, or explicit mutation plans without hidden runtime state. If you want installation and a first program, continue to [Getting Started](./getting-started.mdx). If you want the mental model first, read [Concepts](./concepts.mdx). For runnable reference programs, browse [Examples](./examples/index.mdx). ## Quick start ```sh npm install -g @rank-lang/cli ``` ```rank // src/main.rank Config = Object { host: string, port: number } config: Config = { host: `api.example.com`, port: 8080, } pub main = || config ``` ```sh rank src/main.rank ``` ```yaml host: api.example.com port: 8080 ``` Use `--format json` when you want JSON on stdout instead: ```sh rank src/main.rank --format json ``` ## Recommended reading order 1. [Getting Started](./getting-started.mdx) — install, first program, first output 2. [Concepts](./concepts.mdx) — design principles: immutability, graph evaluation, determinism, providers 3. [Language reference](./language/values-and-types.mdx) — types, bindings, functions, imports, stdlib 4. [Output and manifests](./language/output.mdx) — single-document output, `std::Emit`, `Emit::manifest(...)`, and `--file-root` 5. [Errors and diagnostics](./language/errors.mdx) — what fails when, and how to read diagnostics 6. [CLI reference](./cli.mdx) — all commands and flags with examples 7. [Faker provider](./faker-provider.mdx) — generating typed fake data 8. [Dependencies](./dependencies.mdx) — reusable packages from local paths, npm registries, and git 9. [Provider authoring](/docs/provider-authoring) — writing and publishing your own provider ## LLM-friendly docs For AI assistants, retrieval pipelines, and offline ingestion, the docs build also publishes: - [llms.txt](https://rank-lang.com/llms.txt) — top-level index of the docs corpus - [llms-full.txt](https://rank-lang.com/llms-full.txt) — one combined markdown-oriented bundle - [llms-examples.txt](https://rank-lang.com/llms-examples.txt) — curated runnable examples bundle, including the S3 Logs task-oriented example For the language specification and implementation internals, see [Spec and plan](./spec-and-plan.mdx). --- ## Getting Started This guide takes you from zero to a working Rank program. By the end you will have installed the compiler, written a `.rank` file, and produced your first output. If you want the language model before you install anything, read [Introduction](./intro.mdx) and [Concepts](./concepts.mdx). After this guide, use the [CLI reference](./cli.mdx) and [Examples](./examples/index.mdx) as your working reference. ## Requirements - Node.js 20 or newer - npm 10 or newer ## Install ```sh npm install -g @rank-lang/cli ``` Verify it worked: ```sh rank --help ``` You should see the command summary printed to your terminal. ## Your first program Create a new project directory with a manifest and a source file: ```sh mkdir hello-rank cd hello-rank ``` Create `rank.toml`: ```toml manifestVersion = 1 [package] name = "hello-rank" version = "0.1.0" source = "src" ``` Create `src/main.rank`: ```rank greeting = `Hello, Rank!` config = { message: greeting, version: 1, } pub main = || config ``` ## Check the program Run the type-checker to validate the program without producing output: ```sh rank check src/main.rank ``` No output means no errors. If there is a problem, the compiler prints a diagnostic with a source location. ## Produce output Run the program and emit YAML, which is the default: ```sh rank src/main.rank ``` You should see: ```yaml message: Hello, Rank! version: 1 ``` If you prefer JSON, pass `--format json`: ```sh rank src/main.rank --format json ``` ```json { "message": "Hello, Rank!", "version": 1 } ``` The primary `rank` command prints YAML for ordinary document results by default and supports `--format json` for JSON output. If you need multiple generated artifacts, return `Emit::manifest(...)` from `pub main` instead. See [Output and manifests](./language/output.mdx). ## Add a type annotation Rank lets you annotate bindings with explicit types. Add a type to `config` in `src/main.rank`: ```rank Config = Object { message: string, version: number, } greeting = `Hello, Rank!` config: Config = { message: greeting, version: 1, } pub main = || config ``` Run `rank check src/main.rank` again — the compiler now validates that `config` matches the `Config` type. Try introducing a type error: ```rank config: Config = { message: greeting, version: `not a number`, } ``` ```sh rank check src/main.rank ``` The compiler reports the mismatch with a source location before any output is produced. ## Read an environment variable Rank reads environment variables through the `Env {}` built-in. Update `src/main.rank`: ```rank SysEnv = Object { APP_ENV: string, ...: string, } env = Env {} config = { message: `Hello, Rank!`, version: 1, env: env.APP_ENV, } pub main = || config ``` Run it with the variable set: ```sh APP_ENV=production rank src/main.rank ``` ```yaml message: Hello, Rank! version: 1 env: production ``` The same command also accepts `--format json` when you need machine-readable stdout. If `APP_ENV` is not set, the compiler reports a missing environment variable diagnostic. ## What's next - [Concepts](./concepts.mdx) — understand the design principles behind Rank - [Language reference](./language/values-and-types.mdx) — bindings, types, functions, imports, and the standard library - [Output and manifests](./language/output.mdx) — single-document output and multi-file generation - [Errors and diagnostics](./language/errors.mdx) — common failure modes and how to interpret them - [CLI reference](./cli.mdx) — all commands and flags - [Faker provider](./faker-provider.mdx) — generate typed fake data --- ## Concepts Rank is a **declarative, deterministic, finite configuration language** for generating structured outputs and explicit emit descriptors. Understanding what Rank deliberately *is not* is just as important as understanding what it is. Read this page when you are deciding whether Rank fits typed configuration, provider-backed workflows, or HTTP handlers with explicit external inputs. For runnable programs, browse [Examples](./examples/index.mdx). For commands, see the [CLI reference](./cli.mdx). For server and provider surfaces, jump to [HTTP Server](./server.mdx), [AWS Provider](./aws-provider.mdx), [Faker Provider](./faker-provider.mdx), and [Provider authoring](/docs/provider-authoring). ## What Rank is Rank is a functional, graph-evaluated language for producing structured data and explicit emit descriptors. You write a program that describes an output — a configuration file, a dataset, a manifest, or a commit-aware result — and Rank evaluates it. When a program returns a top-level `std::Emit` descriptor, the host decides how to realize it. Rank is well-suited for: - generating configuration files (Kubernetes manifests, Terraform inputs, CI pipelines) - producing typed, validated JSON or YAML from a structured description - transforming and combining data from external sources in a reproducible way - modeling explicit provider-backed writes without hiding when commit occurs - enforcing invariants (uniqueness, port ranges, cross-field constraints) at compile time ## What Rank is not Rank is **not a general-purpose programming language**. The following are intentional non-goals: | Not supported | Why | |---|---| | In-place mutation or mutable state | Values are assigned once and never change; provider-backed writes use explicit `std::Mutation` plans instead | | Recursion | All computation must terminate statically | | Unbounded loops | `while`-style control flow does not exist | | Hidden side effects | Every external read and provider write is explicit, declared, and host-admitted | | Turing-completeness | The compiler can fully analyze any valid program | Rank is not a general-purpose application runtime. It produces *data* — configuration files, datasets, manifests, and explicit emit descriptors — that other systems consume. The `rank serve` command is the main exception: it provides a lightweight HTTP server for programs whose serve-time `pub main` accepts `Runtime::ExecutionContext` and returns an `HTTP::Response`, but the handler logic itself is still a declarative Rank function evaluated per request. ## Core concepts ### Immutable bindings Every name in a Rank program is assigned exactly once. There is no reassignment, no mutation, and no mutable state: ```rank host = `api.example.com` port = 8080 // host and port cannot be changed after this point ``` This makes the value of any binding trivially traceable — it is always the expression on the right-hand side of its declaration. ### Graph evaluation Rank builds a **directed acyclic graph** (DAG) of bindings before evaluating anything. Nodes are bindings and expressions; edges are data dependencies. The compiler evaluates nodes in topological order — each value is computed exactly once, after all its inputs are ready. Cycles are compile errors. If `a` depends on `b` and `b` depends on `a`, the program is rejected before evaluation begins. ```rank width = 1920 height = 1080 aspect_ratio = width / height // evaluated after width and height ``` ```mermaid graph LR width["width = 1920"] height["height = 1080"] aspect_ratio["aspect_ratio = width / height"] width --> aspect_ratio height --> aspect_ratio ``` Arrows point from dependency to dependent. `width` and `height` have no incoming edges — they are evaluated first. `aspect_ratio` is evaluated last, once both inputs are ready. ### Determinism Given the same source, the same declared external inputs, and the same compiler version, ordinary Rank evaluation and emit descriptor construction always produce the same values. There is no ambient randomness, no hidden clock access, and no implicit network IO during ordinary compute. When a host realizes `Mutation::commit(...)`, possible write outcomes are still explicit in typed commit results rather than hidden inside ordinary evaluation. This makes Rank programs safe to commit, cache, and reproduce. ### Explicit external inputs Rank can read from the outside world, but every external dependency must be explicitly declared. The supported external input forms are: - **Environment variables** — `Env {}` reads named variables from the host environment once per compilation - **Files** — `File::Read { path, format }` reads a local file at a statically-known path, including `dotenv` files with the same flat text `Decode<...>` support as `Env {}` - **HTTP fetches** — `HTTP::Fetch { ... }` makes a network request with explicit cache/pin semantics - **Providers** — out-of-process extensions registered in `rank.toml` that expose typed reads and writes through a namespace Because every external dependency is declared and every provider write crosses an explicit commit boundary, the compiler can list all dependencies (`rank deps`), and repeated runs with cached inputs remain reproducible. ### Data providers Providers are out-of-process extensions that expose typed operations to Rank programs. They can expose pure helper functions, backend-style reads, and explicit mutation exports. `@rank-lang/plugin-faker` and `@rank-lang/plugin-aws` are first-party examples. Providers are the *only* way to introduce capabilities beyond pure data transformation — and even then, the capability (`network`, for example) must be explicitly declared in the provider manifest and admitted by the host. Providers are not plugins to the compiler. They are separate processes that the compiler spawns and communicates with over a JSON-line stdio protocol. A Rank program that uses a provider is still fully type-checked and deterministic with respect to its declared inputs. ### Explicit mutation boundary Rank does support external writes, but not as mutable state inside the language. - ordinary bindings stay immutable - provider-backed writes are declared as `Mutation::Effect` obligations - fulfilled mutation values may feed later mutation payloads, but they stay commit-local - actual external IO happens only when a host realizes a top-level `Mutation::commit(...)` That split is what lets Rank support writes without turning ordinary evaluation into imperative execution. See [Mutations](./language/mutations.mdx) for the full lifecycle. ### The type system Rank has a structural type system. Types describe the *shape* of values, not their identity. Object types are closed by default: ```rank Config = Object { host: string, port: number, } ``` The language supports generics, including declaration-site constraints (`K extends keyof T`) and trailing defaults (`Box`), plus type unions (`A | B`), type intersections (`A & B`), optional fields (`field?: T`), and open object types (`...: T` for additional fields). Type aliases use leading-uppercase names. Structured text is a first-class part of the language too: `parse` template types let programs treat string formats as types, and the same `parse` / pattern syntax reappears in `match` expressions and route matching instead of pushing string handling out into ad-hoc helper code. ### Compile-time constraints Annotations attach compile-time invariants to declarations and to ordinary object-type fields: ```rank @constraint(cond: |self| self >= 1 && self <= 65535) api_port = 8080 @unique(group: ports) api_port = 8080 @unique(group: ports) db_port = 5432 ServiceConfig = Object { @constraint(cond: |self| self >= 1) replicas: number, @sensitive token?: string, } ``` Constraint failures are compile errors, not runtime errors. Declaration annotations validate a specific binding; field annotations travel with the type and are enforced wherever that type is checked. ### Output model For ordinary command-line evaluation, the entrypoint is the exported `pub main` function. Its return value is what the host emits or realizes. Rank can emit: - **One ordinary document** — bare `rank ` prints the evaluated document result as YAML by default, or as JSON with `--format json` - **A multi-file manifest descriptor** — `Emit::manifest(...)` returns `std::Emit` for later materialization - **A commit-aware descriptor** — `Mutation::commit(...)` returns `std::Emit` with typed write outcomes projected into ordinary output Emitted output preserves evaluated field order. There is no implicit reordering. See [Output and manifests](./language/output.mdx) for the `std::Emit` manifest workflow and `--file-root` behavior, and [Mutations](./language/mutations.mdx) for commit-aware output. ## How Rank relates to other tools | Tool | Relationship | |---|---| | Jsonnet | Similar scope; Rank adds a structural type system and explicit external inputs | | CUE | Similar type model; Rank is evaluation-first rather than constraint-unification-first | | Dhall | Similar determinism goal; Rank is array-oriented and supports providers | | Helm / Kustomize | Rank replaces the templating layer with a typed, validated language | | General scripting (Python, Bash) | Rank handles the data-generation step; scripting handles deployment and orchestration | --- ## Bindings and functions ## Bindings A binding assigns a name to a value. Bindings are immutable — every name is assigned exactly once: ```rank host = `api.example.com` port = 8080 config = { host, port } ``` Bindings may carry an explicit type annotation: ```rank port: number = 8080 ``` Bindings can reference other bindings freely. The compiler builds a dependency graph and evaluates everything in the correct order: ```rank base_port = 3000 api_port = base_port + 1 db_port = base_port + 2 ``` ```mermaid graph LR base_port["base_port = 3000"] api_port["api_port = base_port + 1"] db_port["db_port = base_port + 2"] base_port --> api_port base_port --> db_port ``` ## Entrypoint Every Rank program exports a zero-argument function named `pub main`. Its return value is what gets emitted: ```rank config = { host: `localhost`, port: 8080, } pub main = || config ``` `||` is the zero-argument function syntax. `pub` marks the binding as exported. ## Functions Functions are first-class values. They can be bound to names, passed as arguments, and returned: ```rank increment = |x| x + 1 add = |left, right| left + right ``` Functions with a block body use `return` to yield their result: ```rank clamp = |value, min, max| { clamped = value < min ? min : (value > max ? max : value) return clamped } ``` Typed block-bodied functions declare parameter and return types: ```rank normalize = |port: number, max: number| -> number { ratio = port / max return ratio } ``` Functions are compile-time values. They are used during evaluation but cannot appear in the final emitted output. ## Destructuring Object fields can be destructured directly into bindings: ```rank response = { status: 200, body: `ok` } { status, body } = response ``` This is shallow sugar: each identifier reads the same-named field from the source object. Annotations can also attach to a destructuring declaration, but their behavior depends on the annotation: - `@constraint` receives the full source object as `self` - `@unique` registers each extracted field independently - `@sensitive` marks every extracted binding as sensitive For example, a destructuring constraint can validate a relationship between fields on the source object: ```rank @constraint(cond: |self| self.api_port != self.db_port) { api_port, db_port } = config ``` ## Annotations At declaration level, annotations attach compile-time invariants or diagnostic metadata to the immediately following declaration. `@constraint` runs a boolean check at compile time: ```rank @constraint(cond: |self| self >= 1 && self <= 65535) port = 8080 ``` `@unique` enforces uniqueness across all bindings in the same named group within the program: ```rank @unique(group: ports) api_port = 8080 @unique(group: ports) db_port = 5432 @unique(group: ports) metrics_port = 8080 // compile error: 8080 is already in the `ports` group ``` On a destructuring declaration, `@unique` checks each extracted field independently against the group. `@sensitive` marks a binding as redacted in diagnostics and traces: ```rank @sensitive secret_key = env.SECRET_KEY ``` On a destructuring declaration, all extracted bindings become sensitive: ```rank credentials = { access_key_id: `AKIA...`, secret_access_key: `...`, } @sensitive { access_key_id, secret_access_key } = credentials ``` Ordinary object-type fields can also carry field-level `@constraint`, `@unique`, and `@sensitive` annotations: ```rank ServiceConfig = Object { @unique(group: service_ids) id: string, @constraint(cond: |self| self >= 1 && self <= 65535) port: number, @sensitive token?: string, } ``` Field annotations differ from declaration annotations: - `self` inside a field-level `@constraint` is the field value, not the containing object. - They are valid on ordinary fields in `Object { ... }` and refinement bodies. - They are enforced whenever a concrete value is checked against the annotated type. - Utility-type views such as `Pick`, `Partial`, and `Required` preserve them when the field survives. - Mapped bodies and rest fields do not accept field annotations in the current language surface. Doc comments use stacked `///` lines and attach to the immediately following declaration: ```rank /// The port the HTTP API listens on. /// Must be in the unprivileged range. @constraint(cond: |self| self >= 1024 && self <= 65535) api_port = 8080 ``` Doc comments are preserved in YAML and JSONC output. ## Operators ### Arithmetic `+`, `-`, `*`, `/`, `%` operate on numbers. `+` also concatenates strings: ```rank sum = 1 + 2 label = `port-` + `8080` ``` ### Comparison and equality `<`, `<=`, `>`, `>=` operate on numbers. `==` and `!=` use deep structural equality: ```rank same = { a: 1 } == { a: 1 } // true ``` Comparison and equality operators are non-chainable. ### Boolean `&&`, `||`, `!` are boolean operators: ```rank valid = port >= 1024 && port <= 65535 ``` ### Null defaulting `??` returns the left side unless it is `null`, then evaluates and returns the right: ```rank value = maybe_null ?? `default` ``` ### Ternary `condition ? then_value : else_value` — evaluates only the selected branch: ```rank label = is_prod ? `production` : `development` ``` ### Object and list composition `with` performs a deep recursive merge. Right-hand fields win on conflict: ```rank base = { host: `localhost`, port: 80, tls: false } override = base with { port: 443, tls: true } // { host: `localhost`, port: 443, tls: true } ``` `++` composes two lists or two disjoint objects: ```rank all_ports = [80, 443] ++ [8080, 8443] merged = { a: 1 } ++ { b: 2 } // keys must not overlap ``` ### Pipeline `|>` passes the left value as the first argument to the right function. Useful for chaining transformations: ```rank use std::collections::{ map, filter } result = [1, 2, 3, 4, 5] |> filter(|x| x > 2) |> map(|x| x * 10) // [30, 40, 50] ``` ## Pattern matching `match` is Rank's general pattern-matching form. It is how the language branches on literals, types, object shapes, and structured strings. ### Literal and type patterns `match` dispatches on a value using literal patterns and type patterns: ```rank Mode = `dev` | `prod` mode: Mode = `dev` label = match mode { `dev` => `development`, `prod` => `production`, } ``` Named type patterns narrow the scrutinee type inside the arm: ```rank describe = |value: string | number| match value { string => `it is a string`, number => `it is a number`, _ => `unknown`, } ``` ### Object patterns Named type arms can also destructure object-shaped variants and bind fields for that arm only: ```rank Circle = Object { radius: number, color: string, } Square = Object { side: number, color?: string, } Shape = Circle | Square measure = |shape: Shape| -> number { return match shape { Circle { radius } => radius, Square { side } => side, } } ``` ### `parse` string patterns Structured string patterns can bind pieces of a string with `parse` templates: ```rank sumVersionParts = |version_string: string| -> number { return match version_string { parse`{major:number}-{minor:number}-{patch:number}` => major + minor + patch, _ => 0, } } ``` This is the pattern-matching sibling of `parse` template types. In a type alias, `parse` constrains which strings are valid. In a `match` arm, the same syntax binds decoded captures like `major`, `minor`, and `patch`. The same surface works for irregular structured text too, not just version strings: ```rank readAccessStatus = |line: string| -> string | null { return match line { parse`{clientIp:string} - {remoteUser:string} [{timestamp:string}] "{method:string} {target:string} HTTP/{protocol:string}" {status:string} {bytesSent:string} "{referrer:string}" "{userAgent:string}"` => status, _ => null, } } ``` See the [S3 Logs example](../examples/s3-logs.mdx) for a complete server that parses nginx access lines this way. ### Scope and exhaustiveness Bindings introduced by destructuring or `parse` patterns exist only inside the arm where they are declared. `_` is the wildcard arm. Match is exhaustive — if the compiler can prove the arms do not cover all possible values, it reports an error, and if it cannot prove exhaustiveness you should include `_`. Pattern matching is not an isolated feature. The same structured-text and matching ideas also show up in route definitions such as ``path: match`/users/{id:UserId}``` and in `parse` template types used elsewhere in the language. ## Automatic lifting When a function is applied to a list or object, Rank automatically distributes the call across the elements: ```rank increment = |x: number| x + 1 result = increment([1, 2, 3]) // [2, 3, 4] keyed = increment({ a: 1, b: 2 }) // { a: 2, b: 3 } ``` When both arguments are lists of the same length, the function is applied element-wise (zipped): ```rank add = |left: number, right: number| left + right sums = add([1, 2, 3], [10, 20, 30]) // [11, 22, 33] ``` Lifting is shape-based and applies only to lists and objects. `null` is not a collection shape — functions that receive `null` through lifting must handle it explicitly. --- ## Errors and diagnostics Rank prefers explicit diagnostics over coercions, silent fallbacks, or partial output. Most failures fit into a small set of stages. ## Where errors happen ### Parse and resolution These failures happen before type checking: - syntax errors - unresolved names - invalid imports - module cycles ### Type checking and annotations These failures happen once the compiler can analyze program structure: - type mismatches - invalid collection or object shapes - bad `@constraint`, `@unique`, or `@sensitive` usage - invariant violations such as duplicate `@unique` values Use `rank check ` to catch this entire class without evaluating external inputs. ### Evaluation and external inputs These failures happen only when the program is actually evaluated: - missing or invalid `Env` inputs - `HTTP::Fetch` cache or schema mismatches - provider invocation failures - invalid `Emit::manifest(...)` entries - path normalization and path escape errors Use bare `rank ` when you need to exercise these runtime-like evaluation steps. ### Host workflow errors Some errors are about how the CLI command is used rather than the Rank value itself: - `--file-root` used with a non-manifest result - serve-only entrypoints used with the wrong command - output materialization paths that resolve outside the chosen root ## Common categories - **Type and shape errors** — the value does not match its declared or inferred type. - **Annotation errors** — an annotation is unknown, misplaced, malformed, or its invariant evaluates to false. - **External input errors** — environment variables, HTTP responses, files, or providers do not satisfy the declared schema. - **Output manifest errors** — duplicate paths, unsupported formats, or mismatched payloads such as non-string `text` values in `Emit::manifest(...)` entries. - **Path errors** — invalid relative paths, upward traversal, or boundary violations while applying a manifest. ## Sensitive values Use `@sensitive` on bindings that should be redacted in diagnostics: ```rank @sensitive api_key = Env {}.API_KEY ``` When a diagnostic renders that binding, Rank shows `` instead of the real value. ## Practical workflow 1. Run `rank check ` first for syntax, import, type, and annotation problems. 2. Run `rank ` when you need to validate environment inputs, providers, HTTP fetches, or output manifests. 3. Use `rank test ` when you want to pin either expected output or expected diagnostics in a repeatable suite. ## Related pages - See [Bindings and functions](./bindings.mdx) for `@constraint`, `@unique`, and `@sensitive`. - See [External inputs](./external-inputs.mdx) for `Env`, `HTTP::Fetch`, and `File::Read` behavior. - See [Output and manifests](./output.mdx) for manifest-specific errors. --- ## External inputs Rank can read from the outside world, but every external dependency must be explicitly declared. The compiler refuses to perform ambient IO — every network request, file read, or environment lookup is part of the dependency graph and can be listed with `rank deps`. External inputs are **source nodes** in the DAG — they have no incoming edges. Bindings that read from them depend on those nodes like any other: ```mermaid graph LR env["Env<AppEnv> {}"]:::external file["File::Read<Items> {}"]:::external http["HTTP::Fetch<Response> {}"]:::external port["port = env.PORT ?? '5432'"] total["total = items |> ..."] users["users = http.users"] main["pub main"] env --> port file --> total http --> users port --> main total --> main users --> main classDef external fill:#e8f4fd,stroke:#2196f3 ``` ## Environment variables `Env {}` reads named environment variables from the host. The type parameter is an object type describing which variables to read: ```rank AppEnv = Object { DATABASE_URL: string, PORT?: string, DEBUG?: string, ...: string, } env = Env {} port = env.PORT ?? `5432` ``` The environment is snapshotted exactly once per compilation. Only fields declared in the schema type are materialized into the resulting value. The `...: string` rest type allows additional keys to exist in the environment without being read. Optional fields (`field?: string`) become `null` when the variable is absent. Required fields cause a compile error if absent. When the host value is text but the rest of the program wants a non-string type, decode it at the boundary: ```rank use std::Decode AppEnv = Object { PORT?: Decode<8080 | 8443>, DEBUG?: Decode, ...: string, } env = Env {} port = env.PORT ?? 8080 debug = env.DEBUG ?? false ``` This keeps parsing at the edge instead of matching on raw strings later. See [Standard library](./stdlib#stddecode). ## File reads `File::Read { path, format }` reads a structured file at a relative path: ```rank use std::File use std::Path use std::collections::{ map } use std::list::{ reduce } Items = [Object { id: string, name: string, count: number }] response = File::Read { path: Path::join([`data`, `items.json`]), format: `json`, } items = response.body total = items |> map(|item| item.count) |> reduce(0, |acc, n| acc + n) ``` The result is an object with two fields: | Field | Type | Description | |---|---|---| | `body` | `T` | Parsed, type-checked file contents | | `ctx` | `Object { path: string, format: string }` | Metadata about the read | Supported formats: | Format | Decoded as | |---|---| | `json` | Ordinary Rank lists and objects | | `yaml` | Ordinary Rank lists and objects | | `csv` | List of header-keyed objects with string values | | `dotenv` | Flat string-keyed object, with the same field-local and schema-wide `Decode<...>` support as `Env {}` | `path` must be a `Path::RelativePath` value. It resolves relative to the containing module file. Absolute paths and paths that escape upward (`../`) are rejected. The path must be statically evaluable — it cannot depend on a runtime value. For `.env`-style files, keep parsing at the boundary just like `Env {}`: ```rank use std::Decode use std::File use std::Path Config = Object { APP_ENV: `dev` | `prod`, PORT?: Decode<8080 | 9000>, DEBUG?: Decode, ...: string, } config = File::Read { path: Path::join([`.env`]), format: `dotenv`, } port = config.body.PORT ?? 8080 debug = config.body.DEBUG ?? false ``` Dotenv decoding stays intentionally narrow in v0: - the file decodes as a flat string-keyed object - field-local `Decode` and schema-wide `File::Read>` are supported for the same scalar targets as `Env {}` - one dotenv entry does not decode into a nested list or object value - interpolation and shell-style evaluation are not supported ## HTTP fetches `HTTP::Fetch { ... }` makes a network request and decodes the response body against `T`: ```rank use std::HTTP Response = Object { users: [Object { id: number, name: string }], } response = HTTP::Fetch { url: `https://api.example.com/users`, method: `GET`, headers: { Accept: `application/json`, }, cacheKey: `users-v1`, } users = response.body.users status = response.ctx.status ``` ### Request fields | Field | Required | Description | |---|---|---| | `url` | yes | Request URL (must be a string literal or statically evaluable) | | `method` | yes | HTTP method: `GET`, `POST`, etc. | | `headers` | no | Object of header name → value strings | | `cacheKey` | yes | Stable string key used to replay the response deterministically | ### Result shape The result is an object with two fields: | Field | Type | Description | |---|---|---| | `body` | `T` | Parsed, type-checked response body | | `ctx` | see below | Request metadata | `ctx` fields: | Field | Type | Description | |---|---|---| | `status` | `number` | HTTP status code | | `cacheKey` | `string` | The `cacheKey` value from the request | | `headers` | `Object { ...: string }` | Response headers | ### HTTP snapshot caching The `cacheKey` field is required. It makes fetches deterministic: two runs with the same `cacheKey` and a snapshot file replay identically without a live network request. Save a snapshot after a live run: ```sh rank src/main.rank --write-http-cache snapshots.json ``` Replay from the snapshot on subsequent runs: ```sh rank src/main.rank --http-cache snapshots.json ``` The snapshot cache is a `rank/http-cache` JSON document keyed by each fetch's `cacheKey`. Committing the snapshot to source control makes CI runs fully reproducible without network access. ## Providers Providers are out-of-process extensions registered in `rank.toml`. They expose typed functions under a namespace and can come from a local path or an npm package. Pure reusable library code belongs under `[dependencies]` instead, which may be local, npm-backed, or git-backed. See the [Provider guide](../faker-provider.mdx), [Dependencies](../dependencies.mdx), and [Provider authoring guide](/docs/provider-authoring) for full details. ```rank use faker::{ Generate, UUID } spec = { id: UUID {} } pub main = || Generate { spec, seed: 42, } ``` ### Provider capabilities Providers that need network access or other privileged capabilities must declare them in their manifest. At the call site, the capability must be explicitly opted into: ```sh rank src/main.rank --allow-provider-capability network ``` This makes capability use auditable — a CI script that does not pass `--allow-provider-capability` will fail if the program unexpectedly depends on a network-capable provider. --- ## Modules and imports Rank programs are organized into modules. Each `.rank` source file is a module. Modules import from four distinct namespace sources, and project-local code may also use contextual relative anchors for nearby modules. ## Namespace sources | Prefix | Source | |---|---| | `std::` | Official standard library modules and bundled providers | | `root::` | The current project's source tree | | `package_name::` | Installed third-party packages exposing pure Rank modules from local paths, npm registries, or git snapshots | | `provider_name::` | Registered providers from local paths or npm packages, plus any Rank source modules they ship | ## Import syntax Named imports bring specific bindings into scope: ```rank use std::collections::{ map, filter, reduce } use std::object::{ mapValues, keys } use root::helpers::{ make_url } use super::types::{ Config } use self::internal::{ normalize } ``` Wildcard imports bring all exported bindings into scope: ```rank use std::collections::* use root::util::* ``` Named imports are preferred when only a few bindings are needed. Wildcards are useful for namespaces you use heavily. For project-local modules: - `root::...` is the canonical absolute import form - `self::...` resolves from the current module namespace - `super::...` resolves from the parent namespace and may be chained `self::` and `super::` are contextual anchors only when they appear as the leading segment of a qualified module path. ## Standard library imports ```rank use std::collections::{ map, filter, flatMap, find, any, all, count } use std::collections::{ zip, reduce, groupBy, keyBy, sort, sortBy } use std::list::{ range, flatten, slice, transpose } use std::object::{ mapValues, keys, values, entries, fromEntries } use std::Regex::{ matches, search, replace, captures } use std::Time::{ parse, format, add, diff, minutes } use std::Path::{ join, from, toString } use std::Emit use std::Rand::{ seed, derive, int } ``` See the [Standard library](./stdlib.mdx) page for full API details. ## Project module imports Source files inside a project's `source` directory (declared in `rank.toml`) are accessible under the `root::` prefix using their path relative to that directory: ```text src/ main.rank config/ defaults.rank overrides.rank ``` ```rank // in src/main.rank use root::config::defaults::{ base_port } use root::config::overrides::{ production_host } // in src/config/overrides.rank use super::defaults::{ base_port } // in src/config.rank use self::defaults::{ base_port } ``` ```mermaid graph LR std["std::collections"]:::stdlib defaults["root::config::defaults"]:::module overrides["root::config::overrides"]:::module main["src/main.rank"]:::module std --> main defaults --> main overrides --> main classDef stdlib fill:#f0f4e8,stroke:#4caf50 classDef module fill:#fdf3e8,stroke:#ff9800 ``` The DAG applies across modules too — the compiler resolves the full cross-module dependency graph and rejects cycles. Relative anchors are only for project-local modules. Package, provider, and stdlib imports continue to use their ordinary namespace prefixes. ## Exports and visibility Declarations are private by default. Mark a declaration `pub` to export it: ```rank // in src/config/defaults.rank base_port = 3000 // private — not importable from other modules pub api_port = 8080 // exported pub db_port = 5432 // exported ``` Imported names are not automatically re-exported. Use top-level named `pub use` when you want a module to act as a facade: ```rank // in src/billing.rank pub use self::types::{ Quote } pub use self::pricing::{ quote } // in src/main.rank use root::billing::{ Quote, quote } ``` Named `pub use` re-exports the matching public bindings from the target module. If the same spelling exists in both the value and type namespaces, the same `pub use` surface re-exports both. Wildcard re-exports such as `pub use self::types::*` are not supported. ## Provider imports Provider namespaces are imported the same way as other modules. The provider must be registered in `rank.toml` first (see the [Provider guide](../faker-provider.mdx)): ```rank use faker::{ Generate, UUID, Int } use faker::types::{ Spec } ``` The provider namespace corresponds to the `namespace` field in the provider's `rank.toml`. ## Project manifest Every project needs a `rank.toml` at its root. The `source` field declares the directory that becomes the `root::` module tree: ```toml manifestVersion = 1 [package] name = "my-project" version = "0.1.0" source = "src" ``` To register a provider: ```toml [providers] faker = { path = "./providers/faker" } ``` Dependency aliases in `[dependencies]` can point at local packages, npm-published packages, or git-backed packages. Provider aliases in `[providers]` can point at local providers or npm-published provider packages. For packaging and publishing guidance, see [Dependencies](../dependencies.mdx) and [Provider authoring](/docs/provider-authoring). For the full `rank.toml` schema, including dependencies, registries, security defaults, and provider-package metadata, see the [Manifest Reference](../manifest-reference.mdx). --- ## Mutations `std::Mutation` is the language-level boundary for provider-backed writes. The important rule is that Rank does not execute a provider mutation at the moment you call the effect binding. Instead, mutation work is split into fulfillment and commit so the program can stay deterministic during ordinary evaluation and only perform external I/O when a host explicitly realizes a top-level `Mutation::commit(...)` descriptor. ## Why plan and commit are separate Rank keeps provider mutations explicit for three reasons: - fulfilled mutation values can feed later mutation payloads before anything is committed - hosts must admit provider capabilities and exact mutation export names before any external write runs - commit results need one projection point so the final output can include success, rejection, unknown outcomes, and receipt metadata in an ordinary document shape That is why `Mutation::Effect`, `Mutation::plan(...)`, and `Mutation::commit(...)` are separate surfaces instead of one eager `mutate(...)` function. ## Lifecycle ### 1. Declare an effect binding `Mutation::Effect` marks a required external write whose fulfillment payload has type `T`. The annotated binding must be a direct call to a provider `mutation` export. ```rank use std::Mutation Entry = Object { id: string, label: string, } writeEntry: Mutation::Effect = my_provider::actions::write { table: `entries`, } ``` Values like `table` above are declaration-time configuration. The actual payload for the write is provided later when the effect binding is called. ### 2. Fulfill effects during ordinary evaluation Calling an effect binding fulfills one required write. That call still does not commit provider I/O. It produces a commit-local value that may feed later mutation inputs: ```rank use std::Mutation Entry = Object { id: string, label: string, } TaggedEntry = Object { id: string, tag: string, label: string, } writeEntry: Mutation::Effect = my_provider::actions::write { table: `entries`, } writeTaggedEntry: Mutation::Effect = my_provider::actions::write { table: `entry-tags`, } createdEntry = writeEntry({ id: `1`, label: `alpha`, }) taggedEntry = writeTaggedEntry({ id: createdEntry.id, tag: `fresh`, label: `entry-${createdEntry.label}`, }) ``` The key distinction is: - `createdEntry` is the fulfilled payload value, not provider receipt metadata - that value is commit-local, so it can feed later mutation inputs - it must not escape into ordinary public output or drive control flow such as the conditional `condition ? whenTrue : whenFalse` expression or `match` ### 3. Build a plan `Mutation::plan(...)` collects required operations into one opaque plan descriptor: ```rank use std::Mutation Entry = Object { id: string, active: bool, } writeUser: Mutation::Effect = my_provider::actions::write { table: `users`, } writeAudit: Mutation::Effect = my_provider::actions::write { table: `audit`, } plan = Mutation::plan({ createdUser: writeUser({ id: `1`, active: true }), auditUser: writeAudit({ id: `2`, active: false }), }) ``` Each field label becomes one typed `report.operations.