package engine import ( "fmt" "path/filepath" "strings" ) type importNodeKey struct { namespaceKind string namespaceAnchor string resourceKind string resourcePath string } func (key importNodeKey) dependencyKey() dependencyKey { return dependencyKey{key.namespaceKind, key.namespaceAnchor, key.resourceKind, key.resourcePath} } type importContributionRole string const ( importRoleMain importContributionRole = "main" importRoleMapping importContributionRole = "mapping-import" importRoleItem importContributionRole = "item-import" ) type importGraphNode struct { key importNodeKey canonical string sourceID string role importContributionRole } type importInclusionEdge struct { importer importNodeKey imported importNodeKey location Location } // importInclusionGraph retains source inclusion independently of mapping-scope // ownership. Scheduling edges point from each imported prerequisite to its // importer, matching the shared dependency scheduler's convention. type importInclusionGraph struct { nodes []importGraphNode edges []importInclusionEdge index map[importNodeKey]int } func newImportInclusionGraph() importInclusionGraph { return importInclusionGraph{index: make(map[importNodeKey]int)} } func (graph *importInclusionGraph) addNode(node importGraphNode) int { if graph.index == nil { graph.index = make(map[importNodeKey]int) } if index, exists := graph.index[node.key]; exists { return index } index := len(graph.nodes) graph.index[node.key] = index graph.nodes = append(graph.nodes, node) return index } func (graph *importInclusionGraph) addEdge(importer, imported importNodeKey, location Location) int { index := len(graph.edges) graph.edges = append(graph.edges, importInclusionEdge{ importer: importer, imported: imported, location: location, }) return index } func (graph importInclusionGraph) clone() importInclusionGraph { cloned := importInclusionGraph{ nodes: append([]importGraphNode(nil), graph.nodes...), edges: make([]importInclusionEdge, len(graph.edges)), index: make(map[importNodeKey]int, len(graph.index)), } for index, edge := range graph.edges { cloned.edges[index] = importInclusionEdge{ importer: edge.importer, imported: edge.imported, location: edge.location, } } for key, index := range graph.index { cloned.index[key] = index } return cloned } func mergeImportInclusionGraph(target *importInclusionGraph, source importInclusionGraph) { for _, node := range source.nodes { target.addNode(node) } for _, edge := range source.edges { target.addEdge(edge.importer, edge.imported, edge.location) } } func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Location) (Diagnostic, bool) { keys := make([]dependencyKey, len(graph.nodes)) for index, node := range graph.nodes { keys[index] = node.key.dependencyKey() } edges := make([]dependencyEdge, 0, len(graph.edges)) for _, edge := range graph.edges { importer, importerOK := graph.index[edge.importer] imported, importedOK := graph.index[edge.imported] if !importerOK || !importedOK { continue } edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer}) } _, cycle := scheduleDependencies(keys, edges) if len(cycle) < 2 { return Diagnostic{}, false } // Scheduler edges reverse inclusion edges. Reverse its closed cycle to // render the authored import route while keeping the canonical start node. route := make([]int, 0, len(cycle)) route = append(route, cycle[0]) for index := len(cycle) - 2; index > 0; index-- { route = append(route, cycle[index]) } route = append(route, cycle[0]) paths := make([]string, len(route)) for index, node := range route { paths[index] = graph.nodes[node].canonical } related := make([]Location, 0, len(route)-2) for index := 0; index+1 < len(route); index++ { importerKey := graph.nodes[route[index]].key importedKey := graph.nodes[route[index+1]].key edgeIndex := -1 for candidate, edge := range graph.edges { if edge.importer == importerKey && edge.imported == importedKey { edgeIndex = candidate if candidate == closingEdge { break } } } if edgeIndex == closingEdge { continue } if edgeIndex >= 0 { related = append(related, graph.edges[edgeIndex].location) } } return Diagnostic{ Code: CodeImportCycle, Severity: SeverityError, Message: "import cycle detected: " + strings.Join(paths, " -> "), Primary: cloneLocation(&location), Related: related, }, true } // importDependencyIndex is built once per scheduling operation and shared by // its scopes. Inclusion targets are grouped by importer, so local scheduling // need not scan or reserve storage for unrelated graph edges. type importDependencyIndex map[importNodeKey][]importNodeKey func indexImportDependencies(graph importInclusionGraph) importDependencyIndex { index := make(importDependencyIndex) for _, edge := range graph.edges { index[edge.importer] = append(index[edge.importer], edge.imported) } return index } func scheduleAcquisitionScope(scope acquisitionScope, dependencies importDependencyIndex) acquisitionScope { slots := make([]int, 0, len(scope.contributions)) for index, contribution := range scope.contributions { if contribution.scopeOwner != scope.owner { continue } slots = append(slots, index) } scheduled := scope scheduled.contributions = scheduleContributionSlots(scope.contributions, slots, dependencies) return scheduled } func scheduleAcquisitionGroup(scopes []acquisitionScope, graph importInclusionGraph) acquisitionScope { var contributions []acquiredContribution for _, scope := range scopes { contributions = append(contributions, scope.contributions...) } slots := make([]int, len(contributions)) for index := range slots { slots[index] = index } return acquisitionScope{contributions: scheduleContributionSlots(contributions, slots, indexImportDependencies(graph))} } func scheduleContributionSlots(contributions []acquiredContribution, slots []int, dependencies importDependencyIndex) []acquiredContribution { if len(slots) < 2 { return append([]acquiredContribution(nil), contributions...) } positions := make(map[importNodeKey]int, len(slots)) keys := make([]dependencyKey, len(slots)) for position, slot := range slots { contribution := contributions[slot] positions[contribution.identity] = position keys[position] = contribution.identity.dependencyKey() } var edges []dependencyEdge for importerKey, importer := range positions { for _, importedKey := range dependencies[importerKey] { if imported, exists := positions[importedKey]; exists { edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer}) } } } order, _ := scheduleDependencies(keys, edges) if len(order) != len(keys) { return append([]acquiredContribution(nil), contributions...) } scheduled := append([]acquiredContribution(nil), contributions...) for position, scheduledIndex := range order { scheduled[slots[position]] = contributions[slots[scheduledIndex]] } return scheduled } type importNamespaceAnchors struct { kind string host string native string input string } func explicitNamespaceAnchor(group, input int) string { return fmt.Sprintf("%020d/%020d", group, input) } func reanchorImportKey(key importNodeKey, anchors importNamespaceAnchors) importNodeKey { if anchors.kind != "" { key.namespaceKind = anchors.kind } switch { case key.resourceKind == "synthetic": key.namespaceAnchor = anchors.input case key.namespaceKind == "native": key.namespaceAnchor = anchors.native default: key.namespaceAnchor = anchors.host } return key } func reanchorImportGraph(graph importInclusionGraph, anchors importNamespaceAnchors) (importInclusionGraph, map[importNodeKey]importNodeKey) { reanchored := newImportInclusionGraph() keys := make(map[importNodeKey]importNodeKey, len(graph.nodes)) for _, node := range graph.nodes { old := node.key node.key = reanchorImportKey(old, anchors) keys[old] = node.key reanchored.addNode(node) } for _, edge := range graph.edges { reanchored.addEdge(keys[edge.importer], keys[edge.imported], edge.location) } return reanchored, keys } func reanchorAcquisitionScope(scope *acquisitionScope, keys map[importNodeKey]importNodeKey, anchors importNamespaceAnchors) { if scope == nil { return } if key, exists := keys[scope.owner]; exists { scope.owner = key } else { scope.owner = reanchorImportKey(scope.owner, anchors) } for index := range scope.contributions { contribution := &scope.contributions[index] if key, exists := keys[contribution.identity]; exists { contribution.identity = key } else { contribution.identity = reanchorImportKey(contribution.identity, anchors) } if key, exists := keys[contribution.scopeOwner]; exists { contribution.scopeOwner = key } else { contribution.scopeOwner = reanchorImportKey(contribution.scopeOwner, anchors) } reanchorAcquisitionScope(contribution.pendingItem, keys, anchors) if contribution.nested != nil { for nestedIndex := range contribution.nested.acquisition { reanchorAcquisitionScope(&contribution.nested.acquisition[nestedIndex], keys, anchors) } } } } func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey { kind := "host" resourceKind := "file" resourcePath := filepath.ToSlash(canonical) if access.native != nil { kind = "native" } if synthetic { resourceKind = "synthetic" resourcePath = "" } return importNodeKey{ namespaceKind: kind, // This traversal owns the main input position; grouped inputs receive // their explicit positions when their acquisition graphs are combined. namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0), resourceKind: resourceKind, resourcePath: resourcePath, } } func importKeyForMainSource(source *SourceInfo) importNodeKey { if source == nil { return importNodeKey{} } namespaceKind := "host" if source.Kind == SourceKindFilesystem { namespaceKind = "native" } synthetic := source.Kind != SourceKindFile && source.Kind != SourceKindFilesystem resourceKind := "file" resourcePath := filepath.ToSlash(source.Path) if synthetic { resourceKind = "synthetic" resourcePath = "" } return importNodeKey{ namespaceKind: namespaceKind, namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0), resourceKind: resourceKind, resourcePath: resourcePath, } }