summaryrefslogtreecommitdiff
path: root/source/slang/slang-serialize-ast.cpp
diff options
context:
space:
mode:
authorTheresa Foley <10618364+tangent-vector@users.noreply.github.com>2025-08-06 19:30:57 -0700
committerGitHub <noreply@github.com>2025-08-07 02:30:57 +0000
commit631a4c37a597fab7599abc472f25d671bfe37751 (patch)
tree799e3cc30021238cea6d6738338876b154b0f5e3 /source/slang/slang-serialize-ast.cpp
parentb9aad6649e3ba0186e17fc035d4b56f8e6169b59 (diff)
Improve performance of AST deserialization (#7935)
* Improve performance of AST deserialization The primary goal of these changes is to reduce the total time spent in the global session's `loadBuiltinModule()`, which gets called as part of global session creation to load the core module, and thus impacts every invocation of `slangc` and every user of the Slang compiler API. The majority of the time is spent simply deserializing the core module's AST and IR and, of those two, the AST takes significantly longer to load than the IR (in the ballpark of 5x the time). This change is focused on the serialization infrastructure but, given the performance situation described above, the focus is first and foremost on *deserialization* performance for the Slang *AST*, when using the *fossil* format. That focus shows through in the changes that have been implemented. Change serialization framework to use `template` instead of `virtual` ===================================================================== The recently-introduced serialization framework in `slang-serialize.h` was centered around a dynamically-dispatched `ISerializerImpl` interface. As a result, every single invocation of a `serialize(...)` call ultimately went through `virtual` function dispatch. While the overhead of the `virtual` calls themselves does not have a major impact on the total deserialization performance, those calls end up serving as a barrier to further optimization. This change changes operations that used to take a `Serializer const&` (which wraps an `ISerializerImpl*`), to instead declare a template parameter `<typename S>` and take an `S const&`. The main consequence of the change is that `serialize()` functions for user-defined types will need to be template functions, and thus either be defined in headers (alongside the type that they serialize) or else in the specific source file that handles serialization (as is currently being done for the AST-related types in `slang-serialize-ast.cpp`). Note that if we later decide that we want the ability to perform serialization through a dynamically-dispatched interface (e.g., to easily toggle between different serialization back-ends), it will be easier to layer a dynamically-dispatched implementation on top of the statically-dispatched `template` version than the other way around. Generous use of `SLANG_FORCE_INLINE` ==================================== In order to unlock further optimizations, a bunch of operations were marked with `SLANG_FORCE_INLINE`. It is important to note that forcing inlining like this is a big hammer, and needs to be approached with at least a little caution. The simplest cases are: * trivial wrapper function that just delegate to another function * functions that only have a single call site (but exist to keep abstractions clean) Externalize Scope for `begin`/`end` Operations ============================================== The old `ISerializerImpl` interface had a bunch of paired begin/end operations that define the hierarchical structure of data being read. Most serializer implementations (whether for reading or writing) use these operations to help maintain some kind of internal stack for tracking state in the hierarchy. The overhead of maintaining such a stack with something like a `List<T>` amortizes out over many operations, but even that overhead is unnecessary when the begin/end pairs are *already* mirroring the call stack of the code invoking serialization. This change modifies the `ScopedSerializerFoo` types so that they each provide a piece of stack-allocated storage to the serializer back-end's `beginFoo()` and `endFoo()` operations. Currently only the `Fossil::SerialReader` is making use of that facility, but the other implementations of readers and writers in the codebase could be adapted if we ever wanted to. Streamline `Fossil::SerialReader` ================================= The most significant performance gains came from changes to the `Fossil::SerialReader` type, aimed at minimizing the cycles spent in the core `_readValPtr()` routine. That function used to have a large-ish `switch` statement that implemented superficially very different reading logic depending on the outer container/object being read from. The new logic pushes more work back on the `begin` and `end` operations (which get invoked far less frequently than simple scalar/pointer values get read), so that they always set up the state of the reader with direct pointers to the data and layout for the next fossilized value to be read. The remaining work in `_readValPtr()` has been factored into a differnt subroutine - `_advanceCursor()` - that takes responsibility for advancing the data pointer, and updating the various other fields. The `_advanceCursor()` routine is still messier than is ideal, because it has to deal with the various different kinds of logic required for navigating to the next value. Various other conditionals inside the `SerialReader` implementation were streamlined, mostly by collapsing the `State::Type` enumeration down to only represent the cases that are truly semantically distinct. Evaluated: Streamline Layout Rules for Fossil ============================================= One potential approach that I implemented but then reverted (after finding it had little to no performance impact) was changing the fossil format to always write things with 4-byte alignment/granularity. That would mean values smaller than 4 bytes would get inflated to a full 4 bytes, and scalar values larger than 4 bytes get written with only 4-byte alignment (requiring unaligned loads to read them). I found that the only way to take advantage of the simplified layout rules to improve read performance would be to more-or-less eliminate the use of the layout information embedded in the fossil data, which would make it very difficult to validate that the data is correctly structured. Possible Future Work: Further Type Specialization ================================================= As it stands, the biggest overhead remaining on the critical path of `_readValPtr()` is the way the `_advanceCursor()` logic needs to take different approaches depending on the type of the surrounding context (advancing through elements of a container is very different than advancing through fields of a `struct`, for example). The interesting thing to note is that at the use site within a `serialize()` function, it is usually manifestly obvious which case something is in. If the code uses `SLANG_SCOPED_SERIALIZER_ARRAY` it is in a container, while if it uses `SLANG_SCOPED_SERIALIZER_STRUCT` it is in a struct. This means that the contextual information is staticaly available, but just isn't exposed in a way that lets the core reading logic take advantage of it. A logical extension of the work here would be to expand on the `Scope` idea added in this change such that most of the serialization operations (`handleInt32`, `handleString`, etc.) are actually dispatched through the scope, and then have each of the `SLANG_SCOPED_SERIALIZER_...` macros instantiate a *different* scope type (still dependent on the serializer). * fixup * format code * typo --------- Co-authored-by: slangbot <186143334+slangbot@users.noreply.github.com>
Diffstat (limited to 'source/slang/slang-serialize-ast.cpp')
-rw-r--r--source/slang/slang-serialize-ast.cpp166
1 files changed, 94 insertions, 72 deletions
diff --git a/source/slang/slang-serialize-ast.cpp b/source/slang/slang-serialize-ast.cpp
index a288b3bd2..be959b8a1 100644
--- a/source/slang/slang-serialize-ast.cpp
+++ b/source/slang/slang-serialize-ast.cpp
@@ -1,6 +1,7 @@
// slang-serialize-ast.cpp
#include "slang-serialize-ast.h"
+#include "core/slang-performance-profiler.h"
#include "slang-ast-dispatch.h"
#include "slang-check.h"
#include "slang-compiler.h"
@@ -84,7 +85,8 @@ namespace Slang
// they are all just getting dumped here in the AST serialization logic, because
// it is currenly the only place that cares about this stuff.
//
-void serialize(Serializer const&, RefObject&)
+template<typename S>
+void serialize(S const&, RefObject&)
{
// There's actually no data stored in a `RefObject`, since it only exists
// to make reference-counting possible for other types. This function is
@@ -136,7 +138,8 @@ struct FossilizedTypeTraits<RefObject>
// While we could include this among the types we handle using fiddle,
// let's implement it by hand here, starting with the `serialize()` function:
//
-void serialize(Serializer const& serializer, MatrixCoord& value)
+template<typename S>
+void serialize(S const& serializer, MatrixCoord& value)
{
// We start with one of the `SLANG_SCOPED_SERIALIZER_*`
// macros, which basically just handles calling
@@ -210,7 +213,8 @@ struct FossilizedTypeTraits<MatrixCoord>
// as a single scalar value. We'll define our `serialize()` function
// so that it serializes that "raw" value instead:
//
-void serialize(Serializer const& serializer, SemanticVersion& value)
+template<typename S>
+void serialize(S const& serializer, SemanticVersion& value)
{
// This function is doing something a little "clever"
// handle the fact that it might be used to either
@@ -373,33 +377,17 @@ struct ContainerDeclDirectMemberDeclsInfo
};
//
-// Okay, that's enough examples for now. Let's move on to the next big
-// topic...
-//
// Many types in the AST need additional context information to be able to
-// read or write them properly, so instead of passing around the basic
-// `Serializer` type (which wraps an `ISerializerImpl`), for those types
-// that need extra context we will be passing around an `ASTSerializer`
-// (which wraps an `IASTSerializerImpl`, with the latter interface providing
-// the callbacks to handle the data types that need special-case behavior.
+// read or write them properly, so the concrete serializer type being passed
+// around will include an additional "context" type, that will be either
+// `ASTSerialReadContext` or `ASTSerialWriteContext`, depending on the mode
+// in which serialization is being performed.
+//
+// We could define a base class or interface with `virtual` functions for
+// accessing all of the relevant context, but because we are already
+// using template specialization, it is easier to just ensure that the
+// relevant context types both provide the required operations.
//
-
-struct ASTSerialContext;
-using ASTSerializer = Serializer_<ISerializerImpl, ASTSerialContext>;
-
-/// Context interface for AST serialization
-struct ASTSerialContext : SourceLocSerialContext
-{
-public:
- virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& value) = 0;
- virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* value) = 0;
- virtual void handleName(ASTSerializer const& serializer, Name*& value) = 0;
- virtual void handleToken(ASTSerializer const& serializer, Token& value) = 0;
- virtual void handleContainerDeclDirectMemberDecls(
- ASTSerializer const& serializer,
- ContainerDeclDirectMemberDecls& value) = 0;
-};
-
//
// Now that we've covered some of the big-picture structure, and shown
@@ -439,7 +427,8 @@ public:
%for _,T in ipairs(enumTypeNames) do
/// Serialize a `value` of type `$T`.
-void serialize(Serializer const& serializer, $T& value)
+template<typename S>
+void serialize(S const& serializer, $T& value)
{
serializeEnum(serializer, value);
}
@@ -518,7 +507,8 @@ struct Fossilized_$T;
SLANG_DECLARE_FOSSILIZED_TYPE($T, Fossilized_$T);
/// Serialize a `$T`
-void serialize(ASTSerializer const& serializer, $T& value);
+template<typename S>
+void serialize(S const& serializer, $T& value);
%end
#else // FIDDLE OUTPUT:
#define FIDDLE_GENERATED_OUTPUT_ID 1
@@ -545,7 +535,8 @@ struct Fossilized_$T;
SLANG_DECLARE_FOSSILIZED_TYPE($T, Fossilized_$T);
/// Serialize the content of a `$T`
-void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value);
+template<typename S>
+void _serializeASTNodeContents(S const& serializer, $T* value);
%end
#else // FIDDLE OUTPUT:
#define FIDDLE_GENERATED_OUTPUT_ID 2
@@ -566,9 +557,11 @@ void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value);
/// lower-level serialization operations to an underlying
/// `ISerializerImpl`.
///
-struct ASTSerialWriteContext : ASTSerialContext
+struct ASTSerialWriteContext : SourceLocSerialContext
{
public:
+ using ASTSerializer = Serializer<Fossil::SerialWriter, ASTSerialWriteContext>;
+
/// Construct a context for writing a serialized AST.
///
/// * `module` is the module that is being serialized, and will be
@@ -587,21 +580,23 @@ private:
ModuleDecl* _module = nullptr;
SerialSourceLocWriter* _sourceLocWriter = nullptr;
+public:
//
// For the most part, this type just implements the methods
// of the `IASTSerializerImpl` interface, and then has some
// support routines needed by those implementations.
//
- virtual void handleName(ASTSerializer const& serializer, Name*& value) override;
- virtual void handleToken(ASTSerializer const& serializer, Token& value) override;
- virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& node) override;
- virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node) override;
- virtual void handleContainerDeclDirectMemberDecls(
+ void handleName(ASTSerializer const& serializer, Name*& value);
+ void handleToken(ASTSerializer const& serializer, Token& value);
+ void handleASTNode(ASTSerializer const& serializer, NodeBase*& node);
+ void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node);
+ void handleContainerDeclDirectMemberDecls(
ASTSerializer const& serializer,
- ContainerDeclDirectMemberDecls& value) override;
- virtual SerialSourceLocWriter* getSourceLocWriter() override { return _sourceLocWriter; }
+ ContainerDeclDirectMemberDecls& value);
+ SerialSourceLocWriter* getSourceLocWriter() { return _sourceLocWriter; }
+private:
void _writeImportedModule(ASTSerializer const& serializer, ModuleDecl* moduleDecl);
void _writeImportedDecl(
ASTSerializer const& serializer,
@@ -653,9 +648,11 @@ private:
/// contexts could result in the same declaration getting turned
/// into multiple distinct `Decl*`s.
///
-struct ASTSerialReadContext : public ASTSerialContext, public RefObject
+struct ASTSerialReadContext : public SourceLocSerialContext, public RefObject
{
public:
+ using ASTSerializer = Serializer<Fossil::SerialReader, ASTSerialReadContext>;
+
/// Construct an AST deserialization context.
///
/// The `linkage`, `astBuilder`, and `sink` arguments must
@@ -729,6 +726,7 @@ private:
Count _deserializedTopLevelDeclCount = 0;
#endif
+public:
//
// Much like the `ASTSerialWriter`, for the most part this
// type just implements the `IASTSerializer` interface,
@@ -736,15 +734,16 @@ private:
// implementations.
//
- virtual void handleName(ASTSerializer const& serializer, Name*& value) override;
- virtual void handleToken(ASTSerializer const& serializer, Token& value) override;
- virtual void handleASTNode(ASTSerializer const& serializer, NodeBase*& outNode) override;
- virtual void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node) override;
- virtual void handleContainerDeclDirectMemberDecls(
+ void handleName(ASTSerializer const& serializer, Name*& value);
+ void handleToken(ASTSerializer const& serializer, Token& value);
+ void handleASTNode(ASTSerializer const& serializer, NodeBase*& outNode);
+ void handleASTNodeContents(ASTSerializer const& serializer, NodeBase* node);
+ void handleContainerDeclDirectMemberDecls(
ASTSerializer const& serializer,
- ContainerDeclDirectMemberDecls& value) override;
- virtual SerialSourceLocReader* getSourceLocReader() override { return _sourceLocReader; }
+ ContainerDeclDirectMemberDecls& value);
+ SerialSourceLocReader* getSourceLocReader() { return _sourceLocReader; }
+private:
ModuleDecl* _readImportedModule(ASTSerializer const& serializer);
NodeBase* _readImportedDecl(ASTSerializer const& serializer);
@@ -770,7 +769,8 @@ private:
SLANG_DECLARE_FOSSILIZED_AS(Name, String);
-void serializeObject(ASTSerializer const& serializer, Name*& value, Name*)
+template<typename S>
+void serializeObject(S const& serializer, Name*& value, Name*)
{
serializer.getContext()->handleName(serializer, value);
}
@@ -807,7 +807,8 @@ struct FossilizedTypeTraits<Token>
};
};
-void serialize(ASTSerializer const& serializer, Token& value)
+template<typename S>
+void serialize(S const& serializer, Token& value)
{
serializer.getContext()->handleToken(serializer, value);
}
@@ -883,8 +884,8 @@ void ASTSerialReadContext::handleToken(ASTSerializer const& serializer, Token& v
// serialize any pointers to AST nodes.
//
-template<typename T>
-void serializeObject(ASTSerializer const& serializer, T*& value, NodeBase*)
+template<typename S, typename T>
+SLANG_FORCE_INLINE void serializeObject(S const& serializer, T*& value, NodeBase*)
{
// The general-purpose serialization layer defines
// a variant as akin to a struct, but where the
@@ -912,7 +913,8 @@ void serializeObject(ASTSerializer const& serializer, T*& value, NodeBase*)
// object in the reading direction.
//
-void serializeObjectContents(ASTSerializer const& serializer, NodeBase* value, NodeBase*)
+template<typename S>
+SLANG_FORCE_INLINE void serializeObjectContents(S const& serializer, NodeBase* value, NodeBase*)
{
serializer.getContext()->handleASTNodeContents(serializer, value);
}
@@ -930,7 +932,8 @@ void serializeObjectContents(ASTSerializer const& serializer, NodeBase* value, N
SLANG_DECLARE_FOSSILIZED_AS(ContainerDeclDirectMemberDecls, ContainerDeclDirectMemberDeclsInfo);
-void serialize(ASTSerializer const& serializer, ContainerDeclDirectMemberDecls& value)
+template<typename S>
+SLANG_FORCE_INLINE void serialize(S const& serializer, ContainerDeclDirectMemberDecls& value)
{
serializer.getContext()->handleContainerDeclDirectMemberDecls(serializer, value);
}
@@ -943,7 +946,8 @@ void serialize(ASTSerializer const& serializer, ContainerDeclDirectMemberDecls&
SLANG_DECLARE_FOSSILIZED_AS(DiagnosticInfo const*, Int32);
-void serializePtr(Serializer const& serializer, DiagnosticInfo const*& value, DiagnosticInfo const*)
+template<typename S>
+void serializePtr(S const& serializer, DiagnosticInfo const*& value, DiagnosticInfo const*)
{
Int32 id = 0;
if (isWriting(serializer))
@@ -964,8 +968,8 @@ void serializePtr(Serializer const& serializer, DiagnosticInfo const*& value, Di
// and we'll serialize it as such.
//
-template<typename T>
-void serialize(ASTSerializer const& serializer, DeclRef<T>& value)
+template<typename S, typename T>
+void serialize(S const& serializer, DeclRef<T>& value)
{
serialize(serializer, value.declRefBase);
}
@@ -987,7 +991,8 @@ struct FossilizedTypeTraits<DeclRef<T>>
SLANG_DECLARE_FOSSILIZED_AS(SyntaxClass<NodeBase>, ASTNodeType);
-void serialize(Serializer const& serializer, SyntaxClass<NodeBase>& value)
+template<typename S>
+void serialize(S const& serializer, SyntaxClass<NodeBase>& value)
{
ASTNodeType raw = ASTNodeType(0);
if (isWriting(serializer))
@@ -1011,7 +1016,8 @@ void serialize(Serializer const& serializer, SyntaxClass<NodeBase>& value)
SLANG_DECLARE_FOSSILIZED_AS(Modifiers, List<Modifier*>);
-void serialize(ASTSerializer const& serializer, Modifiers& value)
+template<typename S>
+void serialize(S const& serializer, Modifiers& value)
{
SLANG_SCOPED_SERIALIZER_ARRAY(serializer);
@@ -1059,7 +1065,8 @@ void serialize(ASTSerializer const& serializer, Modifiers& value)
//
SLANG_DECLARE_FOSSILIZED_AS_MEMBER(TypeExp, type);
-void serialize(ASTSerializer const& serializer, TypeExp& value)
+template<typename S>
+void serialize(S const& serializer, TypeExp& value)
{
serialize(serializer, value.type);
}
@@ -1071,7 +1078,8 @@ void serialize(ASTSerializer const& serializer, TypeExp& value)
SLANG_DECLARE_FOSSILIZED_AS_MEMBER(CandidateExtensionList, candidateExtensions);
-void serialize(ASTSerializer const& serializer, CandidateExtensionList& value)
+template<typename S>
+void serialize(S const& serializer, CandidateExtensionList& value)
{
serialize(serializer, value.candidateExtensions);
}
@@ -1079,7 +1087,8 @@ void serialize(ASTSerializer const& serializer, CandidateExtensionList& value)
SLANG_DECLARE_FOSSILIZED_AS_MEMBER(DeclAssociationList, associations);
-void serialize(ASTSerializer const& serializer, DeclAssociationList& value)
+template<typename S>
+void serialize(S const& serializer, DeclAssociationList& value)
{
serialize(serializer, value.associations);
}
@@ -1109,7 +1118,8 @@ SLANG_DECLARE_FOSSILIZED_AS(CapabilityTargetSet, CapabilityStageSets);
//
SLANG_DECLARE_FOSSILIZED_AS(CapabilitySet, CapabilityTargetSets);
-void serialize(Serializer const& serializer, CapabilityAtomSet& value)
+template<typename S>
+void serialize(S const& serializer, CapabilityAtomSet& value)
{
SLANG_SCOPED_SERIALIZER_ARRAY(serializer);
if (isWriting(serializer))
@@ -1131,12 +1141,14 @@ void serialize(Serializer const& serializer, CapabilityAtomSet& value)
}
}
-void serialize(Serializer const& serializer, CapabilityStageSet& value)
+template<typename S>
+void serialize(S const& serializer, CapabilityStageSet& value)
{
serialize(serializer, value.atomSet);
}
-void serialize(Serializer const& serializer, CapabilityTargetSet& value)
+template<typename S>
+void serialize(S const& serializer, CapabilityTargetSet& value)
{
serialize(serializer, value.shaderStageSets);
@@ -1154,7 +1166,8 @@ void serialize(Serializer const& serializer, CapabilityTargetSet& value)
}
}
-void serialize(Serializer const& serializer, CapabilitySet& value)
+template<typename S>
+void serialize(S const& serializer, CapabilitySet& value)
{
serialize(serializer, value.getCapabilityTargetSets());
@@ -1190,7 +1203,8 @@ struct FossilizedTypeTraits<RequirementWitness>
};
};
-void serialize(ASTSerializer const& serializer, RequirementWitness& value)
+template<typename S>
+void serialize(S const& serializer, RequirementWitness& value)
{
SLANG_SCOPED_SERIALIZER_VARIANT(serializer);
serialize(serializer, value.m_flavor);
@@ -1228,7 +1242,8 @@ struct FossilizedTypeTraits<ValNodeOperand>
};
};
-void serialize(ASTSerializer const& serializer, ValNodeOperand& value)
+template<typename S>
+void serialize(S const& serializer, ValNodeOperand& value)
{
SLANG_SCOPED_SERIALIZER_VARIANT(serializer);
serialize(serializer, value.kind);
@@ -1289,7 +1304,8 @@ struct Fossilized_$T
};
/// Serialize a `value` of type `$T`
-void serialize(ASTSerializer const& serializer, $T& value)
+template<typename S>
+void serialize(S const& serializer, $T& value)
{
SLANG_UNUSED(value);
SLANG_SCOPED_SERIALIZER_STRUCT(serializer);
@@ -1346,7 +1362,8 @@ struct Fossilized_$T
};
/// Serialize the contents of an AST node of type `$T`
-void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value)
+template<typename S>
+void _serializeASTNodeContents(S const& serializer, $T* value)
{
SLANG_UNUSED(serializer);
SLANG_UNUSED(value);
@@ -1372,7 +1389,8 @@ void _serializeASTNodeContents(ASTSerializer const& serializer, $T* value)
// functions, and dispatches to the correct one based on the type of the given node.
//
-void serializeASTNodeContents(ASTSerializer const& serializer, NodeBase* node)
+template<typename S>
+void serializeASTNodeContents(S const& serializer, NodeBase* node)
{
ASTNodeDispatcher<NodeBase, void>::dispatch(
node,
@@ -1894,7 +1912,7 @@ void ASTSerialReadContext::handleContainerDeclDirectMemberDecls(
// `ISerializerImpl`, whereas we *know* it has a more
// specific type, which we want to make use of.
//
- ISerializerImpl* readerImpl = serializer.getImpl();
+ auto readerImpl = serializer.getImpl();
auto fossilReader = static_cast<Fossil::SerialReader*>(readerImpl);
//
auto fossilizedInfo =
@@ -1927,6 +1945,8 @@ void writeSerializedModuleAST(
ModuleDecl* moduleDecl,
SerialSourceLocWriter* sourceLocWriter)
{
+ SLANG_PROFILE;
+
// TODO: we might want to have a more careful pass here,
// where we only encode the public declarations.
@@ -1965,7 +1985,7 @@ void writeSerializedModuleAST(
//
Fossil::SerialWriter writer(blobBuilder);
ASTSerialWriteContext context(moduleDecl, sourceLocWriter);
- ASTSerializer serializer(&writer, &context);
+ ASTSerialWriteContext::ASTSerializer serializer(&writer, &context);
// Once we have our `serializer`, we can finally invoke
// `serialize()` on the `ASTModuleInfo` to cause everything
@@ -2030,6 +2050,8 @@ ModuleDecl* readSerializedModuleAST(
SerialSourceLocReader* sourceLocReader,
SourceLoc requestingSourceLoc)
{
+ SLANG_PROFILE;
+
// We expect the `chunk` that was passed in to be a RIFF
// data chunk (matching what was written in `writeSerializedModuleAST()`,
// and to be proper fossil-format data.