---
generated: true
model: claude-opus-4.8
generated_at: 2026-06-12T10:13:29Z
source_commit: eb9403ef595a99c2ff6def1d538dbd7a792d9371
watched_paths_digest: 8b042e75fd998180a0b911649454d28b10dc08df645aa95b3ce5e8eb390b7f82
warning: "Auto-generated. May drift from source. Do not edit by hand."
---

# Serialization

This document describes the AST and IR serialization machinery: what
gets serialized, in which format, and how round-tripping works. The
intended reader is a developer adding a serialized field, debugging a
deserialization failure, or working on cross-version stability of IR
modules.

## What is serialized

Three flavors of payload exist, each with its own driver file:

- **AST modules** — handled by
  [slang-serialize-ast.h](../../../../source/slang/slang-serialize-ast.h)
  /
  [slang-serialize-ast.cpp](../../../../source/slang/slang-serialize-ast.cpp).
  The serialized form preserves the checked AST that backs an
  `import`-able module.
- **IR modules** — handled by
  [slang-serialize-ir.h](../../../../source/slang/slang-serialize-ir.h)
  /
  [slang-serialize-ir.cpp](../../../../source/slang/slang-serialize-ir.cpp),
  with the IR-specific type vocabulary in
  [slang-serialize-ir-types.h](../../../../source/slang/slang-serialize-ir-types.h)
  /
  [slang-serialize-ir-types.cpp](../../../../source/slang/slang-serialize-ir-types.cpp).
- **Containers** — the on-disk packaging that bundles AST + IR + side
  data into a single file (the `.slang-module` format and other
  artefacts). Driver:
  [slang-serialize-container.h](../../../../source/slang/slang-serialize-container.h)
  /
  [slang-serialize-container.cpp](../../../../source/slang/slang-serialize-container.cpp);
  underlying chunked format in
  [slang-serialize-riff.h](../../../../source/slang/slang-serialize-riff.h)
  /
  [slang-serialize-riff.cpp](../../../../source/slang/slang-serialize-riff.cpp).

Source-location streams have their own helper file
([slang-serialize-source-loc.h](../../../../source/slang/slang-serialize-source-loc.h)
/
[slang-serialize-source-loc.cpp](../../../../source/slang/slang-serialize-source-loc.cpp)),
because preserving readable diagnostics across deserialization
requires re-establishing the `SourceManager`'s view of files and
expansions.

## The serialize() pattern

The generic interface is
[slang-serialize.h](../../../../source/slang/slang-serialize.h). The
preamble of that header captures the central design choice: a single
`serialize(serializer, value)` function handles both reading and
writing, distinguished by a `SerializerMode` carried on the
`serializer` argument. Per the file's own example:

```cpp
struct MyThing
{
    float a;
    List<OtherThing> otherThings;
    SomeObject* object;
};

template<typename S>
void serialize(S const& serializer, MyThing& value)
{
    SLANG_SCOPED_SERIALIZER_STRUCT(serializer);
    serialize(serializer, value.a);
    serialize(serializer, value.otherThings);
    serialize(serializer, value.object);
}
```

The same template runs in both directions. This rules out
"serialize" / "deserialize" code drift by construction. The
`SLANG_SCOPED_SERIALIZER_STRUCT` macro brackets a structural scope so
the format can record nesting; the field-by-field calls do the work.

`serialize(serializer, value.object)` — pointer fields — work because
the serializer maintains a graph of objects it has already visited
and emits / resolves identifiers for shared and circular references.

## Backends

The generic `serialize(...)` pattern is implemented against multiple
`ISerializerImpl` backends; the backends visible in the watched paths
are:

### Generic / structural backend

[slang-serialize.cpp](../../../../source/slang/slang-serialize.cpp)
implements the core `ISerializerImpl` infrastructure and the dispatch
between reader and writer modes. Concrete encoders for primitive
types live in
[slang-serialize-types.cpp](../../../../source/slang/slang-serialize-types.cpp).

### Fossil backend

[slang-serialize-fossil.h](../../../../source/slang/slang-serialize-fossil.h)
/
[slang-serialize-fossil.cpp](../../../../source/slang/slang-serialize-fossil.cpp).
"Fossil" is a memory-mappable binary format defined in `slang-fossil.h`
(referenced from the fossil header). Per the comments in
[slang-serialize-fossil.h](../../../../source/slang/slang-serialize-fossil.h):

> Deserializing data is an important place where security issues can
> arise, so it is usually important to perform validation checks
> throughout the process, and fail fast rather than risk reading
> mal-formed data.

The validation cost is configurable via the macro
`SLANG_SERIALIZE_FOSSIL_ENABLE_VALIDATION_CHECKS` (default 1). When
enabled, validation failures call `SLANG_UNEXPECTED("invalid format
encountered in serialized data")`. When disabled — used for
performance-critical paths such as loading the embedded core module —
the same conditions become assertions.

The format is designed for **memory-mapped** deserialization: pointers
in the serialized data are relative offsets resolved against the start
of the mapped buffer (`slang-relative-ptr.h`), so loading a module is
nearly free if the file is already mapped.

## RIFF container format

[slang-serialize-riff.h](../../../../source/slang/slang-serialize-riff.h)
/
[slang-serialize-riff.cpp](../../../../source/slang/slang-serialize-riff.cpp)
implement a chunked, tagged container based on the RIFF idiom: each
chunk has a four-character code, a length, and a payload. Container-
level orchestration in
[slang-serialize-container.cpp](../../../../source/slang/slang-serialize-container.cpp)
composes a serialized AST module, a serialized IR module, and the
auxiliary data (source-location stream, dependency list, etc.) into
one RIFF file with well-known chunk codes per payload.

The RIFF wrapping is what allows tools to inspect partial structure
of a `.slang-module` file (chunk types, sizes) without parsing the
inner serialized content — useful for sanity checks and recovery.

## IR flat-module read path

[slang-serialize-ir.cpp](../../../../source/slang/slang-serialize-ir.cpp)
does not serialize the IR object graph directly. It first flattens a
module into parallel arrays — one entry per instruction, walked in
preorder by `traverseInstsInSerializationOrder` in
[slang-serialize-ir.h](../../../../source/slang/slang-serialize-ir.h):
`instAllocInfo`, `childCounts`, `sourceLocs`, plus a single
`operandIndices` list (type-use slot followed by each operand, with a
`nullptr` operand encoded as `-1`), `stringLengths`, and the
concatenated `stringChars`. On load, `deserializeFromFlatModule`
allocates every instruction up front and then a recursive lambda
rebuilds the parent/child links and resolves operand pointers by
indexing back into the allocated `insts` array.

Because the flat tables come from a file that may be malformed,
`deserializeFromFlatModule` treats their relationships as
serialized invariants and validates them with `SLANG_RELEASE_ASSERT`
before dereferencing — for example that `childCounts` and `sourceLocs`
each have one entry per instruction, that every `operandIndices` read
stays in range and resolves to a valid instruction index
(`-1 <= index < numInsts`), and that string-literal lengths are
non-negative, fit in `uint32_t`, and do not run past the end of
`stringChars`. After the walk it asserts that every flat table was
fully consumed (`instIndex == numInsts`,
`operandIndex == operandIndicesCount`, and the literal/string cursors
reach the end of their lists) and that the root is an `IRModuleInst`.
The literal/string-consumption check is skipped when
`readContext._foundUnrecognizedInstructions` is set, because the reader
cannot know whether a future unknown opcode (mapped to `Unrecognized`,
see [Versioning](#versioning-and-backwards-compatibility)) would have
consumed literal or string payloads; that case is left to the
recoverable read failure handled in
[slang-serialize-container.cpp](../../../../source/slang/slang-serialize-container.cpp).

The recursive rebuild and the matching write-side traversal share a
fixed recursion budget, `kMaxIRSerializationDepth` (512) declared in
[slang-serialize-ir.h](../../../../source/slang/slang-serialize-ir.h),
asserted on each level of `go`/`traverseInstsInSerializationOrder` so a
deeply-nested or adversarial module fails fast instead of overflowing
the C++ stack. Keeping the bound on both sides keeps round-trips
symmetric.

## Source-location serialization

Source locations are tricky to round-trip because the integer encoding
in
[slang-source-loc.h](../../../../source/compiler-core/slang-source-loc.h)
is meaningful only relative to the live `SourceManager` of the
session that produced it. The serializer therefore captures the
contributing `SourceFile` records (path, content, expansion stack)
alongside the integer locations and reconstructs them into a fresh
`SourceManager` on load.

Driver: [slang-serialize-source-loc.cpp](../../../../source/slang/slang-serialize-source-loc.cpp).

## Versioning and backwards compatibility

Modules are expected to remain loadable by newer compilers, even
though new IR opcodes are added over time. The constraint that makes
this possible is at the top of
[slang-ir-insts.lua](../../../../source/slang/slang-ir-insts.lua):

> Please make sure to update the supported module versions in
> Slang::IRModule accordingly when modifying this file.

When the Lua file is edited, the C++ side carries a versioning gate
for the deserializer so that older modules continue to load against
a (well-defined) prefix of the opcode set, while newer opcodes
become available only when the module declares the matching version.
The full design is described in
[../../../design/backwards-compat-for-ir-modules.md](../../../design/backwards-compat-for-ir-modules.md).

The `Unrecognized` opcode that appears at the head of
[slang-ir-insts.lua](../../../../source/slang/slang-ir-insts.lua) plays
a role here: it is the placeholder a deserializer substitutes for an
opcode it does not know, and the comment requires that
`Unrecognized` never survive past deserialization.

## Round-trip and repro files

Historical `-dump-repro` / `-load-repro` machinery for capturing a
single `slangc` invocation as a serialized blob is documented in
[CLAUDE.md](../../../../CLAUDE.md) as **deprecated**: it should not be
relied on. The watched-paths set for this document does not include
the repro implementation, so any newer alternative workflow is out
of scope for this page.

## Adding a new serialized field

1. Add the field to its host C++ type as usual.
2. Update the corresponding `serialize(...)` function (typically in a
   `slang-ast-*.cpp` or `slang-ir-*.cpp` file) so that it visits the
   new field with a `serialize(serializer, value.newField);` call.
3. Decide whether the new field needs to be readable by older
   compilers. If not, place the call after a version gate (see
   [../../../design/backwards-compat-for-ir-modules.md](../../../design/backwards-compat-for-ir-modules.md)).
4. If the field is a pointer to an externally-owned object, ensure
   the target type is itself serializable.
5. Add tests under [tests/](../../../../tests) that exercise the
   round-trip — typically a `COMPARE_COMPUTE` or `INTERPRET` test
   plus a separate test that loads a pre-built module file.

## What is not in this document

- The format-level layout of a fossil chunk or a RIFF chunk. The
  authoritative descriptions are in `slang-fossil.h` (referenced
  from
  [slang-serialize-fossil.h](../../../../source/slang/slang-serialize-fossil.h))
  and the chunk-code constants near the top of
  [slang-serialize-container.cpp](../../../../source/slang/slang-serialize-container.cpp).
- The full backwards-compatibility policy, which lives in
  [../../../design/backwards-compat-for-ir-modules.md](../../../design/backwards-compat-for-ir-modules.md).
- The historical repro format. Treat it as removed.
