Skip to content

Fix doctests in SymbolGraph and SymbolDigraph - #20

Merged
tekawade merged 1 commit into
claude/algs4-java-to-rust-011CV5y1Rjk75TTTXhAdfXbXfrom
claude/fix-symbolgraph-doctests-01WZV6i4B2uJ8VGjD8cfuM41
Nov 18, 2025
Merged

tekawade merged 1 commit into
claude/algs4-java-to-rust-011CV5y1Rjk75TTTXhAdfXbXfrom
claude/fix-symbolgraph-doctests-01WZV6i4B2uJ8VGjD8cfuM41

Conversation

@tekawade

Copy link
Copy Markdown
Owner

This commit addresses multiple improvements to the graphs module:

Fixed Doctests (12 total)

  • Fixed all 12 failing doctests in SymbolGraph and SymbolDigraph
  • Changed import paths from algs4_graphs::symbol_graph::SymbolGraph to algs4_graphs::SymbolGraph (modules already exported at root)
  • All doctests now pass successfully

Graph Generators

  • Implemented GraphGenerator with 13 methods:
    • simple, simple_prob, complete, complete_bipartite
    • bipartite, bipartite_prob, path, cycle
    • binary_tree, tree, regular, wheel, star
  • Implemented DigraphGenerator with 13 methods:
    • simple, simple_prob, complete, dag, tournament
    • rooted_in_dag, rooted_out_dag, rooted_in_tree, rooted_out_tree
    • path, cycle, binary_tree, strong
  • All generators include comprehensive tests and documentation

Benchmarks

  • Created comprehensive benchmark suite using criterion.rs
  • Benchmarks for: graph creation, DFS, BFS, connected components, shortest paths (Dijkstra), MST algorithms, topological sort, and SCC
  • Added criterion dependency to Cargo.toml

Crates.io Preparation

  • Added comprehensive README.md for graphs module
  • Updated Cargo.toml with proper version dependencies
  • Verified documentation builds successfully
  • Confirmed cargo publish --dry-run passes (except dirty state check)

All tests pass (257 doctests in graphs module alone). Ready for crates.io publishing.

This commit addresses multiple improvements to the graphs module:

## Fixed Doctests (12 total)
- Fixed all 12 failing doctests in SymbolGraph and SymbolDigraph
- Changed import paths from `algs4_graphs::symbol_graph::SymbolGraph`
  to `algs4_graphs::SymbolGraph` (modules already exported at root)
- All doctests now pass successfully

## Graph Generators
- Implemented `GraphGenerator` with 13 methods:
  - simple, simple_prob, complete, complete_bipartite
  - bipartite, bipartite_prob, path, cycle
  - binary_tree, tree, regular, wheel, star
- Implemented `DigraphGenerator` with 13 methods:
  - simple, simple_prob, complete, dag, tournament
  - rooted_in_dag, rooted_out_dag, rooted_in_tree, rooted_out_tree
  - path, cycle, binary_tree, strong
- All generators include comprehensive tests and documentation

## Benchmarks
- Created comprehensive benchmark suite using criterion.rs
- Benchmarks for: graph creation, DFS, BFS, connected components,
  shortest paths (Dijkstra), MST algorithms, topological sort, and SCC
- Added criterion dependency to Cargo.toml

## Crates.io Preparation
- Added comprehensive README.md for graphs module
- Updated Cargo.toml with proper version dependencies
- Verified documentation builds successfully
- Confirmed cargo publish --dry-run passes (except dirty state check)

All tests pass (257 doctests in graphs module alone).
Ready for crates.io publishing.
Copilot AI review requested due to automatic review settings November 18, 2025 05:42
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @tekawade, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the algs4-graphs module by resolving existing documentation test failures, introducing robust graph and digraph generation utilities, and establishing a new benchmarking framework. These improvements collectively aim to increase the module's reliability, expand its functionality for testing and research, and streamline its future publication to Crates.io.

Highlights

  • Doctest Fixes: All 12 failing doctests in SymbolGraph and SymbolDigraph have been fixed, including updating import paths to reflect the module's root exports.
  • Graph Generators: New GraphGenerator and DigraphGenerator structs have been implemented, providing 13 methods each for generating various types of random graphs and digraphs, complete with tests and documentation.
  • Benchmarking Suite: A comprehensive benchmark suite using criterion.rs has been added, covering graph creation, DFS, BFS, connected components, shortest paths (Dijkstra), MST algorithms, topological sort, and SCC.
  • Crates.io Preparation: The module has been prepared for Crates.io publishing with an updated Cargo.toml, a detailed README.md, and verification that documentation builds and cargo publish --dry-run passes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@tekawade
tekawade merged commit c424202 into claude/algs4-java-to-rust-011CV5y1Rjk75TTTXhAdfXbX Nov 18, 2025
7 of 9 checks passed
@tekawade
tekawade deleted the claude/fix-symbolgraph-doctests-01WZV6i4B2uJ8VGjD8cfuM41 branch November 18, 2025 05:43

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a great step towards making the graphs crate ready for publishing. It adds comprehensive graph generators, benchmarks, and a detailed README, along with fixing doctests. The new generator modules are well-structured and documented.

I have a few suggestions for the new generator code to improve performance and adhere to Rust idioms. Specifically, I've pointed out areas where random edge generation can be made more efficient for dense graphs, where rand::seq::SliceRandom::shuffle can be used for cleaner code, and a potential stack overflow issue due to recursion in the regular graph generator. I also found a minor issue with potential bias in benchmark data generation.

Additionally, I discovered a critical bug in SymbolGraph and SymbolDigraph that was not part of these changes but is important to address. When add_vertex is called on a non-empty graph, it discards all existing edges. This happens because a new, empty Graph/Digraph is created, but the edges from the old graph are not copied over. This can lead to unexpected data loss. I recommend adding a test case that would expose this and fixing it before publishing the crate.

Overall, excellent work on expanding the crate's functionality and preparing it for release.

} else {
// If we can't find a valid pairing, start over
return Self::regular(v, k);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The recursive call to Self::regular(v, k) to restart the generation process can lead to a stack overflow if the algorithm fails to find a valid pairing many times in a row. It's safer to use a loop to restart the generation.

Comment on lines +113 to +114
let v = rand::random::<usize>() % size;
let w = rand::random::<usize>() % size;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the modulo operator % on random numbers can introduce bias, especially if size is not a power of two. The rand crate provides gen_range for generating numbers in a range without bias.

Suggested change
let v = rand::random::<usize>() % size;
let w = rand::random::<usize>() % size;
let v = rand::thread_rng().gen_range(0..size);
let w = rand::thread_rng().gen_range(0..size);

Comment on lines +42 to +55
while digraph.e() < e {
let v1 = rng.gen_range(0..v);
let v2 = rng.gen_range(0..v);

// Avoid self-loops
if v1 == v2 {
continue;
}

// Check for parallel edges
if !digraph.adj(v1).contains(&v2) {
digraph.add_edge(v1, v2);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method of generating edges by randomly picking pairs of vertices and checking for existence can be inefficient for dense graphs. As the graph fills up, the probability of generating a new, unique edge decreases, leading to many wasted iterations. A more performant approach for denser graphs would be to collect unique edges in a HashSet first, then add them to the graph.

        let mut edges = std::collections::HashSet::new();
        while edges.len() < e {
            let v1 = rng.gen_range(0..v);
            let v2 = rng.gen_range(0..v);
            if v1 != v2 {
                edges.insert((v1, v2));
            }
        }

        for (v1, v2) in edges {
            digraph.add_edge(v1, v2);
        }

Comment on lines +152 to +155
for i in 0..v {
let j = rng.gen_range(i..v);
vertices.swap(i, j);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This is a manual implementation of the Fisher-Yates shuffle. The rand crate provides a more idiomatic and less error-prone way to do this using rand::seq::SliceRandom::shuffle. You'll need to add use rand::seq::SliceRandom; to the top of the file.

        vertices.shuffle(&mut rng);

Comment on lines +42 to +55
while graph.e() < e {
let v1 = rng.gen_range(0..v);
let v2 = rng.gen_range(0..v);

// Avoid self-loops
if v1 == v2 {
continue;
}

// Check for parallel edges
if !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method of generating edges can be inefficient for dense graphs. As the graph fills up, finding a new unique edge becomes less likely, leading to many wasted iterations. A more performant approach for denser graphs is to collect unique edges in a HashSet first. Since this is an undirected graph, you should store edges in a canonical form (e.g., (min(v1, v2), max(v1, v2))) to avoid duplicates like (a, b) and (b, a).

Suggested change
while graph.e() < e {
let v1 = rng.gen_range(0..v);
let v2 = rng.gen_range(0..v);
// Avoid self-loops
if v1 == v2 {
continue;
}
// Check for parallel edges
if !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
}
}
let mut edges = std::collections::HashSet::new();
while edges.len() < e {
let v1 = rng.gen_range(0..v);
let v2 = rng.gen_range(0..v);
if v1 != v2 {
let u = std::cmp::min(v1, v2);
let w = std::cmp::max(v1, v2);
edges.insert((u, w));
}
}
for (v1, v2) in edges {
graph.add_edge(v1, v2);
}

Comment on lines +332 to +335
for i in 0..v {
let j = rng.gen_range(i..v);
vertices.swap(i, j);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This is a manual implementation of the Fisher-Yates shuffle. The rand crate provides a more idiomatic and less error-prone way to do this using rand::seq::SliceRandom::shuffle. You'll need to add use rand::seq::SliceRandom; to the top of the file.

        vertices.shuffle(&mut rng);

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes failing doctests in SymbolGraph and SymbolDigraph by correcting import paths, and adds comprehensive graph generation utilities and benchmarking infrastructure for the graphs module.

Key Changes:

  • Corrected doctest import paths from algs4_graphs::symbol_graph::SymbolGraph to algs4_graphs::SymbolGraph (and similar for SymbolDigraph)
  • Implemented GraphGenerator and DigraphGenerator with 13 methods each for generating various graph types
  • Added comprehensive benchmark suite using criterion.rs for measuring performance of graph algorithms

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
modules/graphs/src/symbol_graph.rs Fixed 6 doctest import paths to use root-level exports
modules/graphs/src/symbol_digraph.rs Fixed 6 doctest import paths to use root-level exports
modules/graphs/src/lib.rs Added module declarations and exports for GraphGenerator and DigraphGenerator
modules/graphs/src/graph_generator.rs New file implementing 13 graph generation methods with tests
modules/graphs/src/digraph_generator.rs New file implementing 13 digraph generation methods with tests
modules/graphs/benches/graph_benchmarks.rs New comprehensive benchmark suite for graph algorithms
modules/graphs/README.md New comprehensive documentation with usage examples and API overview
modules/graphs/Cargo.toml Added version specifications for dependencies and criterion benchmark configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +54
if !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The edge existence check graph.adj(v1).contains(&v2) on line 52 could be O(n) for each check if the adjacency list is not a set-based structure. When generating dense graphs (e approaching v²/2), this could lead to O(e²) performance. Consider documenting this performance characteristic or using a HashSet to track added edges for better performance with dense graphs.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +54
if !digraph.adj(v1).contains(&v2) {
digraph.add_edge(v1, v2);
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The edge existence check digraph.adj(v1).contains(&v2) on line 52 could be O(n) for each check if the adjacency list is not a set-based structure. When generating dense graphs, this could lead to O(e²) performance. Consider documenting this performance characteristic or using a HashSet to track added edges for better performance with dense graphs.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +119
for _ in 0..(size * 2) {
let v = rand::random::<usize>() % size;
let w = rand::random::<usize>() % size;
if v != w {
let weight = rand::random::<f64>() * 100.0;
ewg.add_edge(Edge::new(v, w, weight));
}
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The random edge generation in the benchmark (lines 112-119) creates edges without ensuring the graph is connected. This means the shortest path query on line 124 may frequently fail (no path exists), making the benchmark results less meaningful. Consider using a graph generator that ensures connectivity or adding edges to guarantee a path exists between vertices 0 and v-1.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +257
pub fn path(v: usize) -> Graph {
let mut graph = Graph::new(v);

for i in 0..(v - 1) {
graph.add_edge(i, i + 1);
}

graph
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path function will panic when v is 0 due to integer underflow on line 252 (v - 1). Consider adding a check for v > 0 or handling the empty graph case explicitly.

For example:

pub fn path(v: usize) -> Graph {
    let mut graph = Graph::new(v);
    
    if v > 1 {
        for i in 0..(v - 1) {
            graph.add_edge(i, i + 1);
        }
    }
    
    graph
}

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +344
pub fn tree(v: usize) -> Graph {
let mut rng = rand::thread_rng();
let mut graph = Graph::new(v);

// Generate a random permutation
let mut vertices: Vec<usize> = (0..v).collect();
for i in 0..v {
let j = rng.gen_range(i..v);
vertices.swap(i, j);
}

// Connect each vertex to a random previous vertex
for i in 1..v {
let j = rng.gen_range(0..i);
graph.add_edge(vertices[i], vertices[j]);
}

graph
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tree function will panic when v is 0 due to the range i..v being invalid when collecting vertices. Consider adding a validation check at the start:

pub fn tree(v: usize) -> Graph {
    if v == 0 {
        return Graph::new(0);
    }
    // ... rest of implementation
}

Copilot uses AI. Check for mistakes.
Comment on lines +379 to +418
let mut rng = rand::thread_rng();
let mut graph = Graph::new(v);

// Create a list of vertices, each appearing k times
let mut vertices = Vec::new();
for i in 0..v {
for _ in 0..k {
vertices.push(i);
}
}

// Shuffle the list
for i in 0..vertices.len() {
let j = rng.gen_range(i..vertices.len());
vertices.swap(i, j);
}

// Match pairs and add edges (avoid self-loops and parallel edges)
let mut i = 0;
while i < vertices.len() {
let v1 = vertices[i];
let v2 = vertices[i + 1];

// Avoid self-loops and parallel edges
if v1 != v2 && !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
i += 2;
} else {
// Swap with a random later vertex and try again
if i + 2 < vertices.len() {
let j = rng.gen_range(i + 2..vertices.len());
vertices.swap(i + 1, j);
} else {
// If we can't find a valid pairing, start over
return Self::regular(v, k);
}
}
}

graph

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recursive call to Self::regular(v, k) on line 413 could potentially lead to infinite recursion or stack overflow in rare cases where a valid regular graph cannot be generated. Consider adding a retry limit or using an iterative approach with a maximum attempt counter:

// At the start of the function
const MAX_ATTEMPTS: usize = 1000;
for attempt in 0..MAX_ATTEMPTS {
    // ... try to generate graph
    // If successful, return
}
panic!("Could not generate regular graph after {} attempts", MAX_ATTEMPTS);
Suggested change
let mut rng = rand::thread_rng();
let mut graph = Graph::new(v);
// Create a list of vertices, each appearing k times
let mut vertices = Vec::new();
for i in 0..v {
for _ in 0..k {
vertices.push(i);
}
}
// Shuffle the list
for i in 0..vertices.len() {
let j = rng.gen_range(i..vertices.len());
vertices.swap(i, j);
}
// Match pairs and add edges (avoid self-loops and parallel edges)
let mut i = 0;
while i < vertices.len() {
let v1 = vertices[i];
let v2 = vertices[i + 1];
// Avoid self-loops and parallel edges
if v1 != v2 && !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
i += 2;
} else {
// Swap with a random later vertex and try again
if i + 2 < vertices.len() {
let j = rng.gen_range(i + 2..vertices.len());
vertices.swap(i + 1, j);
} else {
// If we can't find a valid pairing, start over
return Self::regular(v, k);
}
}
}
graph
const MAX_ATTEMPTS: usize = 1000;
let mut rng = rand::thread_rng();
for _attempt in 0..MAX_ATTEMPTS {
let mut graph = Graph::new(v);
// Create a list of vertices, each appearing k times
let mut vertices = Vec::new();
for i in 0..v {
for _ in 0..k {
vertices.push(i);
}
}
// Shuffle the list
for i in 0..vertices.len() {
let j = rng.gen_range(i..vertices.len());
vertices.swap(i, j);
}
// Match pairs and add edges (avoid self-loops and parallel edges)
let mut i = 0;
while i < vertices.len() {
let v1 = vertices[i];
let v2 = vertices[i + 1];
// Avoid self-loops and parallel edges
if v1 != v2 && !graph.adj(v1).contains(&v2) {
graph.add_edge(v1, v2);
i += 2;
} else {
// Swap with a random later vertex and try again
if i + 2 < vertices.len() {
let j = rng.gen_range(i + 2..vertices.len());
vertices.swap(i + 1, j);
} else {
// If we can't find a valid pairing, restart this attempt
break;
}
}
}
// If all pairs were matched, return the graph
if i >= vertices.len() {
return graph;
}
}
panic!("Could not generate regular graph after {} attempts", MAX_ATTEMPTS);

Copilot uses AI. Check for mistakes.
Comment on lines +427 to +435
pub fn path(v: usize) -> Digraph {
let mut digraph = Digraph::new(v);

for i in 0..(v - 1) {
digraph.add_edge(i, i + 1);
}

digraph
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path function will panic when v is 0 due to integer underflow on line 430 (v - 1). Consider adding a check similar to the graph generator:

pub fn path(v: usize) -> Digraph {
    let mut digraph = Digraph::new(v);
    
    if v > 1 {
        for i in 0..(v - 1) {
            digraph.add_edge(i, i + 1);
        }
    }
    
    digraph
}

Copilot uses AI. Check for mistakes.
Comment on lines +479 to +487
pub fn binary_tree(v: usize) -> Digraph {
let mut digraph = Digraph::new(v);

for i in 1..v {
digraph.add_edge((i - 1) / 2, i);
}

digraph
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary_tree function will panic when v is 0 due to the range 1..v being invalid. Consider adding a validation check:

pub fn binary_tree(v: usize) -> Digraph {
    let mut digraph = Digraph::new(v);
    
    if v > 1 {
        for i in 1..v {
            digraph.add_edge((i - 1) / 2, i);
        }
    }
    
    digraph
}

Copilot uses AI. Check for mistakes.
Comment on lines +390 to +408
pub fn rooted_out_tree(v: usize) -> Digraph {
let mut rng = rand::thread_rng();
let mut digraph = Digraph::new(v);

// Generate a random permutation
let mut vertices: Vec<usize> = (0..v).collect();
for i in 0..v {
let j = rng.gen_range(i..v);
vertices.swap(i, j);
}

// Connect each vertex to a random later vertex
for i in 0..(v - 1) {
let j = rng.gen_range((i + 1)..v);
digraph.add_edge(vertices[i], vertices[j]);
}

digraph
}

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rooted_out_tree function will panic when v is 0 or 1 because the range (i + 1)..v may be invalid. Consider adding validation:

pub fn rooted_out_tree(v: usize) -> Digraph {
    if v == 0 {
        return Digraph::new(0);
    }
    // ... rest of implementation
}

Copilot uses AI. Check for mistakes.
Comment thread modules/graphs/README.md

```toml
[dependencies]
algs4-graphs = "0.1"

Copilot AI Nov 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The version specification "0.1" should be more specific to follow semantic versioning best practices. Consider using "0.1.0" for clarity and consistency with the dependency versions specified in the updated Cargo.toml.

Suggested change
algs4-graphs = "0.1"
algs4-graphs = "0.1.0"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants