314 lines
11 KiB
Go
314 lines
11 KiB
Go
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
|
|
origins map[string]Origin
|
|
transfers []inheritanceTransfer
|
|
controls []inheritanceControl
|
|
replacements []inheritanceReplacement
|
|
operations []inheritanceOperation
|
|
}
|
|
|
|
type inheritanceOperation struct {
|
|
control *inheritanceControl
|
|
transfer *inheritanceTransfer
|
|
controlOrigin Origin
|
|
parent *document.Node
|
|
effective *document.Node
|
|
parentOrigins map[string]Origin
|
|
effectiveOrigins map[string]Origin
|
|
}
|
|
|
|
// A replacement retains the displaced source/derivation record while the
|
|
// effective origin belongs to the child. Value trees are borrowed immutable
|
|
// inputs or detached templates and are budgeted with retained history later.
|
|
type inheritanceReplacement struct {
|
|
path Path
|
|
before *document.Node
|
|
after *document.Node
|
|
previous []displacedDefinition
|
|
effective Origin
|
|
}
|
|
|
|
// A link represents one shared relationship edge. Each contributor retains
|
|
// its original semantic path independently of source line/column coordinates.
|
|
type inheritanceTrace struct {
|
|
previous *inheritanceTrace
|
|
transfer *inheritanceTransfer
|
|
}
|
|
|
|
type inheritanceContributor struct {
|
|
source Path
|
|
location Location
|
|
trace *inheritanceTrace
|
|
}
|
|
|
|
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) {
|
|
return expandSiblingDefinitionsWithLimits(scope, scopePath, defaultProjectionLimits())
|
|
}
|
|
|
|
func expandSiblingDefinitionsWithLimits(scope *document.Node, scopePath Path, limits projectionLimits) (*inheritanceScope, []Diagnostic) {
|
|
return expandSiblingDefinitionsWithOrigins(scope, scopePath, limits, nil)
|
|
}
|
|
|
|
func expandSiblingDefinitionsWithOrigins(scope *document.Node, scopePath Path, limits projectionLimits, inputOrigins map[string]Origin, budget ...*retainedNodeCounter) (*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))
|
|
resolvedOrigins := make(map[string]map[string]Origin, len(byName))
|
|
state := make(map[string]uint8, len(byName))
|
|
result := &inheritanceScope{}
|
|
counter := newRetainedNodeCounter(historyNodeLimit)
|
|
if len(budget) != 0 {
|
|
counter = budget[0]
|
|
}
|
|
retained := 1 // scope mapping node
|
|
for _, entry := range scope.Mapping {
|
|
if entry.Value.Kind == document.Mapping {
|
|
continue
|
|
}
|
|
remaining := limits.nodes - retained - 1
|
|
if limits.nodes >= 0 && remaining < 0 {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath.Key(entry.Key), entry.Value.Location)}
|
|
}
|
|
count, within := inheritanceLowerBoundCount(entry.Value, remaining)
|
|
if !within {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath.Key(entry.Key), entry.Value.Location)}
|
|
}
|
|
retained += 1 + count // key and value subtree
|
|
}
|
|
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)
|
|
operationStart, replacementStart := len(result.operations), len(result.replacements)
|
|
childOrigins := directInheritanceOrigins(payload, path, inputOrigins)
|
|
if reference, found := inheritanceDirective(entry.Value); found {
|
|
control := inheritanceControl{
|
|
path: path.Key(inheritanceKey),
|
|
locations: []Location{publicLocation(reference.Value.Location)},
|
|
cancelled: reference.Value.Kind == document.Null,
|
|
}
|
|
controlOrigins := directInheritanceOrigins(reference.Value, control.path, inputOrigins)
|
|
result.operations = append(result.operations, inheritanceOperation{
|
|
control: &control, controlOrigin: controlOrigins[control.path.String()],
|
|
})
|
|
}
|
|
if limits.nodes >= 0 {
|
|
remaining := limits.nodes - retained - 1 // the sibling key
|
|
within := false
|
|
if frame.hasParent {
|
|
within = inheritanceOverlayLowerBoundWithin(resolved[frame.parent], payload, remaining)
|
|
} else {
|
|
within = inheritanceLowerBoundWithin(payload, remaining)
|
|
}
|
|
if !within {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(path, entry.Value.Location)}
|
|
}
|
|
}
|
|
if frame.hasParent {
|
|
parent := resolved[frame.parent]
|
|
merged := document.Clone(parent)
|
|
control := inheritanceDirectiveLocation(entry.Value)
|
|
transfer := inheritanceTransfer{
|
|
from: scopePath.Key(frame.parent), to: path,
|
|
reference: path.Key(inheritanceKey), locations: []Location{control},
|
|
}
|
|
mergedOrigins := transferredInheritanceOrigins(resolvedOrigins[frame.parent], transfer.from, path, transfer)
|
|
mergeInheritanceOrigin(mergedOrigins, path, childOrigins[path.String()])
|
|
mergeRecursiveMappings(merged, payload, path, new(mappingIndex), recursiveMergeHooks{
|
|
added: func(childPath Path, incoming *document.Node) {
|
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
|
},
|
|
combined: func(childPath Path, _ *document.Node) {
|
|
mergeInheritanceOrigin(mergedOrigins, childPath, childOrigins[childPath.String()])
|
|
},
|
|
replaced: func(childPath Path, existing, incoming *document.Node) {
|
|
previous := displacedOrigins(existing, mergedOrigins, childPath)
|
|
result.replacements = append(result.replacements, inheritanceReplacement{
|
|
path: childPath.clone(), before: existing, after: incoming,
|
|
previous: previous, effective: cloneOrigin(childOrigins[childPath.String()]),
|
|
})
|
|
deleteOriginSubtree(mergedOrigins, childPath, existing)
|
|
copyInheritanceSubtree(mergedOrigins, childOrigins, incoming, childPath)
|
|
},
|
|
})
|
|
merged.Location = entry.Value.Location
|
|
resolved[frame.name] = merged
|
|
resolvedOrigins[frame.name] = mergedOrigins
|
|
result.transfers = append(result.transfers, transfer)
|
|
result.operations = append(result.operations, inheritanceOperation{
|
|
transfer: &transfer, parent: parent, effective: merged,
|
|
parentOrigins: resolvedOrigins[frame.parent], effectiveOrigins: mergedOrigins,
|
|
})
|
|
} else {
|
|
resolved[frame.name] = document.Clone(payload)
|
|
resolvedOrigins[frame.name] = childOrigins
|
|
}
|
|
count, _ := inheritanceLowerBoundCount(resolved[frame.name], limits.nodes)
|
|
retained += 1 + count
|
|
for _, operation := range result.operations[operationStart:] {
|
|
if !counter.add(operation.parent) || !counter.add(operation.effective) {
|
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(path, entry.Value.Location)}
|
|
}
|
|
}
|
|
for _, replacement := range result.replacements[replacementStart:] {
|
|
if !counter.add(replacement.before) || !counter.add(replacement.after) {
|
|
return nil, []Diagnostic{inheritanceHistoryBudgetDiagnostic(path, entry.Value.Location)}
|
|
}
|
|
}
|
|
state[frame.name] = 2
|
|
stack = stack[:len(stack)-1]
|
|
}
|
|
}
|
|
|
|
root := *scope
|
|
root.Mapping = make([]document.Entry, len(scope.Mapping))
|
|
rootOrigin := directInheritanceOrigins(&document.Node{Kind: document.Mapping, Location: scope.Location}, scopePath, inputOrigins)
|
|
result.origins = rootOrigin
|
|
for index, entry := range scope.Mapping {
|
|
root.Mapping[index] = entry
|
|
if resolved[entry.Key] != nil {
|
|
root.Mapping[index].Value = resolved[entry.Key]
|
|
for pointer, origin := range resolvedOrigins[entry.Key] {
|
|
result.origins[pointer] = origin
|
|
}
|
|
} else {
|
|
root.Mapping[index].Value = document.Clone(entry.Value)
|
|
for pointer, origin := range directInheritanceOrigins(entry.Value, scopePath.Key(entry.Key), inputOrigins) {
|
|
result.origins[pointer] = origin
|
|
}
|
|
}
|
|
}
|
|
result.root = &root
|
|
if !inheritanceLowerBoundWithin(result.root, limits.nodes) {
|
|
return nil, []Diagnostic{inheritanceBudgetDiagnostic(scopePath, scope.Location)}
|
|
}
|
|
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,
|
|
}
|
|
}
|