/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
/**
* colors for the CLRS color algorithm.
*
* these colors are useful because gray is "in between" white and black, but
* (outside of the general move away from using white/black as identifiers) it
* might be easier to conceptualize with descriptive labels like "untested"
* (white), "being tested", (gray) and "testing complete" (black).
*/
export enum Color {
white, gray, black
}
export class Vertex {
/**
* vertex and its subclasses have a type parameter for type
* guards/reflection; each instance has a type that is set
* to the static class type.
*/
public static type = 'vertex';
// --- members ---
public type = Vertex.type; // for type guard
public color = Color.white; // for loop check
/** dependencies */
// public edges_in: Vertex[] = [];
public edges_in: Set = new Set();
/** dependents */
// public edges_out: Vertex[] = [];
public edges_out: Set = new Set();
// --- accessors ---
get has_inbound_edges(): boolean { return this.edges_in.size > 0; }
get has_outbound_edges(): boolean { return this.edges_out.size > 0; }
// --- cleanup operations ---
/** reset this node */
public Reset(): void {
for (const edge of this.edges_out) {
edge.RemoveDependency(this);
}
for (const edge of this.edges_in) {
edge.RemoveDependent(this);
}
//this.edges_out = [];
this.edges_out.clear();
//this.edges_in = [];
this.edges_in.clear();
}
/** search for this vertex among outbound edges */
public SearchOutEdges(test: Vertex, recurse = true) {
for (const edge of this.edges_out.values()) {
if (edge === test) {
return true;
}
else if (recurse) {
if (edge.SearchOutEdges(test, recurse)) {
return true;
}
}
}
return false;
}
/** removes all inbound edges (dependencies) */
public ClearDependencies(): void {
for (const edge of this.edges_in) {
edge.RemoveDependent(this);
// testing inline...
// edge.edges_out = edge.edges_out.filter(check => check !== this);
}
// this.edges_in = [];
this.edges_in.clear();
}
// --- basic node operations ---
/** add a dependent. doesn't add if already in the list */
public AddDependent(edge: Vertex): void {
if (edge === this) return; // circular
if (this.edges_out.has(edge)) {
return;
}
/*
for (const check of this.edges_out) {
if (check === edge) {
return;
}
}
*/
//this.edges_out.push(edge);
this.edges_out.add(edge);
}
/** remove a dependent */
public RemoveDependent(edge: Vertex): void {
// this.edges_out = this.edges_out.filter((check) => check !== edge);
// updated for performance.
// this seems to be faster than any other method of removing an item.
// (also tried: temp loop and copy non-matching).
// this does assume that edges can't be in the list twice, but that
// should already be true (it would cause all sorts of other problems).
// actually does this just win because we break the loop earlier?
// (presumably in 50% of cases)? even if so, if the splice is not
// more expensive this is a win.
// splice should be expensive, though... weird. because what splice
// does (AIUI) is reassign array indexes above the delete index.
// it would be better if we could get rid of indexes altogether, perhaps
// using a set?
// note: tried to improve on splice with some direct methods, nothing
// seemed to work any better (not worse, either, but if there's no
// improvement we should use the native method).
/*
for (let i = 0; i < this.edges_out.length; i++) {
if (this.edges_out[i] === edge) {
this.edges_out.splice(i, 1);
return;
}
}
*/
this.edges_out.delete(edge);
}
/** add a dependency. doesn't add if already in the list */
public AddDependency(edge: Vertex): void {
if (edge === this) return; // circular
/*
for (const check of this.edges_in) {
if (check === edge) {
return;
}
}
*/
if (this.edges_in.has(edge)){
return;
}
//this.edges_in.push(edge);
this.edges_in.add(edge);
}
/** remove a dependency */
public RemoveDependency(edge: Vertex): void {
// this.edges_in = this.edges_in.filter((check) => check !== edge);
this.edges_in.delete(edge);
}
/**
* this is a composite operation, because the operations are always called
* in pairs. this means create a pair of links such that _edge_ depends on
* _this_.
*/
public LinkTo(edge: Vertex): void {
this.AddDependent(edge);
edge.AddDependency(this);
}
/**
* this is an alteranate formulation that may make more intuitive sense.
* it creates a pair of forward/backward links, such that _this_ depends
* on _edge_.
*/
public DependsOn(edge: Vertex): void {
this.AddDependency(edge);
edge.AddDependent(this);
}
/**
* this is called during calculation (if necessary). on a hit (loop), we
* reset the color of this, the test node, to white. there are two reasons
* for this:
*
* one, we want subsequent tests to also find the hit. in some cases we may
* not be marking the node as a loop (if it precedes the backref in the graph),
* so we want subsequent nodes to also hit the loop. [Q: this makes no sense,
* because this would still hit if the node were marked grey, assuming you
* test for that].
*
* two, if you fix the loop, on a subsequent call we want to force a re-check,
* which we can do if the vertex is marked white. [Q: could also be done on
* gray?]
*
* [A: logically you are correct, but this works, and matching grey does not].
*/
public LoopCheck(): boolean {
const stack: Vertex[] = [this];
while (stack.length) {
// note peek: we leave it on the stack
const v = stack[stack.length - 1];
// state flag: unset if we have edges we need to check
let complete = true;
// skip this vertex if it's clean
if (v.color !== Color.black) {
v.color = Color.gray; // set here, not top of function
for (const edge of v.edges_out) {
if (edge.color === Color.gray) {
this.color = Color.white; // note: this, not v
return true; // found a loop
}
// if (edge.color === Color.white && edge.edges_out.length) {
if (edge.color === Color.white && edge.edges_out.size) {
stack.push(edge);
complete = false;
// the only thing this break does is add loops. we can
// safely add all (white) edges as long as we don't color
// them here -- that was the issue in the last version.
// break; // ?
// if you really want fidelity with the recursive version
// you could reverse the order, but the order is arbitrary
// anyway so it makes no difference.
}
}
}
if (complete) {
v.color = Color.black;
stack.pop();
}
}
/*
this.color = Color.gray;
// switch to stack algorithm. see the method in Graph for details.
// NOTE: this is bugged. need to rewrite. it's generating false positives
// where the recursive version still works.
const stack: Vertex[] = [this];
while (stack.length) {
const v = stack[stack.length - 1];
let completed = true;
if (v.color !== Color.black) {
for (const edge of v.edges_out) {
if (edge.color === Color.gray) {
// this is different than the graph algo, here we reset the
// color when we hit a loop.
this.color = Color.white; // someone else can test
return true; // loop
}
else if (edge.color === Color.white) {
edge.color = Color.gray;
stack.push(edge);
completed = false;
}
}
}
if (completed) {
stack.pop();
v.color = Color.black;
}
}
*/
/*
// the old recursive version
for (const edge of this.edges_out) {
if (edge.color === Color.gray || (edge.color === Color.white && edge.LoopCheck())) {
this.color = Color.white; // someone else can test
return true; // loop
}
}
*/
this.color = Color.black;
return false;
}
/*
public LoopCheck2(compare: Vertex = this): boolean {
this.color = Color.gray;
for (const edge of this.edges_out) {
if (edge.color === Color.gray || (edge.color === Color.white && edge.LoopCheck2(compare))) {
this.color = Color.white; // someone else can test
return edge === compare; // loop
}
}
this.color = Color.black;
return false;
}
*/
}