Resolve sibling inheritance with memoized parent templates
This commit is contained in:
176
internal/engine/inheritance.go
Normal file
176
internal/engine/inheritance.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
const inheritanceKey = "extends"
|
||||
|
||||
// These private records keep structural evidence separate from acquired source
|
||||
// records. Later inspection can project a transfer onto individual descendants.
|
||||
type inheritanceTransfer struct {
|
||||
from Path
|
||||
to Path
|
||||
reference Path
|
||||
locations []Location
|
||||
}
|
||||
|
||||
type inheritanceControl struct {
|
||||
path Path
|
||||
locations []Location
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
type inheritanceScope struct {
|
||||
root *document.Node
|
||||
transfers []inheritanceTransfer
|
||||
controls []inheritanceControl
|
||||
}
|
||||
|
||||
type siblingFrame struct {
|
||||
name string
|
||||
parent string
|
||||
hasParent bool
|
||||
entered bool
|
||||
}
|
||||
|
||||
// expandSiblingDefinitions resolves only the mappings directly in scope.
|
||||
// Nested instructions remain in the returned templates for destination-local
|
||||
// resolution. This private entry point does not participate in public loading.
|
||||
func expandSiblingDefinitions(scope *document.Node, scopePath Path) (*inheritanceScope, []Diagnostic) {
|
||||
if scope == nil || scope.Kind != document.Mapping {
|
||||
return nil, []Diagnostic{{
|
||||
Code: CodeInheritanceInvalid, Severity: SeverityError, Path: scopePath,
|
||||
Message: "inheritance scope must be a mapping",
|
||||
}}
|
||||
}
|
||||
|
||||
byName := make(map[string]document.Entry, len(scope.Mapping))
|
||||
names := make([]string, 0, len(scope.Mapping))
|
||||
for _, entry := range scope.Mapping {
|
||||
byName[entry.Key] = entry
|
||||
names = append(names, entry.Key)
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
resolved := make(map[string]*document.Node, len(byName))
|
||||
state := make(map[string]uint8, len(byName))
|
||||
result := &inheritanceScope{}
|
||||
for _, name := range names {
|
||||
if byName[name].Value.Kind != document.Mapping || state[name] == 2 {
|
||||
continue
|
||||
}
|
||||
stack := []siblingFrame{{name: name}}
|
||||
for len(stack) != 0 {
|
||||
frame := &stack[len(stack)-1]
|
||||
entry := byName[frame.name]
|
||||
path := scopePath.Key(frame.name)
|
||||
if !frame.entered {
|
||||
state[frame.name] = 1
|
||||
frame.entered = true
|
||||
reference, found := inheritanceDirective(entry.Value)
|
||||
if found {
|
||||
referencePath := path.Key(inheritanceKey)
|
||||
location := publicLocation(reference.Value.Location)
|
||||
result.controls = append(result.controls, inheritanceControl{
|
||||
path: referencePath, locations: []Location{location},
|
||||
cancelled: reference.Value.Kind == document.Null,
|
||||
})
|
||||
switch reference.Value.Kind {
|
||||
case document.Null:
|
||||
case document.String:
|
||||
frame.parent, frame.hasParent = reference.Value.Text, true
|
||||
parent, exists := byName[frame.parent]
|
||||
if !exists {
|
||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceMissing, referencePath, location, nil,
|
||||
"referenced sibling does not exist")}
|
||||
}
|
||||
if parent.Value.Kind != document.Mapping {
|
||||
related := []Location{publicLocation(parent.Value.Location)}
|
||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, related,
|
||||
"referenced sibling is not a mapping")}
|
||||
}
|
||||
if state[frame.parent] == 1 {
|
||||
related := []Location{publicLocation(parent.Value.Location)}
|
||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceCycle, referencePath, location, related,
|
||||
"sibling inheritance contains a cycle")}
|
||||
}
|
||||
if state[frame.parent] == 0 {
|
||||
stack = append(stack, siblingFrame{name: frame.parent})
|
||||
continue
|
||||
}
|
||||
default:
|
||||
return nil, []Diagnostic{inheritanceDiagnostic(CodeInheritanceInvalid, referencePath, location, nil,
|
||||
"inheritance reference must be a sibling name or null")}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload := withoutInheritanceDirective(entry.Value)
|
||||
if frame.hasParent {
|
||||
parent := resolved[frame.parent]
|
||||
merged := document.Clone(parent)
|
||||
mergeRecursiveMappings(merged, payload, path, new(mappingIndex), recursiveMergeHooks{})
|
||||
merged.Location = entry.Value.Location
|
||||
resolved[frame.name] = merged
|
||||
reference := path.Key(inheritanceKey)
|
||||
control := inheritanceDirectiveLocation(entry.Value)
|
||||
result.transfers = append(result.transfers, inheritanceTransfer{
|
||||
from: scopePath.Key(frame.parent), to: path,
|
||||
reference: reference, locations: []Location{control},
|
||||
})
|
||||
} else {
|
||||
resolved[frame.name] = document.Clone(payload)
|
||||
}
|
||||
state[frame.name] = 2
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
root := *scope
|
||||
root.Mapping = make([]document.Entry, len(scope.Mapping))
|
||||
for index, entry := range scope.Mapping {
|
||||
root.Mapping[index] = entry
|
||||
if resolved[entry.Key] != nil {
|
||||
root.Mapping[index].Value = resolved[entry.Key]
|
||||
} else {
|
||||
root.Mapping[index].Value = document.Clone(entry.Value)
|
||||
}
|
||||
}
|
||||
result.root = &root
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func inheritanceDirective(node *document.Node) (document.Entry, bool) {
|
||||
for _, entry := range node.Mapping {
|
||||
if entry.Key == inheritanceKey {
|
||||
return entry, true
|
||||
}
|
||||
}
|
||||
return document.Entry{}, false
|
||||
}
|
||||
|
||||
func inheritanceDirectiveLocation(node *document.Node) Location {
|
||||
entry, _ := inheritanceDirective(node)
|
||||
return publicLocation(entry.Value.Location)
|
||||
}
|
||||
|
||||
func withoutInheritanceDirective(node *document.Node) *document.Node {
|
||||
payload := *node
|
||||
payload.Mapping = make([]document.Entry, 0, len(node.Mapping))
|
||||
for _, entry := range node.Mapping {
|
||||
if entry.Key != inheritanceKey {
|
||||
payload.Mapping = append(payload.Mapping, entry)
|
||||
}
|
||||
}
|
||||
return &payload
|
||||
}
|
||||
|
||||
func inheritanceDiagnostic(code Code, path Path, primary Location, related []Location, message string) Diagnostic {
|
||||
return Diagnostic{
|
||||
Code: code, Severity: SeverityError, Path: path, Message: message,
|
||||
Primary: &primary, Related: related,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user