SharedContext vs ScopeContext vs SubdocumentContext in Quarkdown: Key Differences Explained
SharedContext delegates every mutable property to its parent via Kotlin property delegation, while SubdocumentContext isolates document metadata for independent file processing, and ScopeContext extends SubdocumentContext to re-share document info for lambda execution while keeping variable libraries isolated.
Quarkdown's compilation pipeline in iamgio/quarkdown relies on a mutable Context object to manage flavor settings, libraries, document metadata, and media storage. When the compiler evaluates code in isolated environments—such as sub-documents, lambda bodies, or temporary file-system changes—it forks the current context into one of three specialized types. Understanding when to use SharedContext, ScopeContext, or SubdocumentContext is essential for debugging pipeline behavior and extending the compiler's functionality.
Core Concept: Context Forking in Quarkdown
The compilation process uses a hierarchical context system centered around the MutableContext class. When a pipeline stage requires a sandboxed environment, the compiler instantiates a forked context that selectively shares or isolates state. Each fork type determines exactly what data is shared versus copied, preventing side effects from leaking across document boundaries while maintaining necessary references to parent resources.
SharedContext: Zero-Copy State Delegation
SharedContext (defined in SharedContext.kt) extends MutableContext and functions as a transparent wrapper around its parent. It delegates every mutable property—including documentInfo, mediaStorage, attributes, and libraries—directly to the parent instance using Kotlin's by parent::property syntax.
Implementation via Property Delegation
In SharedContext.kt, the class overrides mutable properties with full delegation:
override var documentInfo by parent::documentInfo
override var mediaStorage by parent::mediaStorage
override var attributes by parent::attributes
This means no data is copied; all reads and writes operate on the parent's storage. Only the fileSystem property can be overridden during construction.
Primary Use Cases
Use SharedContext when internal helper functions require a temporary file-system change but must preserve all document metadata. For example, an .include directive that switches to a different base directory uses this context to avoid duplicating the entire context state while keeping documentInfo synchronized.
SubdocumentContext: Isolated Document Processing
SubdocumentContext (defined in SubdocumentContext.kt) serves as the foundation for processing included files or linked sub-documents. It creates a local copy of documentInfo while sharing immutable environment settings like flavor, loadableLibraries, and localizationTables.
What Gets Isolated vs Shared
The constructor creates a fresh MutableContext with specific isolation rules:
- Isolated: A new
documentInfoinstance (local metadata changes do not affect the parent) and an emptylibrariesset. - Shared:
options,loadableLibraries,localizationTables,sharedSubdocumentsData, and optionally thefileSystemvia the private_fileSystemparameter.
This isolation prevents metadata pollution when the compiler processes @include directives or external markdown files.
When the Compiler Uses It
The pipeline creates a SubdocumentContext via Context.fork() whenever it starts processing a sub-document, link target, or included file. This ensures that title, author, or attribute changes inside the included file remain local to that compilation unit.
ScopeContext: Lambda Execution Environments
ScopeContext (defined in ScopeContext.kt) extends SubdocumentContext but immediately re-shares specific parent properties to create a hybrid isolation model. It is designed specifically for lambda execution and loop blocks where document metadata should remain visible but variable declarations must stay local.
Selective Property Sharing
Unlike its parent class, ScopeContext delegates documentInfo, attributes, and mediaStorage back to the parent:
override var documentInfo by parent::documentInfo
override var attributes by parent::attributes
override var mediaStorage by parent::mediaStorage
However, it initializes with an empty set of libraries, ensuring that variables declared inside the lambda scope do not leak into the surrounding document.
Variable Scoping in Lambda Bodies
When executing lambda functions, the compiler builds a ScopeContext so that the lambda can read and write the same document metadata as its parent, while new function declarations are stored in the isolated libraries set. The surrounding document remains unaffected by temporary variables defined inside the lambda.
Practical Implementation Examples
The following Kotlin snippets demonstrate how to instantiate each context type during compiler development:
// Assume ctx is the current MutableContext
// 1. Fork a SubdocumentContext to compile an included file
val subDocCtx: SubdocumentContext = ctx.fork().also { subCtx ->
// subCtx.documentInfo is a *copy*; changes here won't affect ctx
subCtx.documentInfo.title = "Included Chapter"
}
// 2. Create a ScopeContext for a lambda body
val lambdaScope: ScopeContext = ctx.fork().also { scope ->
// scope.documentInfo points to the parent's documentInfo
scope.documentInfo.author = "Lambda Author"
// New variables declared here are stored in scope.libraries only
}
// 3. Use a SharedContext when only the file system should differ
val shared: SharedContext = SharedContext(
parent = ctx,
fileSystem = CustomFileSystem(root = "/tmp/isolated")
)
// All other mutable state is the same object as in ctx
shared.documentInfo.title = "Will also change in parent"
Key Source Files
Understanding these context implementations requires examining the source code in the quarkdown-core module:
- Context.kt – Defines the core
Contextinterface and baseMutableContextclass. - SharedContext.kt – Implements the full delegation pattern for zero-copy state sharing.
- SubdocumentContext.kt – Handles metadata isolation for sub-document processing.
- ScopeContext.kt – Provides the lambda execution environment with selective property sharing.
Summary
- SharedContext delegates every mutable property (
documentInfo,mediaStorage,libraries) to its parent via Kotlin property delegation, making it ideal for temporary file-system changes without state duplication. - SubdocumentContext creates isolated copies of
documentInfoand initializes with empty libraries, perfect for processing included files where metadata must remain local. - ScopeContext extends SubdocumentContext but re-shares
documentInfo,attributes, andmediaStoragewith the parent, enabling lambda execution that accesses document metadata while keeping variable declarations scoped. - The compiler chooses the context type based on whether the operation requires full isolation (SubdocumentContext), selective metadata sharing (ScopeContext), or zero-copy delegation (SharedContext).
Frequently Asked Questions
What is the parent class of SharedContext, ScopeContext, and SubdocumentContext?
SharedContext and SubdocumentContext both extend MutableContext (defined in Context.kt), while ScopeContext extends SubdocumentContext. This inheritance hierarchy allows ScopeContext to build upon the isolation logic of SubdocumentContext while selectively re-sharing specific properties with the parent.
When should I use SharedContext instead of SubdocumentContext?
Use SharedContext when you need to temporarily modify only the file system (or another single property) while keeping all document metadata, media storage, and libraries identical to the parent. Use SubdocumentContext when processing a separate markdown file where changes to documentInfo (like title or author) must not propagate back to the parent document.
How does ScopeContext handle variable isolation in lambda functions?
ScopeContext initializes with an empty set of libraries while delegating documentInfo, attributes, and mediaStorage to the parent. This design allows lambda bodies to read and modify document-wide metadata but stores any newly declared variables or functions in the local libraries set, preventing scope leakage into the surrounding document.
Can I manually create these contexts outside the standard fork() method?
Yes, all three classes are public and can be instantiated directly. For example, you can construct a SharedContext by passing a parent MutableContext and an optional custom FileSystem parameter. However, the standard approach within the Quarkdown compiler is to use Context.fork() which automatically selects the appropriate context type based on the compilation stage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →