;# $Id: tsort.pl 1 2006-08-24 12:32:52Z rmanfredi $
;#
;#  Copyright (c) 1991-1997, 2004-2006, Raphael Manfredi
;#  
;#  You may redistribute only under the terms of the Artistic Licence,
;#  as specified in the README file that comes with the distribution.
;#  You may reuse parts of this distribution only within the terms of
;#  that same Artistic Licence; a copy of which may be found at the root
;#  of the source tree for dist 4.0.
;#
;# $Log: tsort.pl,v $
;# Revision 3.0  1993/08/18  12:10:28  ram
;# Baseline for dist 3.0 netwide release.
;#
;# 
;# The topological sort is performed using the following algorithm:
;#
;# We have a list of successors for each item; makefile dependencies of
;# the form 'a b: c d' means successors(a) = successors(b) = { c, d }.
;# From that input, we derive a number of precursors for each item.
;# In our simple example above, c and d both have two precursors and
;# a and b have none. Items with no precursors are called outsiders
;# and are left in a pool. The sort is then initiated and will continue
;# until all the items have been sorted or a cycle is found...
;#
;# Outsiders are ready to be sorted; since the topological sort is a partial
;# order, an external criterion is needed to choose one item among the ones
;# in the pool. That item is assigned a number, and the number of precursors
;# for the remaining items is updated (by following the successors of the
;# sorted item and decrementing the value for each successor). Among those,
;# if any item reaches a precursor count of zero, it becomes an outsider.
;#
;# The algorithm ends when the outsider pool is empty. If it becomes empty and
;# some items remain unsorted, then there is one or more cycles among them.
;# One way to outline that cycle first extract all those items whose precursor
;# count is minimal then visit their dependency graph to find the cycle,
;# extract only those items belonging to the cycle into the outsiders set and
;# resume the main processing stream.
;#
#
# Topological sort of Makefile dependencies with cycle enhancing.
#

package tsort;

# Perform the topological sort of the items and outline cycles.
sub main'tsort {
	local(*Succ, *Prec) = @_;	# Tables of succesors and predecessors
	local(@Out);				# The outsider set
	local(@keys);				# Current active precursors
	local($item);				# Item to sort

	for (@keys = keys %Prec; @keys || @Out; @keys = keys %Prec) {
		&resync;			# Resynchronize outsiders
		if (@Out == 0) {	# Cycle detected
			&extract_cycle(*Prec, *Succ);
			next;
		}
		$item = shift(@Out);	# Sort current item (don't care which one)
		&sort($item);		# Update internal structures
	}
}

# Resynchronize the outsiders stack (those items that have no more precursors).
# If the outsiders stack becomes empty, then there is a cycle.
sub resync {
	foreach $target (keys %Prec) {
		if ($Prec{$target} == 0) {
			delete $Prec{$target};		# We're done with this item
			push(@Out, $target);		# Ready to be sorted
		}
	}
}

# Sort item
sub sort {
	local($item) = @_;
	print "(ok) $item\n" if $main'opt_d && !$Cycle;
	print "(fx) $item\n" if $main'opt_d && $Cycle;
	foreach $succ (split(' ', $Succ{$item})) {
		# The test for definedness is necessary, since when a cycle is found,
		# one item is forced out of %Prec. If we had the guarantee of no
		# cycle, the the test would not be necessary and no decrementation
		# could go past 0.
		$Prec{$succ}-- if defined $Prec{$succ};
	}
}

# Extract cycle... We look through the %Prec array and find all those items
# with the same lowest value. Those are a cycle, so we dump them, and make
# them new outsiders by resetting their count to 0.
sub extract_cycle {
	local(*Prec, *Succ) = @_;
	local($item) = (&sort_by_value(*Prec))[0];
	local($min) = $Prec{$item};			# Minimum value
	local($key, $value);
	local(%candidate);	# Superset of the cycle we found
	warn "    Cycle found for:\n";
	$Cycle++;
	while (($key, $value) = each %Prec) {
		$candidate{$key}++ if $value == $min;
	}
	local(%state);		# State of visited nodes (1 = cycle, -1 = dead)
	local($CYCLE) = 1;	# Possible member of a cycle
	local($DEAD) = -1;	# Dead end, no cycling possible
	foreach $key (keys %candidate) {
		last if $CYCLE == &visit($key, $Succ{$key});
	}
	while (($key, $value) = each %candidate) {
		next unless $state{$key} == $CYCLE;
		$Prec{$key} = 0;			# Members of cycle are new outsiders
		warn "\t(#$Cycle) $key\n";
	}
	local(%involved);	 # Items involved in the cycle...
	while (($key, $value) = each %state) {
		$involved{$key}++ if $state{$key} == $CYCLE;
	}
	&outline_cycle(*Succ, *involved);
}

sub outline_cycle {
	local(*Succ, *member) = @_;
	local($key, $value);
	local($depends);
	local($unit);
	warn "    Cycle involves:\n";
	while (($key, $value) = each %Succ) {
		next unless $member{$key};
		$depends = '';
		foreach $item (split(' ', $value)) {
			$depends .= "$item " if $member{$item};
		}
		$unit = $main'shmaster{"\$$key"};
		$unit =~ s/\s+$//;
		$unit = '?' if $unit eq '';
		warn "\t($unit) $key: $depends\n";
	}
}

# Visit a tree node, following all its successors, until we find a cycle.
# Return $CYCLE if the exploration of the node leaded to a cycle, $DEAD
# otherwise.
sub visit {
	local($node, $children) = @_;	# A node and its children
	# If we have already visited the node, return the status value attached
	# to it.
	return $state{$node} if $state{$node};
	$state{$node} = $CYCLE;			# Assume member of cycle
	local($all_dead) = 1;			# Set to 0 if at least one cycle found
	foreach $child (split(' ', $children)) {
		$all_dead = 0 if $CYCLE == &visit($child, $Succ{$child});
	}
	$state{$node} = $DEAD if $all_dead;
	$state{$node};
}

# Sort associative array by value
sub sort_by_value {
	local(*x) = @_;
	sub _by_value { $x{$a} <=> $x{$b}; }
	sort _by_value keys %x;
}

package main;

1;
