Files
ConfigKit/internal/engine/inheritance_walk.go

254 lines
7.7 KiB
Go

package engine
import (
"cmp"
"slices"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type inheritanceWalker struct {
root **document.Node
origins map[string]Origin
limits projectionLimits
transfers []inheritanceTransfer
controls []inheritanceControl
replacements []inheritanceReplacement
}
// expandInheritanceTree resolves each destination scope after its enclosing
// relationships. It remains a private, test-only operation until the loading
// lifecycle also carries its complete provenance and inspection evidence.
func expandInheritanceTree(input *document.Node, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
return expandInheritanceWithOrigins(input, nil, limits)
}
func expandInheritanceWithOrigins(input *document.Node, inputOrigins map[string]Origin, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
if input == nil || input.Kind != document.Mapping {
return nil, []Diagnostic{{Code: CodeInheritanceInvalid, Severity: SeverityError,
Message: "inheritance requires a mapping document root"}}
}
root := input
walker := inheritanceWalker{root: &root, limits: limits, origins: cloneOriginIndex(inputOrigins)}
if problem := walker.visit(&root, Path{}, true); problem != nil {
return nil, []Diagnostic{*problem}
}
if problem := inheritanceFinalBudget(root, limits); problem != nil {
return nil, []Diagnostic{*problem}
}
return &inheritanceScope{
root: root, origins: walker.origins, transfers: walker.transfers,
controls: walker.controls, replacements: walker.replacements,
}, nil
}
func (w *inheritanceWalker) visit(node **document.Node, path Path, unscoped bool) *Diagnostic {
switch (*node).Kind {
case document.Mapping:
original := *node
if unscoped {
reference, found := inheritanceDirective(*node)
if found {
referencePath := path.Key(inheritanceKey)
location := publicLocation(reference.Value.Location)
if reference.Value.Kind != document.Null {
problem := inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
"inheritance reference has no enclosing sibling scope")
return &problem
}
w.controls = append(w.controls, inheritanceControl{
path: referencePath, locations: []Location{location}, cancelled: true,
})
deleteOriginSubtree(w.origins, referencePath, reference.Value)
*node = withoutInheritanceDirective(*node)
}
}
resolved, diagnostics := expandSiblingDefinitionsWithOrigins(*node, path, w.limits, w.origins)
if len(diagnostics) != 0 {
return &diagnostics[0]
}
*node = resolved.root
deleteOriginSubtree(w.origins, path, original)
for pointer, origin := range resolved.origins {
w.origins[pointer] = origin
}
w.transfers = append(w.transfers, resolved.transfers...)
w.controls = append(w.controls, resolved.controls...)
w.replacements = append(w.replacements, resolved.replacements...)
if !inheritanceLowerBoundWithin(*w.root, w.limits.nodes) {
problem := inheritanceBudgetDiagnostic(path, (*node).Location)
return &problem
}
indices := make([]int, len((*node).Mapping))
for index := range indices {
indices[index] = index
}
slices.SortFunc(indices, func(left, right int) int {
return cmp.Compare((*node).Mapping[left].Key, (*node).Mapping[right].Key)
})
for _, index := range indices {
entry := &(*node).Mapping[index]
if problem := w.visit(&entry.Value, path.Key(entry.Key), false); problem != nil {
return problem
}
}
case document.Sequence:
for index := range (*node).Sequence {
child := &(*node).Sequence[index]
if problem := w.visit(child, path.Index(index), (*child).Kind == document.Mapping); problem != nil {
return problem
}
}
}
return nil
}
// A directive may disappear during later destination resolution, so only
// non-directive nodes form a safe early lower bound on final output size.
func inheritanceLowerBoundWithin(root *document.Node, limit int) bool {
_, within := inheritanceLowerBoundCount(root, limit)
return within
}
func inheritanceLowerBoundCount(root *document.Node, limit int) (int, bool) {
if limit < 0 {
limit = int(^uint(0) >> 1)
}
stack := []*document.Node{root}
count := 0
for len(stack) != 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if node == nil {
continue
}
count++
if count > limit {
return count, false
}
switch node.Kind {
case document.Mapping:
for _, entry := range node.Mapping {
if entry.Key == inheritanceKey {
continue
}
count++ // mapping key
stack = append(stack, entry.Value)
}
case document.Sequence:
stack = append(stack, node.Sequence...)
}
if count > limit {
return count, false
}
}
return count, true
}
// Count the materialized mapping shape before cloning a memoized parent.
// Relationship fields are excluded because later destination resolution
// consumes them; their omission makes this a safe lower bound.
func inheritanceOverlayLowerBoundWithin(lower, upper *document.Node, limit int) bool {
if limit < 0 {
return false
}
index := new(mappingIndex)
count := 0
var visit func(*document.Node, *document.Node) bool
visit = func(base, incoming *document.Node) bool {
if base.Kind != document.Mapping || incoming.Kind != document.Mapping {
added, within := inheritanceLowerBoundCount(incoming, limit-count)
count += added
return within
}
count++ // mapping node
if count > limit {
return false
}
for _, entry := range base.Mapping {
if entry.Key == inheritanceKey {
continue
}
count++ // mapping key
if count > limit {
return false
}
position := index.find(incoming, entry.Key)
if position >= 0 {
if !visit(entry.Value, incoming.Mapping[position].Value) {
return false
}
} else {
added, within := inheritanceLowerBoundCount(entry.Value, limit-count)
count += added
if !within {
return false
}
}
}
for _, entry := range incoming.Mapping {
if entry.Key == inheritanceKey || index.find(base, entry.Key) >= 0 {
continue
}
count++ // mapping key
if count > limit {
return false
}
added, within := inheritanceLowerBoundCount(entry.Value, limit-count)
count += added
if !within || count > limit {
return false
}
}
return true
}
return visit(lower, upper)
}
func inheritanceFinalBudget(root *document.Node, limits projectionLimits) *Diagnostic {
type pending struct {
node *document.Node
path Path
}
stack := []pending{{node: root}}
count := 0
for len(stack) != 0 {
item := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if len(item.path.segments) > limits.depth {
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
return &problem
}
count++
if limits.nodes >= 0 && count > limits.nodes {
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
return &problem
}
switch item.node.Kind {
case document.Mapping:
count += len(item.node.Mapping) // mapping keys
for index := len(item.node.Mapping) - 1; index >= 0; index-- {
entry := item.node.Mapping[index]
stack = append(stack, pending{node: entry.Value, path: item.path.Key(entry.Key)})
}
case document.Sequence:
for index := len(item.node.Sequence) - 1; index >= 0; index-- {
stack = append(stack, pending{node: item.node.Sequence[index], path: item.path.Index(index)})
}
}
if limits.nodes >= 0 && count > limits.nodes {
problem := inheritanceBudgetDiagnostic(item.path, item.node.Location)
return &problem
}
}
return nil
}
func inheritanceBudgetDiagnostic(path Path, location document.Location) Diagnostic {
primary := publicLocation(location)
return Diagnostic{
Code: CodeSchemaProjectionLimit, Severity: SeverityError, Path: path,
Message: "expanded configuration exceeds the projection limit", Primary: &primary,
}
}