This is the abridged developer documentation for codegen # codegen > A scriptable meta-programming engine for modern C++. **codegen** is a production-grade C++ code generation engine. You annotate C++ declarations directly in source — structs, classes, enums, free functions, aliases — and each rule declares in its YAML which AST node kinds it consumes. You write a LuaU script that receives a structured AST node as JSON and emits whatever you want — C++, TypeScript, Markdown, SQL. The engine handles the rest: parsing, scheduling, deduplication, inline injection, and file routing. It ships as a single binary. It calls nothing home. It runs the same on every machine. ## How codegen is built [Section titled “How codegen is built”](#how-codegen-is-built) codegen separates the problem into two clean layers: 1. **Analysis** — handled by the engine. The full C++ AST is resolved and serialised to JSON (namespaces, template arguments, member variables, enumerators, annotations), and on top of it the engine builds a real semantic model: a symbol database, class hierarchy, dependency graphs, and resolved types. 2. **Transformation** — handled by you, in LuaU. Your rule receives one AST node as JSON and returns output text — and can query the semantic model on demand (resolve a field’s type to its declaring symbol, look up references, walk base classes). The rule has no compiler coupling, no filesystem access, and no network access. This split is what makes everything below possible. *** ## Core capabilities [Section titled “Core capabilities”](#core-capabilities) Deterministic output Same source ⇒ same output. Always. The engine is a pure function over your AST. Generated files are stable across machines and CI runs, making diffs meaningful and code review tractable. Non-1:1 generation Most generators emit one output file per input file. codegen does not require this. A `grouping.luau` script routes each matched entity to any output path — fan-in dozens of structs into a single `api-reference.md`, a `registry.cpp`, or a sorted `bindings.ts`. The topology is yours to define. Cross-language generation Rules emit text. The engine does not care whether that text is C++, TypeScript, Markdown, SQL, or a JSON manifest. One annotated C++ type can drive a serialiser, an FFI binding, a documentation page, and a database migration — all from the same source of truth. LLM-ready AST schema The C++ AST is fully serialised as JSON before any rule sees it. Rules are testable without a compiler, inspectable with `jq`, and the full docs are also published as a single plain-text bundle at [`/llms-full.txt`](/llms-full.txt) — drop it into any LLM agent for codegen-aware code intelligence. Semantic-aware rules Rules see more than syntax. A [semantic query API](/concepts/semantic-query/) resolves a field’s type to its declaring symbol, pivots into that type’s own AST, and looks up references, base classes, and members — the same analysis that powers the LSP and MCP servers. Generate against what your types *mean*, not just how they’re spelled. Workspace-cache acceleration codegen connects to the [workspace daemon](/integrations/cli/) on every run — there’s no flag to opt in or out. AST subtrees and semantic queries are served from the shared workspace intelligence cache, and types resolve against a whole-workspace snapshot for accurate cross-header resolution. The daemon must already be serving `--workspace-dir` (defaults to the current directory). Rules testable in isolation Feed a JSON blob to a rule, assert on the output. No compiler, no project, no fixtures. The same property that makes rules LLM-friendly makes them unit-testable in seconds. Isolated execution LuaU runs in a sandboxed VM. Rules have no filesystem access and no process spawning — neither is grantable. Network access is the one escape hatch, and only for hosts you allowlist in the config. A malicious rule in a third-party library cannot exfiltrate your source code. Inline injection Rules can emit a `source` (to a `.g.cpp` file) and an `inline` list — declarations injected back into the original header at a designated anchor comment. Bare anchors are rewritten into idempotent `:begin]] / :end]]` blocks on first run. Self-hosted codegen generates parts of its own infrastructure. We use it ourselves to build it, so every feature you rely on has already been exercised against a 100k+ LoC real-world codebase. *** ## Licensing [Section titled “Licensing”](#licensing) **codegen is free.** There are no tiers and no feature gates — the TUI diff viewer, the VS Code DAP debugger, and shared `.codegen/shared/` libraries are all available to everyone, on every install. # The AST Schema > The JSON structure handler scripts receive, fields, node kinds, and how to navigate namespace and type information. Every handler invocation receives one entity AST node, JSON-encoded. Call `json.decode(input)` to get a Lua table. This page describes the fields available on the most common node kinds. The full per-kind schema is auto-generated and available under [AST Schemas](/reference/schemas/core/). ## What the engine adds before the handler sees the JSON [Section titled “What the engine adds before the handler sees the JSON”](#what-the-engine-adds-before-the-handler-sees-the-json) The engine takes the codex `toJSON` output and: 1. Removes “heavy” fields the handler rarely needs: `memberFunctions`, `staticMemberFunctions`, `constructors`, `destructors`, `operators`, `nestedTypes`, `statements`. Use the [native introspection API](/reference/luau-globals/) (`get_node`, `get_base_classes`, etc.) to retrieve them on demand. 2. Adds `_namespaces` (array of namespace strings, outermost first). 3. Adds `_registryId` (an opaque integer; pass it to `get_node`, `get_base_classes`, etc.). 4. Adds `params` (a copy of `rule.params` from the config). ## Struct / class node [Section titled “Struct / class node”](#struct--class-node) ```json { "kind": "Struct", "identifier": { "name": "ConnectionOptions", "templateArgs": [] }, "_namespaces": ["network", "internal"], "memberVariables": [ /* see below */ ], "attributes": [ { "ns": "codegen", "name": "MarkdownDocs", "args": [] } ] } ``` | Field | Type | Description | | ------------------------- | --------- | ----------------------------------------------------------------------- | | `kind` | string | `"Struct"`, `"Class"`, or `"Union"` | | `identifier.name` | string | Unqualified type name | | `identifier.templateArgs` | array | Template parameter / argument nodes (if templated) | | `_namespaces` | string\[] | Enclosing namespace stack, outermost first | | `memberVariables` | array | Member variable nodes (see below) | | `attributes` | array | C++ attributes on this declaration. Each entry has `ns`, `name`, `args` | ### Building the qualified name [Section titled “Building the qualified name”](#building-the-qualified-name) ```lua local ns = table.concat(node._namespaces, "::") local qualified = ns ~= "" and (ns .. "::" .. node.identifier.name) or node.identifier.name -- result: "network::internal::ConnectionOptions" ``` ## Enum node [Section titled “Enum node”](#enum-node) `enum` declarations with a body have `kind = "EnumeratorSpecifier"` (the codex `NodeKind::EnumSpecifier` stringified): ```json { "kind": "EnumeratorSpecifier", "identifier": { "name": "Color" }, "_namespaces": [], "isScoped": true, "isForwardDeclaration": false, "enumerators": [ { "name": "Red", "value": null }, { "name": "Green", "value": null }, { "name": "Blue", "value": "2" } ] } ``` | Field | Type | Description | | ---------------------- | -------------- | ---------------------------------------------------- | | `isScoped` | bool | `true` for `enum class`, `false` for unscoped `enum` | | `isForwardDeclaration` | bool | `true` for declarations without a body | | `enumerators` | array | Enumerator nodes | | `enumerators[].name` | string | Enumerator identifier | | `enumerators[].value` | string \| null | Explicit value as source text, or `null` | Forward declarations have `kind = "Enumerator"` (codex `NodeKind::Enum`). Anchor-triggered handlers also receive the appropriate kind via the same payload. ## Member variable node [Section titled “Member variable node”](#member-variable-node) ```json { "kind": "Variable", "identifier": { "name": "port" }, "typeSignature": { "identifier": { "name": "uint16_t", "templateArgs": [] }, "isConst": false, "isPointer": false, "isReference": false } } ``` For groups of variables sharing a type (`int x, y, z;`), the kind is `"VariableGroup"` with a `variables` array. Always handle both: ```lua for _, var in ipairs(node.memberVariables or {}) do if var.kind == "Variable" then process(var) elseif var.kind == "VariableGroup" then for _, v in ipairs(var.variables or {}) do process(v) end end end ``` ## Type signatures [Section titled “Type signatures”](#type-signatures) ```lua local function typeName(tSig) if not tSig or not tSig.identifier then return "?" end local name = tSig.identifier.name local args = tSig.identifier.templateArgs or {} -- e.g. name = "vector", args = [{ identifier = { name = "string" }, ... }] return name end ``` Template arguments are themselves type-signature-shaped — navigate them recursively. ## Inspecting a header before writing a rule [Section titled “Inspecting a header before writing a rule”](#inspecting-a-header-before-writing-a-rule) Use the `ast-dump` tool to dump any header in the codex JSON form: ```sh ast-dump -m codex -i include/color.hpp ``` The shape printed is the same the handler decodes, minus the `_namespaces`, `_registryId`, and `params` synthetic keys, and minus the heavy fields the engine strips. Key Takeaways * Structs/classes/unions carry `_namespaces` (outermost-first), `memberVariables`, and `attributes` (note: `attributes`, the C++ term, not `annotations`). * Enum declarations with a body have `kind = "EnumeratorSpecifier"`; forward declarations have `kind = "Enumerator"`. * Member variables have a `typeSignature`; templates recurse via `identifier.templateArgs`. * Heavy fields are stripped before the handler sees the JSON; recover them with the native introspection API. * Use `ast-dump -m codex -i ` to preview the exact JSON your handler will receive. # Grouping & Fan-in > How grouping.luau scripts control output file routing, the feature that enables non-1:1 generation. The `grouping.luau` script is the most powerful part of the codegen rule system. It controls the mapping from matched C++ entities to output files. Understanding it is the key to unlocking codegen’s full capability. ## The default: 1:1 routing [Section titled “The default: 1:1 routing”](#the-default-11-routing) Without a grouping script, each entity produces output written to a file derived from its input header’s path. One header → one output file. For simple cases (enum-to-string functions, for example) this is the right behaviour: the `ToString` rule produces `color.h` → `color.g.cpp` next to the input. ## The problem with 1:1 [Section titled “The problem with 1:1”](#the-problem-with-11) Consider generating API documentation. You have 40 structs spread across 15 headers. The 1:1 policy produces 15 separate Markdown files. But you want one `api-reference.md`. Or a serialization registry: one `registry.cpp` calling `register()` for every annotated type. Or a TypeScript bindings manifest: one `bindings.ts`, one `export interface` per C++ struct, sorted alphabetically. These are **fan-in** patterns. Many entities, one output file. The inverse — **fan-out**, one entity into multiple files — is also possible by combining grouping with multi-anchor `inline` patches. ## How grouping.luau fits the lifecycle [Section titled “How grouping.luau fits the lifecycle”](#how-groupingluau-fits-the-lifecycle) Grouping runs **before** handlers, as the second phase of entity routing. It takes all collected candidate entities and returns a map overriding their default output paths: .codegen/rules/MyRule/grouping.luau ```lua return function(input) local data = json.decode(input) local result = {} for _, entity in ipairs(data.entities) do result[tostring(entity.registryId)] = "generated/docs/api-reference.md" end return json.encode(result) end ``` Each entity exposes only: `registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath` (the 1:1 output path derived from the input header), plus the rule’s `params` at the top level. There is **no** full AST node or attribute list at this stage — if you need that for routing, do the work in the handler and stuff the result into the `includes` or use the `params` channel. The engine then: 1. Looks up each entity’s new path in the returned map. 2. Resolves it against the project root (CWD); rejects escapes with `E005`. 3. Groups entities by output path for later assembly. 4. Calls the handler with the post-grouping output path (available as `_outputPath` in the handler). 5. Runs the preamble once per unique (rule, outputFile) pair, passing the entities bound to that file. 6. Concatenates each entity’s `source` into the assembled file. ## Fan-in: many structs, one file [Section titled “Fan-in: many structs, one file”](#fan-in-many-structs-one-file) The `MarkdownDocs` example demonstrates this pattern. Every struct annotated `[[codegen::MarkdownDocs]]`, regardless of which header it lives in, is routed to a single `api-reference.md`: grouping.luau ```lua return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local OUTPUT = "generated/docs/api-reference.md" local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = OUTPUT end return json.encode(result) end ``` The preamble runs once for the rule; its result lands at the top of `api-reference.md` exactly once thanks to per-output-path content deduplication. Each entity’s handler-rendered `source` follows in match order. ## Fan-out: one entity, multiple files [Section titled “Fan-out: one entity, multiple files”](#fan-out-one-entity-multiple-files) Today’s grouping API maps each entity to **one** primary path. To split content across multiple files for a single entity, the cleanest pattern is: * Use `source` for the primary file (e.g. the implementation `.cpp`). * Use `inline` to inject declarations back into the original header at an anchor. Routing the same entity to two distinct generated files isn’t directly expressible in one `grouping.luau` invocation; the typical workaround is to define two rules and annotate the entity with both attributes. ## Conditional routing [Section titled “Conditional routing”](#conditional-routing) Grouping only sees summary metadata. Routing decisions you can express directly: ```lua for _, entity in ipairs(data.entities) do local isInternal = entity.inputFile:find("internal/") ~= nil local output = isInternal and "generated/internal-api.md" or "generated/public-api.md" result[tostring(entity.registryId)] = output end ``` Routing decisions that need full AST data (attribute arguments, base classes, member shape) need to happen in the **handler**, with the conclusion baked into a `params` value or the rendered `source`, then read out in grouping. ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) Within an output file, entities appear in match order: the order their input headers were enumerated, then declaration order within each header. To impose a different order (alphabetical, for example), sort the entity list inside grouping before assigning paths — assembly visits entities in the post-grouping order. Key Takeaways * Without `grouping.luau`, routing is 1:1 (one entity → one file derived from the input path). * Grouping runs **before** handlers, with summary metadata only (no full AST node, no handler output). * Output is a ` → path` map; missing keys skip the entity (`E004`). * Paths must stay under the project root (CWD); escapes are rejected with `E005`. * The preamble dedup happens at assembly time per output path, so fan-in produces a single header. # How It Works > A top-down walkthrough of the codegen pipeline, from annotated C++ source to generated output files. ## The pipeline [Section titled “The pipeline”](#the-pipeline) ``` graph TD Start([Annotated C++ headers]) --> RL RL[Rule Loader] -- "Load config + scripts + shared/" --> AS AS[Anchor Scan] -- "Index '// [[codegen::generated::...]]' in inputs" --> P P[Workspace Daemon] -- "nodesWithAttribute + getAstSubtrees over the local socket" --> EM EM[Entity Match] -- "Match attribute by node_kinds + collect anchor entities" --> H H[Handler] -- "Run transform.luau per matched entity → source / inline / includes" --> G G[Grouping] -- "If grouping.luau: rewrite each entity's output path" --> CD CD[Collision Check] -- "Non-cpp paths claimed by 2+ rules → drop" --> PR PR[Preamble Run] -- "Run preamble.luau once per (rule, output file)" --> A A[Assemble & Write] -- "banner + preamble (deduped) + self-include + project includes + sources" --> IE IE[Inline Edits] -- "Replace anchor blocks in source headers" --> End([Output Files]) style Start fill:#f9f,stroke:#333,stroke-width:2px style End fill:#f9f,stroke:#333,stroke-width:2px ``` ## Key design decisions [Section titled “Key design decisions”](#key-design-decisions) **The AST is serialised to JSON before scripts see it.** Handlers receive a JSON-encoded entity payload (the codex AST node, with heavy fields stripped, plus synthetic `_namespaces`, `_registryId`, `_outputPath`, plus a `params` key copied from the rule config). This means: * Scripts are pure functions: JSON in, JSON out. * Scripts can be tested with a static fixture, no compiler required. * The full docs are also published as a single plain-text bundle at [`/llms-full.txt`](/llms-full.txt) for LLM agents and offline reference. **Each rule gets its own LuaU execution context.** Each call (preamble, handler, grouping) executes in a fresh execution; nothing carries between invocations. Rules cannot reach each other, the filesystem, or the network unless explicitly granted via `permissions:`. **Grouping runs before handlers.** Grouping rewrites each candidate’s output path using summary metadata (`registryId`, `qualifiedName`, `namespaces`, `inputFile`, `defaultPath`) — the handler hasn’t run yet, so no `source` text is available. The handler then runs with the post-grouping route exposed as `_outputPath`, so it can specialise on the file it’s about to land in. **Preamble runs once per `(rule, outputFile)` pair.** It receives the entities bound to that file (with their handler-emitted `includes`) so the preamble can specialise on what’s actually needed. The result is deduplicated by content when the engine assembles each output file: if two rules contribute identical preambles to the same path, the text appears once. **Inline injection rewrites anchor blocks in place.** Bare anchors written by hand are expanded on first run into one paired `:begin]]/:end]]` block per `inline` item (N items ⇒ N consecutive blocks). Later runs grow or shrink the run in place to match the current item count. ## What the analyzer resolves [Section titled “What the analyzer resolves”](#what-the-analyzer-resolves) Before any rule runs, the engine builds a real semantic model over your source — not just a syntax tree. It indexes a symbol database, resolves a class hierarchy, and builds include and module dependency graphs. On top of that it computes deeper facts — per-expression types, resolved type-to-symbol bindings, references, and (on debug-info builds) byte-level memory layout — the same analysis that powers the LSP and MCP surfaces. Rules reach all of it through two complementary surfaces: the syntactic AST node handed to each handler as JSON (see [The AST Schema](/concepts/ast-schema/)), and the [semantic query API](/concepts/semantic-query/) — a set of native functions that resolve a traversed node’s **type** to its declaring symbol, pivot into that type’s own declaration node, and look up symbol summaries, references, base classes, and members. Both surfaces speak JSON, so rules stay pure functions over JSON while still having the full semantic model a query away. Workspace analysis is unconditional: every run connects to the workspace daemon for the project root, and the deepest facts (types, references) come from that whole-workspace snapshot. ## What the engine does not do [Section titled “What the engine does not do”](#what-the-engine-does-not-do) * It does not instantiate templates. A rule sees the template as written; written template arguments are extracted syntactically, but no specialization is synthesised and an uninstantiated generic resolves to a `dependent` type. * It does not fold `constexpr`. Constant values are represented as their source text; the engine does not evaluate them. * It does not do conversion-ranking overload resolution. Overloads are indexed distinctly, but the engine will not pick the single best-viable candidate for a call site the way a compiler front-end does. * The codegen CLI does not spawn its own daemon. It always connects to the persistent ADR-033 workspace daemon (the same one behind the LSP and MCP servers) for the `--workspace-dir` root; if nothing is already serving that root, the run fails rather than falling back to a self-contained parse. Key Takeaways * Pipeline: load → anchor-scan → parse + analyze → collect candidates → **grouping → handler → preamble** → assemble → inline-edits. * Handlers are pure functions over a JSON AST node. No side effects, no shared state. Receive `_outputPath` (post-grouping route). * Grouping is path-only and runs **before** handlers; it sees summary metadata + `defaultPath`, not handler output. * Preamble runs once per `(rule, outputFile)` pair with the bound entities; the engine dedupes by content per output file at assembly time. # Preamble System > How preamble.luau scripts emit file headers, includes, license banners, and namespace wrappers — and how they're deduplicated when multiple rules share an output file. The preamble script (`preamble.luau`) is required for every rule. It runs **once per (rule, outputFile) pair** — that is, once per unique output file a rule contributes to. The engine deduplicates its result by content per output file at assembly time. It is the right place for: * `#include` directives common to all of the rule’s outputs to this file * License or auto-generated banners specific to this rule (the engine already emits its own banner separately) * Namespace opening brackets * Any text that must appear at the top of the file regardless of which entities land there ## Signature [Section titled “Signature”](#signature) ```lua return function(input) -- input: JSON-encoded context object, decode if you need it return "text to prepend to the file" end ``` The input context is: ```json { "language": "cpp", "outputFile": "path/to/output.cpp", "ruleName": "", "params": { /* rule.params from RuleName/config.yaml */ }, "entities": [ { "registryId": 42, "qualifiedName": "myns::MyStruct", "structName": "MyStruct", "namespaces": ["myns"], "inputFile": "include/myns/my_struct.hpp", "includes": [""] } ] } ``` `outputFile` is the post-grouping output path for this (rule, file) pair. The `entities` array contains every entity bound to this output file, with each entity’s handler-emitted `includes` exposed for conditional preamble logic. This lets you, for example, include `` in the preamble only if any entity in the output file needed a vector. The return value is a raw string — not JSON. Whatever you return is buffered on the rule. ## Example: C++ includes [Section titled “Example: C++ includes”](#example-c-includes) .codegen/rules/MyRule/preamble.luau ```lua return function(input) return "#include \n#include \n\n" end ``` ## Example: Markdown document header [Section titled “Example: Markdown document header”](#example-markdown-document-header) preamble.luau ```lua return function(input) return "# API Reference\n\n" .. "> This file is auto-generated. Do not edit manually.\n\n" .. "---\n\n" end ``` ## Deduplication at assembly [Section titled “Deduplication at assembly”](#deduplication-at-assembly) The engine collects all preamble results from the rules contributing entities to a single output file, deduplicates by **content** (string equality), and emits each unique result once. Two rules that produce identical preamble strings collapse to a single emission; two rules that produce different strings each contribute one block. This is what makes fan-in clean: dozens of entities from one rule routed into `api-reference.md` produce exactly one `# API Reference` header. It also means: preambles don’t have visibility into full entity data. They run after grouping and after the handler, but only ever see the summary fields listed above (plus each entity’s handler-emitted `includes`) — never the full AST node or attribute list. If you find yourself wanting to conditionalise preamble content on entity data (“only include this header if the entity has template parameters”), that’s a signal to emit the include from the handler’s `includes` field instead — the engine will deduplicate those across the file too. ## Preamble vs handler [Section titled “Preamble vs handler”](#preamble-vs-handler) | Belongs in the preamble | Belongs in the handler | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Banner / file-level comment | Per-entity content | | Always-on `#include` directives, or conditional on the `entities` list | `#include` directives specific to a single entity (use `includes` in the return) | | Opening namespace bracket | The actual code/declaration text (`source`, `inline`) | ## Errors [Section titled “Errors”](#errors) A preamble that calls `error()` (or hits a runtime error) aborts the **whole** run with `E007` (exit 1). Unlike handler errors (which skip a single entity), there’s no recovery — if a preamble fails for one output file, the entire codegen run is unsafe. Key Takeaways * The preamble is required (`E003` if missing); use `return function() return "" end` if there’s nothing to emit. * Runs once per (rule, outputFile) pair; results are deduplicated by content per output file at assembly time. * The input context is `{language, outputFile, ruleName, params, entities}`; entities lists all matches bound to this output file. * Use `entities` to conditionally emit includes (e.g., include `` only if any entity has a vector member). * Use `includes` from the handler for entity-conditional includes; the engine deduplicates those too. * Preamble errors abort the whole run. # Rule Lifecycle > The sequence of script invocations for a single rule run, in the order the engine actually executes them. ## Phase 1: Rule load [Section titled “Phase 1: Rule load”](#phase-1-rule-load) The engine resolves each `-a ` to `//` or `///` and reads: handler, preamble, optional grouping, optional `.env`, and `config.yaml` (with `extends:` chasing as needed). It also loads `/../shared/*.luau` (alphabetical by filename, concatenated) as the flat prelude, reads `/../shared//*.luau` as [components](/rules/anatomy/#components) — sources only; nothing runs until a script requires one — and `/../shared/.env` as the baseline dotenv. CLI overrides (`-P`, `--env`, `--allow-http`, `--allow-env`) are folded in. ## Phase 2: Anchor scan + AST build [Section titled “Phase 2: Anchor scan + AST build”](#phase-2-anchor-scan--ast-build) The engine scans every input file (under `-i`) for `// [[codegen::generated::::]]` markers (bare or `:begin]]`/`:end]]` form) and indexes them by `(rule, qualifiedName)`. It does not parse anything itself: the AST comes from the workspace daemon, which serves candidate nodes (`nodesWithAttribute`) and their subtrees (`getAstSubtrees`) over a local socket. ## Phase 3: Entity match [Section titled “Phase 3: Entity match”](#phase-3-entity-match) The engine walks each parsed source. For every node whose kind is in the rule’s `node_kinds` (default: `Struct`), it checks for matching `[[codegen::]]` attributes and queues a match. It then merges in entities referenced only by anchor (no attribute) so they can also fire the handler. ## Phase 4: Collect candidates [Section titled “Phase 4: Collect candidates”](#phase-4-collect-candidates) For each rule, the engine collects all matching entities (from Phase 3). The entities have default output paths derived from their input headers (1:1 routing). ## Phase 5: Grouping [Section titled “Phase 5: Grouping”](#phase-5-grouping) For each rule that ships a `grouping.luau`, the engine calls it **once per invocation, with all candidate entities**: ```plaintext input: JSON { params: , entities: [{ registryId, qualifiedName, structName, namespaces, inputFile, defaultPath }, ...] } output: JSON { "": "path/to/output.cpp", ... } ``` For each returned key, the engine resolves the path against the project root (CWD) and updates that entity’s output path. Paths escaping the project root emit `E005` and skip the entity. Entities missing from the map emit `E004` and skip. A grouping error emits `E008` and the rule’s entities keep their default 1:1 paths. ## Phase 6: Handler [Section titled “Phase 6: Handler”](#phase-6-handler) For each matched entity (with updated output path if grouping fired), the engine calls `transform.luau` once: ```plaintext input: JSON the entity payload (codex AST node with heavy fields stripped, plus _namespaces and _registryId and _outputPath, plus params) output: JSON { source = "...", inline = [{source = "..."}, ...], includes = ["..."] } ``` The `inline` and `includes` keys are optional. A handler `error()` skips this entity and emits `E006`; the run continues. A handler internal/VM error is fatal (exit 2). ## Phase 7: Preamble [Section titled “Phase 7: Preamble”](#phase-7-preamble) For each rule, the engine runs `preamble.luau` **once per unique output file** (the pair of rule + outputPath): ```plaintext input: JSON { language: "cpp", outputFile: "path/to/output.cpp", ruleName: "", params: , entities: [{registryId, qualifiedName, structName, namespaces, inputFile, includes}, ...] } output: string (raw text, stored for assembly) ``` Each entity in the `entities` array is a matching entity bound to this output file, with its handler-emitted `includes` available for conditional preamble logic. The result is held until output assembly. A preamble error aborts the whole run with `E007`. ## Phase 9: Collision check (non-cpp only) [Section titled “Phase 9: Collision check (non-cpp only)”](#phase-9-collision-check-non-cpp-only) The engine groups entities by output path. For non-cpp languages, a path claimed by two or more rules is rejected — the conflicting entities are dropped and the engine prints `error: output path '

' claimed by rules ...`. C++ outputs are exempt by design: stitching multiple rules’ contributions into one `.g.cpp` is intended. ## Phase 10: Assemble & write [Section titled “Phase 10: Assemble & write”](#phase-10-assemble--write) For each output path: 1. Banner: `// Generated with Codegen - DO NOT EDIT!!!` 2. For C++ outputs whose extension is a header (`.hpp` / `.h` / `.hxx` / `.hh`): `#pragma once`. 3. Each contributing rule’s preamble result (one per rule), deduplicated by content. 4. For C++ outputs: a self-include of the input header (rebased when the output is relocated), followed by the union of `includes` from all contributing entities. When `autoDeduceIncludes: true`, project includes already covered by the input header’s transitive include graph are filtered out. 5. Each entity’s `source`, in match order. Files are written via a temp-file-and-rename pattern. Under `--dry-run`, contents are collected and emitted as a unified diff (zero context) to stdout — suitable for CI and LLM pipelines. With `--dry-run --show-tui`, the FTXUI viewer opens instead; accepting in the viewer writes the files. `--show-tui` without `--dry-run` is silently ignored. ## Phase 11: Inline edits [Section titled “Phase 11: Inline edits”](#phase-11-inline-edits) For each entity that returned `inline` items, the engine rewrites the source header at the matching anchor (bare or paired form). Bare anchors are expanded to the paired form on first run. See [Inline Injection](/rules/inline-injection/). Under `--dry-run`, edits are collected (not applied) and shown alongside the assembly previews. ## Error handling summary [Section titled “Error handling summary”](#error-handling-summary) | Failure | Effect | | ------------------------------------------- | ----------------------------------------------------------------- | | Bad CLI / missing input / rule load failure | Exit 1 (with the matching `E0xx`). | | Grouping error | Skip grouping for that rule, fall back to 1:1 paths, emit `E008`. | | Handler `error()` | Skip the entity, emit `E006`, continue. | | Handler internal/VM error | Exit 2 (`E006`, fatal). | | Preamble error | Exit 1 (`E007`). The whole run aborts. | | Inline anchor missing | Plain stderr warning, continue. | Key Takeaways * Order: rule load → anchor scan + parse → match → **collect candidates → grouping → handler → preamble** (per output file) → assemble → inline edits. * Candidates are collected before grouping; grouping overwrites default output paths. Handler fires with post-grouping paths and emits `_outputPath`. * Preamble runs once per (rule, outputFile) pair and sees entities bound to that file. Handler runs once per matched entity. * Grouping errors fall back to default routing. Handler errors skip the entity. Preamble errors abort the whole run. * C++ outputs from multiple rules merge into one file; non-cpp collisions are rejected. # Semantic Queries > Resolve a node's type to a symbol, pivot into its declaration, and query symbols, references, and relations from a LuaU rule — with the JSON model each function returns. The [AST Schema](/concepts/ast-schema/) describes the syntactic node each handler receives. The **semantic query API** goes further: it resolves what those syntactic names actually *refer to*. From a traversed node you can resolve its **type** to a symbol, pivot into that type’s own declaration node, and look up symbol summaries, references, base classes, and members. These functions are the LuaU projection of the engine’s L2 semantic model (the same one behind the LSP and MCP servers), returned as JSON. ## Two ID conventions [Section titled “Two ID conventions”](#two-id-conventions) The API mixes two kinds of handle — keep them straight: | Handle | Type | Where it comes from | | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Node ID** | `int` | The opaque registry integer — the `_registryId` on your payload, or the result of a lookup (`find_struct_by_name`, `type_node_of`, `get_base_classes[].id`). Feed it to `get_node`, `type_of`, etc. | | **Symbol ID** | `string` | A content hash. It crosses the boundary as a **decimal string** because a 64-bit hash would lose precision as a LuaU number. Pass it back verbatim; never coerce it to a number. | Functions whose signature returns `json` hand back a JSON **string** — call `json.decode` on it before use. Type facts and the workspace daemon Every codegen run connects to the workspace daemon for the project root, so type resolution (`type_of`, `type_symbol_of`, `type_node_of`), `references`, and symbol lookups (`find_symbols`, `symbol_summary`, `base_classes`, `members`) all resolve against the same whole-workspace semantic snapshot (cross-header types included). Every function returns `nil` when it has no answer. ## Resolving a node’s type [Section titled “Resolving a node’s type”](#resolving-a-nodes-type) Three functions turn a node into type information. Given a member-variable node’s registry ID: ```lua local varId = var._registryId -- or registry id from a lookup local typeSym = type_symbol_of(varId) -- "8391045..." (symbol id string) or nil local typeNode = type_node_of(varId) -- 57 (registry node id of the type's decl) or nil local resolved = type_of(varId) -- json string of a ResolvedType, or nil ``` * **`type_symbol_of(nodeId) -> string | nil`** — the symbol ID of the node’s *type*. A variable or field resolves to its declared type; an expression to its inferred type; a type declaration (struct/class/enum/…) resolves to *itself*. `nil` for builtins (`int`, `bool`), dependent/unresolved types, and nodes that don’t bear a type. * **`type_node_of(nodeId) -> int | nil`** — the registry node ID of that type’s **declaration**, so you can pivot straight from a field into the AST of its type and introspect it with `get_node`, `struct_has_method`, etc. `nil` when the type has no user-declared symbol. * **`type_of(nodeId) -> json | nil`** — the full `ResolvedType`. ### `ResolvedType` [Section titled “ResolvedType”](#resolvedtype) ```json { "state": "Known", "type": { "identifier": { "name": "ConnectionOptions", "templateArgs": [] }, "isConst": false, "isPointer": false, "isReference": false }, "typeSymbol": "8391045221338712004" } ``` | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | string | `"Known"`, `"Unknown"`, or `"Dependent"` (an uninstantiated template parameter resolves to `"Dependent"`). | | `type` | object \| null | The resolved [type signature](/concepts/ast-schema/#type-signatures) — same shape as a syntactic `typeSignature`. `null` unless `state` is `"Known"`. | | `typeSymbol` | string \| null | Symbol ID of the type this names, when it names a user-declared type known to the database. `null` for builtins, dependent/unknown types, or when no matching symbol exists — the same emptiness as `type_symbol_of` returning `nil`. | ### Pivoting into the type’s declaration [Section titled “Pivoting into the type’s declaration”](#pivoting-into-the-types-declaration) ```lua -- For a member variable, find whether its type is a known struct with a `serialize` method. local typeNode = type_node_of(var._registryId) if typeNode then if struct_has_method(typeNode, "serialize") then -- the field's type declares serialize(...) — emit a delegating call end -- or inspect the whole thing: local decl = json.decode(get_node(typeNode)) end ``` ## Symbol summaries [Section titled “Symbol summaries”](#symbol-summaries) **`symbol_summary(symbolId) -> json | nil`** returns a flat, projection-ready description of a global symbol. ```json { "id": "8391045221338712004", "kind": "Struct", "name": "ConnectionOptions", "qualifiedName": "network::internal::ConnectionOptions", "signature": "struct ConnectionOptions", "location": { "file": "include/network/options.hpp", "startLine": 12, "startColumn": 8, "endLine": 20, "endColumn": 1 }, "access": "none", "moduleName": "", "isModuleExported": false, "isForwardDeclaration": false, "parent": "0" } ``` | Field | Type | Description | | ---------------------- | ------ | --------------------------------------------------------------------------------------------------- | | `id` | string | This symbol’s ID. | | `kind` | string | Symbol kind (see below). | | `name` | string | Unqualified name. | | `qualifiedName` | string | Fully-qualified name. | | `signature` | string | Human-readable declaration signature. | | `location` | object | Declaration site: `file`, `startLine`, `startColumn`, `endLine`, `endColumn`. | | `access` | string | `public` \| `protected` \| `private` \| `none` (member access; `none` for namespace-scope symbols). | | `moduleName` | string | Owning C++20 module, or `""`. | | `isModuleExported` | bool | `true` if `export`ed from a module. | | `isForwardDeclaration` | bool | `true` if this summary is of a forward declaration. | | `parent` | string | Symbol ID of the enclosing symbol, or `"0"` (the invalid ID) at namespace scope. | `kind` is one of: `Namespace`, `Class`, `Struct`, `Union`, `Enum`, `EnumValue`, `Function`, `Constructor`, `Destructor`, `Operator`, `Variable`, `StructuredBinding`, `Concept`, `Typedef`, `TypeAlias`, `Template`, `Module`, `Unknown`. ## Finding symbols [Section titled “Finding symbols”](#finding-symbols) **`find_symbols(qualifiedName) -> json`** returns a JSON array of symbol-ID strings matching a qualified name (all overloads). ```lua local ids = json.decode(find_symbols("network::internal::ConnectionOptions")) for _, id in ipairs(ids) do local s = json.decode(symbol_summary(id)) -- s.qualifiedName, s.kind, s.location, ... end ``` The array is empty (`[]`) when nothing matches. Each element is a string you pass to `symbol_summary`, `references`, `base_classes`, or `members`. ## References [Section titled “References”](#references) **`references(symbolId) -> json | nil`** returns every occurrence of a symbol — declarations, definitions, and use sites — sorted by location. ```json [ { "file": "include/network/options.hpp", "line": 12, "column": 8, "kind": "Definition", "isLocal": false }, { "file": "src/client.cpp", "line": 44, "column": 17, "kind": "Use", "isLocal": false } ] ``` | Field | Type | Description | | --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | string | Path of the occurrence. | | `line` | int | 1-based line. | | `column` | int | 1-based column. | | `kind` | string | `ForwardDeclaration` \| `Declaration` \| `Definition` \| `Use` \| `Comment` (a `Comment` occurrence is the doc comment attached to the declaration). | | `isLocal` | bool | `true` for a function-local symbol’s occurrence; `false` for a global symbol’s. | ## Relations [Section titled “Relations”](#relations) Two functions walk the symbol graph directly. Both return a JSON array of symbol-ID strings (empty when there are none). * **`base_classes(symbolId) -> json | nil`** — direct base classes of a class/struct symbol. * **`members(symbolId) -> json | nil`** — direct members (fields, methods, nested types) of a symbol. ```lua -- Walk a type's direct bases and summarise each. local baseIds = json.decode(base_classes(typeSym) or "[]") for _, id in ipairs(baseIds) do local base = json.decode(symbol_summary(id)) -- base.qualifiedName, base.kind, ... end ``` For the syntactic base list (with access specifiers, as written on the declaration) use [`get_base_classes`](/reference/luau-globals/#native-introspection-api) on the node instead; use `base_classes` when you already hold a symbol ID and want the resolved graph. ## A worked example [Section titled “A worked example”](#a-worked-example) ```lua -- For every member variable of the entity, resolve its type and, when the type is a -- user struct that declares serialize(), delegate to it; otherwise write the field raw. return function(input) local node = json.decode(input) local lines = {} for _, var in ipairs(node.memberVariables or {}) do if var.kind == "Variable" then local typeNode = type_node_of(var._registryId) if typeNode and struct_has_method(typeNode, "serialize") then table.insert(lines, var.identifier.name .. ".serialize(out);") else table.insert(lines, "out << " .. var.identifier.name .. ";") end end end return json.encode({ source = table.concat(lines, "\n") }) end ``` Key Takeaways * Node IDs are integers (registry handles); symbol IDs are **decimal strings** — never coerce them to numbers. * `type_symbol_of` gives a node’s type as a symbol; `type_node_of` pivots into that type’s declaration node; `type_of` returns the full `ResolvedType` (`state` + `type` signature + `typeSymbol`). * `symbol_summary`, `find_symbols`, `references`, `base_classes`, and `members` cover symbol lookup, occurrences, and the class graph. `json`-returning functions hand back a string — `json.decode` it. * Builtins, dependent, and unresolved types return `nil` / `null` — an incompatible node stays empty, never guesses. * All of it — type/reference facts and symbol lookups alike — resolves against the whole-workspace snapshot the daemon serves; there’s no reduced mode. # Markdown Docs: N structs ⇒ 1 file > The MarkdownDocs example demonstrates fan-in grouping, consolidating annotated structs from any number of headers into a single API reference document. The `MarkdownDocs` example is the clearest demonstration of codegen’s grouping capability. It consolidates every annotated C++ struct, regardless of which header it lives in, into a single `api-reference.md`. No post-processing. No shell script. The engine handles the fan-in. ## What it produces [Section titled “What it produces”](#what-it-produces) Given a codebase with annotated structs across multiple headers, the rule produces one file: generated/docs/api-reference.md ```markdown // Generated with Codegen - DO NOT EDIT!!! # API Reference > This file is auto-generated. Do not edit manually. --- ## `network::ConnectionOptions` | Field | Type | |---|---| | `host` | `string` | | `port` | `uint16_t` | | `timeout_ms` | `uint32_t` | --- ## `storage::CacheConfig` | Field | Type | |---|---| | `maxEntries` | `size_t` | | `evictionPolicy` | `EvictionPolicy` | --- ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner is emitted by the engine. The `# API Reference` header and the auto-generated notice come from the rule’s preamble, which runs once per `(rule, outputFile)` pair and is deduplicated by content at output assembly. Each struct contributes one section, in match order. ## Annotating your structs [Section titled “Annotating your structs”](#annotating-your-structs) include/network/connection.hpp ```cpp struct [[codegen::MarkdownDocs]] ConnectionOptions { std::string host; uint16_t port; uint32_t timeout_ms; }; ``` include/storage/cache.hpp ```cpp struct [[codegen::MarkdownDocs]] CacheConfig { size_t maxEntries; EvictionPolicy evictionPolicy; }; ``` Both headers, different directories. The engine finds both; the grouping script routes both to the same output file. ## The rule files [Section titled “The rule files”](#the-rule-files) ### `config.yaml` [Section titled “config.yaml”](#configyaml) config.yaml ```yaml version: 1 output: language: markdown # No outputDirectory or outputNameTemplate - grouping.luau controls all paths. ``` Because grouping handles all routing, the config does not set `outputDirectory` or `outputNameTemplate`. ### `grouping.luau` [Section titled “grouping.luau”](#groupingluau) grouping.luau ```lua -- Routes ALL matched structs into a single consolidated output file. -- Input file topology is irrelevant; structs from any header end up here. return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local OUTPUT = "generated/docs/api-reference.md" local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = OUTPUT end return json.encode(result) end ``` Every entity, one output path. The path is resolved against the project root (CWD) — escapes are rejected with `E005`. Each entity passed to grouping has only the summary fields `registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath`. ### `preamble.luau` [Section titled “preamble.luau”](#preambleluau) preamble.luau ```lua -- Emitted once per (rule, outputFile) pair. Deduplicated by content at output assembly. return function(input) return "# API Reference\n\n" .. "> This file is auto-generated. Do not edit manually.\n\n" .. "---\n\n" end ``` ### `transform.luau` [Section titled “transform.luau”](#transformluau) transform.luau ```lua -- Generates a Markdown section for each annotated struct. -- grouping.luau consolidates all sections into a single api-reference.md. return function(input) local node = json.decode(input) if not node then error("failed to decode input") end local structName = node.identifier and node.identifier.name if not structName or structName == "" then error("struct has no name") end -- Build the fully-qualified name using namespace context local namespaces = node._namespaces or {} local nsPrefix = table.concat(namespaces, "::") local qualifiedName = nsPrefix ~= "" and (nsPrefix .. "::" .. structName) or structName -- Build field table rows local rows = {} local function typeName(tSig) if not tSig or not tSig.identifier then return "?" end return tSig.identifier.name or "?" end local function processVar(varNode) local fname = varNode.identifier and varNode.identifier.name or varNode.name if not fname then return end local t = typeName(varNode.typeSignature) fname = fname:gsub("|", "\\|") t = t:gsub("|", "\\|") table.insert(rows, "| `" .. fname .. "` | `" .. t .. "` |") end for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then processVar(varNode) elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do processVar(v) end end end -- Assemble the section local lines = { "## `" .. qualifiedName .. "`", "" } if #rows > 0 then table.insert(lines, "| Field | Type |") table.insert(lines, "|---|---|") for _, row in ipairs(rows) do table.insert(lines, row) end else table.insert(lines, "_No public member variables._") end table.insert(lines, "") table.insert(lines, "---") return json.encode({ source = table.concat(lines, "\n") }) end ``` ## Running the rule [Section titled “Running the rule”](#running-the-rule) ```sh codegen -i ./include -r .codegen/rules -a MarkdownDocs ``` 1. The engine walks `./include`, finds all headers. 2. The codex pipeline parses + analyzes each header. 3. Each struct annotated `[[codegen::MarkdownDocs]]` matches; the handler runs and emits a `## ...` section. 4. `grouping.luau` rewrites every entity’s output path to `generated/docs/api-reference.md`. 5. The engine assembles the file: banner + preamble (deduplicated) + each entity’s section in match order. 6. The file is written via temp-and-rename. ## Extending this example [Section titled “Extending this example”](#extending-this-example) **Add descriptions.** Accept a string attribute argument: `[[codegen::MarkdownDocs("Optional per-entity description")]]`. The handler reads it via `node.attributes[i]` (look for `attr.ns == "codegen"` and `attr.name == "MarkdownDocs"`, then `attr.args[1]`). **Sort alphabetically.** Inside `grouping.luau`, sort `data.entities` by `entity.qualifiedName` before assigning paths. Output assembly visits entities in the post-grouping order. **Multi-section output.** Route internal entities to `internal-api.md` and public entities to `public-api.md`: ```lua local isInternal = ent.inputFile:find("internal/") ~= nil result[tostring(ent.registryId)] = isInternal and "generated/internal-api.md" or "generated/public-api.md" ``` Key Takeaways * This example demonstrates **fan-in**: N annotated structs from any number of headers ⇒ 1 output file. * The `grouping.luau` script is the only change needed to switch from 1:1 to fan-in routing. * The preamble runs once per (rule, outputFile) pair; the engine deduplicates the result by content per output file. * Grouping sees only summary fields (`registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath`); grouping runs before handlers, so handler output is unavailable. For AST-level routing decisions, look entities up via `get_node()` in the handler. * Paths in grouping must stay under the project root (CWD). # ToString: enum ⇒ switch > The built-in ToString rule generates a std::string_view toString() switch statement for every annotated C++ enum. The `ToString` example is the canonical demonstration of anchor-based triggering and inline injection. It generates a `std::string_view toString(EnumType e)` function for any enum you annotate, placing the implementation in a `.g.cpp` next to the input header and injecting the declaration back into the header. ## What it produces [Section titled “What it produces”](#what-it-produces) Given (write-by-hand form, before the first run): include/color.hpp (before generation) ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color]] } // namespace app ``` The third anchor segment is the **qualified name** of the target entity — namespace path included. Above, `app::Color` because the enum lives in `namespace app`. There is no `qualified` modifier; the path is just the qualified name. After running: ```sh codegen -i include/color.hpp -r .codegen/rules -a ToString ``` The header becomes: include/color.hpp (after generation) ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color:begin]] std::string_view toString(app::Color e); // [[codegen::generated::ToString::app::Color:end]] } // namespace app ``` The bare anchor was expanded into the paired `:begin]] / :end]]` form. Subsequent runs replace only the content between the markers. A sibling `.g.cpp` is produced next to the input header: include/color.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "color.hpp" std::string_view toString(app::Color e) { switch (e) { case app::Color::Red: return "Red"; case app::Color::Green: return "Green"; case app::Color::Blue: return "Blue"; default: return ""; } } ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner, the `#include "color.hpp"` self-include, and any project includes are emitted by the engine; the rule’s preamble (`#include `) is added on top. ## The rule scripts [Section titled “The rule scripts”](#the-rule-scripts) The shipping example lives at `clis/codegen/examples/ToString/`. Key behaviours: * **Preamble** emits `#include `. * **Handler** decodes the entity, reads `node.identifier.name`, `node._namespaces`, `node.isScoped`, and `node.enumerators[]`. It builds one `case` per enumerator, wraps it in a switch, and returns `{ source = impl, inline = { { source = decl } } }`. * **`isScoped`** controls whether case values use the `EnumName::` prefix. Scoped enums (`enum class`) need it; unscoped enums don’t. * **No grouping script.** Each enum’s output goes to a `.g.cpp` next to its input header. The example’s enum is anchor-triggered (no `[[codegen::ToString]]` attribute on the enum itself), because C++ does not allow attributes on enum declarations the same way as on structs. The anchor doubles as the inline-injection site for the declaration. **Note:** No grouping script. Grouping is optional and omitting it is safe; each entity routes to its default 1:1 path derived from the input header. ## Extending this rule [Section titled “Extending this rule”](#extending-this-rule) **Add a reverse `fromString`.** Return a second item in the `inline` list **at a separate anchor** (one anchor per `::`), or extend the handler so its `source` emits both `toString` and `fromString` functions. **Handle explicit values.** `node.enumerators[].value` carries explicit values as source-text strings (`null` when unset). Use it to detect sparse enums. **Namespace-bucketed output.** Add a grouping script that routes by `entity.namespaces[1]` to e.g. `generated//strings.g.cpp`. Key Takeaways * Anchor comments (`// [[codegen::generated::Rule::Qualified]]`) trigger the rule and mark the inline-injection site. * The third anchor segment is the qualified name; `app::Color`, `Outer::Inner::E`, etc. * `node.isScoped` distinguishes `enum class` from plain `enum` for the case-value prefix. * No grouping script ⇒ output goes next to the input header (`color.hpp` → `color.g.cpp`). * The first run rewrites bare anchors into paired `:begin]] / :end]]` blocks; later runs are idempotent. # TypeScript Types: struct ⇒ interface > The built-in TypeScriptTypes rule generates TypeScript interface declarations from annotated C++ structs, with recursive type mapping for generics. The `TypeScriptTypes` rule demonstrates attribute-based triggering and recursive type mapping. It converts C++ structs to TypeScript `export interface` declarations, handling primitives, `std::vector`, `std::optional`, `std::shared_ptr`, and `std::map` recursively. ## What it produces [Section titled “What it produces”](#what-it-produces) include/api\_types.hpp ```cpp struct [[codegen::TypeScriptTypes]] UserProfile { std::string username; uint32_t age; std::vector roles; std::optional bio; }; ``` By default the output lands at `api_types.TypeScriptTypes.ts` next to the input header (the non-cpp default filename pattern is `.`). To centralise the output, set `outputDirectory` and/or `outputNameTemplate` in the rule config. api\_types.TypeScriptTypes.ts ```ts // Generated with Codegen - DO NOT EDIT!!! // Auto-generated TypeScript types for rule: TypeScriptTypes // Source: export interface UserProfile { username: string; age: number; roles: Array; bio: string | undefined; } ``` The `// Sources:` lines in the example’s preamble iterate `ctx.entities` to list the contributing input files. Each entity in `entities` has its `inputFile` available, so the preamble can list the sources that contributed to this output file. Run: ```sh codegen -i include/api_types.hpp -r .codegen/rules -a TypeScriptTypes ``` ## Type mapping [Section titled “Type mapping”](#type-mapping) | C++ type | TypeScript type | | ---------------------------------------------------------------------------- | ------------------------------------------------------ | | `bool` | `boolean` | | `int`, `int8_t`…`int64_t`, `uint8_t`…`uint64_t`, `float`, `double`, `size_t` | `number` | | `std::string`, `string`, `path` | `string` | | `std::vector` | `Array` | | `std::optional` | `T \| undefined` | | `std::shared_ptr` | `T \| null` | | `std::map`, `std::unordered_map` | `Record` | | Anything else | Used as-is (assumed to be a TypeScript type reference) | The mapping is implemented as a recursive `cppToTs(tSig)` function in `transform.luau`. Adding new mappings means editing only that function. ## Extending the type map [Section titled “Extending the type map”](#extending-the-type-map) Open `transform.luau` and add entries to the `primitives` table or new `if name == "..."` branches for generics: ```lua -- Add std::set -> Set if name == "set" then local args = tSig.identifier.templateArgs or {} if args[1] then return "Set<" .. cppToTs(args[1]) .. ">" end return "Set" end ``` ## Fan-in variant [Section titled “Fan-in variant”](#fan-in-variant) To consolidate every TypeScript interface into a single `types.ts`, add a grouping script and (optionally) override the output path: grouping.luau ```lua return function(input) local data = json.decode(input) local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = "generated/types.ts" end return json.encode(result) end ``` The output path is resolved against the project root (the working directory). Key Takeaways * Attribute annotation (`[[codegen::TypeScriptTypes]]`) triggers struct-level rules; no anchor needed. * Default non-cpp output: `.` next to the input. Override with `outputDirectory` / `outputNameTemplate`. * Type mapping is a recursive LuaU function; extend it by adding branches. * Adding `grouping.luau` switches from 1:1 to fan-in routing. * Preamble runs once per (rule, outputFile) pair and receives the `entities` bound to that file, so it can conditionally emit content based on what entities land there. # Your First Rule > Write a rule from scratch — a struct-to-JSON-serializer generator. This guide writes a new rule, `JsonSerializer`, that generates a `toJson()` function for each annotated struct. It covers the three script files every rule needs and the config that ties them together. ## Goal [Section titled “Goal”](#goal) Given: include/config.hpp ```cpp struct [[codegen::JsonSerializer]] AppConfig { std::string host; uint16_t port; bool enableTls; }; ``` Produce, alongside the header, `include/config.g.cpp`: include/config.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "config.hpp" nlohmann::json toJson(const AppConfig& v) { return { {"host", v.host}, {"port", v.port}, {"enableTls", v.enableTls}, }; } ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner and the `#include "config.hpp"` self-include are emitted by the engine. The rule supplies the `` include (via the preamble) and the function body. ## Rule file layout [Section titled “Rule file layout”](#rule-file-layout) No `grouping.luau` — this rule uses 1:1 routing (one struct ⇒ one `.g.cpp` next to the input). ## Step 1: config [Section titled “Step 1: config”](#step-1-config) config.yaml ```yaml version: 1 output: language: cpp autoDeduceIncludes: false ``` `autoDeduceIncludes: false` means the engine will emit every `includes` entry the rule returns verbatim, without filtering against the input header’s transitive include graph. We rely on the preamble for the JSON include, so there’s nothing to filter anyway. ## Step 2: preamble [Section titled “Step 2: preamble”](#step-2-preamble) preamble.luau ```lua return function(input) return "#include \n\n" end ``` The preamble runs **once per (rule, outputFile) pair**. Its result is deduplicated by content per output file at assembly time. With 1:1 routing, each output file ends up with one preamble emission of `#include `. ## Step 3: handler [Section titled “Step 3: handler”](#step-3-handler) transform.luau ```lua return function(input) local node = json.decode(input) if not node then error("failed to decode input") end local structName = node.identifier and node.identifier.name if not structName then error("struct has no name") end -- Collect field initializer-list entries local entries = {} for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then local fname = varNode.identifier and varNode.identifier.name if fname then table.insert(entries, ' {"' .. fname .. '", v.' .. fname .. '},') end elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do local fname = v.identifier and v.identifier.name if fname then table.insert(entries, ' {"' .. fname .. '", v.' .. fname .. '},') end end end end local body = table.concat(entries, "\n") local impl = "nlohmann::json toJson(const " .. structName .. "& v) {\n" .. " return {\n" .. body .. "\n" .. " };\n" .. "}" local decl = "nlohmann::json toJson(const " .. structName .. "& v);" return json.encode({ source = impl, inline = { { source = decl } } }) end ``` The `inline` list injects each item as its own `:begin]]/:end]]` block at an anchor site in the original header (if you’ve placed one). The qualified-name segment of the anchor is built from `node._namespaces` plus the struct name; an anchor like `// [[codegen::generated::JsonSerializer::AppConfig]]` will match this struct in the global namespace. With no anchor present in the header, the engine emits diagnostic `W003` and skips the inline injection — the `.g.cpp` is still written. The `VariableGroup` branch covers `int x, y, z;` declarations. ## Step 4: run [Section titled “Step 4: run”](#step-4-run) ```sh codegen -i ./include -r .codegen/rules -a JsonSerializer ``` The output lands at `include/config.g.cpp`. Key Takeaways * A rule needs three files: `config.yaml`, `preamble.luau` (required, can return `""`), and the handler `transform.luau`. * The handler receives one entity as JSON and returns `{ source, inline?, includes? }`. * `source` lands in the output file (default: next to the input header). `inline` items are injected at matching anchor comments. * No `grouping.luau` ⇒ 1:1 routing; one annotated entity ⇒ one output file derived from the input. * The LuaU sandbox provides `json`, the [native introspection API](/reference/luau-globals/), and a stdlib subset; opt-in to HTTP/env via `permissions:`. # Installation > Install the CodeXX DTDK Manager — the single entry point that installs, updates, and verifies codegen and every other DTDK component. ## How CodeXX DTDK is installed [Section titled “How CodeXX DTDK is installed”](#how-codexx-dtdk-is-installed) codegen ships as one component of the **CodeXX Dev-Tool Development Kit (DTDK)**. You don’t download `codegen` directly. You install the **DTDK Manager** once — a small terminal app — and use it to install, update, and verify codegen and every other component. codegen is free and needs no license activation; the manager still handles that step for the DTDK components that require one. The manager keeps each component in a versioned, side-by-side layout under `~/.codexx` and exposes one binary per tool on your `PATH`. No `sudo`, no system package manager, no admin rights. ## Install [Section titled “Install”](#install) One command downloads the latest stable manager, verifies its checksum, extracts it under `~/.codexx`, and adds `~/.codexx/bin` to your shell profile. No GitHub account or token required. Linux Windows macOS ```sh curl -fsSL https://get.codexx-dtdk.com/install.sh | bash ``` Open a new shell afterwards so the `PATH` change takes effect. To also verify the release signature, append `-s -- --verify` (requires `cosign` on `PATH`). ```powershell iex ((New-Object System.Net.WebClient).DownloadString('https://get.codexx-dtdk.com/install.ps1')) ``` Installs under `%USERPROFILE%\.codexx` and prepends `%USERPROFILE%\.codexx\bin` to your user `PATH`. Open a new shell afterwards. macOS builds are temporarily paused while the release pipeline is validated on Apple hardware. Until macOS archives ship, build from source on macOS — see the project `README` — or run the Linux build in an x86-64 environment. The installer fetches the manager through a download proxy that holds a read-only token server-side — the releases live in a private repository, but you never need credentials. To pin a specific version or install a pre-release channel, download `install.sh` / `install.ps1` and pass `--tag` or `--channel`; those paths read the private repo directly and require a GitHub token. ## Inspect the installer before running it [Section titled “Inspect the installer before running it”](#inspect-the-installer-before-running-it) Don’t want to pipe a remote script straight into a shell? Download it, read it, then run it. Linux Windows ```sh curl -fSL https://get.codexx-dtdk.com/install.sh -o install.sh less install.sh # read it chmod +x install.sh ./install.sh --help # list every flag ./install.sh # default install (stable, ~/.codexx, PATH) ./install.sh --verify # additionally cosign-verify the archive ``` ```powershell Invoke-WebRequest https://get.codexx-dtdk.com/install.ps1 -OutFile install.ps1 Get-Content .\install.ps1 # read it Get-Help .\install.ps1 -Detailed # list every flag .\install.ps1 # default install .\install.ps1 -Verify # additionally cosign-verify the archive ``` This is the same artifact the proxy serves to `curl | bash` / `iex` — saving it to disk just lets you audit it first. ## Manual archive download [Section titled “Manual archive download”](#manual-archive-download) Prefer to inspect the archive before running anything? Download it directly and set things up yourself. Linux (x64) Windows (x64) [Download DTDK Manager — Linux x64](https://get.codexx-dtdk.com/api/download?platform=linux\&arch=x64) Extract it into your install root and add its `bin/` to your `PATH`: ```sh mkdir -p ~/.codexx tar -xzf codexx_dtdk_manager-*-linux-*.tar.gz -C ~/.codexx echo 'export PATH="$HOME/.codexx/bin:$PATH"' >> ~/.bashrc export PATH="$HOME/.codexx/bin:$PATH" ``` [Download DTDK Manager — Windows x64](https://get.codexx-dtdk.com/api/download?platform=windows\&arch=x64) Extract the archive into `%USERPROFILE%\.codexx` and add `%USERPROFILE%\.codexx\bin` to your user `PATH`: ```powershell $root = "$env:USERPROFILE\.codexx" Expand-Archive codexx_dtdk_manager-*-windows-*.zip -DestinationPath $root -Force $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') [Environment]::SetEnvironmentVariable('Path', "$root\bin;$userPath", 'User') ``` Each archive carries a `.sha256` checksum and a Sigstore signature bundle — fetch them with `&asset=sha256` / `&asset=sigstore` on the same URL. See [Supply Chain](/trust/supply-chain/) for how releases are attested and verified. ## Run the manager [Section titled “Run the manager”](#run-the-manager) Open a new shell so the `PATH` change takes effect, then launch: ```sh codexx_dtdk_manager ``` The manager is a terminal UI. Move with the arrow keys; press `Enter` on the **codegen** row to install it. `q` quits. The manager handles the download, checksum verification, and side-by-side version management for you. ## Licensing [Section titled “Licensing”](#licensing) codegen is free — there is no license tier, no activation step, and no `codegen license` command. Installing it through the manager is the whole story. (Other DTDK components still require activation inside the manager; that’s unchanged.) ## Verify the installation [Section titled “Verify the installation”](#verify-the-installation) ```sh codegen --help ``` Should print the codegen option list. If the command isn’t found, confirm `~/.codexx/bin` (or `%USERPROFILE%\.codexx\bin`) is on your `PATH` and that you opened a new shell. ## Uninstall [Section titled “Uninstall”](#uninstall) One command removes the install tree, the `PATH` entry, and — unless you ask otherwise — the per-user config (license tokens) and cache (shared active map). Linux Windows ```sh curl -fsSL https://get.codexx-dtdk.com/uninstall.sh | bash ``` The script prints what it will remove and asks for confirmation (reading your answer from the terminal even when piped), then deletes `~/.codexx`, strips the marker block from `~/.bashrc` / `~/.zshrc` / `~/.profile`, and removes `~/.config/codexx` and `~/.cache/codexx`. For unattended use append `| bash -s -- --yes` to skip the prompt. Add `--keep-config` to retain license tokens (so a reinstall picks them up) or `--keep-cache` to retain the shared active map. `--dry-run` previews without touching anything; `--root

` targets a non-default install location. ```powershell irm https://get.codexx-dtdk.com/uninstall.ps1 | iex ``` The script prints what it will remove, asks for confirmation, then deletes `%USERPROFILE%\.codexx`, removes `%USERPROFILE%\.codexx\bin` from your user `PATH`, and removes `%APPDATA%\CodeXX` and `%LOCALAPPDATA%\CodeXX\Cache`. For unattended use, download it and run with `-Yes` (`irm …/uninstall.ps1 -OutFile uninstall.ps1; .\uninstall.ps1 -Yes`). Add `-KeepConfig` to retain license tokens or `-KeepCache` to retain the shared active map. `-DryRun` previews without touching anything; `-Root ` targets a non-default install location. ### Manual uninstall [Section titled “Manual uninstall”](#manual-uninstall) If you’d rather remove things by hand: Linux Windows ```sh # 1. Install tree (binaries, side-by-side component versions, shim). rm -rf ~/.codexx # 2. License tokens (skip to keep your activation for a future reinstall). rm -rf ~/.config/codexx # 3. Shared active map + per-tool cache. rm -rf ~/.cache/codexx # 4. PATH entry. install.sh appended a marker block to your shell rc files — # open each rc that exists and delete the block bounded by: # # >>> codexx-dtdk install begin >>> # … # # <<< codexx-dtdk install end <<< sed -i '/# >>> codexx-dtdk install begin >>>/,/# <<< codexx-dtdk install end <</dev/null || true ``` ```powershell # 1. Install tree. Remove-Item -Recurse -Force "$env:USERPROFILE\.codexx" # 2. License tokens (skip to keep activation for a future reinstall). Remove-Item -Recurse -Force "$env:APPDATA\CodeXX" # 3. Shared active map + per-tool cache. Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CodeXX\Cache" # 4. PATH entry — strip \bin from the User-scope Path. $bin = "$env:USERPROFILE\.codexx\bin" $parts = ([Environment]::GetEnvironmentVariable('Path','User') -split ';') | Where-Object { $_ -and $_ -ne $bin } [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User') ``` Open a new shell afterwards so the `PATH` change takes effect. Key Takeaways * codegen is installed through the DTDK Manager, not downloaded directly. * One PAT-free command installs the manager — download, checksum, extract, and `PATH` are all automatic. * The manager installs, updates, and verifies every component into `~/.codexx`, and handles license activation for the components that need one. * codegen is free — no license, no activation step. # Quick Start > Generate your first C++ enum-to-string function in under five minutes. This guide walks through the `ToString` example: annotating a C++ enum and generating a `std::string_view toString()` function from it. The shipping example is at `clis/codegen/examples/ToString/` in the source tree. 1. **Set up the rule directory** ```sh mkdir -p .codegen/rules/ToString # Copy config.yaml, transform.luau, and preamble.luau # from clis/codegen/examples/ToString/ into .codegen/rules/ToString/ ``` The four files in a rule directory are documented in [Rule Anatomy](/rules/anatomy/). 2. **Annotate your enum** Add an anchor comment near the enum. The third anchor segment is the entity’s qualified name (with namespaces): include/color.hpp ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color]] } // namespace app ``` `ToString` uses anchor-based triggering because C++ doesn’t allow attributes on enum declarations. The anchor doubles as the inline-injection site for the generated declaration. 3. **Run the engine** ```sh codegen -i ./include -r .codegen/rules -a ToString ``` * `-i` is the input directory (or a single header). * `-r` is the rules root (defaults to `.codegen/rules`; you can omit it). * `-a` selects which rule to run; repeatable. 4. **Inspect the output** The engine writes a sibling `.g.cpp` next to the input header by default: include/color.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "color.hpp" std::string_view toString(app::Color e) { switch (e) { case app::Color::Red: return "Red"; case app::Color::Green: return "Green"; case app::Color::Blue: return "Blue"; default: return ""; } } ``` The header itself is patched: the bare anchor expands into a paired `:begin]] / :end]]` block that holds the forward declaration. Subsequent runs replace only the content between the markers. 5. **Add the generated file to your build** CMakeLists.txt ```cmake target_sources(mylib PRIVATE include/color.g.cpp) ``` To re-trigger codegen automatically when the header changes, drive the binary from `add_custom_command` — see [CMake Integration](/integrations/cmake/) for the current pattern (the packaged `find_package(codegen)` flow is on the roadmap). ## What just happened [Section titled “What just happened”](#what-just-happened) The engine collected `.h`/`.hpp` files under `./include`, parsed them via codex, and matched the anchor comment against the `ToString` rule. It called `transform.luau` once with the enum’s AST node serialised as JSON. The script returned a `source` (the switch implementation) and a single-item `inline` list (the forward declaration). The engine wrote `color.g.cpp` and rewrote the anchor in `color.hpp` into the paired-marker block. ## Next step [Section titled “Next step”](#next-step) Read [Your First Rule](/getting-started/first-rule/) to write a rule from scratch. Working with an LLM agent? The full docs are bundled as a single plain-text file at [`/llms-full.txt`](/llms-full.txt). Paste it into your assistant’s context (Claude Code, Cursor, Copilot Chat, etc.) and it will have everything it needs to write rules, debug configs, and explain the AST schema — no crawling required. A condensed index is also available at [`/llms-small.txt`](/llms-small.txt). Key Takeaways * `codegen` selects rules by name with `-a`; the input is a path with `-i`. * Default outputs land **next to the input header** unless the rule config sets `outputDirectory`. * Anchor comments (`// [[codegen::generated::Rule::QualifiedName]]`) trigger anchor-based rules and mark the inline-injection site. * Attribute annotations (`[[codegen::Rule]]`) are the alternative trigger for declaration-level rules — any node kind listed in the rule’s `node_kinds`. * The first run rewrites bare anchors into idempotent `:begin]] / :end]]` blocks. # CLI Reference > Complete reference for the codegen command-line interface. ## Synopsis [Section titled “Synopsis”](#synopsis) ```sh codegen -i [-r ] -a [-a ...] [OPTIONS] ``` Rules are loaded from `//` for each `-a` on the command line. The rules directory defaults to `.codegen/rules`. The input is a single header or a directory (recursively scanned for `.h` / `.hpp`). ## Required flag [Section titled “Required flag”](#required-flag) | Flag | Description | | -------------------- | ----------------------- | | `-i, --input ` | Input file or directory | `-a` is required in practice — without it the engine has no rules to run. ## Rule selection [Section titled “Rule selection”](#rule-selection) | Flag | Default | Description | | ------------------------ | ---------------- | --------------------------------------------- | | `-a, --attribute ` | (none) | Rule (attribute) name to process. Repeatable. | | `-r, --rules-dir ` | `.codegen/rules` | Root directory containing rule subfolders. | ## Preprocessor / parser [Section titled “Preprocessor / parser”](#preprocessor--parser) | Flag | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------- | | `-D, --define NAME[=VALUE]` | **Inert.** Parsed into `defines` but never read; ADR-075 Phase B removed build-system probing. Repeatable. | | `-I, --include-path ` | **Inert.** Same as `-D` — parsed, never read. Repeatable. | | `--workspace-dir ` | Project root for the workspace daemon connection. Defaults to the current directory. | Source analysis always goes through the workspace daemon — there is no one-shot fallback pipeline and no flag to turn analysis on or off. The daemon must already be serving the root at `/.codexx/daemon.sock` (codegen never auto-spawns it); if it isn’t reachable the run fails with `E018`. `--workspace-dir` only selects which root to connect to. ## Permissions / params [Section titled “Permissions / params”](#permissions--params) | Flag | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-P, --param KEY=VALUE` | Override `params:` for every loaded rule. Value is parsed as JSON when possible, otherwise stored as a string. Repeatable. | | `--allow-http ` | Merge a host pattern into the HTTP allowlist for every loaded rule. `*` matches any host, `*.example.com` matches subdomains only (not the apex); otherwise the pattern is matched by exact host equality, not URL substring. Repeatable. | | `--allow-env ` | Append an OS env name to the env allowlist for every loaded rule. Repeatable. | | `--env KEY=VALUE` | Override a dotenv entry. Repeatable; takes precedence over per-rule and shared `.env`. | ## Run-mode flags [Section titled “Run-mode flags”](#run-mode-flags) | Flag | Default | Description | | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--dry-run` | off | Don’t write outputs. Emits a unified diff (zero context, `--- a/... / +++ b/... / @@`) of every would-be file change to stdout. Intended for CI and LLM-driven workflows. | | `-B`, `--browse-rules-registry` | off | Opens the interactive [rules registry](/rules/registry/) browser and exits; installs into `-r` (default `.codegen/rules`). Takes no `-i`. | | `--show-tui` | off | Opens the interactive FTXUI diff viewer instead of printing plain output. Only consulted when `--dry-run` is also set — silently ignored otherwise. Accepting a change in the viewer writes the files. | | `-v, --verbose` | off | Per-entity progress log. | | `--version` | — | Print the channel-qualified version (e.g. `insider/codegen@0.1.0-rc.1`) and exit `0`. Handled before argument validation, so it needs no `-i`. | | `-c, --config ` | (unused) | **Inert.** Generic profile-config slot inherited from the support CLI; parsed, never read. | | `-p, --profile ` | (unused) | **Inert.** Generic named-profile slot inherited from the support CLI; parsed, never read. | | `-h, --help` | — | Print usage and exit `0`. | ## Debugger flags [Section titled “Debugger flags”](#debugger-flags) | Flag | Default | Description | | ---------------------------- | ------------------------------ | -------------------------------------------------------------------- | | `--debug` | off | Enable the LuaU DAP server; the engine blocks until an IDE attaches. | | `--debug-port ` | `50001` | TCP port the DAP server listens on. | | `--debug-source-root ` | absolute path to `--rules-dir` | Source root reported to the IDE for file navigation. | These flags sit behind the `LUAU_ENV_ENABLE_DEBUGGER` compile-time macro, but shipped builds link `luau_environment_dbg`, which defines that macro publicly — so in practice the debugger flags are always present and free on every build. ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Meaning | | ---- | ------------------------------------------------------------------------------------------ | | `0` | Success — all entities processed and all files written (or the dry-run preview completed). | | `1` | Recoverable failure — bad CLI, missing input, rule load failure, preamble error, etc. | | `2` | Fatal — handler internal error or LuaU environment creation failure. | ## Examples [Section titled “Examples”](#examples) ```sh # Run one rule against a directory of headers codegen -i ./include -r .codegen/rules -a ToString # Multiple rules in one invocation codegen -i ./include -a ToString -a MarkdownDocs # Point at a workspace daemon serving a different project root codegen --workspace-dir . -i src/api.hpp -a TypeScriptTypes # Dry-run preview (unified diff to stdout — ideal for CI / LLMs) codegen -i ./include -a ToString --dry-run # Interactive TUI diff viewer codegen -i ./include -a ToString --dry-run --show-tui # Browse the rules registry and install a rule (no input file needed) codegen --browse-rules-registry # HTTP allowlist override (host-matched, not URL substring) codegen -i ./include -a SchemaSync \ --allow-http "schema.example.com" ``` # CMake Integration > Wire codegen rules into your CMake build graph. **🚧 Roadmap — WIP.** The `find_package(codegen)`, `codegen_rule()`, and `CODEGEN_OUTPUTS` helpers below are the design target for a planned `cmake_integration` package and are **not yet shipped**. The supported path today is `add_custom_command` driving the `codegen` binary; workspace analysis is unconditional (codegen always talks to the workspace daemon), so no flag or licence is needed to enable it. This page documents both the current and the planned path; track progress in the codegen changelog. ## Today: `add_custom_command` [Section titled “Today: add\_custom\_command”](#today-add_custom_command) Until the package ships, drive codegen from a regular `add_custom_command` / `add_custom_target`: CMakeLists.txt ```cmake set(CODEGEN_INPUT ${CMAKE_SOURCE_DIR}/include/color.hpp) set(CODEGEN_OUTPUT ${CMAKE_SOURCE_DIR}/include/color.g.cpp) add_custom_command( OUTPUT ${CODEGEN_OUTPUT} COMMAND codegen -i ${CODEGEN_INPUT} -r ${CMAKE_SOURCE_DIR}/.codegen/rules -a ToString DEPENDS ${CODEGEN_INPUT} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "codegen: ToString -> ${CODEGEN_OUTPUT}" VERBATIM ) add_custom_target(codegen_color DEPENDS ${CODEGEN_OUTPUT}) target_sources(mylib PRIVATE ${CODEGEN_OUTPUT}) add_dependencies(mylib codegen_color) ``` By default the engine writes outputs next to the input header, so the `OUTPUT` path is predictable: `.g.cpp` for cpp rules, `.` for other languages. Override with `outputDirectory` and `outputNameTemplate` in the rule’s config. Workspace analysis is unconditional — codegen always connects to the workspace daemon serving `/.codexx/daemon.sock`. Run codegen from the project root so the daemon’s root matches, or pass `--workspace-dir` to point at a different directory. ## Planned: `find_package(codegen)` + `codegen_rule()` [Section titled “Planned: find\_package(codegen) + codegen\_rule()”](#planned-find_packagecodegen--codegen_rule) A future package will install a CMake config module (`codegenConfig.cmake`) and a `codegen_rule()` helper that wraps the underlying `add_custom_command` and exposes the generated files for `target_sources`: CMakeLists.txt (planned) ```cmake find_package(codegen REQUIRED) codegen_rule( RULE ToString RULES_DIR ${CMAKE_SOURCE_DIR}/.codegen/rules INPUT ${CMAKE_SOURCE_DIR}/include OUTPUT_VAR CODEGEN_TOSTRING_OUTPUTS ) target_sources(mylib PRIVATE ${CODEGEN_TOSTRING_OUTPUTS}) ``` The helper will parse the rule config, derive `OUTPUT` paths from `outputDirectory` / `outputNameTemplate`, depend on the input headers, and chain the generated files into the target’s source list — eliminating the “forgot to regenerate” CI failure class. This page will be updated when the package ships. Key Takeaways * The `find_package(codegen)` / `codegen_rule()` flow is **not yet implemented**. * Today, drive codegen with `add_custom_command` calling the binary directly. * Default output paths are predictable from the rule config (`.g.cpp` for cpp, `.` otherwise). * The packaged helper is a roadmap item, not yet shipped; this page tracks the target. # VS Code DAP Debugger > Set breakpoints in LuaU rule scripts and step through execution in VS Code using the LuaU debugger over DAP. `--debug` is free — no licence required. `LUAU_ENV_ENABLE_DEBUGGER` is a compile-time flag, but shipped builds always link the debug-enabled Luau environment, so the flag is effectively always on. ## Overview [Section titled “Overview”](#overview) codegen embeds the [Roblox luau-debugger](https://github.com/roblox/luau-debugger) (vendored at `vendor/luau-debugger/`). When started with `--debug`, the engine spins up a DAP TCP server and blocks until a debugger attaches. You can then: * Set breakpoints in `.luau` scripts (preamble, handler, grouping) * Inspect the entity payload and any local variables * Step through execution * Watch the call stack ## Setup [Section titled “Setup”](#setup) 1. **Install the LuaU VS Code extension** Use the extension that ships with `vendor/luau-debugger/extensions/vscode/`. There is no codegen-specific VS Code extension. Any DAP-capable editor that speaks the same protocol works equivalently. 2. **Add a launch configuration** .vscode/launch.json ```json { "version": "0.2.0", "configurations": [ { "type": "luau", "request": "attach", "name": "attach to codegen", "address": "localhost", "port": 50001 } ] } ``` The default port is `50001`. Match it with `--debug-port` if you need a different one. 3. **Run codegen with the debugger flag** ```sh codegen --debug \ --debug-port 50001 \ --debug-source-root "$PWD/.codegen/rules" \ -i ./include \ -r .codegen/rules \ -a ToString ``` `--debug-source-root` should be the **absolute** path to the rules root, so VS Code can map breakpoints to the on-disk script files. If omitted, the engine uses the absolute path of `--rules-dir`. The engine prints a “waiting for debugger” line and blocks. 4. **Attach from VS Code** Press `F5` (or use the Run menu) with the launch configuration above active. Once attached, the engine resumes; execution pauses at any breakpoint you’ve set in a `.luau` script. ## What’s available in the inspector [Section titled “What’s available in the inspector”](#whats-available-in-the-inspector) When paused inside a handler, expand the local that holds the decoded entity to navigate the AST: ```lua local node = json.decode(input) -- ← break after this line, expand `node` ``` The Variables panel shows the full table including `_namespaces`, `_registryId`, `attributes`, `memberVariables`, and so on. This is the fastest way to understand the exact JSON shape your rule receives. ## Limitations [Section titled “Limitations”](#limitations) * The debugger attaches to a single rule run; it doesn’t keep a session across re-invocations. * Hot-reload (editing the script while paused) isn’t supported. Restart the debug session after edits. * The `--debug-port` is required for non-default ports; there’s no port discovery. * Debugger support sits behind the `LUAU_ENV_ENABLE_DEBUGGER` compile-time flag, but shipped builds always link `luau_environment_dbg`, which defines it publicly — `--debug` is available in every shipped build. Key Takeaways * Use the `luau` DAP launch type with `request: "attach"`; there is no codegen-specific extension. * The engine blocks at startup with `--debug` and resumes when the debugger attaches. * `--debug-source-root` is the absolute rules root used for VS Code’s source mapping. * Breakpoints work in handler, preamble, and grouping scripts. # Config Schema (.yaml) > Complete reference for the codegen rule configuration file format. Each rule lives in `//` and ships with a `config.yaml` (always named `config.yaml` — the directory name is what carries the rule identity). This page is the authoritative schema reference for that file. ## Top-level structure [Section titled “Top-level structure”](#top-level-structure) ```yaml version: 1 # required, must be 1 extends: ../base.yaml # optional, path relative to this file output: language: cpp # required for non-default; see table below autoDeduceIncludes: true # default: true outputDirectory: "" # optional, override default output directory outputNameTemplate: "" # optional, override output filename pattern permissions: # optional http: allowlist: - "schema-registry.example.com" env: os_allowlist: - "BUILD_CHANNEL" params: # optional, free-form, exposed to scripts as `params` apiVersion: "v1" emitDocs: true node_kinds: # optional, defaults to ["Struct"] - Struct - Class - Enum shared: # optional, controls which shared/*.luau scripts load include: ["json_*"] # absent = include all; [] = include none exclude: ["legacy_*"] # subtracted from include ``` ## `version` [Section titled “version”](#version) Must be the integer `1`. Any other integer emits `E002` and aborts. A non-integer `version:` is silently ignored; omitting the key entirely is accepted. ## `extends` [Section titled “extends”](#extends) Optional path (relative to this config file) to a base config. The base is loaded first, then values in this file override matching keys. Chains deeper than 8 levels emit `E015`. A missing target emits `E015`. ## `output.language` [Section titled “output.language”](#outputlanguage) Selects the default file extension and the C++-specific assembly path (banner, self-include, project-include rebasing). Must be one of: | Value | Default extension | | ------------ | ----------------- | | `cpp` | `.g.cpp` | | `csharp` | `.cs` | | `kotlin` | `.kt` | | `swift` | `.swift` | | `typescript` | `.ts` | | `plain` | `.txt` | | `markdown` | `.md` | | `html` | `.html` | | `mdx` | `.mdx` | Any other value emits `E015`. Defaults to `cpp` when omitted. Only `cpp` triggers the C++ assembly path: `// Generated with Codegen - DO NOT EDIT!!!` banner, `#pragma once` for header outputs, automatic self-include of the input header, and project-include rebasing when `outputDirectory` relocates the output. ## `output.autoDeduceIncludes` [Section titled “output.autoDeduceIncludes”](#outputautodeduceincludes) Default `true`. When enabled, the engine takes the rule’s emitted `includes` list and removes any entries already covered by the input header’s transitive include graph (per the analyzer). Set to `false` to emit every requested include verbatim. This is **not** a stdlib auto-detector — it filters explicit includes against the include graph; it does not synthesize includes from generated source text. ## `output.outputDirectory` [Section titled “output.outputDirectory”](#outputoutputdirectory) Overrides the default output directory (which is the input header’s parent directory). Path is taken as-is — relative paths are resolved against the process working directory. ## `output.outputNameTemplate` [Section titled “output.outputNameTemplate”](#outputoutputnametemplate) Overrides the default output filename. Tokens (Go-template style): | Token | Substitution | | ---------------- | --------------------------------------------------- | | `{{.InputFile}}` | Input header filename without extension | | `{{.InputDir}}` | Input header’s parent directory (generic-form path) | | `{{.RuleName}}` | The rule (attribute) name | Default filename when this is unset: * `cpp` → `.g.cpp` * everything else → `.` The full path is `/`. ## `permissions.http.allowlist` [Section titled “permissions.http.allowlist”](#permissionshttpallowlist) A list of host patterns. Each entry is matched against the **host component** of the URL passed to `http.get(...)` (NOT a substring of the full URL). Supported forms: | Entry | Meaning | | --------------- | ---------------------------------------------------------------------------------------------------------- | | `example.com` | Exact host match. `evil.example.com` is **not** matched. | | `*.example.com` | Wildcard: any subdomain (`api.example.com`, `a.b.example.com`). Does **not** match the apex `example.com`. | | `*` | Match any host (escape hatch). | An empty or absent allowlist disables HTTP entirely; calls to `http.get` raise `http.get: url not in allowlist:` and emit `E010`. The CLI flag `--allow-http ` appends entries to the allowlist at run time. ## `permissions.env.os_allowlist` [Section titled “permissions.env.os\_allowlist”](#permissionsenvos_allowlist) A list of OS environment variable names the rule’s scripts may read. When non-empty, the engine activates the env capability and exposes the named vars to the LuaU env module. The CLI flag `--allow-env ` appends entries to this list at run time. Dotenv files (`//.env` and `/../shared/.env`) are loaded automatically when present and exposed via the same env module; they do not require an allowlist entry. The CLI flag `--env KEY=VALUE` overrides individual dotenv entries. ## `params` [Section titled “params”](#params) Free-form YAML, converted to JSON and exposed to every script as `params`: * Preamble script: `params` is a key on the JSON context object passed in. * Handler (transform) script: `params` is added under the `params` key on the entity payload. * Grouping script: `params` is a sibling of `entities` on the input object. The CLI flag `-P key=value` (repeatable) overrides params per invocation — it is applied to **every** loaded rule’s `params`, not scoped to one rule. The value is parsed as JSON when possible, otherwise stored as a string. An entry without `=` is silently dropped. ## `node_kinds` [Section titled “node\_kinds”](#node_kinds) A list of `NodeKind` enum names. Defaults to `["Struct"]` when omitted. Controls which AST node kinds the rule’s attribute matcher will fire on. Valid entries are the codex `NodeKind` names; the common ones are `Struct`, `Class`, `Union`, `Function`, `Variable`, `Constructor`, `Destructor`, `Operator` and `EnumSpecifier`. Entries are forwarded verbatim to the daemon with no client-side validation — an unknown entry does not fail the load and does not warn. ## `uses` [Section titled “uses”](#uses) The components this rule requires, by `require` name: ```yaml uses: - acme/typemap - acme/codecs ``` Checked at **load** time: a rule naming a component that is not installed fails before any script runs and before any file is written, rather than dying per-entity at the first `require`. Declaring the key at all — even as an empty list — also opts the rule **out of the flat `shared/` prelude** (see [`shared`](#shared) below). A rule that names what it needs should not additionally absorb whatever else is sitting in the consumer’s tree; that is the isolation a published rule wants, and it is unavailable to a rule that declares nothing. ## `shared` [Section titled “shared”](#shared) Controls which `/../shared/*.luau` scripts get prepended to this rule’s transform. Both keys take a list of fnmatch-style globs (`*`, `?`, `[abc]`) matched against the script’s filename **stem** (i.e. `json_helpers.luau` matches as `json_helpers`). Applies to the **flat** scripts only. Namespaced components under `shared//` are reached with `require` and are never part of the prelude, so these globs do not affect them. | Key | Behaviour | | ------------------------------ | --------------------------------------------------------------- | | `shared.include` absent | Include every `shared/*.luau` (default). | | `shared.include: []` | Include none — no shared prelude for this rule. | | `shared.include: ["a", "b_*"]` | Include only the matching stems. | | `shared.exclude: [...]` | Subtract matching stems from whatever the include step yielded. | Independence assumption: flat shared scripts are concatenated in alphabetical filename order with no dependency tracking. If they depend on each other, name them so dependencies sort before dependents — or make them [components](/rules/anatomy/#components), where `require` resolves the order for you. ## Companion files (not part of the YAML) [Section titled “Companion files (not part of the YAML)”](#companion-files-not-part-of-the-yaml) * `transform.luau` — handler script, **required**. * `preamble.luau` — preamble script, **required** (an empty `return function() return "" end` is fine). * `grouping.luau` — grouping script, optional. * `.env` — per-rule dotenv, optional. Plus the workspace-shared directory: * `/../shared/*.luau` — auto-loaded in alphabetical filename order, filtered through each rule’s `shared.include` / `shared.exclude`, and prepended to that rule’s transform. Skipped entirely for a rule that declares [`uses`](#uses). * `/../shared//*.luau` — components, reached with `require("/")`. Never concatenated. * `/../shared/.env` — baseline dotenv; per-rule entries override. ## Full annotated example [Section titled “Full annotated example”](#full-annotated-example) config.yaml ```yaml version: 1 extends: ../base.yaml output: language: cpp autoDeduceIncludes: false outputDirectory: generated/advanced outputNameTemplate: "{{.InputFile}}.{{.RuleName}}.g.cpp" permissions: http: allowlist: - "schema-registry.internal.example.com" env: os_allowlist: - "BUILD_CHANNEL" params: apiVersion: "v1" emitDocs: true node_kinds: - Struct - Class ``` # Diagnostic Codes > Engine error and warning codes, what they mean and how to resolve them. An `[E...]` on `stderr` always means the generated code is not what was asked for — never treat a run as successful just because files were written. All diagnostics are written to `stderr` in the format: ```plaintext [E006] ToString | Color @ include/color.h ``` The fields after the code are: rule name, entity name, source path, detail. Any of the trailing fields can be empty. Diagnostics with no location print as `[E001] codegen: detail` instead. ## Error codes [Section titled “Error codes”](#error-codes) | Code | Trigger | Resolution | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `E001` | Rules directory not found | Check the path passed to `-r`. | | `E002` | `version:` in the rule config is not `1` | Set `version: 1` or upgrade the codegen binary. | | `E003` | `transform.luau` or `preamble.luau` not found in the rule directory | Add the missing handler/preamble. | | `E004` | Grouping script returned a map missing one or more entity `registryId`s | Ensure every entity passed in is keyed in the returned map. The affected entity is skipped and the run **exits 1**. | | `E005` | A path returned by grouping resolves outside the project root (CWD) | Use a path that stays under the working directory. The affected entity is skipped and the run **exits 1**. | | `E006` | Handler script raised a Lua error or an internal LuaU error occurred | See the detail line for the script line and reason. An internal LuaU error is fatal (exit 2); a *script* error skips the entity and the run **exits 1**. | | `E007` | Preamble script raised an error | The whole run aborts with exit 1. Fix the preamble before re-running. | | `E008` | Grouping script raised an error or returned invalid JSON | The affected rule’s grouping is skipped (entities keep their default 1:1 path). | | `E009` | Handler returned something that wasn’t `json.encode({...})` | Wrap the return in `json.encode({...})`. | | `E010` | `http.get(url)` called with a URL whose host is not on `permissions.http.allowlist` | Add the host (`example.com`) or a wildcard (`*.example.com`) to the allowlist, or stop calling `http.get` for that URL. | | `E011` | The `--input` path doesn’t exist | Check the path passed to `-i`. | | `E012` | Output directory could not be created or output file is not writable | Check filesystem permissions and free space. | | `E015` | Rule config schema/shape error: unknown `output.language`, malformed `permissions.http.allowlist` / `permissions.env.os_allowlist`, `extends:` target missing, or `extends:` chain too deep | Fix the offending field in `config.yaml` (see detail line for which one). | | `E016` | Rule config YAML parse failure | The detail line carries the YAML parser error. Fix the syntax in `config.yaml`. | | `E017` | Engine init failure (LuaU environment could not be created) | Fatal (exit 2). Indicates an internal bug — file an issue with the detail line. | | `E018` | Source analysis pipeline failed — in practice, this means the workspace daemon could not be reached | Confirm a daemon is serving `/.codexx/daemon.sock`. Workspace analysis runs on every invocation; there is no auto-spawn and no flag to disable it. | | `E019` | Two or more rules’ default output paths collide. **Non-C++ outputs only** — C++ outputs deliberately co-mingle in one file | Set `outputNameTemplate` or a `grouping.luau` to disambiguate. | | `E021` | A daemon RPC failed mid-run (transport / JSON-RPC error) | The daemon died, is a stale build, or the socket broke. This is **never** “symbol not found” — an empty result is normal, not an error. Restart the daemon and confirm it is a current build. | | `E022` | Registry browser could not start, or a fetch failed outright: **no interactive terminal** on stdout, a malformed payload, or a bad HTTP status | `-B` requires a TTY and has no non-interactive fallback — it exits 1 rather than degrading. For a plain unreachable network you get `W007` instead, and the browser still opens. | | `E023` | Rule install refused from the registry browser | The target directory exists with no `rule.json`, or names a different vendor, or an I/O error occurred. Resolve the conflict and retry. | | `E024` | Rules-registry rate limit hit (anonymous GitHub quota, 60/hour) | Wait for the reset window given in the detail line, or retry later. | | `E025` | Bare `-a ` is ambiguous — two namespaces both provide it | Qualify it: `-a acme/ToString`. The detail lists every candidate. | | `E026` | Two component sources claim the same `require` name | Compared case-insensitively, so a pair that would collide on Windows is caught on Linux too. Remove or rename one. | | `E027` | `require` of an unknown component, or a `uses:` entry with no component installed | Install it from the registry, or fix the name. Checked at load, before anything runs. | | `E028` | A component failed — compile error, raised, or returned nothing | The message names the *component*, not the rule that required it. A component must `return` its module value. | | `E029` | Component `require` cycle | The detail is the chain. | | `E030` | Malformed rule, namespace, or component identifier | Namespaces are `[a-z0-9-]+`, rule names are C++ identifiers, component names are `[a-z][a-z0-9_-]*`. | | `E031` | Install refused: this codegen is outside the bundle’s declared range | Update codegen. The detail gives the range the bundle asked for. | | `E032` | The registry catalog is newer than this codegen understands | Update codegen. Refusing beats reading the half of the catalog we recognise. | | `E033` | Component install refused — the destination is occupied by another vendor or an unmanaged file | Remove the existing file first. | | `E034` | A rule or component source could not be read | Previously silent: an unreadable script became an empty one, and the failure surfaced later as a confusing `E006`. | ## Warning codes [Section titled “Warning codes”](#warning-codes) | Code | Trigger | Notes | | ------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | `W003` | An entity’s anchor is not present in its source header (or the anchor references an unknown entity) | The `.g.cpp` is still written; only the inline forward-declaration injection is skipped. | | `W004` | Inline-edit file I/O issue (cannot open source for read, cannot write the temp file, or rename failed) | The inline edit for the affected file is skipped; the `.g.cpp` itself is unaffected. | | `W006` | Unsupported config feature referenced (e.g. `permissions.registry`) | The entry is ignored; the rule otherwise loads normally. | | `W007` | Rules-registry unreachable (`-B`) | The browser still opens, showing installed rules only. | | `W008` | A bare `-a ` resolved by searching namespaces | Write the qualified form; it is what the rule is actually called. | | `W009` | A flat rule shadows a namespaced one of the same name | The flat rule wins, because that is what the bare name already meant. Qualify to reach the other. | | `W010` | The rule declares `uses:`, so the flat `shared/` prelude was not prepended | Intended: a rule that names what it needs does not also absorb the rest of your tree. | | `W011` | A pre-namespace anchor was migrated in place | Emitted once, when a rule moves under a namespace. Re-run to confirm the tree has settled. | | `W012` | An entry under `rules/` or `shared/` beyond two levels was ignored | The layout is one optional namespace level; anything deeper is not read. | ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Meaning | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | Success — no error code was emitted; all entities processed and all files written (or the dry-run preview completed). | | `1` | Recoverable failure — bad CLI, missing input (`E011`), rule load failure, preamble failure, a daemon connection failure (`E018`), etc. Some output may still have been written and is **not** trustworthy. | | `2` | Fatal — handler internal error or LuaU environment creation failure. | Per ADR-024 §3, an emitted error is converted to an exit code at `main()`, so a diagnostic can never be logged and then silently ignored — a non-zero exit is the guarantee behind the preamble above. # LuaU Globals > Complete reference for the globals injected into every codegen rule script. ## Always-available globals [Section titled “Always-available globals”](#always-available-globals) These are present in every script (preamble, handler, grouping) regardless of permissions. ### `json` [Section titled “json”](#json) | API | Signature | | -------------------- | ---------------------------------------------- | | `json.encode(value)` | `any -> string` | | `json.decode(s)` | `string -> any` (returns `nil` on parse error) | Tables with integer keys serialise as JSON arrays; tables with string keys serialise as objects. Mixed tables are not supported. ```lua local node = json.decode(input) if not node then error("invalid input") end return json.encode({ source = "...", inline = {} }) ``` ### Native introspection API [Section titled “Native introspection API”](#native-introspection-api) These functions are registered by the engine and use stable opaque integer IDs (the `_registryId` field on the entity payload, or the result of a lookup). | Function | Signature | Description | | ------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `get_node(id)` | `int -> string\|nil` | Returns the node’s full JSON, or nil. | | `find_struct_by_name(name)` | `string -> int\|nil` | Returns the registry ID of a struct by unqualified name (first match across all parsed sources). | | `struct_has_attribute(id, name)` | `int, string -> bool\|nil` | True if the struct carries `[[codegen::name]]` (or any namespaced attribute named `name`). | | `struct_has_method(id, name)` | `int, string -> bool\|nil` | True if the struct has a member function, static member function, or constructor with this name. | | `struct_has_member(id, name)` | `int, string -> bool\|nil` | True if the struct has a member or static member variable with this name. | | `struct_has_default_constructor(id)` | `int -> bool\|nil` | True if the struct has no constructors, or if any constructor’s parameters all have default values. | | `get_base_classes(id)` | `int -> string\|nil` | Returns a JSON array of `{access, name, qualifiedName, id}` for each base class. `id` is `null` when the analyzer can’t resolve the symbol. | | `get_definition_header(name)` | `string -> string\|nil` | Returns the path to the header that defines a given qualified or unqualified name, when known to the analyzer. | All functions return `nil` on bad arguments (wrong type or missing); only the lookup functions return `nil` for “not found.” ### Semantic query API [Section titled “Semantic query API”](#semantic-query-api) These functions expose the engine’s semantic model — type resolution, symbol lookup, references, and relations — to rules. They project the codex L2 `SemanticQuery` surface as JSON. Two ID conventions apply: * **Node IDs** are the same opaque registry integers used by the introspection API above (the `_registryId` on the payload, or the result of a lookup). * **Symbol IDs** are content hashes that cross the boundary as **decimal strings** (a 64-bit hash would lose precision as a LuaU number). Pass them back verbatim as strings. Functions whose return type is `json` hand back a JSON **string** — call `json.decode` on it. The shape of every returned model is documented in [Semantic Queries](/concepts/semantic-query/). | Function | Signature | Description | | ----------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type_symbol_of(nodeId)` | `int -> string\|nil` | Symbol ID of the **type** of a node: a variable/field ⇒ its declared type; an expression ⇒ its inferred type; a type declaration ⇒ itself. `nil` for builtins, dependent/unresolved types, and non-type-bearing nodes. | | `type_node_of(nodeId)` | `int -> int\|nil` | Registry node ID of the **declaration** of a node’s type — pivot from a traversed node straight into its type’s own AST. `nil` when the type has no user-declared symbol. | | `type_of(nodeId)` | `int -> json\|nil` | The node’s full [`ResolvedType`](/concepts/semantic-query/#resolvedtype) (state + type signature + resolved type symbol). | | `symbol_summary(symbolId)` | `string -> json\|nil` | Flat [`SymbolSummary`](/concepts/semantic-query/#symbol-summaries) for a symbol ID (kind, qualified name, signature, location, access, parent). | | `find_symbols(qualifiedName)` | `string -> json` | JSON array of symbol-ID strings matching a qualified name (all overloads). | | `references(symbolId)` | `string -> json\|nil` | JSON array of [`Reference`](/concepts/semantic-query/#references) objects — every declaration, definition, and use site of the symbol, sorted by location. | | `base_classes(symbolId)` | `string -> json\|nil` | JSON array of the symbol’s direct base-class symbol-ID strings. | | `members(symbolId)` | `string -> json\|nil` | JSON array of the symbol’s direct member symbol-ID strings. | All of these — including the deepest facts (`type_of`, `type_symbol_of`, `references`) — resolve against a whole-workspace snapshot: workspace analysis runs unconditionally on every invocation, via a connection to the workspace daemon. Like the introspection API, every function returns `nil` on bad arguments or when the query has no answer. ## Context objects [Section titled “Context objects”](#context-objects) ### Preamble context [Section titled “Preamble context”](#preamble-context) Injected via the `input` parameter to `preamble.luau`: ```json { "language": "cpp", "outputFile": "path/to/output.cpp", "ruleName": "RuleName", "params": { /* rule.params */ }, "entities": [ { "registryId": 42, "qualifiedName": "app::MyStruct", "structName": "MyStruct", "namespaces": ["app"], "inputFile": "include/app/my_struct.hpp", "includes": [""] } ] } ``` The `entities` array contains every entity bound to this output file. Use it to conditionally emit includes or other file-level content. ### Grouping context [Section titled “Grouping context”](#grouping-context) Injected via the `input` parameter to `grouping.luau`: ```json { "params": { /* rule.params */ }, "entities": [ { "registryId": 42, "qualifiedName": "app::MyStruct", "structName": "MyStruct", "namespaces": ["app"], "inputFile": "include/app/my_struct.hpp", "defaultPath": "include/app/my_struct.g.cpp" } ] } ``` The `defaultPath` is the 1:1 output path (derived from the input header). Override it by returning a map from `registryId` to a new path. ### Handler context [Section titled “Handler context”](#handler-context) Injected via the `input` parameter to `transform.luau`: The full entity JSON from the codex AST, with these synthetic fields added: * `_namespaces` (array of strings, the qualifying namespace path) * `_registryId` (integer, stable identifier) * `_outputPath` (string, the post-grouping output file path) * `params` (object, copied from rule config) The `_outputPath` is the final output file this entity will land in (after grouping, if present). ## Standard LuaU library [Section titled “Standard LuaU library”](#standard-luau-library) Available subset (the LuaU sandbox restricts the unsafe parts): | Library | Use | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `table` | `insert`, `remove`, `concat`, `sort`, `unpack` | | `string` | `format`, `find`, `gmatch`, `gsub`, `sub`, `len`, `rep`, `upper`, `lower` | | `math` | `floor`, `ceil`, `min`, `max`, `abs`, `huge`, `pi` | | Globals | `ipairs`, `pairs`, `tostring`, `tonumber`, `error`, `pcall`, `xpcall`, `type`, `select`, `assert`, `unpack` | | `os` | opened as part of the base stdlib, then frozen along with every other global by `luaL_sandbox` | | `coroutine` | opened as part of the base stdlib; the engine also drives it internally for `http.get`, so don’t assume a script-created coroutine behaves independently of that | ## Permission-gated globals [Section titled “Permission-gated globals”](#permission-gated-globals) These appear only when the corresponding capability is requested via `permissions:` in the rule config (or via CLI overrides). ### `http` [Section titled “http”](#http) Enabled when `permissions.http.allowlist` is non-empty (or the CLI passes `--allow-http`). | API | Signature | | --------------- | ---------------------------------------------------------------------------------------- | | `http.get(url)` | `string -> string` (response body), or yields `(nil, error_string)` on transport failure | The URL’s host component is matched against the allowlist (exact host, `*.domain` wildcard, or `*` as escape hatch — see [`permissions.http.allowlist`](/reference/config-schema/#permissionshttpallowlist)). A URL whose host is not on the allowlist raises a runtime error and emits diagnostic `E010`. Concurrent requests on the same VM are not supported. ### `env` [Section titled “env”](#env) Enabled when `permissions.env.os_allowlist` is non-empty, or when any dotenv source is configured (per-rule `.env`, shared `/../shared/.env`, or CLI `--env KEY=VALUE`). The env module exposes the allowlisted OS env vars and the merged dotenv (CLI `--env` > per-rule `.env` > shared `.env`). See the [Permissions](/rules/permissions/) page for the surface API. ## `require` [Section titled “require”](#require) Present **only when components are installed**, and only able to name one of them: ```lua local typemap = require("acme/typemap") ``` The set of loadable modules is fixed before any script runs. `codegen` reads every component source from disk itself and hands the resulting map to the sandbox; `require` is a lookup in that map by exact string and is **never joined to a path**. There is no search path, no filesystem access, and no way to name anything the tool did not already read — a component that is not installed cannot be reached, however it is spelled. | Behaviour | | | ------------ | -------------------------------------------------------------------------------------------------------------------------- | | Argument | Must be a string. A component that is not installed raises an error naming what *is*. | | Return | Whatever the component returned. A component that returns nothing is an error. | | Identity | `require` twice in one run yields the same value; a fresh one per entity, matching how shared scripts have always behaved. | | Cycles | Detected and reported as a cycle, with the chain. | | Availability | Transform, preamble and grouping scripts alike. | A tree with no components installed gets no `require` global at all. See [Rule Anatomy](/rules/anatomy/#components) for the on-disk layout. ## Absent globals [Section titled “Absent globals”](#absent-globals) The following are explicitly **not** provided: * `package`, and any module *search* mechanism — `require` resolves only from the fixed set above * `io` (file I/O) — there is no filesystem module of any kind, and no `fs` capability * `debug` (Lua debug library; the DAP debugger is a separate feature, see [VS Code DAP Debugger](/integrations/vscode-debugger/)) * `loadstring` / `loadfile` * Any FFI `os.getenv` reading an arbitrary OS variable is **not** the env capability — that’s the separate `env` module below, gated by `os_allowlist`. Key Takeaways * `json.encode` / `json.decode`, the native introspection API, and the semantic query API are always present. * HTTP and env access are opt-in via `permissions:` in the rule config; nothing else opens the sandbox. * The native API gives scripts cross-entity lookup (base classes, definition headers, finding structs by name) without re-implementing the analyzer. * The semantic query API adds type resolution — resolve a node’s type to a symbol, pivot into that type’s declaration, and query references and relations. Symbol IDs are decimal strings; `json`-returning functions hand back a string to `json.decode`. See [Semantic Queries](/concepts/semantic-query/). # Core Nodes ## `Attribute` [Section titled “Attribute”](#attribute) | Field | Type | | ------ | ---------- | | `ns` | `string` | | `name` | `string` | | `args` | `string[]` | *** ## `CommentNode` *(extends `Node`)* [Section titled “CommentNode (extends Node)”](#commentnode-extends-node) *kind discriminant: `"Comment"`* | Field | Type | | ------ | -------- | | `text` | `string` | *** ## `FunctionArgument` [Section titled “FunctionArgument”](#functionargument) | Field | Type | | ----------------- | --------- | | `expr` | `Node?` | | `isPackExpansion` | `boolean` | *** ## `FunctionParameter` [Section titled “FunctionParameter”](#functionparameter) | Field | Type | | --------------- | --------------- | | `typeSignature` | `TypeSignature` | | `name` | `string` | | `defaultValue` | `Node?` | *** ## `FunctionPointerSignature` [Section titled “FunctionPointerSignature”](#functionpointersignature) | Field | Type | | ----------------------- | --------------------------- | | `scopeName` | `string` | | `parameterTypes` | `TypeSignature[]` | | `returnFunctionPointer` | `FunctionPointerSignature?` | | `isConst` | `boolean` | *** ## `FunctionQualifiers` [Section titled “FunctionQualifiers”](#functionqualifiers) | Field | Type | | ------------------- | --------- | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isFinal` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isConstexpr` | `boolean` | | `isConsteval` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isInline` | `boolean` | | `isStatic` | `boolean` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `refQualifier` | `string` | | `requiresClause` | `Node?` | *** ## `IdentifierNode` *(extends `Node`)* [Section titled “IdentifierNode (extends Node)”](#identifiernode-extends-node) *kind discriminant: `"Identifier"`* | Field | Type | | --------------- | -------------------- | | `qualification` | `IdentifierNode?` | | `name` | `string` | | `templateArgs` | `TemplateArgument[]` | *** ## `LambdaCaptureItem` [Section titled “LambdaCaptureItem”](#lambdacaptureitem) | Field | Type | | ------------ | ----------------- | | `kind` | `string` | | `identifier` | `IdentifierNode?` | | `init` | `Node?` | *** ## `MacroParameter` [Section titled “MacroParameter”](#macroparameter) | Field | Type | | ------------ | --------- | | `name` | `string` | | `isVariadic` | `boolean` | *** ## `MemberInitializer` [Section titled “MemberInitializer”](#memberinitializer) | Field | Type | | -------- | ----------------- | | `member` | `IdentifierNode?` | | `args` | `Node[]` | *** ## `NamespaceSegment` [Section titled “NamespaceSegment”](#namespacesegment) | Field | Type | | ---------- | --------- | | `name` | `string` | | `isInline` | `boolean` | *** ## `Node` [Section titled “Node”](#node) | Field | Type | | ------------- | -------- | | `kind` | `string` | | `startLine` | `number` | | `startColumn` | `number` | | `endLine` | `number` | | `endColumn` | `number` | | `comment` | `Node?` | *** ## `PlaceholderTypeSpecifier` [Section titled “PlaceholderTypeSpecifier”](#placeholdertypespecifier) | Field | Type | | ------------ | ---------------- | | `kind` | `string` | | `constraint` | `TypeSignature?` | *** ## `Source` [Section titled “Source”](#source) | Field | Type | | ------------------ | --------- | | `name` | `string` | | `path` | `string` | | `content` | `string` | | `encoding` | `string` | | `lastModifiedTime` | `number` | | `rawContent` | `string?` | | `sourceMap` | `null` | | `macroTable` | `null` | | `unsavedBuffer` | `string?` | *** ## `SourceNode` *(extends `Node`)* [Section titled “SourceNode (extends Node)”](#sourcenode-extends-node) *kind discriminant: `"Source"`* | Field | Type | | ---------- | --------- | | `source` | `Source?` | | `children` | `Node[]` | *** ## `TemplateArgument` [Section titled “TemplateArgument”](#templateargument) | Field | Type | | --------------- | ---------------- | | `keyword` | `string` | | `typeSignature` | `TypeSignature?` | | `expr` | `Node?` | | `value` | `string` | *** ## `TemplateParameter` [Section titled “TemplateParameter”](#templateparameter) | Field | Type | | ----------------- | --------------------- | | `paramKind` | `string` | | `name` | `string` | | `isVariadic` | `boolean` | | `defaultValue` | `Node?` | | `keyword` | `string` | | `constraint` | `IdentifierNode?` | | `typeSignature` | `TypeSignature` | | `innerParameters` | `TemplateParameter[]` | *** ## `TypeDeclarator` [Section titled “TypeDeclarator”](#typedeclarator) | Field | Type | | --------------- | --------- | | `kind` | `string` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `arraySizeExpr` | `Node?` | *** ## `TypeSignature` [Section titled “TypeSignature”](#typesignature) | Field | Type | | ---------------------- | --------------------------- | | `identifier` | `IdentifierNode?` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isMutable` | `boolean` | | `declarators` | `TypeDeclarator[]` | | `functionPointer` | `FunctionPointerSignature?` | | `decltypeSpecifier` | `DecltypeExpressionNode?` | | `placeholderSpecifier` | `PlaceholderTypeSpecifier?` | *** # Declaration Nodes ## `AttributeDeclarationNode` *(extends `DeclarationNode`)* [Section titled “AttributeDeclarationNode (extends DeclarationNode)”](#attributedeclarationnode-extends-declarationnode) *kind discriminant: `"AttributeDeclaration"`* | Field | Type | | ------------ | ------------- | | `attributes` | `Attribute[]` | *** ## `ClassNode` *(extends `DeclarationNode`)* [Section titled “ClassNode (extends DeclarationNode)”](#classnode-extends-declarationnode) *kind discriminant: `"Class"`* | Field | Type | | ----------------------- | --------------------------------------------------- | | `isFinal` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `baseClasses` | `{ access: string, identifier: IdentifierNode? }[]` | | `derivedClasses` | `string[]` | | `memberVariables` | `{ access: string, identifier: Node? }[]` | | `memberFunctions` | `{ access: string, identifier: Node? }[]` | | `staticMemberVariables` | `{ access: string, identifier: Node? }[]` | | `staticMemberFunctions` | `{ access: string, identifier: Node? }[]` | | `constructors` | `{ access: string, identifier: Node? }[]` | | `destructors` | `{ access: string, identifier: Node? }[]` | | `operators` | `{ access: string, identifier: Node? }[]` | | `friends` | `{ access: string, identifier: Node? }[]` | | `nestedTypes` | `{ access: string, identifier: Node? }[]` | | `statements` | `Node[]` | *** ## `ConceptNode` *(extends `DeclarationNode`)* [Section titled “ConceptNode (extends DeclarationNode)”](#conceptnode-extends-declarationnode) *kind discriminant: `"Concept"`* | Field | Type | | ---------------- | ----------------- | | `identifier` | `IdentifierNode?` | | `constraintExpr` | `Node?` | | `templateDecl` | `Node?` | *** ## `ConstructorNode` *(extends `DeclarationNode`)* [Section titled “ConstructorNode (extends DeclarationNode)”](#constructornode-extends-declarationnode) *kind discriminant: `"Constructor"`* | Field | Type | | ------------------- | --------------------- | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isConstexpr` | `boolean` | | `isInline` | `boolean` | | `isCopyConstructor` | `boolean` | | `isMoveConstructor` | `boolean` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `identifier` | `IdentifierNode?` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `memberInits` | `MemberInitializer[]` | *** ## `DestructorNode` *(extends `DeclarationNode`)* [Section titled “DestructorNode (extends DeclarationNode)”](#destructornode-extends-declarationnode) *kind discriminant: `"Destructor"`* | Field | Type | | ------------------- | ----------------- | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `identifier` | `IdentifierNode?` | *** ## `EnumNode` *(extends `DeclarationNode`)* [Section titled “EnumNode (extends DeclarationNode)”](#enumnode-extends-declarationnode) *kind discriminant: `"EnumeratorSpecifier"`* | Field | Type | | ---------------------- | ----------------- | | `identifier` | `IdentifierNode?` | | `underlyingType` | `TypeSignature` | | `isScoped` | `boolean` | | `isForwardDeclaration` | `boolean` | | `enumerators` | `Node[]` | *** ## `EnumSpecifierNode` *(extends `DeclarationNode`)* [Section titled “EnumSpecifierNode (extends DeclarationNode)”](#enumspecifiernode-extends-declarationnode) *kind discriminant: `"Enumerator"`* | Field | Type | | ------- | -------- | | `name` | `string` | | `value` | `Node?` | *** ## `ExportDeclarationNode` *(extends `DeclarationNode`)* [Section titled “ExportDeclarationNode (extends DeclarationNode)”](#exportdeclarationnode-extends-declarationnode) *kind discriminant: `"ExportDeclaration"`* | Field | Type | | ---------- | -------- | | `children` | `Node[]` | *** ## `ExternCNode` *(extends `DeclarationNode`)* [Section titled “ExternCNode (extends DeclarationNode)”](#externcnode-extends-declarationnode) *kind discriminant: `"ExternC"`* | Field | Type | | ---------- | --------- | | `language` | `string` | | `isBlock` | `boolean` | | `children` | `Node[]` | *** ## `FriendNode` *(extends `DeclarationNode`)* [Section titled “FriendNode (extends DeclarationNode)”](#friendnode-extends-declarationnode) *kind discriminant: `"Friend"`* | Field | Type | | ------------ | ----------------- | | `kind` | `string` | | `identifier` | `IdentifierNode?` | | `definition` | `Node?` | *** ## `FunctionNode` *(extends `DeclarationNode`)* [Section titled “FunctionNode (extends DeclarationNode)”](#functionnode-extends-declarationnode) *kind discriminant: `"Function"`* | Field | Type | | ------------------- | --------------------- | | `isStatic` | `boolean` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isFinal` | `boolean` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `isConsteval` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isTrailingReturn` | `boolean` | | `refQualifier` | `string` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `returnSignature` | `TypeSignature` | | `identifier` | `IdentifierNode?` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `memberInits` | `MemberInitializer[]` | *** ## `ModuleImportNode` *(extends `DeclarationNode`)* [Section titled “ModuleImportNode (extends DeclarationNode)”](#moduleimportnode-extends-declarationnode) *kind discriminant: `"ModuleImport"`* | Field | Type | | ------------ | --------- | | `moduleName` | `string` | | `partition` | `string` | | `header` | `string` | | `isSystem` | `boolean` | | `isExported` | `boolean` | *** ## `ModuleNode` *(extends `DeclarationNode`)* [Section titled “ModuleNode (extends DeclarationNode)”](#modulenode-extends-declarationnode) *kind discriminant: `"Module"`* | Field | Type | | ------------------- | --------- | | `moduleName` | `string` | | `partition` | `string` | | `isExported` | `boolean` | | `isGlobalFragment` | `boolean` | | `isPrivateFragment` | `boolean` | | `children` | `Node[]` | *** ## `NamespaceAliasNode` *(extends `DeclarationNode`)* [Section titled “NamespaceAliasNode (extends DeclarationNode)”](#namespacealiasnode-extends-declarationnode) *kind discriminant: `"NamespaceAlias"`* | Field | Type | | ----------------- | ----------------- | | `aliasName` | `string` | | `targetNamespace` | `IdentifierNode?` | *** ## `NamespaceNode` *(extends `DeclarationNode`)* [Section titled “NamespaceNode (extends DeclarationNode)”](#namespacenode-extends-declarationnode) *kind discriminant: `"Namespace"`* | Field | Type | | ------------- | -------------------- | | `segments` | `NamespaceSegment[]` | | `isAnonymous` | `boolean` | | `isInline` | `boolean` | | `children` | `Node[]` | *** ## `OperatorNode` *(extends `DeclarationNode`)* [Section titled “OperatorNode (extends DeclarationNode)”](#operatornode-extends-declarationnode) *kind discriminant: `"Operator"`* | Field | Type | | ------------------- | --------------------- | | `isStatic` | `boolean` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isFinal` | `boolean` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isTrailingReturn` | `boolean` | | `refQualifier` | `string` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `operatorSymbol` | `string` | | `returnSignature` | `TypeSignature` | | `castTargetType` | `TypeSignature` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | *** ## `StaticAssertNode` *(extends `DeclarationNode`)* [Section titled “StaticAssertNode (extends DeclarationNode)”](#staticassertnode-extends-declarationnode) *kind discriminant: `"StaticAssert"`* | Field | Type | | ----------- | -------- | | `condition` | `Node?` | | `message` | `string` | *** ## `StructNode` *(extends `DeclarationNode`)* [Section titled “StructNode (extends DeclarationNode)”](#structnode-extends-declarationnode) *kind discriminant: `"Struct"`* | Field | Type | | ----------------------- | --------------------------------------------------- | | `isFinal` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `baseClasses` | `{ access: string, identifier: IdentifierNode? }[]` | | `derivedClasses` | `string[]` | | `memberVariables` | `Node[]` | | `memberFunctions` | `Node[]` | | `staticMemberVariables` | `Node[]` | | `staticMemberFunctions` | `Node[]` | | `constructors` | `Node[]` | | `destructors` | `Node[]` | | `operators` | `Node[]` | | `friends` | `Node[]` | | `nestedTypes` | `Node[]` | | `statements` | `Node[]` | *** ## `StructuredBindingNode` *(extends `DeclarationNode`)* [Section titled “StructuredBindingNode (extends DeclarationNode)”](#structuredbindingnode-extends-declarationnode) *kind discriminant: `"StructuredBinding"`* | Field | Type | | --------------- | --------------- | | `names` | `string[]` | | `typeSignature` | `TypeSignature` | | `initializer` | `Node?` | | `isStatic` | `boolean` | | `isConstexpr` | `boolean` | | `isConstinit` | `boolean` | | `isThreadLocal` | `boolean` | | `isInline` | `boolean` | *** ## `TemplateNode` *(extends `DeclarationNode`)* [Section titled “TemplateNode (extends DeclarationNode)”](#templatenode-extends-declarationnode) *kind discriminant: `"Template"`* | Field | Type | | ---------------- | --------------------- | | `parameters` | `TemplateParameter[]` | | `requiresClause` | `Node?` | *** ## `TypeAliasNode` *(extends `DeclarationNode`)* [Section titled “TypeAliasNode (extends DeclarationNode)”](#typealiasnode-extends-declarationnode) *kind discriminant: `"TypeAlias"`* | Field | Type | | -------------- | --------------- | | `aliasName` | `string` | | `targetType` | `TypeSignature` | | `templateDecl` | `Node?` | *** ## `TypedefNode` *(extends `DeclarationNode`)* [Section titled “TypedefNode (extends DeclarationNode)”](#typedefnode-extends-declarationnode) *kind discriminant: `"Typedef"`* | Field | Type | | ------------ | ------------------ | | `aliasName` | `string` | | `typeDecl` | `DeclarationNode?` | | `targetType` | `TypeSignature?` | *** ## `UnionNode` *(extends `DeclarationNode`)* [Section titled “UnionNode (extends DeclarationNode)”](#unionnode-extends-declarationnode) *kind discriminant: `"Union"`* | Field | Type | | ----------------------- | -------------------- | | `isAnonymous` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `memberVariables` | `Node[]` | | `memberFunctions` | `Node[]` | | `staticMemberVariables` | `Node[]` | | `staticMemberFunctions` | `Node[]` | | `constructors` | `Node[]` | | `destructors` | `Node[]` | | `operators` | `Node[]` | | `nestedTypes` | `Node[]` | *** ## `UsingDeclarationNode` *(extends `DeclarationNode`)* [Section titled “UsingDeclarationNode (extends DeclarationNode)”](#usingdeclarationnode-extends-declarationnode) *kind discriminant: `"UsingDeclaration"`* | Field | Type | | ------------ | --------------- | | `aliasName` | `string` | | `targetType` | `TypeSignature` | *** ## `UsingNamespaceNode` *(extends `DeclarationNode`)* [Section titled “UsingNamespaceNode (extends DeclarationNode)”](#usingnamespacenode-extends-declarationnode) *kind discriminant: `"UsingNamespace"`* | Field | Type | | ------------ | ----------------- | | `identifier` | `IdentifierNode?` | *** ## `VariableGroupNode` *(extends `DeclarationNode`)* [Section titled “VariableGroupNode (extends DeclarationNode)”](#variablegroupnode-extends-declarationnode) *kind discriminant: `"VariableGroup"`* | Field | Type | | ----------- | ---------------- | | `variables` | `VariableNode[]` | *** ## `VariableNode` *(extends `DeclarationNode`)* [Section titled “VariableNode (extends DeclarationNode)”](#variablenode-extends-declarationnode) *kind discriminant: `"Variable"`* | Field | Type | | --------------- | ----------------- | | `isStatic` | `boolean` | | `isConstexpr` | `boolean` | | `isThreadLocal` | `boolean` | | `isInline` | `boolean` | | `isExtern` | `boolean` | | `isConstinit` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `typeSignature` | `TypeSignature` | | `identifier` | `IdentifierNode?` | | `defaultValue` | `Node?` | *** # Expression Nodes ## `AlignofExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “AlignofExpressionNode (extends IntrospectionExpressionNode)”](#alignofexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"AlignofExpression"`* *No own serialized fields.* *** ## `AssignmentExpressionNode` *(extends `ExpressionNode`)* [Section titled “AssignmentExpressionNode (extends ExpressionNode)”](#assignmentexpressionnode-extends-expressionnode) *kind discriminant: `"AssignmentExpression"`* | Field | Type | | ----- | -------- | | `lhs` | `Node?` | | `op` | `string` | | `rhs` | `Node?` | *** ## `BinaryExpressionNode` *(extends `ExpressionNode`)* [Section titled “BinaryExpressionNode (extends ExpressionNode)”](#binaryexpressionnode-extends-expressionnode) *kind discriminant: `"BinaryExpression"`* | Field | Type | | ----- | -------- | | `lhs` | `Node?` | | `op` | `string` | | `rhs` | `Node?` | *** ## `BoolLiteralNode` *(extends `LiteralNode`)* [Section titled “BoolLiteralNode (extends LiteralNode)”](#boolliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `value` | `boolean` | | `_litSubtype` | `number` (always `5`) | *** ## `CallExpressionNode` *(extends `ExpressionNode`)* [Section titled “CallExpressionNode (extends ExpressionNode)”](#callexpressionnode-extends-expressionnode) *kind discriminant: `"CallExpression"`* | Field | Type | | ------------------ | -------------------- | | `callKind` | `string` | | `callee` | `Node?` | | `calleeIdentifier` | `IdentifierNode?` | | `args` | `FunctionArgument[]` | *** ## `CastExpressionNode` *(extends `ExpressionNode`)* [Section titled “CastExpressionNode (extends ExpressionNode)”](#castexpressionnode-extends-expressionnode) *kind discriminant: `"CastExpression"`* | Field | Type | | ------------ | --------------- | | `castKind` | `string` | | `targetType` | `TypeSignature` | | `operand` | `Node?` | *** ## `CharLiteralNode` *(extends `LiteralNode`)* [Section titled “CharLiteralNode (extends LiteralNode)”](#charliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `encoding` | `string` | | `value` | `string` | | `_litSubtype` | `number` (always `4`) | *** ## `CoAwaitExpressionNode` *(extends `ExpressionNode`)* [Section titled “CoAwaitExpressionNode (extends ExpressionNode)”](#coawaitexpressionnode-extends-expressionnode) *kind discriminant: `"CoAwaitExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `CoYieldExpressionNode` *(extends `ExpressionNode`)* [Section titled “CoYieldExpressionNode (extends ExpressionNode)”](#coyieldexpressionnode-extends-expressionnode) *kind discriminant: `"CoYieldExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `CommaExpressionNode` *(extends `ExpressionNode`)* [Section titled “CommaExpressionNode (extends ExpressionNode)”](#commaexpressionnode-extends-expressionnode) *kind discriminant: `"CommaExpression"`* | Field | Type | | ----- | ------- | | `lhs` | `Node?` | | `rhs` | `Node?` | *** ## `CompoundRequirementNode` *(extends `RequirementNode`)* [Section titled “CompoundRequirementNode (extends RequirementNode)”](#compoundrequirementnode-extends-requirementnode) *kind discriminant: `"CompoundRequirement"`* | Field | Type | | ---------------------- | --------------- | | `expression` | `Node?` | | `isNoexcept` | `boolean` | | `returnTypeConstraint` | `TypeSignature` | *** ## `ConcatenatedStringNode` *(extends `LiteralNode`)* [Section titled “ConcatenatedStringNode (extends LiteralNode)”](#concatenatedstringnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `parts` | `LiteralNode[]` | | `_litSubtype` | `number` (always `3`) | *** ## `ConditionalExpressionNode` *(extends `ExpressionNode`)* [Section titled “ConditionalExpressionNode (extends ExpressionNode)”](#conditionalexpressionnode-extends-expressionnode) *kind discriminant: `"ConditionalExpression"`* | Field | Type | | ----------- | ------- | | `condition` | `Node?` | | `thenExpr` | `Node?` | | `elseExpr` | `Node?` | *** ## `DecltypeExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “DecltypeExpressionNode (extends IntrospectionExpressionNode)”](#decltypeexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"DecltypeExpression"`* *No own serialized fields.* *** ## `DeleteExpressionNode` *(extends `ExpressionNode`)* [Section titled “DeleteExpressionNode (extends ExpressionNode)”](#deleteexpressionnode-extends-expressionnode) *kind discriminant: `"DeleteExpression"`* | Field | Type | | --------- | --------- | | `isArray` | `boolean` | | `operand` | `Node?` | *** ## `DesignatedInitializerNode` *(extends `ExpressionNode`)* [Section titled “DesignatedInitializerNode (extends ExpressionNode)”](#designatedinitializernode-extends-expressionnode) *kind discriminant: `"DesignatedInitializer"`* | Field | Type | | ------------ | ----------------- | | `designator` | `IdentifierNode?` | | `value` | `Node?` | *** ## `FieldExpressionNode` *(extends `ExpressionNode`)* [Section titled “FieldExpressionNode (extends ExpressionNode)”](#fieldexpressionnode-extends-expressionnode) *kind discriminant: `"FieldExpression"`* | Field | Type | | -------- | ----------------- | | `object` | `Node?` | | `op` | `string` | | `member` | `IdentifierNode?` | *** ## `FoldExpressionNode` *(extends `ExpressionNode`)* [Section titled “FoldExpressionNode (extends ExpressionNode)”](#foldexpressionnode-extends-expressionnode) *kind discriminant: `"FoldExpression"`* | Field | Type | | -------------- | -------- | | `op` | `string` | | `leftOperand` | `Node?` | | `rightOperand` | `Node?` | *** ## `InitializerListNode` *(extends `ExpressionNode`)* [Section titled “InitializerListNode (extends ExpressionNode)”](#initializerlistnode-extends-expressionnode) *kind discriminant: `"InitializerList"`* | Field | Type | | ---------- | -------- | | `elements` | `Node[]` | *** ## `IntrospectionExpressionNode` *(extends `ExpressionNode`)* [Section titled “IntrospectionExpressionNode (extends ExpressionNode)”](#introspectionexpressionnode-extends-expressionnode) | Field | Type | | ------------- | --------------- | | `isTypeForm` | `boolean` | | `typeOperand` | `TypeSignature` | | `exprOperand` | `Node?` | *** ## `LambdaExpressionNode` *(extends `ExpressionNode`)* [Section titled “LambdaExpressionNode (extends ExpressionNode)”](#lambdaexpressionnode-extends-expressionnode) *kind discriminant: `"LambdaExpression"`* | Field | Type | | -------------------- | --------------------- | | `captureDefault` | `string` | | `captures` | `LambdaCaptureItem[]` | | `templateParameters` | `TemplateParameter[]` | | `parameters` | `FunctionParameter[]` | | `trailingReturn` | `TypeSignature` | | `isMutable` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `body` | `BlockNode?` | *** ## `LiteralNode` *(extends `ExpressionNode`)* [Section titled “LiteralNode (extends ExpressionNode)”](#literalnode-extends-expressionnode) *kind discriminant: `"Literal"`* | Field | Type | | ----------- | -------- | | `udlSuffix` | `string` | *** ## `NewExpressionNode` *(extends `ExpressionNode`)* [Section titled “NewExpressionNode (extends ExpressionNode)”](#newexpressionnode-extends-expressionnode) *kind discriminant: `"NewExpression"`* | Field | Type | | ----------------- | --------------- | | `typeSignature` | `TypeSignature` | | `isArray` | `boolean` | | `arraySize` | `Node?` | | `placementArgs` | `Node[]` | | `constructorArgs` | `Node[]` | *** ## `NoexceptExpressionNode` *(extends `ExpressionNode`)* [Section titled “NoexceptExpressionNode (extends ExpressionNode)”](#noexceptexpressionnode-extends-expressionnode) *kind discriminant: `"NoexceptExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `NullptrLiteralNode` *(extends `LiteralNode`)* [Section titled “NullptrLiteralNode (extends LiteralNode)”](#nullptrliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `_litSubtype` | `number` (always `6`) | *** ## `NumberLiteralNode` *(extends `LiteralNode`)* [Section titled “NumberLiteralNode (extends LiteralNode)”](#numberliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `base` | `string` | | `category` | `string` | | `value` | `string` | | `suffix` | `string` | | `_litSubtype` | `number` (always `1`) | *** ## `ParenthesizedExpressionNode` *(extends `ExpressionNode`)* [Section titled “ParenthesizedExpressionNode (extends ExpressionNode)”](#parenthesizedexpressionnode-extends-expressionnode) *kind discriminant: `"ParenthesizedExpression"`* | Field | Type | | ------- | ------- | | `inner` | `Node?` | *** ## `PointerExpressionNode` *(extends `ExpressionNode`)* [Section titled “PointerExpressionNode (extends ExpressionNode)”](#pointerexpressionnode-extends-expressionnode) *kind discriminant: `"PointerExpression"`* | Field | Type | | --------- | -------- | | `op` | `string` | | `operand` | `Node?` | *** ## `RequirementNode` *(extends `Node`)* [Section titled “RequirementNode (extends Node)”](#requirementnode-extends-node) *No own serialized fields.* *** ## `RequiresExpressionNode` *(extends `ExpressionNode`)* [Section titled “RequiresExpressionNode (extends ExpressionNode)”](#requiresexpressionnode-extends-expressionnode) *kind discriminant: `"RequiresExpression"`* | Field | Type | | -------------- | --------------------- | | `parameters` | `FunctionParameter[]` | | `requirements` | `RequirementNode[]` | *** ## `SimpleRequirementNode` *(extends `RequirementNode`)* [Section titled “SimpleRequirementNode (extends RequirementNode)”](#simplerequirementnode-extends-requirementnode) *kind discriminant: `"SimpleRequirement"`* | Field | Type | | ------------ | ------- | | `expression` | `Node?` | *** ## `SizeofExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “SizeofExpressionNode (extends IntrospectionExpressionNode)”](#sizeofexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"SizeofExpression"`* *No own serialized fields.* *** ## `StringLiteralNode` *(extends `LiteralNode`)* [Section titled “StringLiteralNode (extends LiteralNode)”](#stringliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | --------------------- | | `encoding` | `string` | | `isRaw` | `boolean` | | `isMultiLine` | `boolean` | | `value` | `string` | | `_litSubtype` | `number` (always `2`) | *** ## `SubscriptExpressionNode` *(extends `ExpressionNode`)* [Section titled “SubscriptExpressionNode (extends ExpressionNode)”](#subscriptexpressionnode-extends-expressionnode) *kind discriminant: `"SubscriptExpression"`* | Field | Type | | -------- | ------- | | `object` | `Node?` | | `index` | `Node?` | *** ## `ThisExpressionNode` *(extends `ExpressionNode`)* [Section titled “ThisExpressionNode (extends ExpressionNode)”](#thisexpressionnode-extends-expressionnode) *kind discriminant: `"ThisExpression"`* *No own serialized fields.* *** ## `ThrowExpressionNode` *(extends `ExpressionNode`)* [Section titled “ThrowExpressionNode (extends ExpressionNode)”](#throwexpressionnode-extends-expressionnode) *kind discriminant: `"ThrowExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `TypeRequirementNode` *(extends `RequirementNode`)* [Section titled “TypeRequirementNode (extends RequirementNode)”](#typerequirementnode-extends-requirementnode) *kind discriminant: `"TypeRequirement"`* | Field | Type | | ---------- | --------------- | | `typeName` | `TypeSignature` | *** ## `TypeidExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “TypeidExpressionNode (extends IntrospectionExpressionNode)”](#typeidexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"TypeidExpression"`* *No own serialized fields.* *** ## `UnaryExpressionNode` *(extends `ExpressionNode`)* [Section titled “UnaryExpressionNode (extends ExpressionNode)”](#unaryexpressionnode-extends-expressionnode) *kind discriminant: `"UnaryExpression"`* | Field | Type | | --------- | -------- | | `op` | `string` | | `operand` | `Node?` | *** ## `UpdateExpressionNode` *(extends `ExpressionNode`)* [Section titled “UpdateExpressionNode (extends ExpressionNode)”](#updateexpressionnode-extends-expressionnode) *kind discriminant: `"UpdateExpression"`* | Field | Type | | ---------- | --------- | | `op` | `string` | | `isPrefix` | `boolean` | | `operand` | `Node?` | *** # Preprocessor Nodes ## `FunctionLikeMacroNode` *(extends `PreprocessorNode`)* [Section titled “FunctionLikeMacroNode (extends PreprocessorNode)”](#functionlikemacronode-extends-preprocessornode) *kind discriminant: `"FunctionLikeMacro"`* | Field | Type | | ------------ | ------------------ | | `name` | `string` | | `body` | `string` | | `parameters` | `MacroParameter[]` | *** ## `IncludeNode` *(extends `PreprocessorNode`)* [Section titled “IncludeNode (extends PreprocessorNode)”](#includenode-extends-preprocessornode) *kind discriminant: `"IncludeDirective"`* | Field | Type | | ---------- | --------- | | `path` | `string` | | `isSystem` | `boolean` | *** ## `ObjectLikeMacroNode` *(extends `PreprocessorNode`)* [Section titled “ObjectLikeMacroNode (extends PreprocessorNode)”](#objectlikemacronode-extends-preprocessornode) *kind discriminant: `"ObjectLikeMacro"`* | Field | Type | | ------ | -------- | | `name` | `string` | | `body` | `string` | *** ## `PragmaNode` *(extends `PreprocessorNode`)* [Section titled “PragmaNode (extends PreprocessorNode)”](#pragmanode-extends-preprocessornode) *kind discriminant: `"Pragma"`* | Field | Type | | --------------- | ---------- | | `pragmaKind` | `string` | | `rawArg` | `string` | | `packAction` | `string` | | `packAlignment` | `number` | | `packLabel` | `string` | | `warningAction` | `string` | | `warningCodes` | `number[]` | | `messageText` | `string` | | `regionName` | `string` | | `stdcSetting` | `string` | | `stdcValue` | `string` | | `commentType` | `string` | | `commentValue` | `string` | *** # Statement Nodes ## `BlockNode` *(extends `StatementNode`)* [Section titled “BlockNode (extends StatementNode)”](#blocknode-extends-statementnode) *kind discriminant: `"Block"`* | Field | Type | | ------------ | -------- | | `statements` | `Node[]` | *** ## `BreakNode` *(extends `StatementNode`)* [Section titled “BreakNode (extends StatementNode)”](#breaknode-extends-statementnode) *kind discriminant: `"BreakStatement"`* *No own serialized fields.* *** ## `CaseLabelNode` *(extends `StatementNode`)* [Section titled “CaseLabelNode (extends StatementNode)”](#caselabelnode-extends-statementnode) *kind discriminant: `"CaseLabel"`* | Field | Type | | ------------ | -------- | | `value` | `Node?` | | `statements` | `Node[]` | *** ## `CatchClauseNode` *(extends `StatementNode`)* [Section titled “CatchClauseNode (extends StatementNode)”](#catchclausenode-extends-statementnode) *kind discriminant: `"CatchClause"`* | Field | Type | | ------------ | ------------ | | `isCatchAll` | `boolean` | | `parameter` | `Node?` | | `body` | `BlockNode?` | *** ## `ContinueNode` *(extends `StatementNode`)* [Section titled “ContinueNode (extends StatementNode)”](#continuenode-extends-statementnode) *kind discriminant: `"ContinueStatement"`* *No own serialized fields.* *** ## `DefaultLabelNode` *(extends `StatementNode`)* [Section titled “DefaultLabelNode (extends StatementNode)”](#defaultlabelnode-extends-statementnode) *kind discriminant: `"DefaultLabel"`* | Field | Type | | ------------ | -------- | | `statements` | `Node[]` | *** ## `DoWhileNode` *(extends `StatementNode`)* [Section titled “DoWhileNode (extends StatementNode)”](#dowhilenode-extends-statementnode) *kind discriminant: `"DoWhileStatement"`* | Field | Type | | ----------- | ------------ | | `condition` | `Node?` | | `body` | `BlockNode?` | *** ## `ForNode` *(extends `StatementNode`)* [Section titled “ForNode (extends StatementNode)”](#fornode-extends-statementnode) *kind discriminant: `"ForStatement"`* | Field | Type | | ----------- | ------------ | | `init` | `Node?` | | `condition` | `Node?` | | `update` | `Node?` | | `body` | `BlockNode?` | *** ## `ForRangeNode` *(extends `StatementNode`)* [Section titled “ForRangeNode (extends StatementNode)”](#forrangenode-extends-statementnode) *kind discriminant: `"ForRangeStatement"`* | Field | Type | | --------------- | ------------ | | `initStatement` | `Node?` | | `loopVar` | `Node?` | | `range` | `Node?` | | `body` | `BlockNode?` | *** ## `GotoNode` *(extends `StatementNode`)* [Section titled “GotoNode (extends StatementNode)”](#gotonode-extends-statementnode) *kind discriminant: `"GotoStatement"`* | Field | Type | | ------- | -------- | | `label` | `string` | *** ## `IfNode` *(extends `StatementNode`)* [Section titled “IfNode (extends StatementNode)”](#ifnode-extends-statementnode) *kind discriminant: `"IfStatement"`* | Field | Type | | --------------- | ------------ | | `isConstexpr` | `boolean` | | `initStatement` | `Node?` | | `condition` | `Node?` | | `thenBody` | `BlockNode?` | | `elseBody` | `Node?` | *** ## `LabeledStatementNode` *(extends `StatementNode`)* [Section titled “LabeledStatementNode (extends StatementNode)”](#labeledstatementnode-extends-statementnode) *kind discriminant: `"LabeledStatement"`* | Field | Type | | ------- | -------- | | `label` | `string` | | `body` | `Node?` | *** ## `ReturnNode` *(extends `StatementNode`)* [Section titled “ReturnNode (extends StatementNode)”](#returnnode-extends-statementnode) *kind discriminant: `"ReturnStatement"`* | Field | Type | | ------- | ------- | | `value` | `Node?` | *** ## `SwitchNode` *(extends `StatementNode`)* [Section titled “SwitchNode (extends StatementNode)”](#switchnode-extends-statementnode) *kind discriminant: `"SwitchStatement"`* | Field | Type | | --------------- | -------- | | `initStatement` | `Node?` | | `condition` | `Node?` | | `cases` | `Node[]` | *** ## `TryNode` *(extends `StatementNode`)* [Section titled “TryNode (extends StatementNode)”](#trynode-extends-statementnode) *kind discriminant: `"TryStatement"`* | Field | Type | | -------------- | ------------------- | | `body` | `BlockNode?` | | `catchClauses` | `CatchClauseNode[]` | *** ## `WhileNode` *(extends `StatementNode`)* [Section titled “WhileNode (extends StatementNode)”](#whilenode-extends-statementnode) *kind discriminant: `"WhileStatement"`* | Field | Type | | --------------- | ------------ | | `initStatement` | `Node?` | | `condition` | `Node?` | | `body` | `BlockNode?` | *** # Rule Anatomy > The files that make up a codegen rule, config, preamble, grouping, and handler, and the contract each must satisfy. A rule lives in `//` (the default `` is `.codegen/rules`, override with `-r`). The directory contains: The directory name *is* the rule name — it’s how the rule is referenced on the CLI (`-a RuleName`) and as the C++ attribute (`[[codegen::RuleName]]`). Companion files are named by their *role*, not the rule, so a directory always looks the same. ## Namespaced rules [Section titled “Namespaced rules”](#namespaced-rules) A rule can also live one level deeper, under a **vendor** directory: The rule is simple: **a directory holding a `transform.luau` is a rule; one that does not is a namespace, and its children are rules.** Flat is still the default and nothing about an existing tree changes — namespacing exists so two vendors can both publish a `ToString` and you can use both at once. `-a acme::ToString` is accepted too, so you can paste the attribute spelling straight onto the command line. A bare `-a ToString` that two namespaces both provide is an error listing the candidates, never a silent pick. A vendor attribute needs a compiler suppression `[[acme::ToString]]` is an unknown attribute namespace, exactly as `[[codegen::…]]` is, and compilers warn about it by default: ```cpp // clang: -Wno-unknown-attributes // gcc: #pragma GCC diagnostic ignored_attributes "acme::" struct [[acme::ToString]] Color { int r, g, b; }; ``` ## Components [Section titled “Components”](#components) A **component** is a reusable Luau module a rule reaches with `require`: transform.luau ```lua local typemap = require("acme/typemap") return function(input) local node = json.decode(input) return json.encode({ source = typemap.render(node) }) end ``` A component `return`s its value — conventionally a table of functions. It is evaluated the first time something requires it, and it can use every global the environment provides. `require` works in `transform.luau`, `preamble.luau` and `grouping.luau` alike. Components install from the [rules registry](/rules/registry/) into `.codegen/shared//`. See [`require`](/reference/luau-globals/#require) for the exact contract. ## The flat `shared/` prelude [Section titled “The flat shared/ prelude”](#the-flat-shared-prelude) Files directly under `/../shared/` (e.g. `.codegen/shared/`) keep their original behaviour: every `.luau` file there is concatenated in alphabetical filename order and prepended to each rule’s transform at execution time, and a `shared/.env` file becomes the baseline dotenv. By default every shared script is included; rules can narrow the set with `shared.include` / `shared.exclude` globs in their config (see [Config Schema Reference](/reference/config-schema/#shared)). **Namespaced components are never concatenated** — they are reached only through `require`. That is what makes installing one safe: it cannot perturb what any existing rule already sees. A rule that declares [`uses`](/reference/config-schema/#uses) opts out of the flat prelude entirely. A rule that names what it needs should not also absorb whatever else happens to be sitting in your tree, which is exactly the isolation a published rule wants. ## `config.yaml` [Section titled “config.yaml”](#configyaml) Controls engine behaviour for this rule. Minimum viable config: ```yaml version: 1 output: language: cpp ``` Full schema: see [Config Schema Reference](/reference/config-schema/). ## `transform.luau` — handler [Section titled “transform.luau — handler”](#transformluau--handler) Receives one matched entity as JSON, returns JSON describing the output. **Contract:** * Input: a JSON-encoded entity payload (the codex AST node with heavy fields stripped, plus synthetic `_namespaces`, `_registryId`, and `_outputPath`, plus a `params` key copied from the rule config). Decode with `json.decode(input)`. * `_outputPath` is the post-grouping output path (the `defaultPath` if no grouping script exists, or the value returned by grouping). * Output: `json.encode({ source = "...", inline = { {source = "..."}, ... }, includes = { "", "\"my/header.h\"" } })`. The `inline` and `includes` keys are optional. * A Lua `error()` skips this entity, emits `E006`, and continues. An *internal* LuaU error is fatal (exit 2). * The script is a pure function: no I/O, no globals carried between invocations (each call gets a fresh execution). ## `preamble.luau` — preamble [Section titled “preamble.luau — preamble”](#preambleluau--preamble) Required. Runs **once per unique (rule, outputFile) pair**; the result is later deduplicated by content across rules contributing to the same output file. **Contract:** * Input: a JSON-encoded context object: `{ language: "cpp", outputFile: "path/to/output.cpp", ruleName: "", params: , entities: [{registryId, qualifiedName, structName, namespaces, inputFile, includes}, ...] }`. * `entities` contains every entity the handler produced output for that’s bound to this output file, with handler-emitted `includes` exposed for conditional preamble logic. * Output: a raw string (not JSON), prepended to the output file once per `(rule, outputFile)` pair (deduplicated by content across rules contributing to the same file). Return `""` if you don’t need a preamble — but the file must still exist. * A Lua `error()` here aborts the entire run with `E007` (exit 1). ## `grouping.luau` — grouping [Section titled “grouping.luau — grouping”](#groupingluau--grouping) Optional. Runs **once per rule, before handlers fire**, with the list of candidate entities. Used to override each entity’s default output path (the 1:1 routing derived from the input header). **Contract:** * Input: `json.decode(input)` returns `{ params = , entities = [...] }`. Each entity has only the summary fields: * `registryId` (integer) * `qualifiedName` (string, e.g. `"app::Color"`) * `structName` (string, unqualified) * `namespaces` (array of strings) * `inputFile` (string, the source header path) * `defaultPath` (string, the 1:1 output path derived from the input header) * Output: `json.encode({ [""] = "path/to/output.cpp", ... })`. Keys are `registryId`s **stringified**; values are output paths. * Paths are resolved against the project root (the process working directory). A path that escapes the project root is rejected with `E005` and the entity is skipped. An entity missing from the returned map emits `E004` and is skipped. The run continues in both cases. ## `.env` — per-rule dotenv [Section titled “.env — per-rule dotenv”](#env--per-rule-dotenv) Optional. Standard dotenv format (`KEY=VALUE`, `#` comments, optional surrounding quotes on the value). Loaded automatically when present; entries override the shared `/../shared/.env` baseline. Surfaced to scripts via the env module when env capability is active. ## Triggering a rule [Section titled “Triggering a rule”](#triggering-a-rule) Two trigger mechanisms can fire a rule: **Attribute annotation** at declaration sites. Honoured for any node kind in `node_kinds` (default: `Struct` only): ```cpp struct [[codegen::RuleName]] MyStruct { /* ... */ }; ``` **Anchor comment** in the source. Useful for entities the C++ grammar doesn’t allow attributes on (enums, free functions in some contexts) and as the injection site for `inline` patches: ```cpp // [[codegen::generated::RuleName::namespace::EntityName]] enum class MyEnum { A, B, C }; ``` The third segment of the anchor is the *qualified name* of the target entity (namespace path included). Anchors are scanned per-file and matched against the AST to find the corresponding entity. See [Inline Injection](/rules/inline-injection/) for the on-disk anchor lifecycle. ## Published rules [Section titled “Published rules”](#published-rules) A rule shared through the [rules registry](/rules/registry/) adds three files to the directory above — `rule.json`, `README.md`, and `LICENSE`. The engine ignores all three, so a published rule and a hand-written one are the same thing to `codegen`; the extra files carry identity, version, and attribution for humans. Key Takeaways * Up to five files in a rule directory: only `transform.luau` and `preamble.luau` are required (missing either is `E003` and fails the run); `config.yaml`, `grouping.luau` and `.env` are optional. * Handler is a pure function: JSON in, JSON out (`source`, optional `inline`, optional `includes`). Receives `_outputPath` (post-grouping route). * Preamble runs once per (rule, outputFile) pair; its return is a raw string, deduplicated by content per output file. Receives the entities bound to that file. * Grouping runs once per rule, before handlers, and rewrites output paths via a stringified-`registryId` map. * Shared scripts in `/../shared/` and a shared `.env` are loaded automatically. # Grouping Logic > Reference for grouping.luau scripts, input format, output contract, and routing patterns. See [Grouping & Fan-in](/concepts/grouping/) for the conceptual overview. This page is the contract reference. ## When it runs [Section titled “When it runs”](#when-it-runs) The grouping script runs **once per rule, before handler invocations**. The engine collects all candidate entities, calls grouping with that summary list, and uses the returned map to override each entity’s default output path. The handler then runs with the post-grouping paths available as `_outputPath`. This means grouping influences output routing but cannot access handler results. Grouping is purely a path-routing pass. ## Input [Section titled “Input”](#input) ```json { "params": { /* rule.params, copied from config.yaml */ }, "entities": [ { "registryId": 42, "qualifiedName": "myns::MyStruct", "structName": "MyStruct", "namespaces": ["myns"], "inputFile": "include/myns/my_struct.hpp", "defaultPath": "include/myns/my_struct.g.cpp" } ] } ``` These are the **only** per-entity fields exposed to grouping. There is no `attributes` array, no full AST node, no `kind`, and no handler output (that hasn’t run yet). If you need the full AST for routing decisions, look the entity up via `get_node(entity.registryId)` from inside the handler instead, and route in grouping using only the summary fields. ## Output [Section titled “Output”](#output) ```json { "42": "generated/myns/output.cpp" } ``` * Keys are `registryId` values, **stringified** (`tostring(entity.registryId)` from Lua). * Values are output paths, resolved relative to the project root (the process working directory). * A path that resolves outside the project root emits `E005` and the entity is skipped. * An entity missing from the returned map emits `E004` and is skipped. Other entities continue normally; the run does not abort. ## Routing patterns [Section titled “Routing patterns”](#routing-patterns) | Pattern | Description | Example expression | | --------------- | ------------------------------------------------- | --------------------------------------------------------------------- | | 1:1 (default) | Entity → file derived from input header | omit `grouping.luau` | | Fan-in | All entities → one file | `"generated/all.md"` | | Namespace-based | Entity → file named after first namespace segment | `entity.namespaces[1] .. "/registry.cpp"` | | Path-based | Internal vs public split by `inputFile` substring | `entity.inputFile:find("internal/") and "internal.md" or "public.md"` | | Param-driven | Output dir varies by `params.target` | `data.params.target .. "/" .. entity.structName .. ".ts"` | ## Worked example: namespace fan-in [Section titled “Worked example: namespace fan-in”](#worked-example-namespace-fan-in) grouping.luau ```lua return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local result = {} for _, ent in ipairs(data.entities) do local top = ent.namespaces[1] or "global" result[tostring(ent.registryId)] = "generated/" .. top .. "/registry.cpp" end return json.encode(result) end ``` ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) Within a single output file, entities appear in the order they were matched. Matching follows the order of headers passed to the engine, then declaration order within each header. If you need a different order (alphabetical by qualified name, for example), sort the entity list in the grouping script before assigning paths. Output assembly visits the entities in the order they appear in the post-grouping entity list. Key Takeaways * Grouping runs before handlers, with a summary view of each candidate entity (6 fields) plus `params`. * Output is a ` → path` map; missing keys skip that entity. * Paths must stay under the project root; escapes are rejected with `E005`. * Grouping cannot access handler output (not run yet); use the handler (with `get_node`) for AST-level routing decisions. # Inline Injection > How the inline field injects declarations back into original C++ headers at anchor comment sites. ## The problem it solves [Section titled “The problem it solves”](#the-problem-it-solves) For rules that generate function implementations (`.g.cpp`), you typically also need the declaration in the original header. Without inline injection you must add the declaration manually, which defeats the purpose of generation and breaks whenever the generated signature changes. Inline injection lets the rule emit both the implementation and the declaration, keeping them in sync automatically. ## How it works [Section titled “How it works”](#how-it-works) The `inline` field in the handler return value is a list of objects: ```lua return json.encode({ source = "std::string_view toString(Color e) { ... }", inline = { { source = "std::string_view toString(Color e);" } } }) ``` Each item supplies one chunk of source text. The engine looks for the anchor matching this entity’s `::` and writes one paired `:begin]]/:end]]` block per item at that site, in the order returned. N items ⇒ N consecutive blocks (see [Multiple `inline` items](#multiple-inline-items-per-entity) below). ## Anchor formats [Section titled “Anchor formats”](#anchor-formats) Two on-disk forms are recognised. You can write either form by hand; the engine will rewrite to the **paired** form on first run. ### Bare anchor (one line, write-by-hand) [Section titled “Bare anchor (one line, write-by-hand)”](#bare-anchor-one-line-write-by-hand) ```cpp // [[codegen::generated::RuleName::qualifiedName]] enum class Color { Red, Green, Blue }; ``` The anchor must be on its own line, immediately above the declaration. After a successful run, the engine **expands** this single line into a paired block (see below) — bare anchors don’t survive the first regeneration. ### Paired anchor (engine-managed) [Section titled “Paired anchor (engine-managed)”](#paired-anchor-engine-managed) ```cpp // [[codegen::generated::RuleName::qualifiedName:begin]] std::string_view toString(Color e); // [[codegen::generated::RuleName::qualifiedName:end]] enum class Color { Red, Green, Blue }; ``` Subsequent regenerations rewrite the **whole contiguous run** of `:begin]]/:end]]` blocks for this key, replacing it with one block per current `inline` item. Item count can grow or shrink between runs — the engine resizes the run accordingly. Within each block, the markers stay; only the inner text moves. Diff noise is bounded to the run. ## Multiple `inline` items per entity [Section titled “Multiple inline items per entity”](#multiple-inline-items-per-entity) When `inline` has N items, the engine emits N consecutive `:begin]]/:end]]` blocks at the anchor site, one block per item, in the order returned by the handler. For example: ```lua inline = { { source = "void start();" }, { source = "void stop();" }, { source = "bool isRunning();" }, } ``` writes: ```cpp // [[codegen::generated::RuleName::qualifiedName:begin]] void start(); // [[codegen::generated::RuleName::qualifiedName:end]] // [[codegen::generated::RuleName::qualifiedName:begin]] void stop(); // [[codegen::generated::RuleName::qualifiedName:end]] // [[codegen::generated::RuleName::qualifiedName:begin]] bool isRunning(); // [[codegen::generated::RuleName::qualifiedName:end]] ``` On re-run with a different item count, the run is collapsed or expanded in place. Reordering items in the script reorders the blocks on disk. If you need code at *different* sites in the source, use multiple anchors and multiple matching entities — one anchor per qualified name. Future: named slots? We’re considering an additive `inline = { slotName = "..." }` form that would let one entity target multiple distinct anchor sites — e.g. `// [[codegen::generated::Rule::Qual::publicApi]]` and `// [[codegen::generated::Rule::Qual::implDetails]]` — without forcing you to split the entity. If that fits your use case (or if it doesn’t), tell us in the [feedback form](https://tally.so/r/1AbdKM). ## What gets written [Section titled “What gets written”](#what-gets-written) For an entity with N `inline` items, the engine writes N consecutive blocks of the form: ```plaintext // [[codegen::generated::RuleName::qualifiedName:begin]] // [[codegen::generated::RuleName::qualifiedName:end]] ``` The C++ declaration the anchor sits above is left untouched — the engine only modifies the anchor block itself. Files are written via a `.codegen_tmp` rename for atomicity. If an entity returns `inline` but no anchor exists in the source, the engine emits diagnostic `W003` and skips the injection (the `.g.cpp` is still written). The run between the first `:begin]]` and last `:end]]` for a given key is treated as engine-owned: do not write hand-edits between markers. Anything you put there will be overwritten on the next run. ## When to omit `inline` [Section titled “When to omit inline”](#when-to-omit-inline) Rules that only emit `.g.cpp` content can omit the field or return an empty list: ```lua return json.encode({ source = impl }) -- or return json.encode({ source = impl, inline = {} }) ``` Key Takeaways * Bare anchors are write-by-hand convenience; the engine rewrites them on first run into one paired block per `inline` item. * N items ⇒ N consecutive `:begin]]/:end]]` blocks. The contract is “ordered list of injected blocks”, not “concatenated single block”. * Re-runs grow or shrink the run in place to match the current item count and order. * Missing anchors produce diagnostic `W003`, not a fatal error. # LuaU Sandbox > What the LuaU VM provides to rule scripts, what it withholds, and how to reason about rule safety. codegen embeds the LuaU VM (Roblox’s typed Lua variant with a JIT compiler) as the rule execution environment. Each rule invocation gets a fresh execution context with no shared state between calls. **One shared surface: dotenv.** Every rule’s `.env`, the shared `.env`, and any `--env` entries are merged into a **single process-wide map** before execution. A rule that declares `permissions.env` therefore sees keys contributed by *every other rule* in the same run, not just its own. Do not put a secret in a rule’s `.env` on the assumption that a co-running third-party rule cannot read it. ## What rules always have access to [Section titled “What rules always have access to”](#what-rules-always-have-access-to) | API | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `json.encode` / `json.decode` | JSON serialisation | | Native introspection API | `get_node`, `find_struct_by_name`, `struct_has_attribute`, `struct_has_method`, `struct_has_member`, `struct_has_default_constructor`, `get_base_classes`, `get_definition_header` — see [LuaU Globals](/reference/luau-globals/) | | LuaU stdlib subset | `table`, `string`, `math`, `ipairs`, `pairs`, `tostring`, `tonumber`, `error`, `pcall`, `xpcall`, `type`, `select`, `assert` | ## What rules cannot access by default [Section titled “What rules cannot access by default”](#what-rules-cannot-access-by-default) | Capability | Status | | ------------------------------------ | ----------------------------------------------------- | | File system (`io.open`) | Blocked — no `io` library | | Network | Blocked — opt in via `permissions.http.allowlist` | | OS environment variables | Blocked — opt in via `permissions.env.os_allowlist` | | `require` / module loading | Blocked — no module system in the sandbox | | `loadstring` / `loadfile` / FFI | Blocked | | Global state across rule invocations | Impossible — each call gets fresh state | | Inter-rule communication | No shared Lua state — but see the dotenv caveat below | | Process spawning | Blocked | ## Why LuaU specifically [Section titled “Why LuaU specifically”](#why-luau-specifically) * **Typed:** LuaU adds gradual typing, allowing rule authors to annotate their scripts for better IDE support. * **Fast:** The JIT compiler keeps tight loops over large entity lists performant. * **Sandboxable:** The LuaU VM is designed for embedding with a restricted environment; the engine replaces the global table entirely. * **Auditable:** LuaU is a small language. A non-expert can read and audit a rule script in minutes. ## Error handling [Section titled “Error handling”](#error-handling) A handler that calls `error()` (or hits a Lua runtime error like indexing nil) skips that entity, emits `E006`, and the engine continues with remaining entities. An *internal* engine error (failed VM init, etc.) is fatal and exits 2. A preamble that errors aborts the whole run with `E007` (exit 1) — the preamble runs once per `(rule, outputFile)` pair after handlers, and there’s no per-entity granularity to fall back to. A grouping script that errors emits `E008` and the engine falls back to default 1:1 routing for that rule’s entities. ```lua return function(input) local ok, node = pcall(json.decode, input) if not ok or not node then error("failed to decode input: " .. tostring(node)) end -- ... end ``` Validate defensively at the top of every handler. The AST schema can evolve, and future engine versions may pass additional fields or change optional field presence. ## HTTP permission [Section titled “HTTP permission”](#http-permission) Add an allowlist to the rule’s config: ```yaml permissions: http: allowlist: - "schema-registry.internal.example.com" ``` When the allowlist is non-empty, the engine injects `http.get(url)` into the sandbox. Each entry is matched against the URL’s **host** — exact (`example.com`), wildcard (`*.example.com`, subdomains only), or `*` for any host. A URL whose host is not on the allowlist raises an error and emits `E010`. Never add a domain to the allowlist for a rule you didn’t write and audit yourself — an allowlisted host gives that rule read access to your AST data. Wildcards (`*.example.com`) widen the trust boundary to every subdomain; use exact hosts when you can. ## Env permission [Section titled “Env permission”](#env-permission) Add an OS env allowlist or a dotenv file: ```yaml permissions: env: os_allowlist: - "BUILD_CHANNEL" ``` Or drop a `.env` next to the rule, or a shared `/../shared/.env`. Either path activates the env capability and exposes the values via the env module. See [Permissions](/rules/permissions/) for surface details. Key Takeaways * Each rule invocation gets fresh state — no shared globals, no inter-rule communication. * Always available: `json` + the native introspection API + a LuaU stdlib subset. * Network and OS env access are opt-in via `permissions:` in the rule config. * Handler errors skip the entity; preamble errors abort the run; grouping errors fall back to default routing. # Permissions Model > How to grant rules access to HTTP, OS environment variables, and dotenv values, and why the default is to grant nothing. ## Default: no I/O capabilities [Section titled “Default: no I/O capabilities”](#default-no-io-capabilities) Every rule runs with no I/O capabilities by default. The LuaU sandbox provides `json`, the native introspection API, and a stdlib subset (see [LuaU Globals](/reference/luau-globals/)). No filesystem, no network, no env, no process execution. Two opt-in capabilities can be granted in the rule’s config: HTTP and env. ## HTTP [Section titled “HTTP”](#http) config.yaml ```yaml version: 1 output: language: cpp permissions: http: allowlist: - "schema-registry.example.com" - "*.api.example.com" ``` When the allowlist is non-empty, the engine exposes `http.get(url)` to all of the rule’s scripts. Each entry is matched against the URL’s **host component** — exact hosts (`example.com`), wildcard subdomains (`*.example.com`, which does not match the apex), or `*` for any host. Calls whose host is not on the allowlist raise an error and emit `E010`. The CLI flag `--allow-http ` (repeatable) appends entries at run time without editing the config. Allowlist a domain only for rules you’ve written or audited yourself — an allowlisted host gives that rule read access to your AST data and lets it exfiltrate it. Wildcards (`*.example.com`) widen the trust boundary to anything served under the suffix; use exact hosts when you can. ## Env [Section titled “Env”](#env) Three independent sources can populate the env capability: **OS env allowlist** — variables read directly from the host’s environment: ```yaml permissions: env: os_allowlist: - "BUILD_CHANNEL" - "GIT_COMMIT" ``` The CLI flag `--allow-env ` (repeatable) appends entries at run time. **Per-rule dotenv** at `//.env`. Standard format: `KEY=VALUE`, optional surrounding quotes, full-line `#` comments, blank lines ignored. A trailing `#` after a value on a `KEY=VALUE` line is **not** stripped — it stays part of the value. Loaded automatically when present; no allowlist required. **Shared dotenv** at `/../shared/.env` (e.g. `.codegen/shared/.env`). Loaded as a baseline; per-rule entries with the same key override. The CLI flag `--env KEY=VALUE` (repeatable) overrides individual dotenv entries at run time. The merge order, lowest precedence to highest: shared `.env` → per-rule `.env` → CLI `--env`. The CLI `--allow-env` allowlist is unioned across sources. When any of these is non-empty the engine activates the env capability and exposes the values via the env module to the rule’s scripts. ## What the model intentionally excludes [Section titled “What the model intentionally excludes”](#what-the-model-intentionally-excludes) | Capability | Status | | ------------------------------------ | ------------------------------------------------------------ | | Direct filesystem read | Not provided | | Filesystem write | Not provided (engine writes outputs; rules only return text) | | Process spawning | Not provided, not planned | | Outbound HTTP methods other than GET | Not provided | If you need data from the filesystem, point an `http.get(...)` at a local file server, or pipe it through dotenv. The sandbox is intentionally narrow. Key Takeaways * Default: no I/O. HTTP and env are opt-in via `permissions:` in the rule config. * HTTP allowlist entries match the URL’s host (exact, `*.subdomain` wildcard, or `*` for any). * Env values can come from OS allowlist, per-rule `.env`, or shared `.env`; CLI flags override per-invocation. * Security review = read the rule’s config; the script can’t reach anything not declared there. # Rules Registry > Publishing, versioning, and consuming codegen rules through the public CodeXX rules registry, and what editing a published rule means for licensing. The [CodeXX rules registry](https://github.com/CodeXX-DTDK/codexx_rules_registry) is the public catalogue of published `codegen` rules. Rules there are versioned, attributed to their authors, and MIT-licensed — copy one into your project and adapt it. The registry is deliberately not a package manager. There is no `codegen install`, no lockfile, and no resolution step: you clone or copy a directory, and it is yours from that moment on. ## Consuming a rule [Section titled “Consuming a rule”](#consuming-a-rule) Rules are namespaced by **vendor**, so two authors can both publish a `ToString` without colliding: Copy the rule directory into your own project — this is the normal way to use the registry: ```sh git clone https://github.com/CodeXX-DTDK/codexx_rules_registry cp -r codexx_rules_registry/rules/codegen/codexx/ToString .codegen/rules/ codegen -a ToString -i include/app/color.hpp ``` A rule is a handful of text files; vendoring it into your repository means it shows up in your diffs and your build depends on nothing remote. A **vendor directory** is itself a rules root, so you can also point `-r` at one without copying: ```sh codegen -r ../codexx_rules_registry/rules/codegen/codexx -a ToString -i include/app/color.hpp ``` Run codegen from your project `codegen` resolves include paths, defines, and the C++ standard from the **workspace it is invoked in** — your CMake configuration or your `codexx.workspace.yaml`. Running it from a clone of the registry analyses the registry, not your code. The registry ships its own `codexx.workspace.yaml` purely so a rule’s bundled `input.hpp` sample can be run in place; it covers the samples and nothing else. index.json The registry’s root `index.json` is a machine-readable catalogue of every published rule at its current version — identity, description, output language, and tag. Fetch it raw; you do not need to clone to see what exists. ## Installing from the browser [Section titled “Installing from the browser”](#installing-from-the-browser) `codegen` ships an interactive browser for the registry, free on every install: ```sh codegen --browse-rules-registry ``` It lists the catalogue, shows each rule’s published versions, previews its README, and installs the one you pick into `.codegen/rules/` **relative to the directory you ran it from** — so run it from your project root, or pass `-r` to point somewhere else. | Key | Action | | ------- | -------------------------------------------------------- | | `/` | start typing a search; `Esc` stops | | `↑` `↓` | move within a panel | | `Tab` | switch between the registry and installed panels | | `Enter` | open the version list, then install the selected version | | `o` | open the rule’s page on GitHub | | `f` | open the containing folder | | `d` | remove an installed rule (press twice to confirm) | | `q` | quit | The lower panel lists what you already have. Rules installed from the registry show their vendor and version, and are flagged when a newer version exists; hand-written rules are listed as `(local)`. Where a rule lands, and when a name collides A **namespaced** rule installs to `.codegen/rules///` and matches `[[::]]`, so two vendors’ `ToString` coexist happily and you can use both in one project. A rule published before namespacing installs flat, to `.codegen/rules//`, and matches `[[codegen::]]` — its README and sample header say so, and moving it would stop it matching them. There, the name is the attribute, so installing over an existing rule from a different vendor — or over a hand-written rule of the same name — is refused rather than overwritten. Rename or remove the existing one first. Reinstalling the *same* vendor’s rule at a different version is an upgrade, and is allowed. The browser needs no token and no account. It reads the public catalogue anonymously, and it does not start the workspace daemon or parse any of your code — see the [no-call-home guarantee](/trust/no-call-home/). If the registry is unreachable, the browser still opens and shows what you have installed, so you can review or remove rules offline. ## What a published rule contains [Section titled “What a published rule contains”](#what-a-published-rule-contains) A published rule is an ordinary rule directory (see [Rule Anatomy](/rules/anatomy/)) plus three files the registry requires: | File | Purpose | | ----------- | ------------------------------------------------------------------------------- | | `rule.json` | Manifest — identity, version, authors, `codegen` compatibility range | | `README.md` | What it generates, how to trigger it, what comes out | | `LICENSE` | Verbatim copy of the registry’s MIT licence, so the grant travels with the rule | `codegen` reads only `config.yaml`, `transform.luau`, `preamble.luau`, `grouping.luau`, and `.env`, and ignores everything else — which is exactly why a published rule must never carry a `.env` (see [Publishing](#publishing)). A published rule therefore works exactly like a hand-written one — the extra files are metadata for humans and for the registry, not inputs to the engine. ### `rule.json` [Section titled “rule.json”](#rulejson) ```json { "schemaVersion": 1, "id": "acme.to-string", "vendor": "acme", "rule": "ToString", "version": "1.0.0", "description": "Generates std::string_view toString(Enum) switches.", "authors": [{ "name": "Jane Doe", "github": "jdoe" }], "license": "MIT", "keywords": ["enum", "reflection"], "outputLanguage": "cpp", "namespaced": true, "uses": [{ "component": "acme.typemap", "range": ">=1.2.0" }], "requires": [{ "component": "codegen", "range": ">=0.1.0 <1.0.0" }] } ``` `id` is the rule’s identity: the vendor tag and rule name, each kebab-cased, joined by a single `.`. Case boundaries become dashes, so `JSONSerializable` becomes `json-serializable`. `requires[].range` accepts `>=X.Y.Z` or `>=X.Y.Z ::]]`. `uses` names the components it needs — see [Components](#components). ## Components [Section titled “Components”](#components) A **component** is a reusable Luau module. Publish one when two rules — yours or anyone’s — would otherwise carry the same helper twice. ```plaintext components/codegen-component/// component.json manifest — identity, version, authors README.md what it provides LICENSE the registry's MIT, verbatim component.luau the module; it must `return` its value ``` A rule declares what it uses, and the browser installs them together: rule.json ```json "uses": [{ "component": "acme.typemap", "range": ">=1.2.0" }] ``` transform.luau ```lua local typemap = require("acme/typemap") ``` Components install to `.codegen/shared//`, so two vendors’ same-named components never collide. Declared, not resolved A rule’s `uses` must be **closed**: if you use `acme.typemap` and that uses `acme.base`, your rule lists both. The validator enforces it. The result is that installation is a flat download — nothing is discovered at install time and no version is solved, which is what keeps the [no-dependency-confusion](/trust/supply-chain/) property intact. The cost is that a component adding a dependency is a breaking change for its consumers; it surfaces when the component is published, not on a user’s machine. Component names are lowercase. Two names differing only in case are one file on Windows and macOS and two on Linux, so a mixed-case name would resolve differently depending on who installed it. ## Versioning [Section titled “Versioning”](#versioning) Each rule carries its own `version` — plain semver, with no channels. The directory in `main` always holds the current version, and every released version is an immutable git tag: ```plaintext codegen/.@MAJOR.MINOR.PATCH ``` To pin an older version, check out its tag. **Tags are never deleted**, so a pin never breaks. A withdrawn rule is marked `"yanked": true` and flagged in `index.json` rather than removed — anything already pinned keeps working. ## Publishing [Section titled “Publishing”](#publishing) Publishing happens **only through pull requests**. There is no publish command, and that is the point: review is what keeps the catalogue worth reading, and it is how authors get credited. The workflow is: fork, add one directory under `rules/codegen//`, run the validator, open a PR. ```sh python3 scripts/validate_registry.py --write-index ``` CI runs that same script, so a green local run means a green PR. It checks manifest schema and identity, that your `LICENSE` is unmodified, that `README.md` says something, that the version increased, and two rules specific to publication: * **No `.env` file.** The loader reads per-rule dotenvs, so a published one is a published secret. * **Every `require` must be declared.** A rule may use [components](#components) — it just has to name them, in `rule.json`’s `uses`, or ship them itself. The validator reads your scripts and checks it. `require(someVariable)` is refused outright: a rule whose dependencies cannot be enumerated cannot be validated. Between them those two close the same gap: a published rule cannot lean on whatever helpers happen to exist in the tree it was written in. Declaring `uses` also opts the rule out of the consumer’s flat `shared/` prelude, so everything it calls has to come from the rule itself or from a component it names. Full details: [CONTRIBUTING.md](https://github.com/CodeXX-DTDK/codexx_rules_registry/blob/main/CONTRIBUTING.md). ## Trust [Section titled “Trust”](#trust) Rules are Luau scripts, and they run inside the [sandbox](/rules/luau-sandbox/). Network and environment access are opt-in per rule and declared in `config.yaml`; filesystem access is not available at all — see the [Permissions Model](/rules/permissions/). A rule that declares `permissions.http.allowlist` or `permissions.env.os_allowlist` is labelled and reviewed by a maintainer before it can merge, and its README must justify the request. Those permissions are allowed; they are never automatic. Review what you run Human review at publication raises the floor — it does not replace your own judgement. A registry rule is third-party code that runs during your build. Read it before you run it, exactly as you would any dependency. ## Editing a rule [Section titled “Editing a rule”](#editing-a-rule) **Rules are meant to be edited.** A rule encodes one project’s conventions; adapting one to yours is the expected use, not an edge case. Everything in the registry is MIT-licensed, so you may modify a rule and redistribute the result — **including under different licensing terms** — provided you retain the MIT notice and the original authors’ copyright, as the MIT licence requires. Authors are listed in each rule’s `rule.json` and `README.md`. Once you have edited a rule, it is yours. It will not receive updates when the registry’s copy changes, and nothing tracks the divergence. That is the intended trade: a rule you own outright, with no upgrade treadmill. ## Related [Section titled “Related”](#related) * [Rule Anatomy](/rules/anatomy/) — the files and their contracts * [Config Schema Reference](/reference/config-schema/) — every `config.yaml` key * [Permissions Model](/rules/permissions/) — what a rule may reach, and how it asks * [Runnable examples](https://github.com/CodeXX-DTDK/codegen_examples) — learn the shape before publishing # Writing Transforms > Practical patterns for handler scripts, type mapping, string building, defensive validation, and testing. ## Structure of a handler [Section titled “Structure of a handler”](#structure-of-a-handler) Every handler script follows the same pattern: ```lua return function(input) -- 1. Decode the entity payload local node = json.decode(input) if not node then error("failed to decode input") end -- 2. Extract what you need local name = node.identifier and node.identifier.name if not name or name == "" then error("entity has no name") end -- 3. Build the output text local source = "// generated for " .. name .. "\n" -- 4. Return encoded result return json.encode({ source = source }) end ``` The return shape recognised by the engine: `source` (string, written to the entity’s output file), optional `inline` (array of `{source = "..."}` objects injected at the anchor site), optional `includes` (array of include strings such as `""` or `"\"my/header.h\""`). ## Type mapping pattern [Section titled “Type mapping pattern”](#type-mapping-pattern) For rules that translate C++ types to another type system (TypeScript, Rust, protobuf, SQL): ```lua local TYPE_MAP = { ["bool"] = "boolean", ["int"] = "number", ["std::string"] = "string", -- add entries as needed } local function mapType(tSig) if not tSig or not tSig.identifier then return "unknown" end local name = tSig.identifier.name if TYPE_MAP[name] then return TYPE_MAP[name] end -- Handle generics recursively if name == "vector" then local args = tSig.identifier.templateArgs or {} return "Array<" .. mapType(args[1]) .. ">" end -- Fall back to the C++ name return name end ``` ## Handler payload [Section titled “Handler payload”](#handler-payload) The handler receives a full entity JSON with these synthetic fields injected: * `_namespaces` (array) — the qualifying namespace path * `_registryId` (integer) — stable entity ID * `_outputPath` (string) — the post-grouping output file path (after grouping, if present; otherwise the 1:1 default) * `params` (object) — rule config copied in Use `_outputPath` if your handler needs to know the final output location (e.g., to compute relative paths from the output file back to the input header). ## Namespace qualification [Section titled “Namespace qualification”](#namespace-qualification) Always build the qualified name from `_namespaces`; do not assume the unqualified name is unique: ```lua local function qualifiedName(node) local ns = table.concat(node._namespaces or {}, "::") local name = node.identifier.name return ns ~= "" and (ns .. "::" .. name) or name end ``` ## Building multi-line output [Section titled “Building multi-line output”](#building-multi-line-output) Use a lines table and `table.concat` — string concatenation in a loop is O(n²) in Lua: ```lua local lines = {} table.insert(lines, "void process(" .. qualifiedName(node) .. " const& v) {") for _, var in ipairs(node.memberVariables or {}) do table.insert(lines, " handle(v." .. var.identifier.name .. ");") end table.insert(lines, "}") local source = table.concat(lines, "\n") ``` ## Cross-entity lookup [Section titled “Cross-entity lookup”](#cross-entity-lookup) For rules that need to inspect more than the entity they were called for (base classes, sibling structs, methods on related types), use the [native introspection API](/reference/luau-globals/#native-introspection-api): ```lua -- Fetch base classes (returns JSON-encoded array) local baseJson = get_base_classes(node._registryId) if baseJson then for _, base in ipairs(json.decode(baseJson)) do -- base = { access, name, qualifiedName, id } if base.id then local sibling = json.decode(get_node(base.id)) -- sibling is the full node JSON for the base class end end end ``` ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) **Forgetting to handle `VariableGroup`.** Declarations like `int x, y, z;` arrive as `kind = "VariableGroup"` with a `variables` array. Always handle both cases: ```lua for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then processVar(varNode) elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do processVar(v) end end end ``` **Returning plain text instead of JSON.** The handler must return `json.encode({...})`. Returning a plain string emits `E009`. **Reading `node.annotations`.** The actual key is `attributes` (`node.attributes[i].ns`, `node.attributes[i].name`, `node.attributes[i].args`). Codex uses C++ attribute terminology throughout. **Mutating the input table.** LuaU tables are shared by reference within a single invocation. Don’t mutate `node`; build new tables for output. ## Inspecting the JSON your script will receive [Section titled “Inspecting the JSON your script will receive”](#inspecting-the-json-your-script-will-receive) Use the `ast-dump` tool to dump a header in the codex JSON form: ```sh ast-dump -m codex -i include/my_header.hpp ``` The shape printed is the same one the handler decodes (minus the `_namespaces` and `_registryId` synthetic fields and minus the heavy `memberFunctions` / `staticMemberFunctions` / `constructors` / `destructors` / `operators` / `nestedTypes` / `statements` fields the engine strips before invocation). Key Takeaways * Handlers are pure functions: decode → extract → build → encode. No side effects. * Build qualified names from `_namespaces`; never assume uniqueness of unqualified names. * The codex JSON uses `attributes` (C++ terminology), not `annotations`. * Use `table.concat` for multi-line output; the native API for cross-entity lookup. * Return value must be `json.encode({source = "...", inline = {...}, includes = {...}})`. # Support Policy > How to get technical support for codegen, what to expect, what is in and out of scope, and how your privacy is preserved when you send a report. ## How to get help [Section titled “How to get help”](#how-to-get-help) There is a single support channel: * **Email:** codegen is free for everyone, and so is support for it — there is no priority queue and no tier that buys a faster answer. Before emailing, check the [documentation](/) and the [Diagnostic Codes](/reference/diagnostics/) reference — most errors are explained there with a fix. A self-served answer is the fastest answer. ## What to include in a report [Section titled “What to include in a report”](#what-to-include-in-a-report) CodeXX DTDK runs entirely on your machine. We cannot see your code, your environment, or what went wrong unless you tell us. A report that includes the following gets resolved in one round-trip instead of five: * **Version** — output of `codegen --version` (or the affected tool’s `--version`). * **Operating system** and architecture. * **What you ran** — the exact command line. * **What happened** — the full error output, including any `[E0xx]` / `[W0xx]` code. * **What you expected** instead. * **A minimal repro** if you can — the smallest input that triggers it. Only send what you are comfortable sharing. Redact anything proprietary — a repro that reproduces the bug is worth more than a real-but-secret one. See [Privacy](#privacy) below. ## Response targets [Section titled “Response targets”](#response-targets) codegen is free, so support for it is **best effort** — answered as time allows, with no guaranteed response time. That is an honest statement of what a free tool can promise, not a deprioritisation: reports are read, and reproducible bugs get fixed. Need a contractual SLA, a named contact, or air-gapped / on-prem support assurances? Those come with the paid CodeXX DTDK products — email with your requirements. ## Scope [Section titled “Scope”](#scope) **In scope** — we will help with: * Installation issues. * Bugs, crashes, and incorrect output in CodeXX DTDK tools. * Diagnostic codes and how to resolve them. * Documented features behaving differently than documented. * Reasonable “how do I do X” guidance for documented functionality. **Out of scope** — we cannot take on: * Writing your rules, transforms, or build configuration for you (though we will point you at the right docs and examples). * Debugging your application’s own C++ code. * Third-party tools, compilers, or build systems, except where they integrate directly with CodeXX DTDK. ## Privacy [Section titled “Privacy”](#privacy) CodeXX DTDK makes [no outbound network requests during execution](/trust/no-call-home/). Support does not change that: * We never have access to your code, AST, or environment unless **you** attach it to an email. * We only use what you send to reproduce and fix the specific issue you reported. * We never ask for your source tree wholesale — a minimal repro is always sufficient and always preferred. ## Reporting a security issue [Section titled “Reporting a security issue”](#reporting-a-security-issue) Do **not** open a public report for security vulnerabilities. Email with `SECURITY` in the subject line and we will coordinate a private fix and disclosure. This applies to everyone.