<!--
SPDX-FileCopyrightText: The Khronos Group, Inc.
SPDX-License-Identifier: CC-BY-4.0
-->
# Repository Guidelines
## Project Structure & Module Organization
Slang is a shading-language compiler and runtime implemented primarily in C++20 and built with
CMake.
Key directories:
- `source/`: core implementation, including `source/slang/`, `source/core/`,
`source/compiler-core/`, and tools like `source/slangc/`.
- `include/`: public API headers.
- `prelude/` and `source/standard-modules/`: standard/prelude headers.
- `tests/`: test suites grouped by feature or target.
- `tools/`: test infrastructure and developer tools.
- `docs/`: documentation.
- `examples/`: runnable samples.
- `cmake/`: CMake helpers.
- `external/`: vendored dependencies.
## Repository-Local Skills
This repository stores local agent skills under `.claude/skills/`. Codex and other non-Claude
harnesses should still consult those `SKILL.md` files when a user asks for the workflow they
describe.
Review-related skills:
- `slang-review-clarity-workflow`: coordinate the end-to-end clarity review workflow.
- `slang-review-clarity`: generate high-level clarity and explainability review candidates.
- `slang-review-fine-grained-clarity`: generate line-by-line name/comment/type/function
consistency review candidates.
- `slang-review-consolidate-candidates`: merge candidate files and resolve duplicates,
overlap, and superseded comments.
- `slang-review-scope-filter`: conservatively filter candidate comments to issues the PR
author can reasonably own before posting.
- `slang-review-resolve-judgment-calls`: resolve uncertain candidates with focused follow-up
analysis before posting.
- `slang-review-post-github`: post filtered candidates as one proper GitHub PR review.
## WSL and Windows Tooling
When working in this repository from WSL on Windows, use Windows-native developer tools by
default unless the user explicitly asks for the WSL/Linux version.
- Use `git.exe`, not bare `git`. These worktrees use Windows path conventions; WSL Git can
corrupt or misinterpret worktree state, and Windows Git has much better file I/O performance
on this checkout.
- When the `slang-build` skill invokes CMake, use `cmake.exe`, not bare `cmake`, for
Windows-hosted configure and build commands. Windows CMake can find Visual Studio 2026 and
the Windows toolchains required by the `vs2026` preset.
- Use `gh.exe` instead of bare `gh` when GitHub CLI commands need to share the same
Windows-native Git and credential context.
- Convert WSL paths before passing them to Windows tools, for example `wslpath -w "$path"`.
Convert paths printed by Windows tools back before using them in shell commands, for example
`wslpath -u "$win_path"`.
- If a required `.exe` tool is unavailable, stop and report it instead of silently falling back
to the WSL/Linux tool.
## Build, Test, and Development Commands
Slang build setup is platform-specific, especially under WSL. For compiler builds, use the
`slang-build` skill from `skills/slang-build` in the `shader-slang/slang-skills` repository
instead of following hard-coded commands in this file.
If the skill is unavailable because skills cannot be installed or network access is limited, use
`docs/building.md` as the fallback build reference.
Examples:
- `/slang-build build debug`: build the Debug configuration.
- `/slang-build rebuild debug`: discard the existing build directory and rebuild Debug.
- `/slang-build configure releasewithdebug`: configure an optimized build with symbols.
- `/slang-build clean`: rename and remove the existing build directory.
Do not infer WSL build commands from generic Linux instructions. Follow the platform detection,
host-tool selection, CMake preset choice, and clean-build steps defined by the skill.
After building, run tests from the repository root using the generated `slang-test` binary in
the directory for the selected configuration:
- `build/Debug/bin/slang-test`: run the Debug test suite.
- `build/RelWithDebInfo/bin/slang-test -use-test-server -server-count 8`: run optimized
tests with symbols in parallel using test servers.
- `build/Release/bin/slang-test -use-test-server -server-count 8`: run Release tests in
parallel using test servers.
On Windows-hosted builds, use the `.exe` suffix if that is the generated binary name.
## Include Path Conventions
Prefer direct paths over relative traversal in `#include` directives. The `source/` directory is
on the compiler include path (exposed by the `core` CMake target), so cross-module headers are
reachable without `../`:
```cpp
// Preferred in new code
#include "core/slang-string.h"
#include "compiler-core/slang-source-loc.h"
// Existing code still uses the relative form; do not change it purely for style
#include "../core/slang-string.h"
#include "../compiler-core/slang-source-loc.h"
```
New files should use direct paths. Existing files need not be converted purely for style, but may
be opportunistically updated when the file is already being substantially modified for other
reasons (e.g., a security fix or feature addition touching many lines).
## Coding Style & Naming Conventions
Formatting:
- Use four-space indentation for C, C++, headers, and Slang files.
- Run `./extras/formatting.sh` before committing to apply rules from `.clang-format`
and `.editorconfig`.
- Follow Allman braces, a 100-column limit, left-aligned pointers, and final newlines.
Conventions:
- Follow `docs/design/coding-conventions.md`.
- Avoid STL containers, iostreams, RTTI, and exceptions for ordinary errors.
- Use `UpperCamelCase` for types and `lowerCamelCase` for values.
- Use `SLANG_`-prefixed `SCREAMING_SNAKE_CASE` for macros.
- Prefer comments that explain why code exists.
Review conventions (recurring review feedback β following them avoids review round-trips):
- Comment functions in complete sentences: what it does first, then why if non-obvious; include a
concrete example for non-trivial logic.
- Write explanatory comments in a conversational style. Prefer "Consider this example:" followed
by the relevant user code over abstract labels such as "Full source shape", "AST trace", or
"IR trace". After the example, explain what happens step by step in natural prose: which
producer creates the AST/IR/value shape, what invariant this code is preserving, and which
downstream consumer relies on it. Include enough of the original user code for the example to be
understood without reconstructing the surrounding program from memory.
- Reuse before you write: check shared headers (`slang-ast-type.h`, `slang-ir-util.h`, the `*-util.h`
files) for an existing helper (e.g. `isDeclRefTypeOf<T>`) before adding one. When the logic is
genuinely new, extract it into a named, documented helper rather than an inline lambda/long block.
- Keep one source of truth for a mapping or classification, and delete any branch/fallback a refactor
makes unreachable.
- Don't create a second AST/IR/`Val` representation of a value that already has one (it breaks
`equals`/dedup); `SLANG_ASSERT` such invariants at the construction site.
- `SLANG_RELEASE_ASSERT` on out-of-contract input instead of silently returning a default.
## Shell Scripts
Scripts under `extras/` (and other repository shell scripts) must run on bash 3.2, the version
Apple ships as `/bin/bash` on macOS. Avoid bash 4+ only features such as `${var,,}`/`${var^^}`
case conversion, associative arrays (`declare -A`), `mapfile`/`readarray`, and namerefs
(`local -n`). Prefer portable equivalents (for example, lowercase with
`tr '[:upper:]' '[:lower:]'`). Validate with `bash -n script.sh` under the system bash.
## Testing Guidelines
Add tests near related coverage in `tests/`.
Slang tests:
- Use leading directives such as `//TEST(smoke):SIMPLE:`.
- Use `//DISABLE_TEST` only with a clear reason.
- For targeted runs, pass a prefix, for example
`build/Debug/bin/slang-test tests/diagnostics/my-test`.
Unit tests live under `tools/slang-unit-test` and typically use `SLANG_UNIT_TEST(name)`.
## Problem-Solving Methodology
Follow the principled path, not the minimal-edit-distance path.
- Fix root causes, not symptoms. A bug surfacing in emit/codegen is usually caused upstream (an IR
pass, lowering, type legalization, specialization, or the AST/IR representation). Trace it there.
- Question every change. If you cannot name a test that fails without a change, it probably should
not exist. Ask whether the problem is telling you the direction/representation is flawed.
- Do not mask. A guard, null-check, or special case that papers over malformed AST/IR/witness-table
data is a band-aid hiding a representation bug. Make the representation correct so consumers stay
simple.
- Interrogate the input shape. For any code that handles a particular shape of input (AST node, IR
inst, witness, type, ...), always ask: is that shape itself correct and principled, or should the
upstream producer be fixed instead? Fix the producer when the shape is wrong; handle it here only
when the shape is genuinely valid input. Record the answer in the PR description (Process report).
- Address conceptually unordered keyβvalue data (witness-table / interface requirement entries) by
role/key, never by position/index.
- Keep a working log throughout the task: the problem and a motivating example, how issues cascade
(one fix exposing the next), the fix chosen for each and why it is principled (with a code trace),
and rejected alternatives. Distill this log into the PR description; do not commit it.
### Self-Review for Unprincipled Changes
Before finalizing a non-trivial compiler change, review the diff for signs that the fix is
compensating for a bad AST/IR/`Val`/witness representation. Treat the following patterns as
high-risk until you can prove they are the right layer:
- A new custom equivalence relation over `DeclRef`, `Val`, `Type`, `Witness`, or IR shapes, such as
recursive helpers named like `are...Equivalent`, `does...Match`, or `try...Match`. First ask why
normal `substitute`, `resolve`, `getCanonicalType`, `equals`, or an existing canonical builder
does not already make the two values identical.
- A new helper, fallback, or "try..." function that exists only to make one failing test pass. Audit
every new helper, even small ones: if it redoes substitution, resolution, AST copy, generic
solving, lookup, or lowering, it is probably hiding the actual invariant break.
- Code that converts checked semantic data back into syntax, such as rebuilding an `Expr` or
`TypeExp` from a `Val`, `Type`, `DeclRef`, or witness. The checked semantic field should usually
remain the source of truth; reconstructing syntax is a strong signal that a producer or copier is
storing the wrong representation.
- Code that walks arbitrary operand graphs, substitution chains, witness chains, or lookup paths to
rediscover context such as generic arguments, requirement keys, canonical paths, or parent
declarations. The producer should usually store or construct the canonical form directly.
- Lowering, emit, specialization, or typeflow logic that patches a malformed AST/IR shape from an
earlier phase. These consumers should be simple; if they need target-specific knowledge of a
front-end representation accident, trace the producer instead.
- Hardcoded knowledge of particular `DeclRef` subclasses, builtin magic type names, generic
argument indices, witness-table entry order, or nested-vs-flat specialization shape. Such code
needs a strong invariant and should usually live at a canonical construction boundary.
- Guards that silently return a default value for an "impossible" shape. Use an assertion when the
shape is truly out of contract; otherwise explain why the shape is valid input and add coverage.
Start each review by making a short inventory of every new helper/fallback/special case in the
diff. For each entry, record whether it survives, is reverted, or needs a producer-side fix. For
every flagged change, write down the input-shape audit before keeping it:
1. What exact shape reaches this code? Include a concrete example and the producing function.
2. Is that shape canonical and intentionally allowed, or is it an accidental alternative spelling?
3. If it is accidental, can the producer be fixed so downstream code uses the existing
`substitute`/`resolve`/canonicalization path?
4. What semantic source of truth already exists, and is this code rebuilding syntax or structural
shape from it instead of preserving it?
5. Which test fails if this change is removed, and does that test prove this layer is responsible?
Do the revert drill when practical: remove the helper/special case, run the smallest failing
test, and use the failure to identify the real producer-consumer break.
6. Can the special case be replaced by an assertion plus a producer-side fix, or by reusing an
existing helper?
Do not keep a flagged change merely because it makes tests pass. If it remains necessary, the
`Process report` section of the PR description must justify why this input shape is valid and why
this layer owns the logic, with a code trace from producer to consumer.
## Commit & Pull Request Guidelines
- Use short, imperative commit subjects, for example `Reject invalid descriptor heap access`.
- Keep PRs small and based on `master`.
- PRs require passing workflows, review approval, and a `pr: non-breaking` or
`pr: breaking change` label.
- Human contributors should sign the CLA when prompted.
- For formatting failures, run installed hooks from `./extras/install-git-hooks.sh` or
request the format bot with `/format`.
Write the PR description in this five-part format:
1. **Motivation** β the problem, with a concrete example / motivating test case.
2. **Proposed solution** β the approach and why it is principled.
3. **Change summary** β the files/areas touched and what each does.
4. **Concepts and vocabulary** β a short glossary between the change summary and the process report.
Restate only the codebase-specific or subtle terms the report relies on (e.g. witness, facet,
the fixpoint solver, a non-obvious distinction the fix hinges on), as a reminder. Do not explain
basic, well-known concepts (interface, associated type) β assume them.
5. **Process report** β explain every change with a logical reason. For a change addressing a
cascading issue, describe the issue (with its motivating test case) and justify the fix with a
code trace (the exact functions/insts involved), explaining why it is necessary and principled
rather than a workaround. For any change that handles, guards, or special-cases a particular
input shape, the report must answer the input-shape check from the methodology β is that shape
correct and principled, or should its producer have been fixed instead? β so a reviewer can
confirm the fix sits at the right layer.
Write for a reviewer without the full context in their head. Use the same conversational style
expected in code comments: start from a concrete user-code example, include the full relevant
snippet rather than just a type or function name, and explain the logical steps in order. Say what
the compiler builds, how that representation flows through named functions or IR instructions, and
why the chosen fix preserves the invariant. Avoid terse headings like "AST trace"; make the prose
read like an explanation to a reviewer who is learning the scenario for the first time.
<!--
SPDX-FileCopyrightText: The Khronos Group, Inc.
SPDX-License-Identifier: CC-BY-4.0
-->
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Repository**: shader-slang/slang - A shading language for GPU programming
**Primary Language**: C++ with custom Slang language
**MCP Tool Available**: `mcp__deepwiki__ask_question` with repoName: "shader-slang/slang"
Reference other instruction files as well:
- @.github/copilot-instructions.md (shares formatting/testing/debugging info; this CLAUDE.md is the canonical source)
User-specific instructions for Slang (optional, may not exist):
- @~/.claude/slang-instructions.md
## Build System and Common Commands
### Building the Project
If you are running in a Windows sandbox, run extras\win-sandbox-build.bat to produce a build in
debug configuration. This script discovers Visual Studio, runs vcvarsall.bat, configures with the
`vs2022-dev` preset, prefers locally cached dependencies instead of fetching them over the network,
and defaults to building `slangc`, `slang-test`, and `slangi`. Pass extra target names if you need
something other than that default target set.
On non-Windows platforms (Linux/macOS), run cmake directly to build:
```bash
# Configure with default settings (Ninja Multi-Config)
cmake --preset default
# Configure with visual studio 2022 settings (Preferred on Windows)
# On Windows, include -DSLANG_IGNORE_ABORT_MSG=ON to suppress
# modal abort dialogs during unattended/LLM-driven builds.
# Use -DSLANG_EMBED_CORE_MODULE=OFF to keep core module compilation separate
# from C++ source compilation. This way errors in *.meta.slang files (e.g.
# hlsl.meta.slang) do not break the C++ compile β slangc and slang-test still
# compile successfully, and the module errors are reported by the separate
# `slang-bootstrap -compile-core-module` step instead.
# Those errors still fail the build, though: generate_core_module_cache is an
# ALL target that depends on generate_core_module (source/slang/CMakeLists.txt),
# generate_core_module runs the bootstrap compile
# (source/slang-core-module/CMakeLists.txt), and slangc itself takes
# `REQUIRES generate_core_module_cache`, so even `--target slangc` runs it.
# What OFF buys you is a clean separation of meta-source errors from C++
# compilation errors, not a build that ignores meta-source errors.
cmake.exe --preset vs2022 -DSLANG_IGNORE_ABORT_MSG=ON -DSLANG_EMBED_CORE_MODULE=OFF
# Build Release/Debug binaries.
# It can take from 5 minutes to 20 minutes depending on the machine.
cmake --build --preset debug # Debug binary
cmake --build --preset release # Release binary
# Alternative: use workflow preset (configure + build in one step)
cmake --workflow --preset debug
# Build specific targets
cmake --build --preset debug --target slangc
cmake --build --preset debug --target slang-test
```
**sccache**: Pass `-DSLANG_USE_SCCACHE=ON` at configure time (or set `SLANG_USE_SCCACHE=1` env var) to use sccache as the compiler launcher for faster rebuilds. This automatically disables precompiled headers due to a known incompatibility. Requires `sccache` in PATH.
When building with `cmake --build`, redirect all of outputs to null-device.
When the build failed, then, re-run the same command without the redirections.
It is to avoid wasting the token usage of LLM.
Example,
```
# Print the build logs only when the initial attempt failed.
cmake --build --preset debug >/dev/null 2>&1 || cmake --build --preset debug
```
### Formatting
**Run `./extras/formatting.sh` before committing changes.** PRs must conform to the project's coding style. Use `./extras/formatting.sh --check-only` to verify without modifying files.
### Suppressing Unused Variable Warnings
When a variable declared in an `if` condition is unused inside the body (the condition exists only for its type-check side-effect), use the **C++17 if-init-statement** pattern instead of `SLANG_UNUSED`:
```cpp
// Preferred: C++17 if-init pattern
if (auto foo = as<IRFoo>(inst); foo)
{
// foo not needed in body β the type check is the point
}
// Avoid: SLANG_UNUSED inside the body
if (auto foo = as<IRFoo>(inst))
{
SLANG_UNUSED(foo);
}
```
For variables that are set but never read outside an `if` (e.g., a plain local variable), use `SLANG_UNUSED(var)` with a comment explaining why.
### Problem-Solving Methodology
Follow the **principled path**, not the minimal-edit-distance path. The goal is a correct
representation that is robust by construction, even when that means a larger rework.
- **Fix root causes, not symptoms.** When a bug appears in emit/codegen, the cause is usually
upstream (an IR pass, type legalization, specialization, lowering, or the AST/IR representation
itself). Trace it there and fix it there.
- **Question every change.** Before keeping a change, answer: _Why is this change necessary? What
test fails without it? Is this the right fix, or is the problem telling me the
direction/representation is flawed?_ If you cannot name a test that fails without a change, the
change probably should not exist.
- **Do not mask.** A guard, null-check, or special case that papers over a malformed
AST/IR/witness-table is a band-aid that hides a representation bug. A guard that is never hit
under correct input is dead code. Prefer making the representation correct so consumers stay
simple.
- **Interrogate the input shape.** Whenever you write or change code that handles a particular
shape of input β an AST node, IR inst, witness, type, etc. β always ask: _is that input shape
itself correct and principled, or should the upstream producer of it be fixed instead?_ If the
shape is wrong or accidental, fix the producer; handle it here only when the shape is genuinely
valid input. This is the routine double-check that root-causing was done at the right layer, and
its answer is required in the PR description (see the Process report below).
- **Prefer correct representation over edit distance.** If two surface forms _should_ be
equivalent, model them identically. If a consumer reads data by position/index/identity when the
data is conceptually an unordered keyβvalue set (e.g. witness-table / interface requirement
entries), make the access by role/key, not by position.
- **Keep a working log/report.** Maintain a scratch markdown document throughout the task that
records: the problem and a motivating example, road-blockers encountered, how issues **cascade**
(one fix exposing the next), the fix chosen for each and _why it is principled_ (with a concrete
code trace), and alternatives that were rejected and why. This log is what you distill into the
PR description below. (Keep the log out of the commit β it feeds the PR body, it is not a repo
artifact.)
### Self-Review for Unprincipled Changes
Before finalizing a non-trivial compiler change, review the diff for signs that the fix is
compensating for a bad AST/IR/`Val`/witness representation. Treat these patterns as red flags until
you can prove they are the right layer:
- **Custom semantic equivalence.** New recursive helpers over `DeclRef`, `Val`, `Type`, `Witness`,
or IR shapes (for example `are...Equivalent`, `does...Match`, or `try...Match`) often mean two
alternative representations were allowed to survive. First ask why `substitute`, `resolve`,
`getCanonicalType`, `equals`, or an existing canonical builder does not already make the values
identical.
- **Unaudited helper growth.** Treat every new helper, fallback, and `try...` function as a review
target, not only large or complicated ones. A helper that exists to make one failing test pass,
or that reimplements part of substitution, resolution, AST copy, generic solving, lookup, or
lowering, is often the place where an unprincipled fix hides.
- **Semantic-to-syntax reconstruction.** Code that turns checked semantic data (`Val`, `Type`,
`DeclRef`, witness, lowered IR value) back into syntax (`Expr`, `TypeExp`, parser-shaped AST) is
a strong smell. The checked semantic field should usually be the source of truth; rebuilding
surface syntax, as in a helper that recreates an expression from an `IntVal`, usually means the
producer/copier/substitution path is preserving the wrong representation.
- **Context rediscovery by graph walking.** Code that walks arbitrary operand graphs, substitution
chains, witness chains, lookup paths, or IR users to recover generic arguments, requirement keys,
canonical paths, or parent declarations is usually downstream repair. Prefer storing or building
the canonical form at the producer.
- **Consumer-side patching.** Lowering, emit, specialization, typeflow, and backend code should not
patch malformed AST/IR shapes from earlier phases. If these consumers need front-end-specific
knowledge of an accidental representation, trace and fix the producer instead.
- **Hardcoded representation trivia.** Special cases for particular `DeclRef` subclasses, builtin
magic type names, generic argument indices, witness-table entry order, or nested-vs-flat
specialization shape need a strong invariant and usually belong at a canonical construction
boundary.
- **Silent impossible-shape handling.** A guard that returns a default value for an out-of-contract
shape hides bugs. Assert impossible shapes; handle a shape only when you can explain why it is
valid input.
Begin the review with a "suspect helpers" inventory: list every new helper/fallback/special case,
what existing mechanism it overlaps with, the test that fails without it, and whether it survived,
was reverted, or was replaced by a producer-side fix. For every flagged change, run this audit
before keeping it:
1. Name the exact input shape, the producing function, and a concrete source or IR example.
2. Decide whether that shape is canonical and intentionally allowed, or an accidental alternative
spelling that should be eliminated.
3. If it is accidental, try the producer-side fix first so downstream code can use the normal
`substitute`/`resolve`/canonicalization path.
4. Name the semantic source of truth that already exists. If the change rebuilds syntax or a
parallel structural form from that source of truth, justify why the stored representation cannot
be fixed instead.
5. Name the test that fails without the change and explain why that test proves this layer owns the
logic. When practical, do the revert drill: remove the helper/special case, run the smallest
failing test, and use the failure to trace the real producer-consumer break.
6. Prefer replacing the special case with an assertion plus a producer fix, or with an existing
helper that already encodes the invariant.
Do not keep a flagged change only because it makes tests pass. If it remains necessary, the
`Process report` section of the PR description must justify why the input shape is valid and why
this layer owns the logic, with a code trace from producer to consumer.
### Code Style and Review Conventions
Recurring review feedback distilled into rules β following them avoids review round-trips. (These
govern how code reads and is structured; the Problem-Solving Methodology above governs _what_ to
change.)
- **Write function comments as complete sentences: what, then why.** State what the function does
first; then, if the reason it exists isn't obvious from that description, add a brief summary of
why. For non-trivial behavior, include a concrete example. Avoid terse fragment- or bullet-only
comments on a function. For instance, `substituteElementOfCompositeType` should read like _"Return
`target` with its element type replaced by `newElementType`, preserving shape: scalar β
newElementType, `vector<T,N>` β `vector<newElementType,N>`, `matrix<T,R,C>` β `matrix<newElementType,R,C>`."_
β not _"element coerce target"_.
- **Use conversational examples for code comments and PR explanations.** When explaining a subtle
compiler path, prefer "Consider this example:" followed by the relevant user code. Do not use
abstract labels such as "Full source shape", "AST trace", or "IR trace" as a substitute for
explanation. After the code, describe what happens step by step in natural prose: which parser,
checker, copier, lowering pass, or IR pass creates the shape; what invariant the local code
preserves; and which downstream consumer depends on that invariant. Include enough of the user's
original code for the example to make sense on its own.
- **Reuse before you write; then extract non-trivial logic into a named, documented helper.** Before
writing a new helper, search for an existing one β what you need is often already provided by a
shared header (the AST/IR helpers in `slang-ast-type.h`, `slang-ir-util.h`, and the various
`*-util.h` files). For example, to test whether a type is a `DeclRefType` of a particular
declaration, use the existing `isDeclRefTypeOf<T>(type)` rather than re-deriving it. When the
logic genuinely is new, don't bury a multi-step computation in an inline lambda or a long inline
block: give it an intention-revealing name (`coerceOperandsOfBuiltinBinaryExpr`,
`substituteElementOfCompositeType`, `unifyBaseType`) and a doc comment, so the caller stays
readable and the helper is reusable.
- **Keep one source of truth; delete dead code after a refactor.** Map or classify a given thing in
exactly one place β e.g. the operator-name β operation-kind mapping lives only in
`getBuiltinOperationKindFromString`, not re-implemented at call sites. When a change makes a
branch, fallback, or helper unreachable, remove it rather than leaving it as dead code.
- **One canonical representation per value; assert the invariant.** Don't introduce a second
AST/IR/`Val` representation for something that already has one β multiple forms of the same logical
value break `equals`/identity checks and deduplication. When an invariant guarantees a
representation is never produced for certain inputs (e.g. `+`/`-`/`*` are always a
`PolynomialIntVal`, never a `BuiltinOperationIntVal`), `SLANG_ASSERT` it at the construction site
so a violation is caught rather than silently producing a divergent form.
- **Fail loudly on out-of-contract input.** When a helper is only valid for a restricted set of
inputs, `SLANG_RELEASE_ASSERT` on anything outside that set instead of silently returning a default
β e.g. `substituteElementOfCompositeType` asserts its operand is a builtin scalar/vector/matrix.
### PR Workflow
1. **Format your code**: Run `./extras/formatting.sh` before committing
2. **Label your PR**: Use "pr: non-breaking" (default) or "pr: breaking change" (for ABI/language breaking changes)
3. **Include tests**: Add regression tests as `.slang` files under `tests/`
4. **Write the PR description in this required five-part format:**
1. **Motivation** β the problem being solved, with a concrete example / motivating test case.
2. **Proposed solution** β the approach, and why it is the principled one.
3. **Change summary** β a table or list of the files/areas touched and what each does.
4. **Concepts and vocabulary** β a short glossary, placed between the change summary and the
process report. Restate only the _codebase-specific or subtle_ terms the report relies on, as
a reminder for the reviewer (e.g. witness / `getSub`, facet / `getInheritanceInfo`, the
fixpoint solver, or a non-obvious distinction the fix hinges on). Do **not** explain basic,
well-known concepts (e.g. interface, associated type) β assume them.
5. **Process report** β explain _every_ change with a logical reason. For a change that
addresses a **cascading** issue, describe the issue (with its motivating test case) and
justify why the fix is correct with a **code trace** (the exact functions/insts involved),
not just a description. State explicitly why each change is necessary and principled rather
than a workaround. For any change that handles, guards, or special-cases a particular input
shape, the report **must** answer the input-shape check from the methodology β _is that shape
correct and principled, or should its producer have been fixed instead?_ β so a reviewer can
confirm the fix sits at the right layer.
Write for a reviewer who does not have the full context in their head. Use the same
conversational style required for code comments: start from a concrete user-code example, include
the full relevant snippet instead of just naming a type or function, and explain the logical
steps in order. Say what the compiler builds, how that representation flows through named
functions or IR instructions, and why the chosen fix preserves the invariant. Avoid terse labels
such as "AST trace"; make the prose read like an explanation to a reviewer who is learning the
scenario for the first time.
### Testing
slang-test must run from repository root
```bash
# Run all tests with multiple servers (takes from 10 to 30 minutes)
./build/Release/bin/slang-test -use-test-server -server-count 8
# Run specific test
# The test file must be placed under "tests/" directory
./build/Release/bin/slang-test tests/path/to/test.slang
# Run unit tests
./build/Release/bin/slang-test slang-unit-test-tool/
```
**Writing Tests Without GPU**:
- Use CPU compute: `//TEST:COMPARE_COMPUTE(filecheck-buffer=CHECK):-cpu -output-using-type`
- Use interpreter: `//TEST:INTERPRET(filecheck=CHECK):`
- Example test structure in `tests/language-feature/lambda/lambda-0.slang`
**Diagnostic Tests** (see `docs/diagnostics.md` for full details):
Use `//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK):` as the test directive to verify that the compiler emits expected diagnostics. Annotations in comments match against compiler output by message text, severity, or error code. Carets align to columns on the preceding source line:
```slang
//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK):-target spirv
int foo = undefined;
//CHECK: E01234
//CHECK: ^^^^^^^^^ error
```
**SPIRV Validation**:
- Set `SLANG_RUN_SPIRV_VALIDATION=1` when using `slangc -target spirv`
- Don't use system's `spirv-val` tool (may be outdated)
### Slang Command Line Usage
**IMPORTANT:** Slang uses single dashes for multi-character options (not double dashes like most tools):
- Use `-help` (not `--help`)
- Use `-target spirv` (not `--target spirv`)
- Use `-dump-ir` (not `--dump-ir`)
- Use `-stage compute` (not `--stage compute`)
### AVOID These Debugging Options
**DO NOT USE** these options as they are unmaintained, unreliable or unnecessary:
- slangc with `-dump-ast`, `-dump-intermediate-prefix`, `-dump-intermediates`,
`-dump-ir-ids`, `-serial-ir`, and `-dump-repro`.
- slang-test with `-category` and `-api`
### Repro Tooling
`-load-repro` and `-extract-repro` are specialized repro tools; use them when
working on repro handling. Inputs are validated before use.
## Architecture Overview
### Core Components
**Compiler Pipeline**:
- **Lexer** (`source/compiler-core/slang-lexer.cpp`): Tokenizes source code
- **Preprocessor** (`source/slang/slang-preprocessor.cpp`): Handles #include, macros, conditionals
- **Parser** (`source/slang/slang-parser.cpp`): Recursive descent parser producing AST
- **Semantic Checker** (`source/slang/slang-check.cpp`): Type checking, name resolution, validation
- **IR Generation** (`source/slang/slang-lower-to-ir.cpp`): Converts AST to Slang IR
- **IR Passes** (`source/slang/slang-ir-*.cpp`): Optimization and lowering passes
- **Code Emission** (`source/slang/slang-emit-*.cpp`): Target-specific code generation
**Key Directories**:
- `source/core/`: Core utilities (strings, containers, file system, platform abstractions)
- `source/compiler-core/`: Compiler infrastructure (diagnostics, downstream compilers)
- `source/slang/`: Main compiler implementation (frontend, IR, backend)
- `source/slangc/`: Command-line compiler tool
- `source/slang-core-module/`, `source/slang-glsl-module/`, `source/standard-modules/`: Standard library modules
- `source/slang-wasm/`: WebAssembly bindings
- `source/slang-record-replay/`: API call record/replay
- `source/slang-rt/`: Runtime library
- `tools/`: Development and testing tools
- `include/`: Public API headers (`slang.h`)
- `external/`: Third-party dependencies and submodules
- `prelude/`: Built-in language definitions and standard library
- `tests/`: Comprehensive test suite
- `docs/`: Project documentation (user guide in `docs/user-guide/`)
- `build/source/slang/fiddle/`: Generated code from FIDDLE macros (created during build)
### Compilation Model
**Key Concepts**:
- **CompileRequest**: Bundles options, input files, and code generation requests
- **TranslationUnit**: Collection of source files (HLSL: one per file, Slang: all files together)
- **EntryPoint**: Function name + pipeline stage to compile
- **Target**: Output format (DXIL, SPIR-V, etc.) + capability profile
**Supported Targets**:
- Direct3D 11/12 (HLSL output)
- Vulkan (SPIR-V, GLSL output)
- Metal (MSL output) - experimental
- WebGPU (WGSL output) - experimental
- CUDA/OptiX (C++ output)
- CPU (C++ output, executables, libraries)
## Development Workflow
### Adding New Language Features
1. Update lexer for new tokens (`source/compiler-core/slang-lexer.cpp`)
2. Extend parser for new syntax (`source/slang/slang-parser.cpp`)
3. Add semantic analysis (`source/slang/slang-check-*.cpp`)
4. Implement IR generation (`source/slang/slang-ir-*.cpp`)
5. Add code generation for each target backend (`source/slang/slang-emit-*.cpp`)
6. Write comprehensive tests under `tests/`
### Common Development Tasks
- **Adding an IR instruction**: Update the Lua definition files in `source/slang/slang-ir-insts.lua`, then regenerate
- **Adding a built-in function**: Add to appropriate module in `prelude/`
- **Adding a new target**: Implement new emitter in `source/slang/slang-emit-*.cpp`
### Capability Atoms Documentation
**`docs/user-guide/a4-02-reference-capability-atoms.md` is auto-generated β never edit it directly.**
It is produced by `slang-capability-generator` from `source/slang/slang-capabilities.capdef`. To add or update a capability atom's description:
1. Add or update the `///` doc comment immediately before the `def` or `alias` in `slang-capabilities.capdef`:
```
/// My description here.
alias myatom = ...;
```
2. Regenerate the doc:
```bash
cmake --build --preset debug --target slang-capability-generator
mkdir -p build/capgen-out
./build/generators/Debug/bin/slang-capability-generator \
source/slang/slang-capabilities.capdef \
--target-directory build/capgen-out \
--doc docs/user-guide/a4-02-reference-capability-atoms.md
```
3. Commit the updated `slang-capabilities.capdef` and the regenerated `.md` together.
Note: the `///` comment must be on the **public alias** (e.g. `alias node = _node;`), not on the internal `def _node : stage;` atom, for the description to appear under the public name.
### Modifying Public Headers (`include/`)
All files under `include/` are public API. Changes must preserve binary (ABI) and source
compatibility for callers compiled against older versions of the header.
#### Enums
- **Never insert a new enumerator in the middle of an existing enum.** Insertion shifts all
subsequent integer values, silently breaking any caller that stores or compares the value.
- **Always append** new enumerators immediately before the terminal count/sentinel member
(e.g. `CountOf`, `Count`, `NUM_*`), assigning an explicit integer value (the next sequential
integer after the preceding enumerator).
- **Removed enumerators**: rename to `REMOVED_<Name>` and keep the original integer value.
Never reuse or reclaim a retired integer.
#### Virtual tables (COM interfaces)
Slang's public interfaces (`ISession`, `IModule`, `IComponentType`, etc.) are COM-style
vtables declared with `virtual` methods in `include/slang.h`. The vtable layout is fixed by
declaration order. Violating these rules corrupts the vtable and causes silent crashes or
wrong-method dispatch for any caller compiled against an older header.
- **Never reorder virtual methods** within an interface.
- **Never change a virtual method's signature** (return type, parameter types, calling
convention, or `SLANG_MCALL` decoration).
- **Never insert a new virtual method** in the middle of an interface β append only, at the
end of the interface before the closing brace.
- **Never remove a virtual method** β replace its body with a stub that returns
`SLANG_E_NOT_IMPLEMENTED` and keep the declaration in place.
- Avoid extending an existing public COM interface in place when clients may implement or
query it by UUID. Prefer adding a new derived/versioned interface with its own UUID, while
keeping the original interface declaration and UUID supported for existing callers.
### Debugging tools
#### IR Dump (`-dump-ir`)
```bash
# Dump IR at every pass (use with -target and -o to avoid mixing output)
slangc -dump-ir -target spirv-asm -o tmp.spv test.slang | python extras/split-ir-dump.py
# Dump IR before/after a specific pass
slangc -dump-ir-before lowerGenerics -dump-ir-after lowerGenerics -target spirv-asm -o tmp.spv test.slang > pass.dump
```
- Always combine `-dump-ir` with `-target` (otherwise compilation stops early) and `-o <file>` (otherwise target code mixes with IR on stdout)
- Use `extras/split-ir-dump.py` to split large dumps into per-pass files. See `extras/split-ir-dump.md` for details.
- You can insert `dumpIRToString()` in C++ code and write to a file with `File::writeAllText()` for ad-hoc inspection.
- When debugging, focus on root causes in IR passes (specialization, inlining, type legalization, buffer lowering) rather than band-aid fixes in emit logic. The compiler philosophy is to keep emission simple and do heavy transforms in IR passes.
#### InstTrace
Trace where a problematic IR instruction was created:
```bash
python3 ./extras/insttrace.py <debugUID> ./build/Debug/bin/slangc tests/my-test.slang -target spirv
```
#### SPIRV Tools
- `slangc -target spirv-asm` β compile to SPIRV assembly
- Set `SLANG_RUN_SPIRV_VALIDATION=1` for static validation; use `-skip-spirv-validation` to see SPIRV output even when validation fails
- `slangc -target spirv-asm -emit-spirv-via-glsl` β generate reference SPIRV via GLSL for comparison
#### Assertion Behavior (`SLANG_ASSERT`)
On Windows, assertion failures normally open a modal dialog that blocks execution. Set the `SLANG_ASSERT` environment variable to control this:
| Value | Behavior |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `system` | Use the system `assert()`, which shows a modal dialog and allows the developers to attach the debugger |
| `debugbreak` | When a debugger is already attached, it will hit a debug-break; fall back to `system` behavior when a debugger is not attached |
| `release-assert-only` | Skip debug-only assertions (`SLANG_ASSERT`, `SLANG_ASSERT_FAILURE`) and continue; `SLANG_RELEASE_ASSERT` still fires |
| _(unset)_ | Throws an exception |
The behavior on Windows after an exception is thrown is controlled by the CMake option `SLANG_IGNORE_ABORT_MSG`.
This option is highly recommended for unattended automation with LLM workflow; it bakes the behavior into all built executables at compile time.
#### RTX Remix Testing
Use the `/repro-remix` skill or see `extras/repro-remix.md`.
### IR System
- Slang uses a custom SSA-based IR (not LLVM)
- IR instructions defined in `slang-ir-insts.h` (generated from Lua)
- Extensive IR pass framework for optimization and lowering
- Target-specific legalization passes before code emission
### Language Server
- Language Server Protocol implementation in `source/slang/slang-language-server.cpp`
- Supports IntelliSense, completion, diagnostics, formatting
- Used by VS Code and Visual Studio extensions
### Module System
- Slang supports separate compilation via modules
- Modules can be compiled to IR and linked at runtime
- Optional obfuscation for distributed modules
- Core language features defined as modules in `prelude/`
### Generated files
- The enum values starting with `kIROp_` are defined in a generated file, `build/source/slang/fiddle/slang-ir-insts-enum.h.fiddle`
- `FIDDLE()` and `FIDDLE(...)` statements in AST node declarations indicate that additional source is generated and included from `build/source/slang/fiddle`, providing static type system and reflection metadata, visitor support, and serialization support.
### Rebuilding after `hlsl.meta.slang` / `core.meta.slang` changes
The core module source (`hlsl.meta.slang`, `core.meta.slang`, etc.) is embedded into the `slang-bootstrap` binary at compile time. After modifying these files, force CMake to observe the newer timestamp, regenerate the core-module headers through the build graph, then rebuild `slangc` with the preset/configuration you are using:
```bash
cmake -E touch source/slang/hlsl.meta.slang # or whichever meta file changed
cmake --build --preset <preset> --target generate_core_module_headers
cmake --build --preset <preset> --target slangc
```
Use the same `<preset>` you use for the build, such as `debug`, `release`, or `releaseWithDebugInfo`. The `generate_core_module_headers` target invokes the correct `slang-bootstrap` binary for that host/configuration, including Windows `.exe` paths and non-Debug output directories.
If you skip the `cmake -E touch` step the cached bootstrap binary may silently embed the OLD source, and diagnostics from the bootstrap step will not match the current source file β a sure sign the binary is stale.
### HLSL named-constant emission rule
**Never emit HLSL enum / named-constant values as hard-coded integers.** DXC maps named constants (attribute strings, flag identifiers, etc.) at parse time; if we bake in a numeric value and DXC later changes the internal mapping the generated HLSL will silently break.
The correct pattern:
1. **Define a Slang enum** (or a set of named intrinsic-backed constants) for each group of conceptual values (e.g. `NodeLaunch` mode, Barrier flag sets).
2. **Store the name, not the integer, in the IR.** Use `IRStringLit` operands (as `NodeLaunchDecoration` does) or a `Ref<T>` / intrinsic-based accessor that preserves the identifier through to emission.
3. **Provide a mapping function** in the HLSL emitter (`slang-emit-hlsl.cpp` or `slang-emit-c-like.cpp`) that converts the stored enum/string value back to the HLSL source name so that emitted code reads e.g. `[NodeLaunch("broadcasting")]` not `[NodeLaunch(0)]`.
Examples of this pattern already in the codebase:
- `NodeLaunchDecoration` stores the mode as `IRStringLit("broadcasting")` and the emitter re-emits the string verbatim.
- Work-graph output record `Get()` returns `Ref<T>` backed by `__intrinsic_asm ".Get"` so the emitted HLSL says `.Get(i)` (an l-value in HLSL) rather than an integer offset.
#### Pattern: emitting enum values as target named constants
Use this when a Slang enum must be emitted as named constants rather than integers (e.g. `UAV_MEMORY` instead of `1`).
1. **Define the C++ enum in `slang-type-system-shared.h`** (inside `namespace Slang`, plain `enum` not `enum class` so values implicitly convert to `int`). This header is transitively included by both the core-module source and the emitters.
2. **Mirror it as a Slang enum in the appropriate `*.meta.slang` file**, pulling the actual values from C++ via `$(...)` splices so the two definitions stay in sync:
```slang
enum MyFlags : uint { FlagA = $(MyFlags::FlagA), FlagB = $(MyFlags::FlagB) }
```
3. **Declare a `__intrinsic_op` converter in the `.meta.slang` file** to represent the enum-to-string conversion in the IR:
```slang
__intrinsic_op(getEnumMyFlags)
int GetEnumMyFlags(MyFlags f);
```
The mnemonic passed to `__intrinsic_op(...)` must exactly match the Lua key in the next step.
4. **Register the new IR op in `slang-ir-insts.lua`** and add a stable ID in `slang-ir-insts-stable-names.lua`.
5. **Emit the named-constant string in the target emitter** (e.g. `tryEmitInstExprImpl` in `slang-emit-hlsl.cpp`): keep the IR operation tied to the symbolic enum or intrinsic value, then map each accepted bit or value to its HLSL named-constant string and write it out with `m_writer->emit(...)`. Do not document or implement examples that recover HLSL source names from raw integer positions.
### Git commit message
- Don't mention Claude on the commit message
### Debugging with slangpy
Use the `/slangpy-debug` skill to build slangpy from source with your local Slang build for compatibility testing.
## Cross-Platform Considerations
**Supported Platforms**:
Windows (x64/ARM64), Linux (x64/ARM64), macOS (x64/ARM64), WebAssembly
**Platform Abstractions**:
Use utilities in `source/core/` for file system, process management, platform detection
**Graphics APIs**:
Code generation supports all major APIs but runtime testing requires appropriate drivers/SDKs
**WSL on Windows**:
When running under WSL environment, try to append `.exe` to the executables to avoid using Linux binaries
- Use `cmake.exe` instead of `cmake`,
- Use `python.exe` instead of `python`,
- Use `gh.exe` instead of `gh` and so on.
### Release Process
Use the `/slang-release-process` skill to push a new release. See `.claude/skills/slang-release-process/SKILL.md` for the full workflow.
## Additional Documents
- User-facing documentation: `docs/user-guide/`
- Language specification: see below
### Formal Specification
Clone `https://github.com/shader-slang/spec.git` under `external/` if needed. Specification files are in `external/spec/specification/`, feature proposals in `external/spec/proposals/`.