How KCL Rule Constraint Validation Works: A Deep Dive into the Compiler Architecture
KCL validates rule constraints through a two-phase pipeline: static semantic analysis for type-checking and schema validation, followed by runtime evaluation of user-defined check expressions against concrete values.
The KCL (Kusion Configuration Language) compiler enforces configuration correctness through a sophisticated rule constraint validation system implemented in the kcl-lang/kcl repository. This mechanism operates across both compile-time semantic analysis and runtime evaluation phases to ensure that configuration data adheres to defined schemas and custom validation logic.
The Two-Phase Validation Architecture
KCL's rule constraint system bridges static compilation and runtime execution through distinct but tightly coupled phases. The semantic resolver ensures structural correctness before code execution, while the runtime evaluator executes boolean constraints against actual configuration values.
| Phase | Source File | Primary Function |
|---|---|---|
| Semantic Analysis | crates/sema/src/resolver/config.rs |
Validates config entries, types, and schema definitions statically |
| Runtime Evaluation | crates/evaluator/src/rule.rs |
Executes rule bodies and evaluates check expressions against concrete values |
The semantic phase guarantees that each check expression is well-typed and schema-compliant, while the runtime phase performs the actual constraint validation that can trigger user-defined error messages.
Semantic-Phase Constraint Checking
During compilation, the resolver validates configuration expressions before any code execution occurs. This static checking catches type mismatches and undefined attributes early in the pipeline.
Entry Point: Config Expression Validation
When the resolver encounters a ConfigExpr (a configuration literal), it invokes check_config_entry as the primary entry point for attribute-level validation.
// crates/sema/src/resolver/config.rs
pub(crate) fn check_config_entry(&mut self, key: &str, value: &ValueRef) -> TypeRef {
// …
self.check_config_expr_by_key_name(name, key);
// …
}
This function extracts the attribute name and delegates to key-level validation routines that verify the attribute exists within its parent schema's type definition.
Type Context and Attribute Resolution
The check_config_expr_by_key_name function retrieves the current object type from the config_expr_context stack and forwards the key to must_check_config_attr for concrete validation.
pub(crate) fn check_config_expr_by_key_name(
&mut self,
name: &str,
key: &'ctx ast::NodeRef<ast::Expr>,
) {
// Obtain the object type from the context stack
if !name.is_empty() && let Some(Some(obj)) = self.ctx.config_expr_context.last() {
let ty = obj.ty.clone();
self.must_check_config_attr(name, &ty, &key.get_span_pos(), None);
}
}
The must_check_config_attr function acts as a dispatcher, determining which specific validation rule applies (check_defined, check_type, etc.) before invoking check_config_attr to handle schema mapping and error reporting via kcl_error::SEMANTIC_ERROR_MSG.
Recursive Schema Validation
For nested configurations, check_config_value_recursively walks into dict literals and nested schemas, invoking check_attr_recursively for each nested attribute. This ensures that all constraints defined in parent schemas are verified statically, including inherited attributes from base schemas.
Runtime-Phase Rule Evaluation
After successful compilation, the evaluator executes rule bodies and their associated check blocks against concrete configuration instances.
Rule Evaluation Context Creation
The rule_body function in crates/evaluator/src/rule.rs initializes the runtime environment for rule execution.
// crates/evaluator/src/rule.rs
pub fn rule_body(
s: &Evaluator,
ctx: &RuleEvalContextRef,
args: &ValueRef,
kwargs: &ValueRef,
) -> ValueRef {
// ...
}
The RuleEvalContext encapsulates:
- The rule's AST node
- The current schema value being validated
- The configuration under validation
- Auxiliary metadata including
config_metaandoptional_mapping
Rule Body Execution Flow
The evaluator executes the rule body through a structured sequence:
- Base Schema Handling — For sub-rules (indicated by
for_host_name), the evaluator invokes the parent schema's constructor viacall_schema_body_from_rule - Scope Management —
push_schemaandenter_scopecreate fresh variable environments, with arguments bound throughwalk_arguments - Decorator Evaluation — All rule decorators execute via
walk_decorator_with_name - Constraint Invocation — For sub-schemas (marked by
is_sub_schema), the system callsrule_check
Executing Check Expressions
The rule_check function implements the core constraint validation logic, handling both inheritance and local constraint evaluation.
pub fn rule_check(
s: &Evaluator,
ctx: &RuleEvalContextRef,
args: &ValueRef,
kwargs: &ValueRef,
) -> ValueRef {
// ① Call base-rule checks (inheritance)
for parent_name in &ctx.borrow().node.parent_rules {
let base_constructor_func = s
.walk_identifier_with_ctx(&parent_name.node, &ast::ExprContext::Load, None)
.expect(kcl_error::RUNTIME_ERROR_MSG);
call_rule_check(s, &base_constructor_func, args, kwargs);
}
// ② Execute the rule's own check expressions
for check_expr in &ctx.borrow().node.checks {
s.walk_check_expr(&check_expr.node)
.expect(kcl_error::RUNTIME_ERROR_MSG);
}
ctx.borrow().value.clone()
}
Parent Rule Checks — If the rule extends other rules (via parent_rules), their checks execute first, ensuring constraint inheritance from base rules.
Local Check Evaluation — Each check_expr represents an AST node containing a boolean expression (optionally with an error message). The walk_check_expr method evaluates this expression; if it returns false, the evaluator raises a runtime error containing the user-defined message.
VM Integration
The call_rule_check and call_schema_body_from_rule functions serve as thin proxies to the runtime VM (implemented in crates/runtime/src/lib.rs). These helpers fetch compiled function objects and invoke them while preserving the evaluation stack state.
Complete Execution Flow
The end-to-end validation process follows this pipeline:
- Parsing — The AST contains
RuleStmtnodes with associatedchecksarrays - Semantic Resolution —
check_config_entryvalidates static constraints while building the type context - Context Creation — The evaluator instantiates a
RuleEvalContextfor the target rule - Body Execution —
rule_bodyruns initialization logic and decorator checks - Constraint Validation —
rule_checkiterates parent rules and local expressions, callingwalk_check_expron each - Error Reporting — Violations trigger runtime errors with precise source spans from the AST
Key Source Files
Understanding KCL's constraint validation requires familiarity with these core components:
crates/sema/src/resolver/config.rs— Static constraint checking for config expressions and schema attributescrates/evaluator/src/rule.rs— Runtime rule evaluation, body execution, and check block processingcrates/runtime/src/lib.rs— Low-level VM integration forcall_rule_checkand related proxy functionscrates/ast/src/ast.rs— Definitions forRuleStmt,CheckExpr, and related AST structurescrates/evaluator/src/evaluator.rs— Core evaluation primitives includingwalk_check_exprand scope management
Summary
- Two-phase validation separates static type checking (semantic resolver) from runtime constraint evaluation (rule evaluator)
- Semantic analysis in
config.rsvalidates configuration structure throughcheck_config_entryand recursive attribute checking - Runtime evaluation uses
RuleEvalContextto maintain state whilerule_checkexecutes inherited and local constraints - Inheritance support allows rules to extend parent rules, with
rule_checkautomatically invoking parent validations before local checks - AST-based reporting preserves source spans throughout the pipeline for precise error localization
Frequently Asked Questions
How does KCL handle rule inheritance during constraint validation?
When rule_check executes, it iterates through the parent_rules collection in the rule's AST node before evaluating local checks. For each parent, it resolves the base rule's identifier and invokes call_rule_check, ensuring parent constraints execute recursively. This guarantees that sub-rules satisfy all constraints defined in their ancestor rules.
What happens when a runtime check expression fails?
The evaluator calls walk_check_expr on each check expression in the rule's checks array. If the boolean evaluation returns false, the runtime raises an error using the message specified in the CheckExpr AST node. This error bubbles up through the evaluation stack with precise source span information from the original AST, allowing the error reporter to highlight the exact line in the KCL source code.
Can semantic analysis catch all constraint violations before runtime?
No. The semantic phase in crates/sema/src/resolver/config.rs validates structural correctness—ensuring attributes exist, types match, and schema definitions are well-formed. However, user-defined check blocks containing arbitrary boolean expressions (e.g., check age >= 18) require runtime evaluation because they depend on concrete configuration values that are only available during execution.
How does the evaluator maintain isolation between different rule evaluations?
The RuleEvalContext struct encapsulates all state specific to a single rule evaluation, including the rule's AST node, the schema value being validated, and configuration metadata. The rule_body function creates fresh scopes using push_schema and enter_scope, ensuring that variables and mutations within one rule execution do not leak into subsequent evaluations.
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 →