How KCL's Automatic Merge Mechanism Handles Complex Configuration Merging
KCL automatically merges configuration files during the pre-process phase using a three-stage AST transformation pipeline that collects declarations, unifies entries based on merge kinds, and eliminates redundant statements to produce deterministic configuration outputs.
The KCL configuration language (kcl-lang/kcl) eliminates manual configuration consolidation through its automatic merge mechanism, which reconciles overlapping definitions across multiple files during compilation. This process operates entirely within the semantic analysis phase, ensuring that unification logic remains predictable and order-independent for complex infrastructure definitions.
Three-Stage Merge Pipeline
The merge implementation lives in crates/sema/src/pre_process/config.rs, where the ConfigMergeTransformer executes three sequential passes over the abstract syntax tree.
Stage 1: Collecting Declarations
The transformer indexes every top-level name that introduces a configuration, including unification statements and schema assignments. For each declaration, it stores the filename, module index, statement index, and a merge kind classification—either Union for standard unifications or Override for schema assignments and private fields. This collection logic resides in ConfigMergeTransformer::merge (lines 20‑48).
Stage 2: Merging Config Entries
When a name appears in multiple modules, the transformer gathers the individual ConfigEntry lists, concatenates them, and normalizes the result using unify_config_entries.
- Union merges retain all entries, allowing values to coexist
- Override merges clear previous entries before inserting the new definition, ensuring the latest assignment wins
This logic appears in the match arms for ConfigMergeKind::Union and ConfigMergeKind::Override (lines 94‑128), with the normalization helper defined at lines 96‑138.
Stage 3: Cleaning Up Redundant Statements
After the merged configuration replaces the final occurrence, the transformer prunes earlier duplicate statements from the AST. This elimination pass (lines 131‑152) prevents duplicate evaluation while preserving the semantic intent of the consolidated configuration.
Handling Complex Configuration Scenarios
Nested Attribute Merges
Before merging occurs, the ConfigNestAttrTransformer rewrites dot-notation expressions such as {a.b.c = 1} into hierarchical config entry trees (a: {b: {c = 1}}). This transformation occurs in fix_config_expr_nest_attr (lines 152‑156), ensuring nested attributes merge correctly regardless of their original syntactic form.
Private Fields
Fields prefixed with underscores (e.g., _secret) trigger Override semantics even when declared within unification contexts. The is_private_field check (lines 61‑68) forces the declaration list to clear before inserting the private entry, guaranteeing that sensitive or internal values dominate previous definitions.
Mixed Union and Override Semantics
The mechanism handles scenarios where the same identifier uses different merge kinds across files. The index stores the last encountered kind for each name, and the transformer selects the appropriate Union or Override branch based on this stored classification. This guarantees consistent merge semantics even when configuration files disagree on merge strategy.
Recursive Config Values
unify_config_entries recursively descends into Config and Schema nodes (lines 124‑138), flattening nested structures before performing deduplication. This recursion ensures that configurations nested inside schemas merge with the same deterministic rules as top-level declarations.
Multiple Files with Identical Module Names
To prevent collisions when identical filenames appear in different packages, modules are keyed by both filename and module index throughout the collection and merging passes (see line 22). This distinction ensures隔离 between packages while permitting intentional merges within the same logical module.
Practical Merge Example
Consider a base configuration and an overlay that modify the same resource:
# base.k
config = {
name = "app"
replicas = 2
env = {
LOG_LEVEL = "info"
}
}
# overlay.k
config = {
replicas = 3 # Union: value overridden by later entry
env = {
DEBUG = "true" # Union: additive merge with base.env
}
_secret = "top" # Private field => Override semantics
}
Compile with automatic merging enabled (default):
kcl run base.k overlay.k
The resulting configuration applies Union merging to env (combining both maps) and replicas (overriding with the final value), while the private field _secret uses Override to ensure it replaces any previous definition:
config:
name: app
replicas: 3
env:
LOG_LEVEL: info
DEBUG: true
_secret: top
Disabling Automatic Merge
For debugging scenarios requiring unmerged AST inspection, pass the --no-merge flag. This option sets merge_program to false in ResolverOptions (see crates/sema/src/resolver/mod.rs, line 182), bypassing the ConfigMergeTransformer entirely.
Summary
- The automatic merge mechanism operates during the pre-process phase in
crates/sema/src/pre_process/config.rs - Three distinct passes collect declarations, unify entries using
UnionorOverridesemantics, and prune redundant AST nodes - Union merges combine entries additively while Override merges replace previous definitions entirely
- Private fields (prefixed with
_) force Override behavior regardless of context - Nested attributes are normalized via
fix_config_expr_nest_attrbefore merging occurs - The runtime complements this with
dict_mergeincrates/runtime/src/value/val_dict.rs(lines 337‑353) for dynamic dictionary operations
Frequently Asked Questions
How does KCL resolve conflicts when the same field is defined in multiple files?
KCL resolves conflicts using the merge kind associated with each declaration. Standard unifications use Union semantics that preserve all entries but allow later values to override earlier ones for scalar fields. Schema assignments and private fields use Override semantics that explicitly clear previous definitions. The transformer applies the strategy recorded during the collection pass (lines 20‑48) when merging entries (lines 94‑128).
What is the difference between Union and Override merge kinds in KCL?
Union merges concatenate ConfigEntry lists and keep all historical values, enabling additive configuration patterns where maps and lists accumulate across files. Override merges clear the existing entry list before inserting the new definition, ensuring complete replacement. The mechanism selects between these behaviors based on the stored ConfigMergeKind enum value associated with each identifier.
Can I disable automatic merging for specific configuration blocks?
The automatic merge mechanism is a global compilation setting controlled by the merge_program flag in ResolverOptions. You cannot selectively disable merging for individual blocks within a single compilation unit. To prevent merging for specific files, compile them separately or use the --no-merge CLI flag to disable the entire pipeline, as implemented in crates/sema/src/resolver/mod.rs (line 182).
How does KCL handle deeply nested configuration overrides?
Deeply nested attributes are normalized before merging occurs. The ConfigNestAttrTransformer rewrites dot-notation paths like a.b.c = 1 into nested config entry trees. The unify_config_entries function then recursively processes these trees (lines 124‑138), applying Union or Override semantics at each level to ensure nested structures merge with the same deterministic rules as top-level attributes.
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 →