@@ -2188,51 +2188,126 @@ <h3>Phases</h3>
21882188 return arr [ Symbol . iterator ] ( ) ;
21892189 }
21902190
2191- // Derive array of operations used for graph layout with pruning of noop leaves.
2191+ // Derive array of operations used for graph layout with pruning of noop nodes.
2192+ // New behavior: skip noop nodes that are leaves (no dependents) OR that have exactly
2193+ // one incoming edge OR exactly one outgoing edge. When pruning a noop, we will
2194+ // rewire dependencies so remaining ops connect directly (i.e., bypass removed noops).
21922195 function computeGraphOperations ( ) {
21932196 // Stable layout: always all operations; filtering only adjusts classes
21942197 const base = Array . from ( operations . values ( ) ) ;
21952198 computeFilterSets ( ) ;
21962199 if ( ! base . length ) return base ;
2197- // Build dependency reverse map (dependents list) inside the filtered set.
2200+ // Build lookup and static dependents map (dependents = incoming adjacency)
21982201 const byName = new Map ( ) ;
21992202 base . forEach ( ( o ) => byName . set ( o . name , o ) ) ;
2200- const dependents = new Map ( ) ; // name -> Set of dependent op names
2203+ const dependents = new Map ( ) ; // name -> Set of direct dependent op names
22012204 base . forEach ( ( o ) => {
22022205 ( o . dependencies || [ ] ) . forEach ( ( d ) => {
22032206 if ( ! byName . has ( d ) ) return ; // dependency outside filtered set not considered for pruning
22042207 ( dependents . get ( d ) || dependents . set ( d , new Set ( ) ) . get ( d ) ) . add ( o . name ) ;
22052208 } ) ;
22062209 } ) ;
2207- // We want to remove noop operations that have no dependents OR whose dependents are all pruned noop as well.
2208- // Algorithm: iterative pruning queue starting with noop nodes with zero dependents.
2210+
2211+ // Active set initially contains all nodes; we'll iteratively remove noop nodes
2212+ // that meet the pruning criteria.
22092213 const active = new Set ( base . map ( ( o ) => o . name ) ) ;
22102214 const noopSet = new Set ( base . filter ( ( o ) => o . noop ) . map ( ( o ) => o . name ) ) ;
2211- const dependentsCount = new Map ( ) ;
2212- active . forEach ( ( n ) => dependentsCount . set ( n , ( dependents . get ( n ) || new Set ( ) ) . size ) ) ;
2215+
2216+ // Compute initial counts (incoming = number of dependents, outgoing = number of deps)
2217+ const incomingCount = new Map ( ) ;
2218+ const outgoingCount = new Map ( ) ;
2219+ base . forEach ( ( o ) => {
2220+ incomingCount . set ( o . name , ( dependents . get ( o . name ) || new Set ( ) ) . size ) ;
2221+ const out = ( o . dependencies || [ ] ) . filter ( ( d ) => byName . has ( d ) ) . length ;
2222+ outgoingCount . set ( o . name , out ) ;
2223+ } ) ;
2224+
2225+ // Queue noop nodes that are leaves or have exactly one incoming or one outgoing edge.
22132226 const queue = [ ] ;
22142227 active . forEach ( ( n ) => {
2215- if ( noopSet . has ( n ) && dependentsCount . get ( n ) === 0 ) queue . push ( n ) ;
2228+ if ( ! noopSet . has ( n ) ) return ;
2229+ const inc = incomingCount . get ( n ) || 0 ;
2230+ const out = outgoingCount . get ( n ) || 0 ;
2231+ if ( inc === 0 || inc === 1 || out === 1 ) queue . push ( n ) ;
22162232 } ) ;
2233+
22172234 while ( queue . length ) {
22182235 const name = queue . pop ( ) ;
22192236 if ( ! active . has ( name ) ) continue ;
2220- // Remove this noop leaf
2237+ // Remove this noop node
22212238 active . delete ( name ) ;
2222- // Decrement dependency counts for its dependencies (making them closer to leaves)
22232239 const op = byName . get ( name ) ;
22242240 if ( ! op ) continue ;
2241+
2242+ // For each dependency of the removed node, decrement its incoming count
22252243 for ( const dep of op . dependencies || [ ] ) {
22262244 if ( ! active . has ( dep ) ) continue ;
2227- const cnt = dependentsCount . get ( dep ) ;
2228- if ( cnt === undefined ) continue ;
2229- dependentsCount . set ( dep , cnt - 1 ) ;
2230- if ( cnt - 1 === 0 && noopSet . has ( dep ) ) {
2231- queue . push ( dep ) ;
2232- }
2245+ const prev = incomingCount . get ( dep ) || 0 ;
2246+ incomingCount . set ( dep , Math . max ( 0 , prev - 1 ) ) ;
2247+ const inc = incomingCount . get ( dep ) ;
2248+ const out = outgoingCount . get ( dep ) || 0 ;
2249+ if ( noopSet . has ( dep ) && ( inc === 0 || inc === 1 || out === 1 ) ) queue . push ( dep ) ;
2250+ }
2251+
2252+ // For each dependent (nodes that depended on the removed node), decrement their outgoing count
2253+ const depsOf = dependents . get ( name ) || new Set ( ) ;
2254+ for ( const dependent of depsOf ) {
2255+ if ( ! active . has ( dependent ) ) continue ;
2256+ const prevOut = outgoingCount . get ( dependent ) || 0 ;
2257+ outgoingCount . set ( dependent , Math . max ( 0 , prevOut - 1 ) ) ;
2258+ const inc = incomingCount . get ( dependent ) || 0 ;
2259+ const out = outgoingCount . get ( dependent ) || 0 ;
2260+ if ( noopSet . has ( dependent ) && ( inc === 0 || inc === 1 || out === 1 ) ) queue . push ( dependent ) ;
22332261 }
22342262 }
2235- return base . filter ( ( o ) => active . has ( o . name ) ) ;
2263+
2264+ // At this point `active` contains node names to keep. We must return op objects
2265+ // whose dependency lists are rewritten so that any dependency chains that went
2266+ // through removed noop nodes are bypassed.
2267+ const removed = new Set ( base . map ( ( o ) => o . name ) . filter ( ( n ) => ! active . has ( n ) ) ) ;
2268+
2269+ // Memoize resolved dependencies to avoid repeated recursion.
2270+ const resolvedMemo = new Map ( ) ;
2271+ function resolveDeps ( name , seen ) {
2272+ if ( resolvedMemo . has ( name ) ) return resolvedMemo . get ( name ) ;
2273+ if ( seen . has ( name ) ) return new Set ( ) ; // break cycles defensively
2274+ seen . add ( name ) ;
2275+ const op = byName . get ( name ) ;
2276+ const out = new Set ( ) ;
2277+ if ( ! op ) return out ;
2278+ for ( const d of op . dependencies || [ ] ) {
2279+ if ( ! byName . has ( d ) ) continue ;
2280+ if ( active . has ( d ) ) {
2281+ out . add ( d ) ;
2282+ } else {
2283+ // dependency was removed; splice through it
2284+ const sub = resolveDeps ( d , seen ) ;
2285+ for ( const s of sub ) out . add ( s ) ;
2286+ }
2287+ }
2288+ seen . delete ( name ) ;
2289+ resolvedMemo . set ( name , out ) ;
2290+ return out ;
2291+ }
2292+
2293+ const result = base
2294+ . filter ( ( o ) => active . has ( o . name ) )
2295+ . map ( ( o ) => {
2296+ // Compute effective dependencies for o by splicing through removed noops
2297+ const deps = new Set ( ) ;
2298+ for ( const d of o . dependencies || [ ] ) {
2299+ if ( ! byName . has ( d ) ) continue ;
2300+ if ( active . has ( d ) ) deps . add ( d ) ;
2301+ else {
2302+ const sub = resolveDeps ( d , new Set ( ) ) ;
2303+ for ( const s of sub ) deps . add ( s ) ;
2304+ }
2305+ }
2306+ // Return a shallow copy so callers can freely inspect/modify dependencies
2307+ return Object . assign ( { } , o , { dependencies : Array . from ( deps ) } ) ;
2308+ } ) ;
2309+
2310+ return result ;
22362311 }
22372312
22382313 function render ( ) {
0 commit comments